Files
linuxcncWebWASM/web/worker/io-ack.mjs

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();
}
}