// locus.history.ts import type { LiveMapAuthority, LiveMapAnyOp, LiveMapCommit, LiveMapGraphOp, LiveMapOp, LivePath, } from "../../types/livemap.types.js"; import type { LiveMapProjectedGraphEnsureQuidOp } from "../livemap/livemap.identity.types.js"; import type { JsonValue } from "../../core/types.js"; import type { LocusCanonicalCommit, LocusCanonicalCommitListener, LocusCanonicalHistory, LocusCanonicalHistoryDiagnostics, LocusCanonicalOp, LocusCanonicalStream, LocusCanonicalStreamOptions, LocusIncarnationId, LocusLogicalMapId, LocusWireValue, } from "../../types/locus.types.js"; import { create_live_trace_context, type LocusCommitCausation } from "./locus.trace.js"; import { clone_node } from "../../core/clone-node.js"; import { is_Node } from "../../core/node-guards.js"; import { clone_live_root } from "../livemap/livemap.editor.js"; import { validate_document_path } from "../livemap/livemap.document.path.js"; import { encode_locus_graph_content } from "./locus.graph-content-codec.js"; import { admit_projected_value } from "../../core/projected-value-admission.js"; import { materialize_projected_value } from "../../core/projected-value-materialization.js"; const DEFAULT_MAX_COMMITS = 1_024; const DEFAULT_MAX_BYTES = 4 * 1_024 * 1_024; const ABSENT_VALUE: Readonly<{ present: false }> = Object.freeze({ present: false }); const textEncoder = new TextEncoder(); let logicalMapIdIncrement = 0; let incarnationIdIncrement = 0; type RetainedCommit = Readonly<{ commit: LocusCanonicalCommit; encodedBytes: number; }>; function make_logical_map_id(): LocusLogicalMapId { logicalMapIdIncrement += 1; const uuid = globalThis.crypto?.randomUUID?.(); if (uuid) return `lhm-${uuid}`; return `lhm-${Date.now().toString(36)}-${logicalMapIdIncrement.toString(36)}`; } function make_incarnation_id(): LocusIncarnationId { incarnationIdIncrement += 1; const uuid = globalThis.crypto?.randomUUID?.(); if (uuid) return `lhi-${uuid}`; return `lhi-${Date.now().toString(36)}-${incarnationIdIncrement.toString(36)}`; } function must_identity(value: string, name: string): string { if (value.length > 0) return value; throw new Error(`Locus ${name} must be a non-empty string.`); } function must_bound(value: number | undefined, fallback: number, name: string): number { if (value === undefined) return fallback; if (Number.isFinite(value) && value >= 0) return Math.trunc(value); throw new Error(`Locus canonical history ${name} must be a finite non-negative number.`); } function freeze_projected_value(value: JsonValue): JsonValue { if (value === null || typeof value !== "object") return value; for (const child of Object.values(value)) freeze_projected_value(child); Object.freeze(value); return value; } function clone_projected_value(value: JsonValue, field: string): JsonValue { try { return freeze_projected_value( materialize_projected_value(admit_projected_value(value)), ); } catch { throw new Error(`Locus canonical commit ${field} is not a supported data value.`); } } function wire_value(value: JsonValue | undefined, field: string): LocusWireValue { if (value === undefined) return ABSENT_VALUE; return Object.freeze({ present: true, value: clone_projected_value(value, field) }); } function must_path(path: LivePath): LivePath { if (!Array.isArray(path)) throw new Error("Locus canonical commit path is not an array."); for (const part of path) { if (typeof part === "string") continue; if (typeof part === "number" && Number.isInteger(part) && part >= 0) continue; throw new Error("Locus canonical commit path contains an invalid segment."); } return Object.freeze([...path]); } function must_json_array(values: readonly JsonValue[], field: string): readonly JsonValue[] { if (!Array.isArray(values)) { throw new Error(`Locus canonical commit ${field} is not a data array.`); } return Object.freeze(values.map((value, index) => clone_projected_value(value, `${field}[${index}]`))); } function canonical_op(op: LiveMapOp): LocusCanonicalOp { const path = must_path(op.path); if (op.kind === "delete") { if (op.next !== undefined) throw new Error("Locus canonical delete next value must be absent."); return Object.freeze({ kind: "delete", path, prev: wire_value(op.prev, "delete prev value"), next: ABSENT_VALUE, }); } if (op.kind === "splice") { if (!Number.isInteger(op.start) || op.start < 0) { throw new Error("Locus canonical splice start must be a non-negative integer."); } if (!Array.isArray(op.prev) || !Array.isArray(op.next)) { throw new Error("Locus canonical splice prev and next values must be arrays."); } return Object.freeze({ kind: "splice", path, start: op.start, removed: must_json_array(op.removed, "splice removed value"), inserted: must_json_array(op.inserted, "splice inserted value"), prev: wire_value(op.prev, "splice prev value"), next: wire_value(op.next, "splice next value"), }); } if (op.kind === "rename") { if (op.prev === null || typeof op.prev !== "object" || Array.isArray(op.prev) || op.next === null || typeof op.next !== "object" || Array.isArray(op.next)) { throw new Error("Locus canonical rename prev and next values must be objects."); } return Object.freeze({ kind: op.kind, path, from: op.from, to: op.to, prev: wire_value(op.prev, "rename prev value"), next: wire_value(op.next, "rename next value"), }); } if (op.kind === "move") { if (!Number.isSafeInteger(op.from) || op.from < 0 || !Number.isSafeInteger(op.to) || op.to < 0) { throw new Error("Locus canonical move indexes must be non-negative safe integers."); } if (!Array.isArray(op.prev) || !Array.isArray(op.next)) { throw new Error("Locus canonical move prev and next values must be arrays."); } return Object.freeze({ kind: op.kind, path, from: op.from, to: op.to, prev: wire_value(op.prev, "move prev value"), next: wire_value(op.next, "move next value"), }); } if (op.next === undefined) { throw new Error(`Locus canonical ${op.kind} next value must be present.`); } if (op.kind === "set") { return Object.freeze({ kind: "set", path, prev: wire_value(op.prev, "set prev value"), next: wire_value(op.next, "set next value"), }); } if (op.kind === "replace") { return Object.freeze({ kind: "replace", path, prev: wire_value(op.prev, "replace prev value"), next: wire_value(op.next, "replace next value"), }); } throw new Error("Locus canonical commit operation kind is invalid."); } function is_projected_ensure_quid_op( op: LiveMapGraphOp | LiveMapProjectedGraphEnsureQuidOp, ): op is LiveMapProjectedGraphEnsureQuidOp { return op.op === "ensure-quid" && "projected" in op.target && op.target.projected === true; } function canonical_graph_op(op: LiveMapGraphOp | LiveMapProjectedGraphEnsureQuidOp): LocusCanonicalOp { if (op.op === "replace-root") { return Object.freeze({ domain: "graph", op: "replace-root", mode: op.mode, root: encode_locus_graph_content(clone_live_root(op.root)), }); } if (is_projected_ensure_quid_op(op)) { return Object.freeze({ domain: "graph", op: "ensure-quid", target: Object.freeze({ kind: "path", path: must_path(op.target.path), projected: true }), quid: op.quid, }); } const documentOp: LiveMapGraphOp = op; const target = Object.freeze({ kind: "path" as const, path: validate_document_path(documentOp.target.path), ...(documentOp.target.witness === undefined ? {} : { witness: Object.freeze({ quid: documentOp.target.witness.quid }) }), }); if (documentOp.op === "set-attr") { return Object.freeze({ domain: "graph", op: "set-attr", target, name: documentOp.name, value: clone_node(documentOp.value), }); } if (documentOp.op === "remove-attr") { return Object.freeze({ domain: "graph", op: "remove-attr", target, name: documentOp.name }); } if (documentOp.op === "replace-attrs") { return Object.freeze({ domain: "graph", op: "replace-attrs", target, attrs: clone_node(documentOp.attrs), }); } if (documentOp.op === "ensure-quid") { return Object.freeze({ domain: "graph", op: "ensure-quid", target, quid: documentOp.quid, }); } if (documentOp.op === "replace-content") { return Object.freeze({ domain: "graph", op: "replace-content", target, index: documentOp.index, replacement: encode_locus_graph_content( is_Node(documentOp.replacement) ? clone_live_root(documentOp.replacement) : documentOp.replacement, ), }); } if (documentOp.op === "insert-content") { return Object.freeze({ domain: "graph", op: "insert-content", target, index: documentOp.index, content: encode_locus_graph_content( is_Node(documentOp.content) ? clone_live_root(documentOp.content) : documentOp.content, ), }); } if (documentOp.op === "remove-content") { return Object.freeze({ domain: "graph", op: "remove-content", target, index: documentOp.index }); } if (documentOp.op === "move-content") { return Object.freeze({ domain: "graph", op: "move-content", target, from: documentOp.from, to: documentOp.to, }); } throw new Error("Locus canonical graph operation discriminant is invalid."); } /** @internal Construct the exact detached canonical envelope used by history and persistence. */ export function make_locus_canonical_commit( map: TMap, commit: LiveMapCommit, logicalMapId: LocusLogicalMapId, incarnationId: LocusIncarnationId, expectedPrevRev: number, ): LocusCanonicalCommit { if (!commit.changed) throw new Error("Locus canonical history received an unchanged commit."); if (!Number.isInteger(commit.prevRev) || commit.prevRev < 0) { throw new Error("Locus canonical commit prevRev is invalid."); } if (!Number.isInteger(commit.rev) || commit.rev !== commit.prevRev + 1) { throw new Error("Locus canonical commit revision transition is invalid."); } if (commit.prevRev !== expectedPrevRev) { throw new Error( `Locus canonical commit is not contiguous: expected prevRev ${expectedPrevRev}, received ${commit.prevRev}.`, ); } if (!Array.isArray(commit.ops) || commit.ops.length === 0) { throw new Error("Locus canonical changed commit must contain operations."); } const documentMode = map.mode === "document"; const projectedIdentityOnly = !documentMode && commit.ops.every((operation) => ( "domain" in operation && operation.op === "ensure-quid" && "projected" in operation.target && operation.target.projected === true )); if (commit.ops.some((operation) => ("domain" in operation) !== documentMode && !projectedIdentityOnly)) { throw new Error(`Locus canonical commit operation domain is incompatible with ${map.mode}.`); } if (documentMode && commit.ops.some((operation) => "domain" in operation && operation.op === "replace-root" && operation.mode !== map.mode)) { throw new Error(`Locus canonical root replacement is incompatible with ${map.mode}.`); } if (!documentMode && !projectedIdentityOnly && ( commit.format !== "structural-json" || typeof commit.payload !== "string" )) { throw new Error("Locus projected canonical commit requires exact structural transport."); } return Object.freeze({ logicalMapId, incarnationId, prevRev: commit.prevRev, rev: commit.rev, mode: map.mode, ops: Object.freeze(commit.ops.map((operation) => "domain" in operation ? canonical_graph_op(operation) : canonical_op(operation))), ...(documentMode || projectedIdentityOnly ? {} : { format: "structural-json" as const, payload: commit.payload as string, }), }); } function encoded_bytes(commit: LocusCanonicalCommit): number { return textEncoder.encode(JSON.stringify(commit)).byteLength; } /** * Attach canonical commit history and ordered publication to one authoritative * LiveMap. This is stream machinery only; it does not add recovery behavior. */ export function make_locus_canonical_stream( map: TMap, options: LocusCanonicalStreamOptions = {}, ): LocusCanonicalStream { return make_locus_canonical_stream_runtime(map, options).stream; } /** Internal stream construction with explicit action-to-commit correlation. */ export function make_locus_canonical_stream_runtime( map: TMap, options: LocusCanonicalStreamOptions = {}, runtime: Readonly<{ observeCommits?: boolean; initialHistory?: Readonly<{ baseRevision: number; commits: readonly LocusCanonicalCommit[]; }>; }> = {}, ): Readonly<{ stream: LocusCanonicalStream; correlateCommit: (commit: LiveMapCommit, causation: LocusCommitCausation) => void; ingestAccepted: (commit: LiveMapCommit) => void; }> { const logicalMapId = must_identity(options.logicalMapId ?? make_logical_map_id(), "logical map ID"); const incarnationId = must_identity(options.incarnationId ?? make_incarnation_id(), "incarnation ID"); const maxCommits = must_bound(options.history?.maxCommits, DEFAULT_MAX_COMMITS, "maxCommits"); const maxBytes = must_bound(options.history?.maxBytes, DEFAULT_MAX_BYTES, "maxBytes"); const retained: RetainedCommit[] = []; const publicationQueue: Array; }>> = []; const listeners = new Set(); let retainedEncodedBytes = 0; let headRev = runtime.initialHistory?.baseRevision ?? map.rev; let isPublishing = false; let publishedCommitCount = 0; let publicationErrorCount = 0; const commitCausation = new WeakMap, LocusCommitCausation>(); const pendingCommitTraces = new WeakMap, Readonly<{ listenerCount: number; publicationFailureCount: number; }>>(); function trim_history(): void { while (retained.length > maxCommits || retainedEncodedBytes > maxBytes) { const removed = retained.shift(); if (removed) retainedEncodedBytes -= removed.encodedBytes; } } function append_history(commit: LocusCanonicalCommit): void { const entry = Object.freeze({ commit, encodedBytes: encoded_bytes(commit) }); retained.push(entry); retainedEncodedBytes += entry.encodedBytes; trim_history(); } if (runtime.initialHistory !== undefined) { if (!Number.isInteger(headRev) || headRev < 0) { throw new Error("Locus initial history base revision is invalid."); } for (const commit of runtime.initialHistory.commits) { if (commit.logicalMapId !== logicalMapId || commit.incarnationId !== incarnationId || commit.mode !== map.mode || commit.prevRev !== headRev || commit.rev !== headRev + 1) { throw new Error("Locus initial history is not contiguous with its authority."); } append_history(commit); headRev = commit.rev; } if (map.rev !== headRev) { throw new Error("Locus restored authority revision does not match initial history."); } } function drain_publication_queue(): void { if (isPublishing) return; isPublishing = true; try { while (publicationQueue.length > 0) { const entry = publicationQueue.shift(); if (!entry) continue; const { canonical: commit, source } = entry; const listenerCount = listeners.size; let publicationFailureCount = 0; for (const listener of [...listeners]) { try { listener(commit); } catch { publicationErrorCount += 1; publicationFailureCount += 1; } } publishedCommitCount += 1; if (options.trace !== undefined) { pendingCommitTraces.set(source, Object.freeze({ listenerCount, publicationFailureCount })); } } } finally { isPublishing = false; } } function ingest(commit: LiveMapCommit, origin: "authoritative" | "replay"): void { if (origin !== "authoritative") { trace_commit(commit, origin, "skip"); return; } const canonical = make_locus_canonical_commit(map, commit, logicalMapId, incarnationId, headRev); append_history(canonical); headRev = canonical.rev; publicationQueue.push(Object.freeze({ canonical, source: commit })); drain_publication_queue(); if (options.trace !== undefined) { // The second microtask lets a synchronously mutating async action record // its state-transition boundary before the separate stream narrative. queueMicrotask(() => queueMicrotask(() => trace_commit(commit, origin, "success"))); } } function trace_commit( commit: LiveMapCommit, origin: "authoritative" | "replay", status: "success" | "skip", ): void { const sink = options.trace; if (sink === undefined) return; const trace = create_live_trace_context(sink, `locus-stream-${logicalMapId}-${commit.rev}`); const first = commit.ops[0]; const causation = commitCausation.get(commit); const publication = pendingCommitTraces.get(commit); const operationKinds = commit.ops.map((operation) => "domain" in operation ? operation.op : operation.kind); const causalDetails = causation === undefined ? {} : { sourceTraceId: causation.sourceTraceId, ...(causation.requestId !== undefined ? { requestId: causation.requestId } : {}), ...(causation.attemptId !== undefined ? { attemptId: causation.attemptId } : {}), sourceAction: causation.sourceAction, }; trace.emit({ subsystem: "livemap", phase: "commit.creation", status, details: () => ({ logicalMapId, incarnationId, mapMode: map.mode, origin, prevRev: commit.prevRev, rev: commit.rev, operationCount: commit.ops.length, operationKinds, ...causalDetails, }), }); trace.emit({ subsystem: "livemap", phase: "commit.publication", status: publication !== undefined && publication.publicationFailureCount > 0 ? "failure" : status, details: () => ({ logicalMapId, incarnationId, mapMode: map.mode, prevRev: commit.prevRev, rev: commit.rev, revision: commit.rev, operationDomain: first !== undefined && "domain" in first ? "graph" : "data", operationCount: commit.ops.length, operationKinds, origin, ...(publication !== undefined ? { listenerCount: publication.listenerCount, outcome: publication.publicationFailureCount > 0 ? "failed" : "published", ...(publication.publicationFailureCount > 0 ? { publicationFailureCount: publication.publicationFailureCount, errorCode: "LOCUS_COMMIT_PUBLICATION_FAILED", } : {}), } : {}), ...causalDetails, }), }); } function replayAfter(fromRev: number, throughRev = headRev): readonly LocusCanonicalCommit[] | undefined { if (!Number.isInteger(fromRev) || fromRev < 0) return undefined; if (!Number.isInteger(throughRev) || throughRev < 0 || throughRev > headRev) return undefined; if (fromRev === throughRev) return Object.freeze([]); if (fromRev > throughRev) return undefined; let cursor = fromRev; const commits: LocusCanonicalCommit[] = []; for (const entry of retained) { const commit = entry.commit; if (commit.rev <= fromRev) continue; if (commit.rev > throughRev) break; if (commit.prevRev !== cursor) return undefined; commits.push(commit); cursor = commit.rev; } return cursor === throughRev ? Object.freeze(commits) : undefined; } function debug(): LocusCanonicalHistoryDiagnostics { const first = retained[0]?.commit; const last = retained[retained.length - 1]?.commit; return Object.freeze({ logicalMapId, incarnationId, headRev, ...(first ? { firstRetainedCommitRev: first.rev } : {}), ...(last ? { lastRetainedCommitRev: last.rev } : {}), earliestResumableBaseRev: first?.prevRev ?? headRev, retainedCommitCount: retained.length, retainedEncodedBytes, maxCommits, maxBytes, publishedCommitCount, publicationErrorCount, }); } const history: LocusCanonicalHistory = Object.freeze({ canReplay(fromRev, throughRev = headRev): boolean { return replayAfter(fromRev, throughRev) !== undefined; }, replayAfter, debug, }); const stream: LocusCanonicalStream = Object.freeze({ mode: map.mode, logicalMapId, incarnationId, get headRev() { return headRev; }, history, onCommit(listener): () => void { listeners.add(listener); return () => { listeners.delete(listener); }; }, }); // Register before the Locus exposes its map so canonical ingestion is the // first Locus-owned observer of every later authoritative mutation. if (runtime.observeCommits !== false) { map.commits.observe((event) => { if (event.kind === "commit") ingest(event.commit, event.origin); else trace_snapshot(event.revision); }); } function trace_snapshot(revision: number): void { if (map.mode !== "document") return; const sink = options.trace; if (sink === undefined) return; const trace = create_live_trace_context(sink, `locus-stream-${logicalMapId}-snapshot-${revision}`); trace.emit({ subsystem: "livemap", phase: "snapshot.installation", status: "skip", details: () => ({ mapMode: map.mode, revision, origin: "snapshot" }), }); } return Object.freeze({ stream, correlateCommit(commit, causation): void { if (!commit.changed) return; commitCausation.set(commit, causation); }, ingestAccepted(commit): void { ingest(commit, "authoritative"); }, }); }