Files
linuxcncWebWASM/web/worker/recovery-runtime.mjs

92 lines
4.6 KiB
JavaScript

import { createCheckpoint, recoverSimulation } from "../workspace/recovery.mjs";
import { createOfflinePackage, migrateWorkspacePackage, verifyOfflinePackage, verifyWorkspacePackage } from "../workspace/migration.mjs";
import { fnv1a64 } from "./replay.mjs";
export const RECOVERY_RUNTIME_SCHEMA = "worker-recovery.v1";
const clone = (value) => structuredClone(value);
export class RecoveryRuntime {
constructor({ contextId } = {}) {
this.contextId = contextId;
this.reset();
}
reset() {
this.state = { schema: RECOVERY_RUNTIME_SCHEMA, context_id: this.contextId ?? null, status: "idle", crash_count: 0, restart_count: 0, offline: false, checkpoint_hash: null, package_hash: null, last_error: null, recovery_hash: null };
this.checkpoint = null;
this.offlinePackage = null;
return this.export();
}
export() {
const value = { ...clone(this.state), checkpoint: this.checkpoint ? clone(this.checkpoint) : null, offline_package: this.offlinePackage ? clone(this.offlinePackage) : null };
return { ...value, state_hash: fnv1a64(value) };
}
fail(code) { this.state.status = "blocked"; this.state.last_error = code; return { status: "blocked", code, state: this.export() }; }
saveCheckpoint(options) {
try {
this.checkpoint = createCheckpoint({ contextId: this.contextId, ...options });
this.state.status = "checkpointed";
this.state.checkpoint_hash = this.checkpoint.checkpoint_hash;
this.state.last_error = null;
return { status: "verified", checkpoint: clone(this.checkpoint), state: this.export() };
} catch (error) { return this.fail(String(error?.message ?? error)); }
}
markCrash() {
if (!this.checkpoint) return this.fail("recovery_checkpoint_missing");
this.state.status = "crashed"; this.state.crash_count += 1; this.state.last_error = "worker_crash";
return { status: "verified", state: this.export() };
}
restart({ events = null, operations = null, apply, reconcile } = {}) {
if (!this.checkpoint) return this.fail("recovery_checkpoint_missing");
const result = recoverSimulation({ checkpoint: this.checkpoint, contextId: this.contextId, events, operations, apply, reconcile });
this.state.restart_count += 1;
this.state.status = result.status;
this.state.last_error = result.status === "blocked" ? result.blocked?.[0]?.reason ?? "recovery_blocked" : null;
this.state.recovery_hash = result.state_hash;
return { status: result.status === "blocked" ? "blocked" : "verified", recovery: result, state: this.export() };
}
createOffline(workspacePackage, resources, options = {}) {
try {
const packageValue = verifyWorkspacePackage(workspacePackage, { contextId: this.contextId });
this.offlinePackage = createOfflinePackage({ workspacePackage: packageValue, resources, ...options });
this.state.package_hash = this.offlinePackage.bundle_hash;
this.state.offline = true; this.state.last_error = null;
return { status: "verified", package: clone(this.offlinePackage), state: this.export() };
} catch (error) { return this.fail(String(error?.message ?? error)); }
}
verifyOffline(available, { online = false, workspacePackage = null } = {}) {
if (!this.offlinePackage) return this.fail("offline_package_missing");
try {
const result = verifyOfflinePackage(this.offlinePackage, { contextId: this.contextId, workspacePackage, available, online });
this.state.status = result.status === "ready" ? "offline_ready" : "network_required";
this.state.last_error = null;
return { status: "verified", result, state: this.export() };
} catch (error) { return this.fail(String(error?.message ?? error)); }
}
migrate(workspacePackage, targetVersion) {
try {
const migrated = migrateWorkspacePackage(verifyWorkspacePackage(workspacePackage, { contextId: this.contextId }), targetVersion);
this.state.package_hash = migrated.package_hash; this.state.last_error = null;
return { status: "verified", package: clone(migrated), state: this.export() };
} catch (error) { return this.fail(String(error?.message ?? error)); }
}
restore(value) {
if (!value || value.schema !== RECOVERY_RUNTIME_SCHEMA) throw new Error("recovery_runtime_schema");
const unsigned = clone(value); delete unsigned.state_hash;
if (value.state_hash !== fnv1a64(unsigned)) throw new Error("recovery_runtime_hash");
this.contextId = unsigned.context_id ?? this.contextId;
this.state = { ...unsigned }; this.checkpoint = this.state.checkpoint; this.offlinePackage = this.state.offline_package;
delete this.state.checkpoint; delete this.state.offline_package;
return this.export();
}
}