import type { Session as SessionDetails } from "../types.js"; import type { RailSession as Session } from "./session-list-entry.js"; type SessionChannelProjection = Pick; type SessionChannelAcceptedRead = SessionChannelProjection & { readGeneration: number; }; export type SessionChannelMoveRequest = Readonly<{ workspaceId: string; sessionId: string; operation: number; readGeneration: number; }>; export type SessionChannelMoveResponseDisposition = "accepted" | "verification-required" | "rejected"; /** * Tracks list, exact-read, and settled-move ownership of channel projections * without putting browser-only provenance onto the public Session contract. */ export declare class SessionChannelProjectionAuthority { private readonly owners; private readonly reads; private readonly moves; private readonly retainedOwners; private readonly detailReads; private readonly listeners; private readonly requests; private readClock; private moveClock; private revision; readonly beginRead: () => number; /** * Register an exact/detail request whose delayed result can outlive a list * owner. One logical consumer keeps only its latest request generation; a * successor starts after the prior shared GET settled, so the older result * can no longer arrive and no longer blocks owner cleanup. */ beginDetailRead(owner: object, projection: Pick): number; /** Retire every exact/detail request owned by one route or point probe. */ finishDetailReads(owner: object): void; /** React to persistent accepted server/write authority advancing. */ readonly subscribe: (listener: (accepted?: SessionChannelAcceptedRead) => void) => (() => void); /** Reactive snapshot for consumers whose projected rows depend on persistent authority. */ readonly getRevision: () => number; /** * Persist one pending move across rail lifetimes. The same mounted owner * cannot duplicate its request; a new mount may supersede it with a new * explicit user intent, after which the older response loses authority. */ beginMove(owner: object, projection: Pick): SessionChannelMoveRequest | null; ownsMove(owner: object, request: SessionChannelMoveRequest): boolean; recordMove(owner: object, request: SessionChannelMoveRequest, projection: SessionChannelProjection): SessionChannelMoveResponseDisposition; finishMove(owner: object, request: SessionChannelMoveRequest): void; replace(owner: object, projections: readonly SessionChannelProjection[], priority?: number, readGeneration?: number): void; replaceOwner(owner: object, evidence: readonly (readonly [projection: SessionChannelProjection, readGeneration: number])[], priority?: number): void; clear(owner: object): void; /** Drop browser-only evidence when its workspace principal/route fence is retired. */ clearWorkspace(workspaceId: string): void; /** Retain an accepted exact/detail read so older list requests cannot revive stale filing. */ recordRead(projection: SessionChannelProjection, readGeneration: number): boolean; /** Fence a not-found point read without treating absence as a channel projection. */ recordMissing(projection: Pick, readGeneration: number): boolean; private recordObservation; private publish; /** * Transfer a priority-0 owner's newest value before cleanup when compaction * already delegated an accepted fence or an older exact request for the same * session can still arrive. Request-causal transfers are released with their * blocker; optimistic priority owners are never promoted. */ private retainOwner; private highestOwnerEvidence; private blockingDetailReads; private highestPersistentGeneration; /** * The limit is deliberately soft: an accepted read is the only fence that * can stop an older retained branch/page owner from reviving stale filing. * Compact only after every current owner for that session is at least as * new; otherwise retaining the winner is required for correctness. Workspace * transitions provide the hard lifecycle bound for unresolved evidence. */ private compact; private highestGeneration; project(projection: T, readGeneration: number): T; owns(projection: SessionChannelProjection | null): boolean; } /** * Merge only personal pin fields from a list/page projection into the open * route projection. Lifecycle and event-driven session fields remain owned by * the route/SSE reducer and cannot be regressed by a slower list poll. */ export declare function applySessionPinProjection(current: T | null, projected: Pick): T | null; /** Merge the list-owned project filing without replacing route/SSE content. */ export declare function applySessionChannelProjection(current: T | null, projected: Pick): T | null; /** * Merge a detail/SSE projection into the root context without allowing a * slower detail read to erase newer list-owned pin or project projections. * Detail remains authoritative for route-owned fields; the current context * contributes personal pin fields and the project filing. */ export declare function mergeSessionContextProjection(current: SessionDetails | null, projected: SessionDetails | null, channelAuthority: SessionChannelProjectionAuthority, source: "detail" | "live"): SessionDetails | null; /** * Merge a completed detail request only through the channel evidence that won * its request generation. A rejected late detail may still contribute its * route-owned fields when persistent authority can project the newer channel; * without such a winner it cannot seed stale route context after the rail (and * its transient owner evidence) has unmounted. */ export declare function mergeSessionDetailReadProjection(current: SessionDetails | null, projected: SessionDetails, channelAuthority: SessionChannelProjectionAuthority, readGeneration: number, accepted: boolean): SessionDetails | null; /** * Merge list-owned personal pin and hierarchy fields into route-owned session * content. A route/SSE object must never overwrite a newer cross-device unpin, * while a list poll must never regress lifecycle state or message content. * Activity ordering belongs to the list too: borrowing the selected route's * timestamp would move that row on selection and move it back on deselection. * List refreshes advance recency consistently for selected and unselected rows. * Creation ordering also keeps the list's exact SQL timestamp; route/lineage * Date hydration can discard the microseconds that distinguish adjacent rows. * When both agree the session needs a person and the list row is at least as * fresh as the detail, the list's `requiresActionSince` fills in for a detail * read, which never carries it, so selecting a row does not erase how long it * has been waiting. */ export declare function applySessionRailProjection(current: T, projected: Session, options?: { channelOwned?: boolean | undefined; }): T; /** * Reconcile the point read performed after a failed pin request. * * An optimistic first pin projects version 1 before the server responds. If the * request fails before commit, the authoritative point read correctly returns * the absent relation at version 0. The normal monotonic merge must reject an * arbitrary lower revision, but doing so here would leave the exact optimistic * projection stuck forever. Allow the lower authoritative revision only while * the current state is still byte-for-byte the projection installed by this * operation. Any intervening poll, mutation, or device response wins instead. */ export declare function reconcileFailedSessionPin(current: SessionDetails | null, optimistic: Pick | null, authoritative: Pick): SessionDetails | null; export declare function reconcileFailedSessionPin(current: Session | null, optimistic: Pick | null, authoritative: Pick): Session | null; export declare function notifySessionPinChanged(workspaceId: string, sessionId: string): void; export declare function subscribeToSessionPinChanges(workspaceId: string, onChange: (sessionId: string) => void): () => void; export {};