Files
linuxcncWebWASM/web/worker/xyzac-command-ledger.mjs

65 lines
2.8 KiB
JavaScript

import { OperationLedger, LEDGER_SCHEMA, LEDGER_PHASES } from "./operation-ledger.mjs";
import { fnv1a64 } from "./replay.mjs";
export const XYZAC_COMMAND_LEDGER_SCHEMA = "xyzac-command-ledger.v1";
const TERMINAL_PHASES = new Set(["committed", "aborted", "failed"]);
export class XyzacCommandLedger extends OperationLedger {
constructor(entries = []) {
super(entries);
this.scope = "selected_xyzac_simulation";
}
prepare(input) {
if (["HELLO", "CAPABILITIES", "SNAPSHOT_REQUEST", "CANCEL"].includes(input?.kind)) return { status: "untracked", replay: false, entry: null };
const result = super.prepare(input);
if (result.entry) {
result.entry.scope = this.scope;
result.entry.intent_hash = result.entry.intent_hash ?? fnv1a64({ kind: input.kind, payload: input.payload ?? {}, operation_id: input.operation_id });
}
return result;
}
acknowledge(operationId, response) {
return this.verify(operationId, response);
}
result(operationId, response, seq) {
this.acknowledge(operationId, response);
return this.commit(operationId, seq);
}
pending() {
return [...this.entries.values()]
.filter((entry) => !TERMINAL_PHASES.has(entry.phase))
.sort((left, right) => left.seq - right.seq)
.map((entry) => structuredClone(entry));
}
reconcileAll() {
return this.pending().map((entry) => ({ operation_id: entry.operation_id, status: entry.phase, entry }));
}
timeoutReconcile(operationId) {
const reconciliation = this.reconcile(operationId);
return { status: "reconcile_required", operation_id: operationId, retry_allowed: false, reconciliation };
}
audit({ includeResponses = true, maxEntries = Number.POSITIVE_INFINITY } = {}) {
// High-frequency SIM_STEP responses request only a small audit window.
// OperationLedger.export() clones every historical response before the
// window can be selected, which turns a long run into quadratic browser
// serialization work. The map is insertion ordered by command sequence,
// so select the tail first and clone only the returned entries.
const values = [...this.entries.values()];
const pending = this.pending();
const totalEntries = values.length;
const windowed = Number.isFinite(maxEntries) && maxEntries >= 0 ? values.slice(-Math.floor(maxEntries)) : values;
const entries = includeResponses
? windowed.map((entry) => structuredClone(entry))
: windowed.map((entry) => { const { response, ...summary } = entry; return structuredClone(summary); });
const unsigned = { schema: XYZAC_COMMAND_LEDGER_SCHEMA, operation_schema: LEDGER_SCHEMA, phases: [...LEDGER_PHASES], scope: this.scope, entries, entry_count: totalEntries, truncated: entries.length < totalEntries, pending_count: pending.length };
return { ...unsigned, state_hash: fnv1a64(unsigned) };
}
}