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(); } }