/** * SS4.8 gap recovery by splice-fill (spec 14990 FR-020 / T032, tdd SS4.8). * * Split out of `session.ts` for the same reason `approval.ts` and * `turn-submit.ts` were: this is a delivery-plane concern with its own buffer, * page walk, and failure vocabulary, and `Session` was already the package's * largest module. * * THE RECIPE, verbatim from tdd SS4.8 and unchanged here: buffer live events * at cursors at or after `next` as they arrive; page forward from `after` with * `view/page` until the walk reaches `next`; discard the paged events the * buffer already holds; splice the buffer after the paged prefix. D-030's * second sanctioned path (drop state and re-anchor at the latest compaction * snapshot) is NOT built here — it is the #208 lane's spec work, and building * both would be a second concrete path for one current use. * * CURSORS STAY OPAQUE (tdd SS4.1). Nothing below orders two cursors: "at or * after `next`" is delivery ORDER, not a comparison — every live frame that * arrives after the gap marker is, by the server's own delivery contract, at * or after `next`. The walk stops on cursor EQUALITY with the target or on the * server's own end-of-view, and the overlap is discarded by set membership. */ import { MuseGapFillError } from "../errors.js"; import type { Connection } from "../connection/connection.js"; import type { SessionFold } from "../fold/session-fold.js"; import type { PendingRetirement } from "../pending/pending-command-set.js"; /** One wire frame, exactly as `Session.apply`'s wide overload accepts it. */ export interface WireFrame { readonly method: string; readonly params?: unknown; } /** * Where a drained frame's side effects go. The filler re-feeds frames through * `Session`'s own fold-and-route path, so the retirements and the client→server * I/O they trigger belong to `Session`, not here. */ export interface DrainSink { readonly retirements: PendingRetirement[]; readonly tasks: Promise[]>[]; } export interface GapFillerOptions { readonly sessionId: string; /** Absent on a fold-only `Session`: there is nothing to page through. */ readonly connection: Connection | undefined; readonly fold: SessionFold; /** Fold and route ONE frame, collecting what it produced. */ readonly apply: (frame: WireFrame, into: DrainSink) => void; /** SS2.13.3b: a discharged session folds nothing, fill or no fill. */ readonly discarded: () => boolean; } /** Reported, never thrown — see {@link MuseGapFillError}. */ export type GapFillFailureHandler = (failure: MuseGapFillError) => void; export declare class GapFiller { #private; constructor(options: GapFillerOptions); /** While true, every live frame but the gap marker itself is held. */ get filling(): boolean; /** * Was this frame already applied from a page, as part of the overlap the * splice discards? Consuming: a twin is refused exactly once. * * Returns the matched CURSOR rather than a boolean so the caller can report * which event it refused without re-reading (and re-defaulting) a field this * method already proved is there. */ claimServedTwin(frame: WireFrame): string | undefined; /** Observe fills that did not complete. See {@link MuseGapFillError}. */ onError(handler: GapFillFailureHandler): void; /** Hold one live frame until the paged prefix is in. */ hold(frame: WireFrame): void; /** * Recover the fold's outstanding hole. * * Called on every `view/gap`, including one that lands mid-fill: the running * walk reads its target from `fold.pendingGap` on each pass, so a coalesced * hole extends the walk in flight rather than starting a second one racing * it for the same cursor stream. */ start(): Promise[]>; }