import { clone_node } from "../../core/clone-node.js"; import type { LiveMapAnyOp, LiveMapCommit, LiveMapRootMode } from "../../types/livemap.types.js"; import type { LiveMapAggregateCommit } from "./livemap.library.js"; export type LiveMapTransitionNotificationPolicy = "legacy" | "isolate"; /** Opaque, detached view of one prepared but unapplied LiveMap transition. */ export type PreparedLiveMapTransition = Readonly<{ readonly commit: LiveMapCommit; readonly baseRevision: number; readonly nextRevision: number; readonly mode: LiveMapRootMode; }>; /** Opaque prepared aggregate transition; never accepted by the legacy Locus boundary. @internal */ export type PreparedLiveMapAggregateTransition = Readonly<{ readonly kind: "aggregate"; readonly commit: LiveMapAggregateCommit; readonly baseRevision: number; readonly nextRevision: number; readonly libraryModes: readonly LiveMapRootMode[]; }>; export type LiveMapTransitionAcceptance = Readonly<{ commit: LiveMapCommit; notificationFailureCount: number; }>; /** Internal acceptance result retaining the non-serializable aggregate commit. @internal */ export type LiveMapAggregateTransitionAcceptance = Readonly<{ commit: LiveMapAggregateCommit; notificationFailureCount: number; }>; export type LiveMapTransitionErrorCode = | "LIVEMAP_TRANSITION_FOREIGN" | "LIVEMAP_TRANSITION_STALE" | "LIVEMAP_TRANSITION_ALREADY_ACCEPTED" | "LIVEMAP_TRANSITION_DISCARDED" | "LIVEMAP_TRANSITION_INVALID" | "LIVEMAP_MANAGED_MUTATION_REJECTED" | "LIVEMAP_ALREADY_MANAGED"; export class LiveMapTransitionError extends Error { readonly code: LiveMapTransitionErrorCode; constructor(code: LiveMapTransitionErrorCode, message: string, options?: ErrorOptions) { super(message, options); this.name = "LiveMapTransitionError"; this.code = code; } } type TransitionState = "pending" | "accepted" | "discarded"; type TransitionRecord = { state: TransitionState; baseRevision: number; generation: number; mode: LiveMapRootMode; commit: LiveMapCommit; baseStillCurrent: () => boolean; install: () => void; notify: (commit: LiveMapCommit) => void; }; type AggregateTransitionRecord = { state: TransitionState; baseRevision: number; generation: number; commit: LiveMapAggregateCommit; baseStillCurrent: () => boolean; install: () => void; notify: (commit: LiveMapAggregateCommit) => void; }; export type LiveMapTransitionPreparation = Readonly<{ commit: LiveMapCommit; baseStillCurrent: () => boolean; install: () => void; notify: (commit: LiveMapCommit) => void; }>; /** Internal aggregate preparation accepted by the same map-wide controller. @internal */ export type LiveMapAggregateTransitionPreparation = Readonly<{ commit: LiveMapAggregateCommit; libraryModes: readonly LiveMapRootMode[]; baseStillCurrent: () => boolean; install: () => void; notify: (commit: LiveMapAggregateCommit) => void; }>; export type LiveMapTransitionController = Readonly<{ prepare: (preparation: LiveMapTransitionPreparation) => PreparedLiveMapTransition; /** @internal */ prepareAggregate: ( preparation: LiveMapAggregateTransitionPreparation, ) => PreparedLiveMapAggregateTransition; accept: ( transition: PreparedLiveMapTransition, policy?: LiveMapTransitionNotificationPolicy, ) => LiveMapTransitionAcceptance; /** @internal */ acceptAggregate: ( transition: PreparedLiveMapAggregateTransition, policy?: LiveMapTransitionNotificationPolicy, ) => LiveMapAggregateTransitionAcceptance; discard: (transition: PreparedLiveMapTransition) => void; /** @internal */ discardAggregate: (transition: PreparedLiveMapAggregateTransition) => void; invalidate: () => void; assertPublicMutationAllowed: () => void; claimManagement: (owner: object, schedule: LiveMapManagedMutationScheduler) => void; releaseManagement: (owner: object) => void; /** Run a preparation owned by the current exclusive manager. @internal */ runManaged: (owner: object, operation: () => T) => T; scheduleManaged: ( mutation: (draft: object) => LiveMapCommit, ) => Promise> | undefined; readonly generation: number; }>; export type LiveMapManagedMutationScheduler = ( mutation: (draft: TMap) => LiveMapCommit, ) => Promise>; export type LiveMapStagedAuthority = Readonly<{ prepare: (mutation: (draft: TMap) => LiveMapCommit) => PreparedLiveMapTransition; accept: LiveMapTransitionController["accept"]; discard: LiveMapTransitionController["discard"]; claimManagement: (owner: object, schedule: LiveMapManagedMutationScheduler) => void; releaseManagement: (owner: object) => void; runManaged: (owner: object, operation: () => T) => T; scheduleManaged: (mutation: (draft: TMap) => LiveMapCommit) => Promise> | undefined; }>; const authorities = new WeakMap>(); /** Construct one closure-local transition controller for a single LiveMap authority. */ export function make_livemap_transition_controller( mode: LiveMapRootMode, getRevision: () => number, ): LiveMapTransitionController { const records = new WeakMap(); const aggregateRecords = new WeakMap(); let generation = 0; let management: Readonly<{ owner: object; schedule: LiveMapManagedMutationScheduler; }> | undefined; let managedExecutionOwner: object | undefined; function prepare(preparation: LiveMapTransitionPreparation): PreparedLiveMapTransition { const baseRevision = getRevision(); const commit = preparation.commit; if (commit.prevRev !== baseRevision || commit.rev !== (commit.changed ? baseRevision + 1 : baseRevision)) { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_INVALID", "Prepared LiveMap transition revisions are invalid.", ); } const token = Object.freeze({ commit: deep_freeze(clone_node(commit)), baseRevision, nextRevision: commit.rev, mode, }); records.set(token, { state: "pending", baseRevision, generation, mode, commit, baseStillCurrent: preparation.baseStillCurrent, install: preparation.install, notify: preparation.notify, }); return token; } function record_for(transition: PreparedLiveMapTransition): TransitionRecord { const record = records.get(transition); if (record !== undefined) return record; throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_FOREIGN", "Prepared LiveMap transition belongs to another authority.", ); } function aggregate_record_for( transition: PreparedLiveMapAggregateTransition, ): AggregateTransitionRecord { const record = aggregateRecords.get(transition); if (record !== undefined) return record; throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_FOREIGN", "Prepared aggregate LiveMap transition belongs to another authority.", ); } function prepareAggregate( preparation: LiveMapAggregateTransitionPreparation, ): PreparedLiveMapAggregateTransition { const baseRevision = getRevision(); const commit = preparation.commit; if (commit.prevRev !== baseRevision || commit.rev !== (commit.changed ? baseRevision + 1 : baseRevision) || preparation.libraryModes.length === 0) { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_INVALID", "Prepared aggregate LiveMap transition revisions or libraries are invalid.", ); } const token = Object.freeze({ kind: "aggregate" as const, commit, baseRevision, nextRevision: commit.rev, libraryModes: Object.freeze([...preparation.libraryModes]), }); aggregateRecords.set(token, { state: "pending", baseRevision, generation, commit, baseStillCurrent: preparation.baseStillCurrent, install: preparation.install, notify: preparation.notify, }); return token; } function accept( transition: PreparedLiveMapTransition, policy: LiveMapTransitionNotificationPolicy = "legacy", ): LiveMapTransitionAcceptance { const record = record_for(transition); if (record.state === "accepted") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_ALREADY_ACCEPTED", "Prepared LiveMap transition was already accepted.", ); } if (record.state === "discarded") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_DISCARDED", "Prepared LiveMap transition was discarded.", ); } if (record.mode !== mode || record.baseRevision !== getRevision() || record.generation !== generation || !record.baseStillCurrent()) { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_STALE", "Prepared LiveMap transition is stale.", ); } if (record.commit.changed) { try { record.install(); } catch (cause) { record.state = "discarded"; generation += 1; throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_INVALID", "Prepared LiveMap transition installation failed.", { cause }, ); } generation += 1; } record.state = "accepted"; let notificationFailureCount = 0; if (record.commit.changed) { if (policy === "legacy") { record.notify(record.commit); } else { try { record.notify(record.commit); } catch { notificationFailureCount = 1; } } } return Object.freeze({ commit: record.commit, notificationFailureCount, }); } function discard(transition: PreparedLiveMapTransition): void { const record = record_for(transition); if (record.state === "accepted") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_ALREADY_ACCEPTED", "Accepted LiveMap transition cannot be discarded.", ); } if (record.state === "discarded") return; record.state = "discarded"; } function acceptAggregate( transition: PreparedLiveMapAggregateTransition, policy: LiveMapTransitionNotificationPolicy = "legacy", ): LiveMapAggregateTransitionAcceptance { const record = aggregate_record_for(transition); if (record.state === "accepted") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_ALREADY_ACCEPTED", "Prepared aggregate LiveMap transition was already accepted.", ); } if (record.state === "discarded") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_DISCARDED", "Prepared aggregate LiveMap transition was discarded.", ); } if (record.baseRevision !== getRevision() || record.generation !== generation || !record.baseStillCurrent()) { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_STALE", "Prepared aggregate LiveMap transition is stale.", ); } if (record.commit.changed) { try { record.install(); } catch (cause) { record.state = "discarded"; generation += 1; throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_INVALID", "Prepared aggregate LiveMap transition installation failed.", { cause }, ); } generation += 1; } record.state = "accepted"; let notificationFailureCount = 0; if (record.commit.changed) { if (policy === "legacy") { record.notify(record.commit); } else { try { record.notify(record.commit); } catch { notificationFailureCount = 1; } } } return Object.freeze({ commit: record.commit, notificationFailureCount }); } function discardAggregate(transition: PreparedLiveMapAggregateTransition): void { const record = aggregate_record_for(transition); if (record.state === "accepted") { throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_ALREADY_ACCEPTED", "Accepted aggregate LiveMap transition cannot be discarded.", ); } if (record.state !== "discarded") record.state = "discarded"; } return Object.freeze({ prepare, prepareAggregate, accept, acceptAggregate, discard, discardAggregate, invalidate(): void { generation += 1; }, assertPublicMutationAllowed(): void { if (management === undefined || managedExecutionOwner === management.owner) return; throw new LiveMapTransitionError( "LIVEMAP_MANAGED_MUTATION_REJECTED", "LiveMap mutation is controlled by an exclusive Locus authority.", ); }, claimManagement(owner, schedule): void { if (management !== undefined && management.owner !== owner) { throw new LiveMapTransitionError( "LIVEMAP_ALREADY_MANAGED", "LiveMap is already controlled by another exclusive authority.", ); } management = Object.freeze({ owner, schedule }); }, releaseManagement(owner): void { if (management?.owner === owner) management = undefined; }, runManaged(owner: object, operation: () => T): T { if (management?.owner !== owner) { throw new LiveMapTransitionError( "LIVEMAP_MANAGED_MUTATION_REJECTED", "LiveMap managed preparation belongs to another authority.", ); } const previous = managedExecutionOwner; managedExecutionOwner = owner; try { return operation(); } finally { managedExecutionOwner = previous; } }, scheduleManaged(mutation) { return management?.schedule(mutation); }, get generation() { return generation; }, }); } export function register_livemap_staged_authority( map: TMap, authority: LiveMapStagedAuthority, ): void { authorities.set(map, authority as unknown as LiveMapStagedAuthority); } /** Package-internal accessor used by the later hosted-authority layer and focused tests. */ export function get_livemap_staged_authority( map: TMap, ): LiveMapStagedAuthority { const authority = authorities.get(map); if (authority !== undefined) return authority as unknown as LiveMapStagedAuthority; throw new LiveMapTransitionError( "LIVEMAP_TRANSITION_INVALID", "LiveMap has no staged authority controller.", ); } /** Route a managed link-target mutation without importing Locus into LiveMap. */ export function schedule_livemap_managed_mutation( map: TMap, mutation: (draft: TMap) => LiveMapCommit, ): Promise> | undefined { const authority = authorities.get(map); return authority?.scheduleManaged(mutation as (draft: object) => LiveMapCommit); } function deep_freeze(value: T): T { if (typeof value !== "object" || value === null || Object.isFrozen(value)) return value; for (const item of Object.values(value)) deep_freeze(item); return Object.freeze(value); }