92 lines
4.6 KiB
JavaScript
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();
|
|
}
|
|
}
|