Complete M16 action select circle parity
This commit is contained in:
88
web/protocol/atomic-checkpoint.ts
Normal file
88
web/protocol/atomic-checkpoint.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* 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;
|
||||
Reference in New Issue
Block a user