Checkpoint web parity through Chromium input tasks
This commit is contained in:
134
web/protocol/glb-recovery.ts
Normal file
134
web/protocol/glb-recovery.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user