49 lines
2.7 KiB
JavaScript
49 lines
2.7 KiB
JavaScript
import { ReplayTimeline, REPLAY_SCHEMA, fnv1a64 } from "./replay.mjs";
|
|
import { validateEventLog, validateSnapshot } from "./contracts.mjs";
|
|
|
|
export const TRACE_SCHEMA = "simulation-trace.v1";
|
|
|
|
function clone(value) { return structuredClone(value); }
|
|
|
|
export function createTrace({ contextId, timeline, state, task, tick = null } = {}) {
|
|
if (typeof contextId !== "string" || contextId.length === 0) throw new Error("trace_context_invalid");
|
|
if (!(timeline instanceof ReplayTimeline)) throw new Error("trace_timeline_invalid");
|
|
if (state === undefined) throw new Error("trace_state_missing");
|
|
const exported = timeline.export();
|
|
const latest = timeline.snapshots.latest();
|
|
if (!latest || latest.seq !== timeline.events.cursor || latest.tick !== timeline.events.tick) throw new Error("trace_snapshot_required");
|
|
const value = {
|
|
schema: TRACE_SCHEMA,
|
|
context_id: contextId,
|
|
seq: timeline.events.cursor,
|
|
tick: tick ?? timeline.events.tick,
|
|
state: clone(state),
|
|
state_hash: fnv1a64(state),
|
|
task: task ? clone(task) : null,
|
|
timeline: exported,
|
|
event_hash: fnv1a64(exported.events),
|
|
snapshot_hash: fnv1a64(latest),
|
|
};
|
|
return { ...value, trace_hash: fnv1a64(value) };
|
|
}
|
|
|
|
export function validateTrace(trace, { contextId } = {}) {
|
|
if (!trace || trace.schema !== TRACE_SCHEMA) throw new Error("trace_schema");
|
|
if (contextId !== undefined && trace.context_id !== contextId) throw new Error("trace_context_mismatch");
|
|
if (!Number.isSafeInteger(trace.seq) || trace.seq < 0 || !Number.isSafeInteger(trace.tick) || trace.tick < 0) throw new Error("trace_cursor_invalid");
|
|
if (fnv1a64(trace.state) !== trace.state_hash) throw new Error("trace_state_hash");
|
|
if (!trace.timeline || trace.timeline.schema !== REPLAY_SCHEMA) throw new Error("trace_timeline_schema");
|
|
validateEventLog(trace.timeline.events);
|
|
const snapshots = trace.timeline.snapshots?.snapshots;
|
|
if (!Array.isArray(snapshots) || snapshots.length === 0) throw new Error("trace_snapshot_missing");
|
|
for (const snapshot of snapshots) validateSnapshot(snapshot);
|
|
const timeline = new ReplayTimeline(trace.timeline);
|
|
const latest = timeline.snapshots.latest();
|
|
if (timeline.events.cursor !== trace.seq || timeline.events.tick !== trace.tick || !latest || latest.seq !== trace.seq || latest.tick !== trace.tick) throw new Error("trace_cursor_mismatch");
|
|
if (trace.event_hash !== fnv1a64(trace.timeline.events)) throw new Error("trace_event_hash");
|
|
if (trace.snapshot_hash !== fnv1a64(latest)) throw new Error("trace_snapshot_hash");
|
|
const unsigned = { ...trace }; delete unsigned.trace_hash;
|
|
if (trace.trace_hash !== fnv1a64(unsigned)) throw new Error("trace_hash");
|
|
return { trace: clone(trace), timeline };
|
|
}
|