export const GLB_RECOVERY_SCHEMA_VERSION = 1 as const; export type GLBRecoveryOperation = "IMPORT" | "EXPORT"; export type GLBRecoveryStatus = "RUNNING" | "CANCELLED" | "COMMITTED" | "RECOVERED" | "BLOCKED"; export type GLBRecoveryErrorCode = | "GLB_OPERATION_CANCELLED" | "GLB_WORKER_RESTARTED" | "GLB_OPFS_QUOTA" | "GLB_RECOVERY_INVALID"; export interface GLBRecoveryReceipt { schemaVersion: typeof GLB_RECOVERY_SCHEMA_VERSION; operationId: string; operation: GLBRecoveryOperation; status: GLBRecoveryStatus; workerGeneration: number; baseRevision: number; candidateRevision: number; inputBytes: number; inputSha256: string; outputBytes: number; outputSha256: string | null; temporaryBytes: number; liveRequests: number; committed: boolean; errorCode?: GLBRecoveryErrorCode; } const SHA256 = /^[a-f0-9]{64}$/; const OPERATION_ID = /^[A-Za-z0-9_-]{1,96}$/; function assertBase(receipt: GLBRecoveryReceipt): void { if (receipt.schemaVersion !== GLB_RECOVERY_SCHEMA_VERSION || !OPERATION_ID.test(receipt.operationId) || (receipt.operation !== "IMPORT" && receipt.operation !== "EXPORT") || !Number.isSafeInteger(receipt.workerGeneration) || receipt.workerGeneration < 1 || !Number.isSafeInteger(receipt.baseRevision) || receipt.baseRevision < 0 || !Number.isSafeInteger(receipt.candidateRevision) || receipt.candidateRevision < receipt.baseRevision || !Number.isSafeInteger(receipt.inputBytes) || receipt.inputBytes <= 0 || !SHA256.test(receipt.inputSha256) || !Number.isSafeInteger(receipt.outputBytes) || receipt.outputBytes < 0 || (receipt.outputSha256 !== null && !SHA256.test(receipt.outputSha256)) || !Number.isSafeInteger(receipt.temporaryBytes) || receipt.temporaryBytes < 0 || !Number.isSafeInteger(receipt.liveRequests) || receipt.liveRequests < 0 || typeof receipt.committed !== "boolean") { throw new Error("GLB_RECOVERY_INVALID: receipt fields are malformed"); } } function clone(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { assertBase(receipt); return { ...receipt }; } export function beginGLBRecoveryOperation(input: { operationId: string; operation: GLBRecoveryOperation; workerGeneration: number; baseRevision: number; inputBytes: number; inputSha256: string; }): GLBRecoveryReceipt { const receipt: GLBRecoveryReceipt = { schemaVersion: GLB_RECOVERY_SCHEMA_VERSION, operationId: input.operationId, operation: input.operation, status: "RUNNING", workerGeneration: input.workerGeneration, baseRevision: input.baseRevision, candidateRevision: input.baseRevision + 1, inputBytes: input.inputBytes, inputSha256: input.inputSha256, outputBytes: 0, outputSha256: null, temporaryBytes: input.inputBytes, liveRequests: 1, committed: false, }; assertBase(receipt); return receipt; } export function commitGLBRecoveryOperation(receipt: GLBRecoveryReceipt, output: { bytes: number; sha256: string }): GLBRecoveryReceipt { const next = clone(receipt); if (next.status !== "RUNNING" || !Number.isSafeInteger(output.bytes) || output.bytes <= 0 || !SHA256.test(output.sha256)) { throw new Error("GLB_RECOVERY_INVALID: operation cannot commit"); } next.status = "COMMITTED"; next.outputBytes = output.bytes; next.outputSha256 = output.sha256; next.temporaryBytes = 0; next.liveRequests = 0; next.committed = true; return next; } export function cancelGLBRecoveryOperation(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { const next = clone(receipt); if (next.status !== "RUNNING") throw new Error("GLB_RECOVERY_INVALID: operation is not running"); next.status = "CANCELLED"; next.errorCode = "GLB_OPERATION_CANCELLED"; next.temporaryBytes = 0; next.liveRequests = 0; next.committed = false; return next; } export function blockGLBRecoveryForQuota(receipt: GLBRecoveryReceipt): GLBRecoveryReceipt { const next = clone(receipt); if (next.status !== "RUNNING") throw new Error("GLB_RECOVERY_INVALID: operation is not running"); next.status = "BLOCKED"; next.errorCode = "GLB_OPFS_QUOTA"; next.temporaryBytes = 0; next.liveRequests = 0; next.committed = false; return next; } export function recoverGLBRecoveryOperation(receipt: GLBRecoveryReceipt, workerGeneration: number): GLBRecoveryReceipt { const next = clone(receipt); if (next.status !== "COMMITTED" || !Number.isSafeInteger(workerGeneration) || workerGeneration <= next.workerGeneration) { throw new Error("GLB_RECOVERY_INVALID: only a committed operation can recover"); } next.status = "RECOVERED"; next.workerGeneration = workerGeneration; next.errorCode = "GLB_WORKER_RESTARTED"; return next; } export function parseGLBRecoveryReceipt(value: unknown): GLBRecoveryReceipt { if (!value || typeof value !== "object") throw new Error("GLB_RECOVERY_INVALID: receipt is not an object"); const receipt = value as GLBRecoveryReceipt; assertBase(receipt); if (!["RUNNING", "CANCELLED", "COMMITTED", "RECOVERED", "BLOCKED"].includes(receipt.status)) throw new Error("GLB_RECOVERY_INVALID: status"); if (receipt.status === "CANCELLED" && receipt.errorCode !== "GLB_OPERATION_CANCELLED") throw new Error("GLB_RECOVERY_INVALID: cancellation code"); if (receipt.status === "BLOCKED" && receipt.errorCode !== "GLB_OPFS_QUOTA") throw new Error("GLB_RECOVERY_INVALID: quota code"); if (receipt.status === "COMMITTED" && (!receipt.committed || receipt.outputBytes <= 0 || !receipt.outputSha256)) throw new Error("GLB_RECOVERY_INVALID: committed receipt"); if (receipt.status === "RECOVERED" && (!receipt.committed || receipt.errorCode !== "GLB_WORKER_RESTARTED")) throw new Error("GLB_RECOVERY_INVALID: recovered receipt"); return { ...receipt }; }