52 lines
3.5 KiB
JavaScript
52 lines
3.5 KiB
JavaScript
import { fnv1a64 } from "./replay.mjs";
|
|
|
|
export const IO_ACK_SCHEMA = "sim_io_ack.v1";
|
|
const STATUSES = new Set(["pending", "acked", "timed_out", "aborted"]);
|
|
|
|
function clone(value) { return structuredClone(value); }
|
|
|
|
export class IoAckTracker {
|
|
constructor() { this.reset(); }
|
|
reset() { this.state = { schema: IO_ACK_SCHEMA, tick: 0, commands: [] }; return this.export(); }
|
|
export() { const value = clone(this.state); return { ...value, state_hash: fnv1a64(value) }; }
|
|
hash() { return this.export().state_hash; }
|
|
find(operationId) { return this.state.commands.find((command) => command.operation_id === operationId); }
|
|
issue(operationId, kind, tick, timeoutTicks = 10) {
|
|
if (typeof operationId !== "string" || operationId.length === 0 || typeof kind !== "string" || !Number.isSafeInteger(tick) || tick < this.state.tick || !Number.isSafeInteger(timeoutTicks) || timeoutTicks < 1 || timeoutTicks > 1000000) return { status: "error", code: "io_ack_argument" };
|
|
const existing = this.find(operationId);
|
|
if (existing) return existing.status === "acked" ? { status: "verified", replay: true, command: clone(existing), state: this.export() } : { status: "error", code: "io_ack_duplicate", command: clone(existing), state: this.export() };
|
|
const command = { operation_id: operationId, kind, issued_tick: tick, deadline_tick: tick + timeoutTicks, status: "pending", ack_tick: null, side_effect_count: 0 };
|
|
this.state.tick = tick; this.state.commands.push(command);
|
|
return { status: "verified", command: clone(command), state: this.export() };
|
|
}
|
|
step(tick) {
|
|
if (!Number.isSafeInteger(tick) || tick < this.state.tick) return { status: "error", code: "io_ack_tick_regression", state: this.export() };
|
|
this.state.tick = tick;
|
|
for (const command of this.state.commands) if (command.status === "pending" && tick >= command.deadline_tick) command.status = "timed_out";
|
|
return { status: "verified", command: "IO_TICK", state: this.export() };
|
|
}
|
|
ack(operationId, tick) {
|
|
const command = this.find(operationId);
|
|
if (!command) return { status: "error", code: "io_ack_missing", state: this.export() };
|
|
if (!Number.isSafeInteger(tick) || tick < this.state.tick) return { status: "error", code: "io_ack_tick_regression", state: this.export() };
|
|
this.state.tick = tick;
|
|
if (command.status !== "pending") return { status: "error", code: command.status === "timed_out" ? "io_ack_late" : "io_ack_state", command: clone(command), state: this.export() };
|
|
command.status = "acked"; command.ack_tick = tick; command.side_effect_count = 1;
|
|
return { status: "verified", command: clone(command), state: this.export() };
|
|
}
|
|
abort(operationId) {
|
|
const command = this.find(operationId);
|
|
if (!command) return { status: "error", code: "io_ack_missing", state: this.export() };
|
|
if (command.status === "acked") return { status: "error", code: "io_ack_too_late", command: clone(command), state: this.export() };
|
|
command.status = "aborted";
|
|
return { status: "verified", command: clone(command), state: this.export() };
|
|
}
|
|
restore(value) {
|
|
if (!value || value.schema !== IO_ACK_SCHEMA || !Array.isArray(value.commands)) throw new Error("io_ack_restore_schema");
|
|
const unsigned = { ...value }; delete unsigned.state_hash;
|
|
if (value.state_hash !== fnv1a64(unsigned)) throw new Error("io_ack_restore_hash");
|
|
if (value.commands.some((command) => !STATUSES.has(command.status))) throw new Error("io_ack_restore_status");
|
|
this.state = clone(unsigned); return this.export();
|
|
}
|
|
}
|