Files
workinf_Blender_Wasm/web/protocol/atomic-checkpoint.ts
mes123456 6a0b980d75
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
Complete M16 action select circle parity
2026-08-20 22:57:59 -04:00

89 lines
3.8 KiB
TypeScript

/**
* The durable part of a streamed model response. Deliberately no text or
* provider payload is accepted here: a checkpoint is a resume cursor, not a
* second copy of the response.
*/
export type CheckpointState = "STREAMING" | "COMPLETED" | "FAILED";
export interface AtomicCheckpoint {
state: CheckpointState;
byteCount: number;
chunkCount: number;
sha256: string;
}
export interface AtomicCheckpointStore {
writeTemporary(path: string, checkpoint: AtomicCheckpoint): Promise<void> | void;
renameTemporary(temporaryPath: string, targetPath: string): Promise<void> | void;
}
const CHECKPOINT_KEYS = ["state", "byteCount", "chunkCount", "sha256"] as const;
const HASH_PATTERN = /^[a-f0-9]{64}$/u;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
/** Validate and clone so callers cannot mutate a value after the atomic write. */
export function createAtomicCheckpoint(value: AtomicCheckpoint): AtomicCheckpoint {
if (!isRecord(value)) throw new TypeError("CHECKPOINT_INVALID: checkpoint must be an object");
const keys = Object.keys(value).sort();
const expected = [...CHECKPOINT_KEYS].sort();
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
throw new TypeError("CHECKPOINT_INVALID: checkpoint may contain only state, byteCount, chunkCount and sha256");
}
if (!["STREAMING", "COMPLETED", "FAILED"].includes(value.state as string)) {
throw new TypeError("CHECKPOINT_INVALID: state is invalid");
}
if (!Number.isSafeInteger(value.byteCount) || value.byteCount < 0 || !Number.isSafeInteger(value.chunkCount) || value.chunkCount < 0) {
throw new TypeError("CHECKPOINT_INVALID: counts must be non-negative safe integers");
}
if (typeof value.sha256 !== "string" || !HASH_PATTERN.test(value.sha256)) {
throw new TypeError("CHECKPOINT_INVALID: sha256 must be a lowercase SHA-256 digest");
}
return Object.freeze({
state: value.state as CheckpointState,
byteCount: value.byteCount,
chunkCount: value.chunkCount,
sha256: value.sha256,
});
}
/**
* Write a checkpoint to a temporary key and publish it with one rename. A
* failed write never replaces the previously committed checkpoint.
*/
export async function commitAtomicCheckpoint(store: AtomicCheckpointStore, targetPath: string, value: AtomicCheckpoint): Promise<AtomicCheckpoint> {
if (!store || typeof store.writeTemporary !== "function" || typeof store.renameTemporary !== "function") {
throw new TypeError("CHECKPOINT_STORE_INVALID: atomic write and rename are required");
}
if (typeof targetPath !== "string" || targetPath.length === 0) throw new TypeError("CHECKPOINT_TARGET_INVALID: target path is required");
const checkpoint = createAtomicCheckpoint(value);
const temporaryPath = `${targetPath}.tmp`;
await store.writeTemporary(temporaryPath, checkpoint);
await store.renameTemporary(temporaryPath, targetPath);
return checkpoint;
}
/** In-memory store useful for recovery tests and callers without OPFS. */
export function createMemoryCheckpointStore(): AtomicCheckpointStore & { get(path: string): AtomicCheckpoint | undefined } {
const files = new Map<string, AtomicCheckpoint>();
return {
writeTemporary(path, checkpoint) {
files.set(path, createAtomicCheckpoint(checkpoint));
},
renameTemporary(temporaryPath, targetPath) {
const checkpoint = files.get(temporaryPath);
if (!checkpoint) throw new Error("CHECKPOINT_RENAME_FAILED: temporary checkpoint is missing");
files.set(targetPath, checkpoint);
files.delete(temporaryPath);
},
get(path) {
return files.get(path);
},
};
}
export const createCheckpoint = createAtomicCheckpoint;
export const commitCheckpoint = commitAtomicCheckpoint;