Implement AXIS parity UI and execution baseline
This commit is contained in:
106
web/worker/operation-ledger.mjs
Normal file
106
web/worker/operation-ledger.mjs
Normal file
@@ -0,0 +1,106 @@
|
||||
import { fnv1a64 } from "./replay.mjs";
|
||||
|
||||
export const LEDGER_SCHEMA = "operation-ledger.v1";
|
||||
export const LEDGER_PHASES = Object.freeze(["prepared", "executing", "verified", "committed", "aborted", "failed"]);
|
||||
|
||||
function clone(value) { return structuredClone(value); }
|
||||
function text(value) { return typeof value === "string" && value.length > 0 ? value : null; }
|
||||
|
||||
export class OperationLedger {
|
||||
constructor(entries = []) {
|
||||
this.entries = new Map();
|
||||
for (const entry of entries) this.importEntry(entry);
|
||||
}
|
||||
|
||||
intent(input) {
|
||||
return fnv1a64({ kind: input.kind, payload: input.payload ?? {}, context_id: input.context_id ?? null, session_id: input.session_id ?? null });
|
||||
}
|
||||
|
||||
prepare(input) {
|
||||
if (!text(input?.operation_id)) return { status: "untracked", replay: false, entry: null };
|
||||
const intentHash = this.intent(input);
|
||||
const existing = this.entries.get(input.operation_id);
|
||||
if (existing) {
|
||||
if (existing.kind !== input.kind) throw new Error("ledger_operation_conflict");
|
||||
return { status: existing.status, replay: existing.phase === "committed" || existing.status === "verified", entry: clone(existing) };
|
||||
}
|
||||
const entry = {
|
||||
operation_id: input.operation_id,
|
||||
kind: input.kind,
|
||||
seq: input.seq,
|
||||
intent_hash: intentHash,
|
||||
status: "prepared",
|
||||
phase: "prepared",
|
||||
side_effect_count: 0,
|
||||
result_hash: null,
|
||||
commit_seq: null,
|
||||
response: null,
|
||||
};
|
||||
this.entries.set(entry.operation_id, entry);
|
||||
return { status: entry.status, replay: false, entry: clone(entry) };
|
||||
}
|
||||
|
||||
execute(operationId) {
|
||||
const entry = this.require(operationId);
|
||||
if (entry.phase === "committed" || entry.phase === "aborted") return clone(entry);
|
||||
if (entry.phase !== "prepared") throw new Error("ledger_execute_phase");
|
||||
entry.phase = "executing";
|
||||
entry.status = "executing";
|
||||
return clone(entry);
|
||||
}
|
||||
|
||||
verify(operationId, response) {
|
||||
const entry = this.require(operationId);
|
||||
if (entry.phase === "committed") return clone(entry);
|
||||
if (entry.phase !== "executing" && entry.phase !== "prepared") throw new Error("ledger_verify_phase");
|
||||
entry.phase = "verified";
|
||||
entry.status = response?.payload?.status === "error" || response?.kind === "ERROR" ? "failed" : "verified";
|
||||
entry.result_hash = fnv1a64(response?.payload ?? {});
|
||||
entry.response = clone(response);
|
||||
return clone(entry);
|
||||
}
|
||||
|
||||
commit(operationId, seq) {
|
||||
const entry = this.require(operationId);
|
||||
if (entry.phase === "committed") return clone(entry);
|
||||
if (entry.phase !== "verified") throw new Error("ledger_commit_phase");
|
||||
if (entry.status === "failed") { entry.phase = "failed"; return clone(entry); }
|
||||
entry.phase = "committed";
|
||||
entry.status = "verified";
|
||||
entry.commit_seq = seq;
|
||||
entry.side_effect_count = 1;
|
||||
return clone(entry);
|
||||
}
|
||||
|
||||
abort(operationId, reason = "abort_requested") {
|
||||
const entry = this.require(operationId);
|
||||
if (entry.phase === "committed") return { status: "too_late", entry: clone(entry) };
|
||||
if (entry.phase === "aborted") return { status: "aborted", entry: clone(entry) };
|
||||
entry.phase = "aborted";
|
||||
entry.status = "aborted";
|
||||
entry.abort_reason = reason;
|
||||
return { status: "aborted", entry: clone(entry) };
|
||||
}
|
||||
|
||||
reconcile(operationId) {
|
||||
if (!text(operationId)) throw new Error("ledger_operation_required");
|
||||
const entry = this.entries.get(operationId);
|
||||
return entry ? { status: entry.phase === "committed" ? "committed" : entry.phase, entry: clone(entry) } : { status: "missing", entry: null };
|
||||
}
|
||||
|
||||
require(operationId) {
|
||||
const entry = this.entries.get(operationId);
|
||||
if (!entry) throw new Error("ledger_operation_missing");
|
||||
return entry;
|
||||
}
|
||||
|
||||
importEntry(entry) {
|
||||
if (!entry || typeof entry !== "object" || !text(entry.operation_id) || !text(entry.kind) || !LEDGER_PHASES.includes(entry.phase) || !["prepared", "executing", "verified", "aborted", "failed"].includes(entry.status)) throw new Error("ledger_entry_invalid");
|
||||
if (this.entries.has(entry.operation_id)) throw new Error("ledger_operation_duplicate");
|
||||
this.entries.set(entry.operation_id, clone(entry));
|
||||
}
|
||||
|
||||
export() {
|
||||
return { schema: LEDGER_SCHEMA, entries: [...this.entries.values()].sort((a, b) => a.operation_id.localeCompare(b.operation_id)).map(clone) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user