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