Rindle

API index and search · Build metadata

@rindle/optimistic

0.0.0 · Public export map; development manifest version (0.0.0).

Source revision 05d0bf2c2e56 · build details
Source revision: 05d0bf2c2e56.
TypeScript input SHA-256: aabe6cfcc4172b870d5e272142958e9ea8d8784c2aa23133156e5a7ee633318e
Generated 2026-09-04T23:58:25.590Z with TypeScript 6.0.3. Public TypeScript checks and declaration emit passed. Package runtime tests are separate.

Entry point source

attachLocalPersistence

FunctionDeclaration · Source: packages/optimistic/src/local-persist.ts:74 · Supporting declarations

Attach the persistence layer to a backend (standalone form — createRindleClient calls this for you). The observer is wired SYNCHRONOUSLY, but the mirror starts empty: attach before the first writeLocal (§5.2, a v1 constraint createRindleClient satisfies by construction). Await handle.ready before first render if the app wants restored rows at first paint.

export declare function attachLocalPersistence<S extends ColsMap>(backend: OptimisticBackend<S>, schema: Schema<S>, opts: PersistLocalOptions): LocalPersistence;

ClientMutator

TypeAliasDeclaration · Source: packages/optimistic/src/backend.ts:155 · Supporting declarations

A client mutator: optimistic, deterministic, replayable — a pure function of (base, args) (§5: no clock, no randomness; it is RE-INVOKED on every rebase). Either shape is accepted:

  • a plain synchronous function (tx, args) => void (client-only), OR
  • a shared GENERATOR (tx, args, ctx) => MutationGen (the isomorphic form: the SAME body the API server runs against a live async transaction — MUTATORS-ISOMORPHIC). The driver detects which.
export type ClientMutator = ((tx: MutationTx, args: never) => void) | ((tx: IsoTx, args: never, ctx: MutatorCtx) => MutationGen);

ClientRegistry

TypeAliasDeclaration · Source: packages/optimistic/src/backend.ts:161 · Supporting declarations

The client registry (§4.2) — one of the two registries; the server's authoritative twin shares names (and possibly code), never the wire.

export type ClientRegistry = Record<string, ClientMutator>;

createOptimisticStore

FunctionDeclaration · Source: packages/optimistic/src/index.ts:107 · Supporting declarations

A {@link Store} over an {@link OptimisticBackend}, plus the named-mutator entry (mutate.createIssue(args) — the §9 dream shape) and the §6 lifecycle surface.

export declare function createOptimisticStore<S extends ColsMap, R extends ClientRegistry>(schema: Schema<S>, source: OptimisticSource, registry: R, opts: OptimisticBackendOptions): {
    store: Store<S>;
    backend: OptimisticBackend<S>;
    mutate: {
        [K in keyof R]: MutateFn<Parameters<R[K]>[1]>;
    };
};

createRindleClient

FunctionDeclaration · Source: packages/optimistic/src/client.ts:345 · Supporting declarations

Create a synced application client over the local WASM engine. Connects named query leases, WebSocket subscriptions, and the optimistic mutation queue to the application's API routes. Await construction before using the store; when configured, it awaits the local-table restore attempt as well. Construction does not mean remote queries are synchronized: retain queries or use ensure for the required readiness boundary.

Keep one client per application session. Call close() when the session ends, and create a new client when its authenticated principal changes. The application server owns authorization.

export declare function createRindleClient<S extends ColsMap, R extends ClientRegistry>(opts: RindleClientOptions<S, R>): Promise<RindleClient<S, R>>;

deleteLocalPersistence

FunctionDeclaration · Source: packages/optimistic/src/local-persist.ts:66 · Supporting declarations

Delete a user's local-persistence database — the sanctioned LOGOUT hook (§3.2). Never called implicitly; the old database otherwise remains on disk (fast re-login, and a privacy decision the app owns). Close this tab's live client for user first; a SIBLING tab's connection is released automatically (it closes on versionchange and degrades to broadcast-only), so a multi-tab logout completes instead of parking behind the other tab forever.

export declare function deleteLocalPersistence(user: string, env?: PersistEnv): Promise<void>;

DowngradeStuckEvent

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:419 · Supporting declarations

The I-v stuck-downgrade event ({@link OptimisticBackend.onDowngradeStuck}): the ghost's fence is satisfied but these SENT room-domain mids never resolved (an entry that never reached the room — sent-but-undelivered when the socket died — is undecidable in general, §7.5). The ghost HOLDS (fail LOUD, never silent; no timeout-retire is invented) and the mids are surfaced once, actionably.

export interface DowngradeStuckEvent {
    sourceKey: string;
    doc: string;
    mids: number[];
}

EnsureQueryOptions

InterfaceDeclaration · Source: packages/client/src/ensure.ts:22 · Supporting declarations

export interface EnsureQueryOptions {
    /** Readiness policy. Defaults to `complete`. */
    until?: EnsureQueryUntil;
    /** Cancel this caller's wait. The shared query may stay retained for another waiter/prefetch. */
    signal?: AbortSignal;
}

EnsureQueryUntil

TypeAliasDeclaration · Source: packages/client/src/ensure.ts:20 · Supporting declarations

When an {@link QueryEnsureCache.ensure} call may resolve.

  • complete waits for the server-authoritative result (the default).
  • present resolves as soon as the local view contains a result, while the remote retain keeps revalidating in the background. An authoritative empty result also resolves it, so a real not-found query never waits forever.

present deliberately does not add a partial {@link ResultType}: a locally useful answer and server authority are independent facts. While it resolves early, the view's result type remains unknown until the server says otherwise.

export type EnsureQueryUntil = "complete" | "present";

FoldClock

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:290 · Supporting declarations

A virtual-clock seam for fold debounce timers and elapsed-time checks (FOLDED-MUTATIONS-DESIGN §9): the oracle injects a deterministic scheduler; production defaults to real timers + Date.now.

export interface FoldClock {
    setTimeout(cb: () => void, ms: number): unknown;
    clearTimeout(handle: unknown): void;
    now(): number;
}

FoldHandle

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:320 · Supporting declarations

A folded mutation window. flush() assigns its mutation ID and queues the latest arguments for delivery. mid resolves with that ID at flush; it does not wait for server acceptance or confirming-stream progress. Each call in the same window shares this promise.

export interface FoldHandle {
    flush(): void;
    readonly mid: Promise<number>;
}

FoldInspect

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:464 · Supporting declarations

One folded entry's debounce window, for the timeline's fold drill-down (§4.1).

export interface FoldInspect {
    /** The fold key (`${name}\0${identityJSON}`) collapsing same-key invokes into one entry. */
    foldKey: string;
    debounceMs: number;
    maxWaitMs?: number;
    deferAcrossWrites: boolean;
    /** Whether the window has flushed (a real `mid` was dealt); an un-flushed fold has `mid == null`. */
    flushed: boolean;
}

FoldOptions

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:298 · Supporting declarations

Options for a folded call site (FOLDED-MUTATIONS-DESIGN §3). key is the identity half of the fold key (combined with the mutator name); the rest tune the debounce policy.

export interface FoldOptions {
    /** The identity half of the fold key (typically the targeted primary key). Required. */
    key: unknown;
    /** Trailing debounce: the flush fires this long after the LAST invoke for `key`. Default 120ms. */
    debounceMs?: number;
    /** Elapsed-time threshold checked on each invocation. Once reached, that invocation flushes
     *  immediately. This does not arm a separate timer: without another invocation, the trailing
     *  debounce still controls the flush. Omit to use only the trailing debounce. */
    maxWaitMs?: number;
    /** For a declared room route, use this value for both `debounceMs` and `maxWaitMs`.
     *  The domain policy selects the route on the window's first invocation; it stays fixed
     *  for that window. `0` flushes each invocation. Omit to use the ordinary fold options.
     *  This does not detect collaborators or infer a route from the writes. */
    roomDebounceMs?: number;
    /** Keep deferring across overlapping non-fold writes for maximum economy, accepting the §4.2
     *  read-dependent reorder snap. Default `false` (flush-on-enqueue — correct-and-boring). */
    deferAcrossWrites?: boolean;
}

isoTx

VariableDeclaration · Source: packages/client/src/mutation-ops.ts:127 · Supporting declarations

The one shared effect factory (stateless — see {@link IsoTx}). Its methods just BUILD a {@link MutationOp}, so the single instance serves every schema; the generic {@link IsoTx} view is applied at the authoring site (a json<T> cell is a parsed object here and is stringified by the funnels, {@link toCell }), hence the cast — the runtime shape is schema-agnostic.

export declare const isoTx: IsoTx;

IsoTx

InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:102 · Supporting declarations

The tier-AGNOSTIC effect factory a generator mutator writes against. Every method just BUILDS an effect to yield — it performs no I/O and holds no state, so the single {@link isoTx} instance is shared by every mutator on both tiers; only the driver differs. insert/upsert/insertIgnore require non-nullable columns and permit nullable omissions, which become null. update/delete require the PK columns; update also names the columns to change.

export interface IsoTx<S extends ColsMap = ColsMap, P extends Record<string, string> = PkMap<S>> {
    /** Insert a row. Omitted nullable columns become `null`; database defaults are not applied. */
    insert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Update the row identified by its pk columns (REQUIRED); only the named non-pk columns change. */
    update<N extends keyof S & string>(table: N, row: UpdateOf<S[N], PkColsOf<S, P, N>>): MutationOp;
    /** Insert, or replace non-PK columns on PK conflict, with {@link IsoTx.insert}'s omission rules. */
    upsert<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Insert with {@link IsoTx.insert}'s omission rules, or do nothing on PK conflict. */
    insertIgnore<N extends keyof S & string>(table: N, row: InsertOf<S[N]>): MutationOp;
    /** Delete the row identified by its pk columns. */
    delete<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): MutationOp;
    /** Read one row by primary key (read-your-writes). */
    row<N extends keyof S & string>(table: N, pk: PkOf<S[N], PkColsOf<S, P, N>>): ReadEffect;
    /** Run a full query (`where`/`orderBy`/`limit`/join) over the state this mutator is mutating —
     *  read-your-writes, like {@link row} but for arbitrary shapes. Pass a query from the tier-agnostic
     *  builder, e.g. `tx.query(q.issue.where("ownerId", "=", ctx.user))` where `q = newQueryBuilder(schema)`.
     *  The `yield` evaluates to {@link QueryResultRow}`[]` (cast it — the generator's single next-type is a row). */
    query(query: QueryArg): QueryEffect;
    all(effects: readonly YieldEffect[]): BatchEffect;
}

KeyedRow

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:13 · Supporting declarations

A keyed row: column name → cell. The ergonomic write shape (validated against the schema at runtime). JSON columns carry their raw JSON string (a {@link WireValue}), never a parsed object.

export type KeyedRow = Record<string, WireValue>;

LIFECYCLE_QUERY_NAME

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:97 · Supporting declarations

The reserved name system retains subscribe under (never an app query — see the module doc). Its args carry the {@link SystemStreamSpec} identity fields PLUS the parent labeled query's (name, args), so a RE-resolution (reconnect / gate overflow) can re-lease the parent and pick the matching lifecycle entry — renewal-as-reauthorization, the room-token precedent.

export declare const LIFECYCLE_QUERY_NAME = "_rindle/lifecycle";

LIFECYCLE_TABLE_SCHEMAS

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:65 · Supporting declarations

export declare const LIFECYCLE_TABLE_SCHEMAS: readonly NormalizedTableSchema[];

LifecycleLeaseBlock

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:119 · Supporting declarations

The §4 lifecycle block on a query lease (mirror of the api-server's QueryLeaseLifecycle): doorbell on every labeled lease under the opt-in server config, fence (watermark + ledger

  • outcomes) only when the lease is ALSO room-served. Absent ⇒ this client behaves exactly as today — the whole plane is inert-until-fed.
export interface LifecycleLeaseBlock {
    doorbell: LifecycleLeaseEntry;
    fence?: LifecycleLeaseEntry[];
}

LifecycleLeaseEntry

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:103 · Supporting declarations

One minted SYSTEM-STREAM lease on the lifecycle block (mirror of the api-server's QueryLeaseLifecycleLease; Slice I-iii): an ordinary daemon materialization over one of the four _rindle_* lifecycle tables, presented on the wire exactly like the primary lease (subscribe-with-leaseToken). The identity fields document the minted predicate — this client keys its retains (idempotence per (table, scope/doc/clientId)) and the backend keys its release-time row filters on them.

export interface LifecycleLeaseEntry {
    table: string;
    leaseToken: string;
    wsEndpoint?: string;
    /** DOORBELL only: the §4.1 occupancy scope (= the wire room doc, `"<profile>/<key>"`). */
    scope?: string;
    /** FENCE entries only: the room doc. */
    doc?: string;
    /** FENCE ledger/outcomes when the server could client-scope the predicate. */
    clientId?: string;
}

LocalPersistence

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:46 · Supporting declarations

The attached layer's handle. createRindleClient awaits {@link ready} before returning (§5.2).

export interface LocalPersistence {
    /** Resolves after the initial restore attempt. It can resolve in a degraded mode when browser
     *  storage is unavailable or an operation fails; it is not a durability guarantee. */
    readonly ready: Promise<void>;
    /** Best-effort final persist/forward (the `pagehide` hook, §9): re-posts unacked ops (any
     *  role), then a leader drains its persist queue and retries P9-degraded batches. */
    flush(): Promise<void>;
    /** Release leadership (or abandon the queued lock request), close the channel + IDB (P10).
     *  A leader DRAINS its queued persist steps first (they carry already-committed writes) and
     *  releases the lock only after the last one lands; a follower re-posts its unacked ops. */
    close(): void;
    /** Introspection for tests/devtools: current role. */
    role(): "leader" | "follower";
}

MutateFn

TypeAliasDeclaration · Source: packages/optimistic/src/index.ts:20 · Supporting declarations

One entry of the {@link createOptimisticStore} mutate facade: call it for a normal optimistic write (returns the mid), or .folded(opts, args) for a debounced, last-value-wins folded write (returns a {@link FoldHandle} — the mid is assigned at flush, FOLDED-MUTATIONS-DESIGN §3).

export type MutateFn<Args> = ((args: Args) => number) & {
    folded(opts: FoldOptions, args: Args): FoldHandle;
};

MutationEnvelope

InterfaceDeclaration · Source: packages/client/src/types.ts:236 · Supporting declarations

The upstream named-mutator envelope (OPTIMISTIC-WRITES-DESIGN.md §8.1): the wire carries the name + JSON args, never code; mid totally orders a client's mutations.

export interface MutationEnvelope {
    clientID: string;
    mid: number;
    name: string;
    args: unknown;
}

MutationGen

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:148 · Supporting declarations

What a generator mutator IS: yield tx.<op>() on every side effect; a yield tx.row() expression evaluates to the row. Neither sync nor async — the tier's driver decides, which is what lets one body run synchronously on the client and against a live async transaction on the server.

export type MutationGen = Generator<YieldEffect, void, KeyedRow | undefined>;

MutationTx

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:119 · Supporting declarations

The write handle a client mutator runs against (the client MutationTx, §4.2): reads see the current base + this transaction's own staged writes (§4.1).

Prefer the KEYED methods (insert/update/upsert/delete/row) — named columns, schema-checked. The positional methods (get/add/remove/edit) are the raw wire shape: bare cells in schema column order, pk cells in primaryKey order.

export interface MutationTx {
    /** Read one row by primary key (e.g. `tx.row("issue", { id: 1 })`). */
    row(table: string, pk: KeyedRow): KeyedRow | undefined;
    /** Insert a FULL row (every column named; missing or unknown columns throw). */
    insert(table: string, row: KeyedRow): void;
    /** Update the row identified by the pk columns; only the named non-pk columns change.
     *  A missing row is a NO-OP (rebase-friendly: the row may have vanished upstream). */
    update(table: string, row: KeyedRow): void;
    /** Insert, or fully replace when the pk already exists (a FULL row, like `insert`). */
    upsert(table: string, row: KeyedRow): void;
    /** Insert a FULL row, or do nothing if the pk already exists (the isomorphic form of the classic
     *  `if (!tx.row(pk)) tx.insert(row)` upsert-if-absent; renders `ON CONFLICT DO NOTHING` server-side). */
    insertIgnore(table: string, row: KeyedRow): void;
    /** Delete the row identified by the pk columns. A missing row is a NO-OP. */
    delete(table: string, pk: KeyedRow): void;
    /** Run a one-shot read query (`where`/`orderBy`/`limit`/join) over the state this
     *  mutator is mutating — it sees this transaction's own writes-so-far, the same
     *  read-your-writes contract as `get`/`row` (§4.1; 203-MUTATOR-READS-DESIGN.md §5.2).
     *  Synchronous; returns the matching rows in the query's order, each with its materialized
     *  relationship children nested by name (presented identically to a `view.data` row). Pass
     *  a query from the typed builder, e.g. `tx.query(q.issue.where("owner", "=", me))`.
     *  Refused inside a FOLDED mutator (a reading mutator is non-absorbing, §9.1). */
    query(query: QueryArg): QueryResultRow[];
    get(table: string, pk: WireValue[]): WireValue[] | undefined;
    add(table: string, row: WireValue[]): void;
    remove(table: string, row: WireValue[]): void;
    edit(table: string, oldRow: WireValue[], newRow: WireValue[]): void;
}

MutatorCtx

InterfaceDeclaration · Source: packages/client/src/mutation-ops.ts:141 · Supporting declarations

The minimal per-invocation context a shared mutator sees on BOTH tiers: the acting principal. The server injects its AUTHENTICATED user; the client injects its local user. (The server's own MutationContext is a superset of this.)

export interface MutatorCtx {
    user: string;
}

OptimisticBackend

ClassDeclaration · Source: packages/optimistic/src/backend.ts:532 · Supporting declarations

export declare class OptimisticBackend<S extends ColsMap> implements Backend {
    private readonly local;
    private readonly sync;
    private readonly source;
    private readonly registry;
    private readonly clientID;
    /** The acting principal provider for a shared mutator's `ctx.user` (§ shared mutators). */
    private readonly user;
    private readonly bufferCap;
    /** Column order + pk indices per table, for the keyed `MutationTx` methods. */
    private readonly specs;
    /** Local-only table names (`201-LOCAL-ONLY-TABLES-DESIGN.md` §4). Drives: the agg-rewrite gate
     *  (L1 — a local-child count stays a native reduce), the mutator guard (M1 — a replayable
     *  mutator may not read/write one), and `writeLocal` (M2 — it accepts ONLY these). */
    private readonly localTables;
    /** Each table's full column count (union-row width) + column-name → base ColId — to learn a
     *  projected query's per-table projection off its `hello` and register it with the sync layer,
     *  so it scatters that query's narrower rows into the shared union (PROJECTION-SUPPORT-DESIGN
     *  §5.2). Without this a projected query's short rows reach the wasm `Db` un-scattered and fail
     *  its width check. */
    private readonly colCounts;
    private readonly colIndex;
    /** Per-table pk column indices — held so `connectSource` can build a fresh per-source
     *  `NormalizedSync` with the same layout the daemon's uses. */
    private readonly pkCols;
    /** The client's OWN typed per-table schemas + the reserved lmid table — the fixed base
     *  of the expected-schema set (CRIT#4 validation). Synthetic agg tables are appended as
     *  queries arrive (`ensureSyntheticTables`). */
    private readonly clientTablesBase;
    /** Synthetic aggregate tables (`__agg_*`) registered so far, by name (AGGREGATE-SYNC-DESIGN
     *  §3.3). Per aggregate DEFINITION (not per query), so two queries over the same count
     *  share one table. */
    private readonly synthetic;
    /** Synthetic table name → how many registered LOCAL queries reference it. A table is
     *  materialized on the `0→1` transition and reclaimed (engine source + baseline + refcount
     *  layer + overlay def) on `1→0` — so aggregate state is not permanent (§4). */
    private readonly syntheticRefs;
    /** Local qid → the synthetic tables it referenced at registration, to decrement on teardown. */
    private readonly queryAggTables;
    /** The optimistic aggregate overlay (§4–§6): the per-aggregate definitions + the per-group
     *  pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
    private readonly overlay;
    private handler;
    private catchUpQids;
    /** Newly-hydrated qids whose reconcile ACTUALLY emitted a (catch-up-stamped) batch — recorded by the
     *  local-event forwarder alongside {@link catchUpQids}. After the reconcile, any newly-hydrated qid
     *  NOT in here folded nothing (0 rows, or its result already present via a sibling → 0 net muts, or
     *  the reconcile was skipped), so `onProgress` sends it an explicit empty catch-up — else its SSR
     *  seed would never retire (the view freezes). Non-null only for the reconcile's duration. */
    private catchUpEmitted;
    /** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
     *  local engine's `dispatch` brackets so the Store folds every affected view before notifying any
     *  subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
     *  local engine, so its commit brackets are this backend's commit brackets. */
    private boundaryHandler;
    private readonly devObservers;
    private pendingMutations;
    /** The next mid to deal, PER DOMAIN (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.1): a client
     *  writing through room + daemon concurrently must not alias one lmid counter. Seeded with the
     *  `"daemon"` stream at 1; a domain absent from the map starts at 1. In the single-domain
     *  configuration only `"daemon"` is ever touched, so the sequence is byte-for-byte as before. */
    private nextMid;
    /** The client-global deal counter behind {@link PendingMutation.seq}: one sequence across ALL
     *  domains, bumped whenever any domain's mid is dealt. The replay order (mids are per-domain and
     *  incomparable across domains — see the `seq` field doc). */
    private dealSeq;
    /** The explicit confirming-stream override (§7.1/§3) — see
     *  {@link OptimisticBackendOptions.domainPolicy}. `undefined` from it ⇒ H-iii derivation. */
    private readonly domainPolicy;
    /** The final-rejection reason surface ({@link OptimisticBackendOptions.onRejected}). */
    private readonly rejectedHandler;
    /** Processed `(domain, mid)` outcome frames (H-v) — the deopt handshake's idempotence guard: a
     *  duplicate frame (the original plus a reconnect re-send's re-answer, or two re-answers across
     *  two reconnects) must not double-invoke. Needed precisely because a deopt frame can arrive for
     *  an ALREADY-RETIRED mid (the replay gotcha) — "no matching entry" alone cannot distinguish
     *  "handle it fresh" from "already handled". Per-domain FIFO, capped like the shell's
     *  recorded-outcome map ({@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}); past the cap a duplicate of
     *  an evicted mid would be re-processed — the same bounded-window trade the shell makes, and it
     *  takes 512 interleaving non-applied outcomes on one domain to open it. */
    private readonly outcomesProcessed;
    /** THE room-table registry (302 §2 — one source per table): per connected room `sourceKey`, the
     *  wire-table → engine-table map for the tables that room OWNS (its writable scope). Written by
     *  {@link registerRoomTables} (same breath as the engine registration); read by the gate's
     *  release rename/filter, the mutator staging map, the view swap ({@link processSwapIns}), and
     *  the client's `__realtimeInspect` bookkeeping. The record outlives a downgrade's disconnect —
     *  the ghost's views still read the engine tables — and drops at {@link dropGhost} (or the last
     *  clean release via {@link unregisterRoomTables}). */
    private readonly roomTables;
    /** Local view qids currently REGISTERED on a room's namespaced tables (302 §4 swap-in), →
     *  their sourceKey. Set by {@link processSwapIns}; cleared by the swap-back ({@link dropGhost})
     *  and view teardown. The original AST stays in {@link asts} throughout — the swap re-registers
     *  only the ENGINE query. */
    private readonly roomSwappedViews;
    /** Room subs whose FIRST snapshot released in the current release — their views swap onto the
     *  room tables at the release tail ({@link processSwapIns}), strictly AFTER the reconcile folded
     *  the snapshot into those tables (swapping earlier would hydrate the view EMPTY, a flash). */
    private readonly pendingSwapIns;
    /** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
     *  (FOLDED-MUTATIONS-DESIGN §8). Insertion order is creation order (the drain/flush tiebreak). */
    private readonly folds;
    /** The fold debounce clock (real timers by default; the oracle injects a virtual one). */
    private readonly clock;
    /** The high-water confirmed mutation id, PER DOMAIN (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md
     *  §7.2 per-domain confirm-drop): an entry with `mid <= watermark[entry.domain]` has been
     *  confirmed. The `"daemon"` domain is folded from the lmid system query's RELEASED ops
     *  (lmid-as-data) — never from a frame; a room domain will fold from its own lmid stream (later
     *  slice). Seeded with `"daemon"` at 0; the daemon scalar `confirmedLmid` (devtools) is
     *  `watermark.get("daemon")`. */
    private watermark;
    /** The per-source coherence gates (§5.1), by source key. Seeded with the daemon gate at
     *  construction; a room channel attaches later (`connectSource`). Single-domain: one entry,
     *  and every gate-generalized path degenerates to the old single-buffer code. NOT the same
     *  space as {@link watermark}/{@link nextMid}: a DOMAIN can confirm with no gate connected
     *  (the `__testRelease` seam); a gate's `key` names the domain its lmid stream folds into. */
    private readonly gates;
    /** The daemon's gate — the always-present channel (constructor-attached). The devtools
     *  scalars (`__inspect`) read it directly; its `sync` IS {@link sync} (the agg overlay and
     *  synthetic tables are daemon-tracked by design). */
    private readonly daemonGate;
    /** System retains by source qid ({@link retainSystemQuery}): a subscription with NO store view
     *  and NO user-visible table — its frames buffer on its gate exactly like {@link LMID_QID}'s and
     *  fold at RELEASE time ({@link foldSystemFrames}), never entering the sync layer or the local
     *  engine. The spec names which system table the qid serves and the scope/doc it was minted for
     *  (the fold's row filter). Empty on every non-lifecycle client — every partition below is then
     *  a structural no-op and the release path is byte-identical to before. */
    private readonly systemQids;
    /** The §4.2 fence state: room doc → highest `flush_seq` delivered through the daemon plane
     *  (monotone max-fold; a remove never regresses it). Slice I-v's ghost-drop consumer — I-iii
     *  only maintains + exposes it (`__inspectDomains().lifecycle`). */
    private readonly roomWatermarks;
    /** The §4.1 occupancy state: scope → (client_id → expires_at) from the doorbell stream. Slice
     *  I-iv's doorbell consumer (the 1→2 re-lease reaction) — I-iii only maintains + exposes it.
     *  A snapshot REPLACES the scope's map (authoritative re-hydrate); a batch folds add/edit/remove
     *  incrementally (the age-out sweep's deletes arrive as removes). */
    private readonly scopeSessions;
    /** The I-iv doorbell event sink ({@link onScopeSessions}) — fired once per scope a release's
     *  scope-session fold touched, AFTER the whole release applied. Default no-op: a client that
     *  never registers (no lifecycle plane) pays nothing. */
    private scopeSessionsHandler;
    /** Deferred old-channel row GC for in-flight upgrade retargets ({@link retargetRemoteQuery}):
     *  sub sourceQid → the channel it left. The rows the OLD gate's sync holds for the qid stay
     *  visible (merge: daemon tier) until the sub's first snapshot RELEASES on its new room channel
     *  ({@link flushRetargetGc}) — dropping them at retarget time would emit net removes ahead of
     *  the room's re-adds, the flicker the two-phase cutover exists to avoid. Doubles as the
     *  wrong-channel GRACE window in {@link onFrame}: a frame already in flight from the old
     *  channel when the sub moved is stale, not a wiring bug. Empty on every non-upgrade client —
     *  every consultation below is then a structural no-op. */
    private readonly pendingRetargetGc;
    /** The §4.2 GHOSTS (Slice I-v): demoted room sources awaiting their watermark fence, by
     *  sourceKey. Written only by {@link demoteRoomSource}; evaluated after every release
     *  ({@link evaluateGhosts}) and dropped by {@link dropGhost} once the fence clears with no
     *  sent room-domain pending left. Empty on every non-downgrade client — the per-release
     *  evaluation is then a structural no-op. */
    private readonly ghosts;
    /** The I-v stuck-downgrade surface ({@link onDowngradeStuck}) — fired AT MOST ONCE per ghost
     *  when its fence is satisfied but sent room-domain mids remain unresolved (§7.5: they retire
     *  only through outcome resolution; the ghost holds rather than inventing a timeout-retire).
     *  Default no-op. */
    private downgradeStuckHandler;
    private readonly asts;
    /** Per query: the base tables its result can draw from (from the AST tree). */
    private readonly queryTables;
    private readonly remoteSubs;
    private readonly sourceToRemote;
    private readonly localToRemote;
    private readonly remoteRetainToLocal;
    private readonly resultTypes;
    /** Local view qids that are server-authoritative: a query with no remote sub (purely local) is
     *  hydrated on registration; a remote query is hydrated when its sub's first snapshot releases.
     *  An un-hydrated query reports `unknown` (still loading) — the basis of `resultType`. */
    private readonly hydrated;
    private resultTypeHandler;
    /** The pending AXIS (§7.2), split off `ResultType`: per query, whether any pending mutation
     *  touches its tables. Cached so `onPending` fires only on transitions (invoke ↔ confirm). */
    private readonly pendingState;
    private pendingHandler;
    /** The local-persistence write-through tap (`207-LOCAL-TABLE-PERSISTENCE-DESIGN.md` §5.1):
     *  {@link writeLocal} invokes it post-commit; {@link applyLocalReplica} deliberately does not. */
    private localWriteObserver;
    constructor(schema: Schema<S>, source: OptimisticSource, registry: ClientRegistry, opts: OptimisticBackendOptions);
    /** Wire one authority channel into its own coherence gate (§5.1): every frame the channel
     *  delivers buffers on THIS gate's cv timeline, its progress frames release THIS buffer, its
     *  restart resets THIS gate alone, and its reserved lmid stream folds into `watermark[key]`.
     *  Validates each server hello against our OWN typed schema → reject a schema skew (CRIT#4);
     *  the reserved lmid table is part of the expected set so the system query's hello passes, and
     *  synthetic agg tables join the set as queries register them. */
    private attachGate;
    /** Attach a SECOND authority channel (§5.1) — the seam Slice G's room upgrade calls with the
     *  ws-backed room feed. Rooms speak the daemon protocol verbatim (§2.4: the client cannot tell
     *  a room from the daemon), so the argument is a full {@link OptimisticSource} — exactly what
     *  `@rindle/remote` builds from `{roomUrl, leaseToken}`. The channel buffers/releases on its
     *  own cv timeline (an independent §5.1 gate: coherent within, eventual across) and its
     *  reserved lmid stream folds into `watermark[sourceKey]` — so `sourceKey` must equal the
     *  `domainPolicy` name for the mutations this authority confirms. The converse is NOT required:
     *  a domain may exist with no connected gate (`__testRelease` drives confirms gate-less); the
     *  live production path stays daemon-only until G calls this. */
    connectSource(sourceKey: string, source: OptimisticSource): void;
    /** Register the tables room `sourceKey` OWNS (its writable scope — 302 §2): each wire table
     *  gets its own namespaced ENGINE table (`{@link roomEngineTable}`), an ordinary tracked table
     *  whose sole authority is the room channel. From here on the channel's released deltas rename
     *  into these tables (wire tables outside the map are DROPPED — context stays daemon-owned,
     *  302 §6), room-domain mutators stage onto them, and a room-homed view swaps onto them once
     *  the room sub hydrates ({@link processSwapIns}). Idempotent per (sourceKey, table); a wire
     *  table unknown to the schema is skipped (nothing to hold rows for). */
    registerRoomTables(sourceKey: string, tables: readonly string[]): void;
    /** The wire-table → engine-table map for room `sourceKey`'s owned tables (empty when none) —
     *  the client's idempotence check and `__realtimeInspect` read THIS record (one source of
     *  truth; the client keeps no shadow copy). */
    roomTablesFor(sourceKey: string): ReadonlyMap<string, string>;
    /** `channel` (G-iii registration-time routing) names the authority channel the remote sub
     *  registers on — a `connectSource`d gate key; default `"daemon"` (every existing caller is
     *  byte-identical). Slice G-v threads the lease's `realtime.sourceKey` here. Validated FIRST
     *  (like the E3 check below): a bad channel must throw before any per-query state is recorded. */
    registerQuery(qid: QueryId, ast: Ast, remote?: RemoteQuery, channel?: string): void;
    /** Register every synthetic aggregate table `ast` needs that we haven't seen yet: on the
     *  local engine (which auto-tracks it for the optimistic rebase loop), on `NormalizedSync`
     *  (so its rows refcount/GC by group key), and into the source's expected-schema set (so
     *  the server's `hello` — which advertises the same table — passes CRIT#4 validation).
     *  Idempotent across queries that share an aggregate definition. */
    private ensureSyntheticTables;
    /** Decrement the refcount of every synthetic table query `qid` referenced; for each one that
     *  reaches 0 (no live reader left), remove it from the engine, the refcount layer, and the
     *  overlay — so aggregate state is reclaimed, not permanent (§4). Must run AFTER
     *  `local.unregisterQuery(qid)` so the engine source has no live connection when
     *  `unregisterTable` frees it (the engine refuses otherwise). */
    private releaseSyntheticTables;
    unregisterQuery(qid: QueryId): void;
    /** `channel` as in {@link registerQuery} (G-iii): the gate the remote sub registers on; default
     *  `"daemon"`. This is the split-retain seam G-v's resolve-then-register drives — resolve the
     *  lease, learn `realtime.sourceKey`, `connectSource` it, then retain the query on that channel.
     *  Validated FIRST so a bad channel throws before any synthetic-table refcount moves. */
    retainRemoteQuery(qid: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast, channel?: string): void;
    releaseRemoteQuery(qid: QueryId): void;
    /** The Slice I-iv upgrade retarget (§4.1 "Retarget" / the doorbell reaction): move a LIVE
     *  (name, args) sub — every retain of it and every local view it feeds, wholesale — from the
     *  channel it lives on onto `sourceKey`'s (already-`connectSource`d, already-promoted) room
     *  channel, WITHOUT the view ever dropping its rows. Returns the sub's wire `sourceQid` (the
     *  identity the client's renewal loop re-subscribes with).
     *
     *  Why a dedicated primitive: the one-channel-per-(name,args) invariant ({@link retainRemote}'s
     *  loud throw) is correct — a sub's frames must never split across two cv timelines — so the
     *  upgrade cannot simply retain a second sub on the room and release the daemon one; and the
     *  naive release-then-retain order GCs the daemon sync's rows synchronously (net removes emit,
     *  the view flashes empty) a full ws round trip before the room's seq-0 snapshot refills it.
     *  The cutover is therefore TWO-PHASE around the room's first release:
     *
     *   1. NOW (here): unsubscribe the old channel's wire sub, sweep its still-buffered frames for
     *      this qid (their cv timeline continues without the sub — the hello-supersession
     *      precedent), flip `sub.channel`, re-arm `sub.hydrated` (the room's own snapshot is the
     *      cutover point), and register on the room source (its resolver presents the handed
     *      roomToken). The old gate's SYNC rows are deliberately NOT dropped: they keep the view's
     *      plain tables populated through the window — the view still reads them until the swap.
     *   2. AT THE ROOM'S FIRST RELEASED SNAPSHOT: the reconcile folds the snapshot into the room's
     *      namespaced tables, the release tail SWAPS every local view onto them (302 §4.1,
     *      {@link processSwapIns} — the accepted-flash boundary), and {@link flushRetargetGc}'s
     *      deferred `dropQuery`+reconcile on the OLD gate then GCs the plain-table rows the sub
     *      alone referenced — invisible to the swapped views.
     *
     *  Idempotent per target channel: a sub already on `sourceKey` returns immediately (the
     *  double-doorbell / re-entrancy guard — one retarget per (query, sourceKey)). Validates before
     *  mutating: a throw here leaves the sub fully daemon-attached (the client's fail-open). */
    retargetRemoteQuery(remote: RemoteQuery, sourceKey: string): QueryId;
    /** Phase 2 of {@link retargetRemoteQuery}, run at the end of every gate release: once a
     *  retargeted sub's first snapshot has RELEASED on its new channel (`sub.hydrated` re-armed at
     *  retarget, re-set by {@link markSubHydrated} inside this very release), drop the qid's rows
     *  from the OLD gate's sync and reconcile them out — after the room's rows are already applied,
     *  so the winner flip is value-equal (net-zero; see the phase table above). A sub torn down
     *  mid-window was already swept by `releaseRemoteQuery`/`unregisterQuery` (which delete the
     *  record); a vanished record here is pruned defensively. */
    private flushRetargetGc;
    /** The I-v downgrade orchestration primitive (§4.2/§7.4, re-expressed by 302 §4.2 as the
     *  SWAP-BACK GATE): retire room `sourceKey` behind the watermark fence. The caller has ALREADY
     *  retargeted every live sub off the channel ({@link retargetRemoteQuery} room→daemon —
     *  validated loudly below) and holds the fence from the api-server's downgrade response
     *  (`finalFlushSeq` = the room's last COMMITTED flush seq; `doc` keys the §4.2 watermark fold,
     *  {@link roomWatermarks}). Steps, in order:
     *
     *   1. **Disconnect** the channel ({@link disconnectSource}): handlers detached, gate + buffer
     *      dropped. `nextMid`/`watermark`/processed-outcomes for the domain are KEPT FOREVER (§7.1:
     *      an assigned mid pins its domain; a later re-upgrade of the same doc continues the
     *      sequence — {@link connectSource} attaches a fresh gate and the lmid snapshot max-folds
     *      into the surviving watermark). Disconnecting BEFORE the daemon sub's first release is
     *      load-bearing: it makes {@link flushRetargetGc}'s deferred old-channel GC a no-op (gate
     *      gone ⇒ record deleted, nothing dropped). The room's namespaced tables — and the views
     *      swapped onto them — deliberately stay: frozen at the room's last state, they keep the
     *      document visible while the falling-back follower may still lack the final flush.
     *      Swapping back earlier would show its pre-flush images — the regression §4.2 prevents.
     *   2. **Ghost + first evaluation**: the record joins {@link ghosts} and is evaluated once
     *      immediately — `finalFlushSeq === 0` (a never-flushed room) with no room-domain pending
     *      drops on the spot, the single-daemon first-frame case.
     *
     *  In-flight discipline (§7.5): entries with `mid !== null` on `sourceKey` stay PINNED (rule
     *  2 — never re-route a sent mutation); their resolution arrives via the daemon-carried
     *  ledger+outcome folds (I-iii) and blocks the drop until then. Idempotent per sourceKey (a
     *  second labeled query sharing the room demotes into the existing ghost). */
    demoteRoomSource(sourceKey: string, doc: string, finalFlushSeq: number): void;
    /** Detach one connected room channel (Slice I-v step 3): the source's handlers are replaced
     *  with no-ops (the {@link OptimisticSource} handler seam is single-registration, so this IS
     *  the detach — a late frame from a dying socket can no longer touch any bookkeeping), its
     *  reserved lmid sub is unregistered, and the gate — buffer, per-source sync, cv watermark —
     *  is dropped from {@link gates}. The DOMAIN state deliberately survives forever:
     *  `nextMid[sourceKey]`, `watermark[sourceKey]`, and the processed-outcome set are untouched
     *  (§7.1 — an assigned mid pins its domain; a re-upgrade must continue, never restart, the mid
     *  sequence; {@link connectSource} then attaches a fresh gate whose lmid snapshot max-folds
     *  into the surviving watermark via {@link foldConfirm}). Closing the underlying transport is
     *  the caller's job. Idempotent (a missing gate is a no-op). */
    disconnectSource(sourceKey: string): void;
    /** Register the I-v stuck-downgrade sink — see {@link DowngradeStuckEvent}. One handler (a
     *  later registration replaces it, the {@link onScopeSessions} convention); client.ts maps it
     *  onto the loud anomaly surface. */
    onDowngradeStuck(handler: (event: DowngradeStuckEvent) => void): void;
    /** The I-v ghost-drop watcher (§4.2), run after every applied release ({@link applyRelease} —
     *  the seam where {@link roomWatermarks} has just folded and the confirm-drop has just run) and
     *  once at demote time. For each ghost: the fence must be satisfied
     *  (`roomWatermarks[doc] ≥ finalFlushSeq`; 0 is trivially satisfied) AND no SENT room-domain
     *  pending may remain (§7.5 — such entries resolve only through the daemon-carried
     *  outcome/ledger folds; an entry that never reached the room is undecidable, so the ghost
     *  HOLDS and the stuck event fires exactly once, naming the mids). Both satisfied ⇒
     *  {@link dropGhost}. */
    private evaluateGhosts;
    /** Drop one cleared ghost — the 302 §4.2 SWAP-BACK: under the fence the daemon tables are
     *  value-equal-or-ahead of the room's final state, so (1) every view swapped onto the room's
     *  namespaced tables re-registers on its ORIGINAL (daemon-table) AST — visually a no-op, the
     *  Store folds the re-hello as an in-place reset; (2) the namespaced tables unregister (no
     *  reader is left after the swap); (3) ONE daemon reconcile re-invokes the pending set so any
     *  entry whose writes had staged onto the now-gone room tables re-stages onto the daemon tables
     *  (its domain policy stopped naming the dead room when the client dropped it). The whole drop
     *  runs under one commit boundary so the swap and the re-staged predictions notify as ONE step.
     *  After this, a FUTURE upgrade of the same doc registers again from scratch. */
    private dropGhost;
    /** Unregister room `sourceKey`'s namespaced engine tables and drop the {@link roomTables}
     *  record. Callers must have no view registered on them (the engine refuses otherwise —
     *  loud by design). No-op for an unknown sourceKey. */
    unregisterRoomTables(sourceKey: string): void;
    /** Retain one minted SYSTEM subscription (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §4, Slice
     *  I-iii): a wire sub with NO store view and NO user-visible table. Registered through the same
     *  {@link RemoteSub} bookkeeping as any remote retain — so qid→channel ownership, the overflow
     *  re-subscribe, and refcounted release all work unchanged — but with an EMPTY `localQids` set
     *  (no hydration/resultType coupling) and a {@link systemQids} record telling the release path
     *  which system table this qid's frames carry (`spec.table`) and which scope/doc it was minted
     *  for (the fold's row filter). Its frames then buffer on the channel's gate exactly like
     *  {@link LMID_QID}'s and fold at RELEASE time in {@link foldSystemFrames} — riding the SAME
     *  buffered cv path as the data they co-committed with (fence coherence: an out-of-band
     *  shortcut would break I-ii's co-commit ordering guarantee).
     *
     *  `channel` defaults to `"daemon"` — the system tables live in the DAEMON store (that is the
     *  point: outcome/ledger/watermark rows must be readable with no room socket alive, §7.1
     *  "load-bearing for §7.5"). Idempotence per (table, scope/doc) is the CALLER's job (client.ts
     *  keys its retains on exactly that); a duplicate retain of the SAME remote identity refcounts
     *  like any sub. */
    retainSystemQuery(retainQid: QueryId, remote: RemoteQuery, spec: SystemStreamSpec, channel?: string): void;
    /** Release a {@link retainSystemQuery} retain. Refcounted like any sub; the LAST release
     *  unregisters from the owning channel, sweeps its buffered frames, and drops the
     *  {@link systemQids} record. The folded lifecycle STATE (`roomWatermarks`/`scopeSessions`/
     *  processed outcomes) deliberately survives — the fence is monotone truth about the store, not
     *  about the subscription (a re-retained fence must not forget a cleared watermark). */
    releaseSystemQuery(retainQid: QueryId): void;
    /** Raw CRUD has no optimistic story (§9 replaces it with named mutators). Register a
     *  mutator — even a trivial one — and `invoke` it. */
    mutate(_mutations: Mutation[]): Promise<void>;
    /** Direct-commit a batch of LOCAL-only writes (`201-LOCAL-ONLY-TABLES-DESIGN.md` §6 / M2):
     *  straight to the local engine, OUTSIDE the optimistic pending stack — a local table is
     *  untracked, so it never rebases, reverts, or waits on a confirmation. Rejects a synced/tracked
     *  table (the local engine's `writeLocal` is the chokepoint). Reconcile is synchronous and
     *  non-reentrant (A5), so a local write can never interleave with an open server cycle.
     *
     *  Fires the {@link onLocalWrite} observer AFTER the engine commit but BEFORE subscriber
     *  delivery — i.e. for exactly the batches that passed the M2 guard and committed (the
     *  persistence layer's write-through tap, `207-LOCAL-TABLE-PERSISTENCE-DESIGN.md` §5.1). A
     *  subscriber throwing during delivery re-raises out of this call, but only after the tap has
     *  seen the batch: a committed write can never be invisible to the persistence plane. */
    writeLocal(mutations: Mutation[]): void;
    /** The write-through tap for the local-persistence layer (207 §5.1): `observer` sees every
     *  {@link writeLocal} batch post-commit. One observer (the layer); a later registration
     *  replaces it. The observer must not throw — a persistence failure degrades durability, never
     *  the write path (P9); the layer catches internally. */
    onLocalWrite(observer: (mutations: Mutation[]) => void): void;
    /** Apply a REPLICATED local batch (a restore snapshot / a leader `commit` — 207 §5.1): delegates
     *  to the engine's `writeLocal`, so the M2 locality guard still fires (P8 — a corrupt record
     *  naming a synced table dies loudly here), but does NOT invoke the {@link onLocalWrite}
     *  observer — the echo guard is structural, so the persistence layer can never re-enter itself.
     *  `onCommitted` fires post-commit pre-delivery (same anchor as {@link writeLocal}'s tap): the
     *  layer updates its mirror there, so a subscriber throw can never desync mirror from engine. */
    applyLocalReplica(mutations: Mutation[], onCommitted?: () => void): void;
    onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void;
    onCommitBoundary(handler: (phase: "begin" | "end") => void): void;
    /** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
     *  notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
     *  and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
     *  logical mutation: a relationship-`count` view and the data view it counts must update together.
     *  The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
     *  outer pair and the Store flushes every affected view (data AND count) once, together, at the
     *  outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
     *  half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
     *  prediction never wedges the Store in deferred mode. */
    private inOneCommit;
    /** Run one client mutator against the staged `tx`, accepting BOTH forms (§ shared mutators):
     *  a plain sync function runs as-is; a shared GENERATOR is driven synchronously — every yielded
     *  write applies to the wasm txn now, every `tx.row` read is resolved against the same staged
     *  state (read-your-writes), the SAME body the API server drives asynchronously. `ctx.user` is
     *  the acting principal (re-read per invoke, stable across a rebase re-invoke). */
    private runMutator;
    /** Deal the next wire mid from `domain`'s ledger (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md
     *  §7.1) and advance that counter. A domain absent from the map starts at 1. Per-domain, so a
     *  client writing through room + daemon concurrently keeps two gapless, non-aliasing sequences.
     *  The client-global `seq` is stamped in the same breath — the ONE cross-domain total order
     *  (confirmation order is per-domain; replay order is client-global). Bundled here so no call
     *  site can deal a mid without its seq. ONE caller discards the seq deliberately: the H-v deopt
     *  flip ({@link handleMutationOutcome}) keeps the entry's ORIGINAL seq — its replay position —
     *  and takes only the fresh mid (the dealSeq bump is harmless: seq consumers order, never
     *  count). */
    private dealMid;
    /** The declared confirming stream for one invocation: the `domainPolicy`'s verdict, `"daemon"`
     *  when it abstains. Resolved BEFORE the prediction runs — the domain picks the staging map
     *  (a room domain stages its owned tables onto the room's namespaced twins). */
    private resolveDomain;
    /** The staging table map for a `domain`-routed prediction ({@link trackingTx}'s `stage`):
     *  wire table → the room's namespaced engine table for the tables the room owns; identity for
     *  everything else (including the whole map for the daemon domain). */
    private stagingMap;
    /** The PLAIN (daemon-homed) engine AST for `ast` — aggregate relationships rewritten to their
     *  synthetic `__agg_*` reads, no room renames. The ONE form every non-swapped engine
     *  registration uses ({@link registerQuery}, {@link dropGhost}'s swap-back) and the base the
     *  swap-in renames ({@link processSwapIns}). */
    private plainEngineAst;
    /** Mutator names the cross-authority warn below already fired for (once per name). */
    private readonly warnedCrossAuthority;
    /** 302 §5.1 dev-time guard: a room-DECLARED mutator wrote tables the room does not own. Those
     *  writes staged onto the PLAIN daemon tables (the staging map covers only owned tables), but
     *  the entry confirms on the ROOM stream — and only the room's OWNED tables flush back to the
     *  daemon, so nothing upstream ever echoes them: once the room confirm retires the entry, the
     *  next release's whole-store rewind reverts them for good. The first-party room shell refuses
     *  such a mutation (the §3.3 deopt/reject backstop re-routes it to the daemon), so this warns
     *  for the shapes where that backstop may be absent (a BYO relay) — loud, once, soft (§5.1:
     *  misdeclarations never throw). */
    private warnCrossAuthorityWrites;
    /** Run the named client mutator optimistically: the prediction applies to the live
     *  engine now (affected views update synchronously), `(mid, name, args)` joins the
     *  pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
    invoke(name: string, args: unknown): number;
    /** {@link invoke} with an optional PINNED confirming domain (H-v): the deopt handshake's
     *  already-retired arm re-invokes the frame's echoed `(name, args)` as a FRESH invocation pinned
     *  to `"daemon"` — an honest re-prediction on the current base, never derived (`pin` bypasses
     *  {@link resolveDomain} entirely, so the router never runs and no Q6 counter moves). Every
     *  other step is `invoke` verbatim: prediction now, capture, drainOverlapping, mid dealt from
     *  the pinned domain's ledger, envelope on its channel. */
    private invokeWith;
    /** Run a FOLDED invoke (FOLDED-MUTATIONS-DESIGN §8): apply the prediction to the live engine now
     *  (like `invoke`), but collapse a run of same-key invokes into ONE pending entry whose `args`
     *  are overwritten in place, debounce the server write, and ship only the last value. The `mid`
     *  is assigned at flush, not here (§4.1) — so the return is a {@link FoldHandle}, not a mid. */
    invokeFolded(name: string, opts: FoldOptions, args: unknown): FoldHandle;
    /** Flush-on-enqueue (§4.2): for each outstanding fold whose touched tables overlap `tables`,
     *  assign its mid NOW and ship it — in creation (insertion) order, so the wire stays gapless. A
     *  `deferAcrossWrites` fold opts out (it keeps deferring, accepting the read-dependent snap). The
     *  incoming write's own fold key (if any) is skipped — it is being folded into, not flushed. */
    private drainOverlapping;
    /** Flush one fold (§8): deal its `mid` from `nextMid` (SEND order — never reserved, so gapless
     *  by construction), stamp the entry, ship the envelope with the LATEST args, resolve the handle.
     *  The entry stays on `pendingMutations` (now with a real mid) until the lmid release confirms it. */
    private flushFold;
    /** The transport a `domain`-confirmed mutation ships on (§7.5 sent-pins-domain: only the
     *  domain's own authority can confirm it, so its channel is the only correct transport). A
     *  domain with NO connected gate ships on the daemon channel — the gate-less configurations
     *  (`__testRelease`-driven tests) and today's entire live path resolve `"daemon"` anyway. */
    private channelFor;
    /** The gate a channel-keyed retain registers through (G-iii registration-time routing). The
     *  channel MUST already be connected (`connectSource`; the daemon is constructor-attached) —
     *  loud by design: a typo'd or not-yet-connected sourceKey must throw at retain time, never
     *  silently register on the daemon and split the query's frames across channels. */
    private requireGate;
    /** The channel that owns `sourceQid` — {@link RemoteSub.channel}, the ONE source of truth for
     *  qid routing (G-iii). `undefined` when no sub owns the qid (a harness-delivered raw feed, or
     *  a just-released sub): such frames buffer on whatever gate they arrive at. */
    private channelOf;
    /** Record `(domain, mid)` as processed; `false` if it already was (a duplicate frame —
     *  ignore it). FIFO-capped per domain ({@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}). */
    private markOutcomeProcessed;
    /** One `mutationOutcome` frame from `domain`'s channel (H-v — the §3.3 handshake's client
     *  half). The frame arrives OUT-OF-BAND (see {@link attachGate}); the state machine:
     *
     *  1. `mid` never issued on `domain` ⇒ ignore (a confused/foreign frame must not invent work).
     *  2. `(domain, mid)` already processed ⇒ ignore — idempotence under duplicate frames (the
     *     original + a re-send's re-answer; a deopt for a mid whose entry ALREADY FLIPPED also
     *     lands here harmlessly on its second frame).
     *  3. `kind:"rejected"` ⇒ FINAL. Surface the reason through {@link rejectedHandler} (room-plane
     *     parity with the HTTP queue's callback) and STOP — the drop + snap-back is the EXISTING
     *     failed-mutation machinery: the room burnt the mid, its lmid release retires the entry
     *     per-domain and the reconcile rewinds the prediction, exactly the daemon path's
     *     processed-as-no-op rejection. No new drop path.
     *  4. `kind:"deopt"`, entry found (pending `(domain, mid)`) ⇒ FLIP IN PLACE: `domain` becomes
     *     `"daemon"`, a fresh daemon mid is dealt and the envelope ships NOW on the daemon channel
     *     ("deal-and-send-now" — the conforming §3.3 re-enqueue: there is no flush machinery for
     *     non-fold entries, so the design's "mid: null until the daemon flush" is satisfied
     *     momentarily inside this call). THE ENTRY'S `seq` IS KEPT — settled (§5.3, commit
     *     68141096): `seq` is the client-global REPLAY order; re-sequencing would move the entry's
     *     overlay position and change read-dependent SIBLINGS' replay base. Everything else stays
     *     (writes/reads/touched/touchedSources/writeSources — union-never-shrink), the prediction
     *     stays applied (the entry never leaves `pendingMutations`, so no rewind fires), and the
     *     router does NOT re-run nor does `drainOverlapping` (§3.3 re-enqueues, never re-derives;
     *     any open overlapping fold was invoked later and flushes later with a larger mid).
     *  5. `kind:"deopt"`, entry NOT found ⇒ the burnt-mid confirm won the race, or the frame is a
     *     replay re-answer for an entry a previous session retired (the replay gotcha): re-invoke
     *     the frame's echoed `name`/`args` as a FRESH invocation PINNED to `"daemon"` — an honest
     *     re-prediction on the current base, never a derived route ({@link invokeWith}). A frame
     *     without `name` (not self-contained) has nothing to re-invoke and is dropped; a re-invoke
     *     that THROWS (the base moved from under it) is surfaced through {@link rejectedHandler} —
     *     the mutation is dead with no stream left to confirm it.
     *
     *  A `"deopt"` bump joins the Q6 routing counters either way (`routing.reasons.deopt`) —
     *  derived-and-deopted routes are visible beside derived successes. */
    private handleMutationOutcome;
    /** §7.5 rule 3 (H-v): re-send `domain`'s unconfirmed pending envelopes with their ORIGINAL
     *  mids, in mid order, on the domain's own channel. Folds with `mid === null` are excluded —
     *  nothing was ever sent for them (the flush deals their mid). Envelopes are reconstructed from
     *  the pending entries exactly as `invoke` shipped them (`clientID`/`mid`/`name`/`args` —
     *  entries carry everything the wire needs). Idempotent under the domain's ledger: an APPLIED
     *  mid dedups silently and its lmid coverage retires the entry; a NON-APPLIED mid is re-answered
     *  from the shell's recorded-outcome map into {@link handleMutationOutcome}. Confirmed entries
     *  are already gone from `pendingMutations`, so no filter against the watermark is needed. */
    private resendPending;
    /** Drain every outstanding fold immediately (FOLDED-MUTATIONS-DESIGN §3): the explicit
     *  `app.flushFolds()` and the `beforeunload`/`close` hook. Creation (insertion) order. */
    flushFolds(): void;
    /** A `trackingTx` op collector that records only the ops over a tracked aggregate's child
     *  table (the others can't move any count). Applied to the overlay by the caller AFTER the
     *  mutator succeeds, so a throwing mutator (whose staged write is discarded) leaves no delta. */
    private opCollector;
    /** Push the optimistic per-group delta onto the `__agg` head rows (§4):
     *  `target = server_base ⊕ delta`. A head-only write to the (tracked) synthetic table, so it
     *  joins the optimistic layer and is rewound/rebuilt by the reconcile cycle like any
     *  prediction. `server_base` is read from `NormalizedSync` (the authoritative base) — NOT
     *  from head, which already carries the optimistic layer (a torn read). Works standalone (an
     *  ordinary delivery) and inside an open cycle (the write buffers into it). */
    private reconcileAggHead;
    /** The SERVER CHANNEL's state for a query (§7): `unknown` while not hydrated, else `complete`.
     *  A pending local mutation no longer moves it — see {@link pending}. */
    resultType(qid: QueryId): ResultType;
    onResultType(handler: (qid: QueryId, rt: ResultType) => void): void;
    __attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void;
    /** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
     *  pending here?" (FOLDED-MUTATIONS-DESIGN §7.2). Orthogonal to {@link resultType}; this is the
     *  same `queryTables ∩ pending.touched` computation that used to be smuggled into `unknown`. */
    pending(qid: QueryId): boolean;
    /** Reactive pending axis (§7.2): fires when a query's pending-ness flips (invoke ↔ confirm), so a
     *  "saving…" affordance clears on its own when `lmid` catches up. */
    onPending(handler: (qid: QueryId, pending: boolean) => void): void;
    /** The coarse, table-level pending indicator set (§7.2): every table some pending mutation touched. */
    pendingTables(): Set<string>;
    /** A read-only snapshot of the optimistic loop for a devtools pane (DEBUG-TOOLS-BROWSER-DESIGN
     *  §4.1). Built fresh per call from state the backend already holds — no new instrumentation, no
     *  mutation. Only ever called by `@rindle/devtools` (imported in dev). */
    __inspect(): OptimisticInspect;
    /** Test-only per-domain ledger snapshot (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.1/§8.5).
     *  Kept separate from {@link __inspect} so the devtools `OptimisticInspect` mirror stays byte-for-
     *  byte identical (daemon-scalar-only). Exposes the per-domain `nextMid`/`watermark` maps plus each
     *  pending entry's confirming domain — the axes the §8.5 ledger-isolation assertion checks. */
    __inspectDomains(): {
        nextMid: Record<string, number>;
        watermark: Record<string, number>;
        /** Per connected CHANNEL (§5.1): its release watermark + buffered-frame depth — the axis the
         *  gate-isolation assertions read (one source's laggy cvMin must never move the other's). */
        gates: Record<string, {
            appliedCv: number;
            bufferedFrames: number;
        }>;
        /** Per connected/registered room: its wire-table → engine-table map (302 §2) and which local
         *  view qids are currently swapped onto it (302 §4). */
        roomTables: Record<string, Record<string, string>>;
        swappedViews: Record<number, string>;
        /** The §4 lifecycle plane's folded state (Slice I-iii introspection): the per-doc §4.2 fence
         *  value (`roomWatermarks`, I-v's ghost-drop input), the per-scope §4.1 occupancy map
         *  (`scopeSessions`: scope → client_id → expires_at, I-iv's doorbell input), and the live
         *  I-v ghosts (demoted room sources still awaiting their swap-back fence). */
        lifecycle: {
            roomWatermarks: Record<string, number>;
            scopeSessions: Record<string, Record<string, number>>;
            ghosts: Record<string, {
                doc: string;
                finalFlushSeq: number;
            }>;
        };
        pending: {
            mid: number | null;
            seq: number | null;
            name: string;
            domain: string;
        }[];
    };
    /** Recompute the pending axis for every query and fire `onPending` on transitions only. Called
     *  from the two points that move the pending set: invoke/invokeFolded (add) and the confirm-drop
     *  (remove) — exactly where `:359`/`:468` used to flip ResultType (§7.3). */
    private refreshPending;
    private onFrame;
    private emitServerDelta;
    private localQidsForSource;
    /** One gate's release (§5.1 release gate): compute the coherent delta from THIS gate's cv-buffer,
     *  then apply it against the gate's source/domain. Split into {@link computeRelease} (buffer →
     *  delta, lmid → watermark) and {@link applyRelease} (per-source confirm-drop + reconcile) —
     *  N independent gates all feed the ONE apply half; {@link __testRelease} drives it directly. */
    private onGateProgress;
    /** Compute one coherent release from ONE gate's cv-buffer (§5.1) — gate-scoped: its buffer, its
     *  cvMin timeline. Take every buffered frame at `cv ≤ cvMin`, in (cv, arrival) order, and fold
     *  it: the lmid system-query frame advances `watermark[gate.key]` (via {@link foldLmidOps} — the
     *  daemon stream folds "daemon", a room stream folds its own domain); data frames fold through
     *  this SOURCE's cross-query refcount into ONE net base delta — the §1.3 `D`. Returns that delta
     *  plus the set of local views this release JUST hydrated (so their reconcile batch phases as a
     *  `snapshot`). Mutates the gate's buffer/`appliedCv`, hydration, and the gate's domain
     *  watermark; the pending set and the reconcile are {@link applyRelease}'s job. */
    private computeRelease;
    /** Apply one released delta against `sourceKey`'s domain (§7.2 per-domain confirm-drop + the §1.3
     *  reconcile cycle). `watermarkUpdate`, when given, advances `watermark[sourceKey]` first — the
     *  hook a per-source lmid confirm rides on (the daemon path folds its watermark in
     *  {@link computeRelease} and passes `undefined`). Then: drop every pending entry its OWN domain's
     *  watermark now covers (a room confirm can never retire a daemon entry, and vice-versa — the §7.1
     *  ledger-collision fix), and run the reconcile cycle against `sourceKey` when the base delta or the
     *  pending set changed. `newlyHydrated` stamps the initial-hydration batch as a catch-up. */
    private applyRelease;
    /** Test-only per-source release seam (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.2/§8.5): drive
     *  {@link applyRelease} for `sourceKey` directly — an explicit `watermarkUpdate` (a simulated lmid
     *  confirm for that domain) and `deltas` (a coherent base delta), with no real gate. Lets a harness
     *  exercise a room-domain confirm before the real second lmid stream / per-source gate is wired
     *  (E-iii-b/c). The `__`-prefix marks it a test hook, alongside {@link __inspect}. */
    __testRelease(sourceKey: string, deltas: Mutation[], watermarkUpdate?: number): void;
    /** Swap every view of each just-hydrated ROOM sub onto the room's namespaced tables (302 §4.1):
     *  re-register the local engine query with the AST's room-owned table references renamed
     *  ({@link remapAstTables}); the Store folds the re-hello as an in-place reset, so the caller's
     *  view reference survives and subscribers see ONE transition. Runs at the applyRelease tail —
     *  the reconcile has already folded the sub's snapshot into the room tables, so the swapped
     *  view hydrates straight to the room state (swapping earlier would flash it empty). The
     *  ORIGINAL ast stays in {@link asts}; the swap-back ({@link dropGhost}) re-registers it.
     *
     *  This is the accepted-flash boundary (302 §4.1/§7.1): the room's copy may be behind the
     *  daemon rows the view showed a moment ago — accepted by decision, revisit on a real
     *  two-region deploy. */
    private processSwapIns;
    /** Fold `domain`'s lmid system query's released ops (lmid-as-data): the one row's
     *  `last_mutation_id` cell is this client's confirmed high-water mid in that domain — it advances
     *  `watermark[domain]` and, on a fresh session ahead of our issued mids, `nextMid[domain]`. The
     *  daemon stream folds `"daemon"`; a room stream folds its own `"room:doc:X"`; the daemon-carried
     *  §7.1 ledger rows fold through the same {@link foldConfirm} core (Slice I-iii). */
    private foldLmidOps;
    /** THE one confirm fold (§7.1/§7.2): advance `watermark[domain]` to `lmid` (monotone max) and,
     *  on a fresh session ahead of our issued mids, adopt `nextMid[domain]`. Shared verbatim by the
     *  per-channel lmid system query ({@link foldLmidOps}) and the daemon-carried room-ledger rows
     *  ({@link foldSystemFrames} — one core so the two paths cannot drift). */
    private foldConfirm;
    /** Fold one release's SYSTEM frames in a FIXED category order — the order is STRUCTURAL (one
     *  function, categories in sequence), because it is the client half of THE NAMED INVARIANT
     *  (§3.3's shipped note; documented above {@link handleMutationOutcome}): **never retire a
     *  room-domain entry off a daemon-carried lmid without outcome resolution.**
     *
     *    1. **outcome rows** (`_rindle_room_mutation_outcomes`) — each row for OUR clientID is
     *       synthesized into a {@link MutationOutcomeFrame} and routed through
     *       {@link handleMutationOutcome}, the SAME H-v state machine the room socket's frames use
     *       (one verdict path: frames and rows cannot drift). A deopt flips its pending entry to
     *       the daemon IN PLACE (keep-seq, deal-and-send-now); a rejection surfaces + stays for the
     *       ordinary burnt-mid retire; a duplicate (frame already seen, or the row re-delivered) is
     *       absorbed by the processed set — which doubles as the resolved-verdict memory across
     *       releases (per-domain FIFO, {@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}, mirroring the
     *       shell's recorded-outcome cap).
     *    2. **room-ledger rows** (`_rindle_room_client_mutations`) — the FIRST daemon-carried
     *       room-lmid path: OUR row's `last_mutation_id` folds into `watermark[room:<doc>]` via
     *       {@link foldConfirm}. Because step 1 ALREADY resolved every non-applied verdict this
     *       release carries (and earlier releases' verdicts were resolved at their own release),
     *       the confirm-drop that follows in {@link applyRelease} retires only entries whose
     *       outcome is resolution-by-absence — which I-ii's co-commit atomicity defines as APPLIED
     *       (a room flush co-commits the ledger row and every non-applied mid's outcome row in ONE
     *       daemon transaction, so a covering lmid without a row IS the applied verdict).
     *       Processing this category before step 1 is the violation, in two proven directions
     *       (each run break→fail→revert against `test/system_streams.test.ts`): (a) the ledger's
     *       fresh-session `nextMid` ADOPTION must not run before historical outcome rows are
     *       judged — adopted-first, a previous session's retained deopt row passes the
     *       "never-issued" guard and spuriously re-invokes a mutation that session already handled
     *       (a double-apply); (b) the RETIRE must not precede resolution — it does not BECAUSE the
     *       confirm-drop runs in {@link applyRelease}, strictly after this whole function. That
     *       deferral is load-bearing: an "optimization" retiring inline with the watermark fold
     *       retires a deopted entry as a silent success (the exact lost-write H-v exists to
     *       prevent) and mis-attributes a rejected row's reason.
     *    3. **watermark rows** (`_rindle_room_watermark`) — the §4.2 fence value, max-folded per
     *       doc ({@link roomWatermarks}); I-v's ghost-drop consumer, no reaction here.
     *    4. **scope-session rows** (`_rindle_scope_sessions`) — the §4.1 occupancy map
     *       ({@link scopeSessions}); I-iv's doorbell consumer, no reaction here.
     *
     *  Ordinary data ops fold AFTER all of these (the caller's main loop) — outcome/ledger state
     *  must be in place before {@link applyRelease}'s confirm-drop + reconcile consume the release.
     *  Every row is filtered against the retain's {@link SystemStreamSpec} scope/doc AND (for the
     *  client-keyed tables) our own `clientID` — defense in depth: the server predicate may have
     *  been minted doc-only (no `clientId` at lease time), so other clients' rows are expected and
     *  must be ignored, and a row for a doc this retain was not minted for is never folded.
     *
     *  Returns the scopes category 4 touched (snapshot or ops) — the I-iv doorbell events' input;
     *  `null` when none (every non-lifecycle release). The events themselves fire from
     *  `onGateProgress` AFTER the release applies, never from inside the fold. */
    private foldSystemFrames;
    /** The I-iv occupancy count — THE one rule (§4.1/D7): unexpired (`expires_at >` the fold
     *  clock's now) sessions under `scope` from OTHER clientIDs. Shared by the doorbell events
     *  ({@link onGateProgress}) and the client's registration-time check (a doorbell that folded
     *  BEFORE a candidate registered must still be able to trigger it) so the two can never
     *  disagree. Own-clientID rows never count — a solo client cannot ring its own bell — and
     *  expiry is judged on the injectable {@link FoldClock} (deterministic in a virtual-clock
     *  harness, the folded-oracle discipline). */
    otherScopeSessions(scope: string): number;
    /** Register the I-iv doorbell event sink — see {@link ScopeSessionsEvent}. One handler (a later
     *  registration replaces it, the {@link onLocalWrite} convention); client.ts is the consumer. */
    onScopeSessions(handler: (event: ScopeSessionsEvent) => void): void;
    /** One §1.3 reconcile cycle: rewind the optimistic layer and fold the coherent SERVER
     *  delta into BOTH head AND the `sync` baseline (`serverBatchBegin`), re-invoke every
     *  still-pending mutator to re-stage the optimistic layer (the rewind un-applied it), then
     *  deliver the coalesced result (`serverBatchEnd`). This is the engine's only sync-moving
     *  boundary — `onProgress` releases and `unregisterQuery`'s GC both go through here so head
     *  and sync never diverge (the §1.2 invariant; CRIT#2). */
    private runReconcileCycle;
    /** ONE channel's authority restarted (a new boot id): it lost all materialization + `cv` state
     *  and its `cv` sequence reset, so previously-released `cv`s no longer bound the new stream. The
     *  source has already re-subscribed every query (reconnect → resync); drop THIS gate's buffer
     *  and `cv` watermark so the fresh, low-`cv` snapshots are RELEASED instead of dropped as stale
     *  (`onFrame`/`computeRelease` gate on `appliedCv`). The OTHER gates are untouched — an
     *  authority restart is per-channel (§5.1). Pending optimistic mutations stay put — they
     *  re-apply on the next reconcile, and the channel's lmid system query's fresh snapshot restores
     *  its domain's confirmation watermark. */
    private resetGate;
    /** The §8.5 escape: ONE gate's buffer outgrew its cap (a pinned `cvMin` under churn on that
     *  channel). Drop everything it buffered and re-register every query on that source — the fresh
     *  snapshots arrive as ordinary frames and the next release re-hydrates via the footprint diff
     *  (the §5.3 path); still-pending optimism re-applies in that cycle. The other gates' buffers
     *  and subscriptions are untouched. */
    private overflow;
    private setResultType;
    /** Recompute a query's server-channel state from hydration alone (§7): a pending mutation no
     *  longer affects it. Used when a remote sub attaches to or hydrates a local view. */
    private recomputeResultType;
    /** A remote sub's first snapshot landed: mark it (and every local view it feeds) hydrated, then
     *  lift those views out of `unknown` (loading). A ROOM sub's hydration additionally queues the
     *  302 §4.1 swap-in — performed at the applyRelease TAIL ({@link processSwapIns}), once the
     *  reconcile has folded this snapshot into the room tables. Idempotent — a re-hydrate snapshot
     *  re-marks harmlessly; a source qid with no sub (the lmid system query) is a no-op. */
    private markSubHydrated;
    /** `channel` (G-iii registration-time routing): the gate the sub registers on — the qid's
     *  ownership is fixed HERE, at retain time (no lazy claim; `onFrame` only asserts it). Default
     *  `"daemon"`, so every channel-less caller is byte-identical to before. */
    private retainRemote;
    private releaseRemote;
    private addServerDependencyTables;
}

OptimisticBackendOptions

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:425 · Supporting declarations

export interface OptimisticBackendOptions {
    /** Stable per-client identity for the upstream envelopes (§8.1). */
    clientID: string;
    /** Supplies a shared mutator's local `ctx.user` on every run, including reconciliation.
     *  Keep the principal stable for this client's lifetime; recreate the client on account changes.
     *  The server obtains its own authenticated principal. Defaults to the empty string. Plain
     *  client-only mutators ignore this option. */
    user?: () => string;
    /** Buffered-frame ceiling before the §8.5 escape (drop + re-hydrate). */
    bufferCap?: number;
    /** Virtual-clock seam for the fold debounce timers (FOLDED-MUTATIONS-DESIGN §9). Defaults to
     *  real `setTimeout`/`clearTimeout`/`Date.now`; the fold oracle injects a deterministic clock. */
    clock?: FoldClock;
    /** The DECLARED confirming stream per mutation (302 §5: declared, not derived — there is no
     *  routing proof). A policy returning a string pins that domain verbatim: the mutation stages
     *  onto that room's namespaced tables and ships on its channel. Returning `undefined` (or
     *  configuring no policy) means `"daemon"`. The client layer builds this from the app's declared
     *  realtime mutators + the currently attached rooms; a misdeclaration fails SOFT (302 §5.1) —
     *  the write lands on the other authority's tables and the view simply stops feeling instant
     *  until the echo relays it. */
    domainPolicy?: (name: string, args: unknown) => string | undefined;
    /** A FINAL (authz/validation) mutation rejection's reason surface — the room plane's twin of the
     *  HTTP mutate route's `onRejected` (H-v; the H-iv-b `mutationOutcome {kind:"rejected"}` frame).
     *  The prediction's snap-back is NOT this callback's job: the room burns the mid and its lmid
     *  release drops the entry exactly as a daemon-path rejection does (processed-as-no-op) — this
     *  is where the REASON reaches the app, same contract as the queue's callback. Also invoked when
     *  a DEOPT's fresh re-invocation (the already-retired arm) throws — that mutation is dead on the
     *  current base with no stream left to confirm it, the closest thing to a rejection there is. */
    onRejected?: (envelope: MutationEnvelope, reason: string) => void;
}

OptimisticInspect

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:497 · Supporting declarations

A read-only snapshot of the optimistic loop ({@link OptimisticBackend.__inspect}).

export interface OptimisticInspect {
    /** The pending stack in array order (assigned mids ascending, un-flushed folds interleaved by
     *  creation — the re-invoke order is derived from this in `runReconcileCycle`). */
    pending: PendingInspect[];
    /** High-water confirmed mid: an entry with `mid <= confirmedLmid` has been confirmed/dropped. */
    confirmedLmid: number;
    /** The next mid to be dealt (so `nextMid - 1` is the highest issued). */
    nextMid: number;
    /** The applied coherent-release watermark (`cvMin`, §8.6). */
    appliedCv: number;
    /** Frames still buffered awaiting their release point (§8.5) — a backpressure gauge. */
    bufferedFrames: number;
    /** Every table some pending mutation currently touches (the coarse pending indicator set, §7.2). */
    pendingTables: string[];
}

OptimisticSource

InterfaceDeclaration · Source: packages/client/src/types.ts:303 · Supporting declarations

The server side of the OPTIMISTIC path (OPTIMISTIC-WRITES-DESIGN.md §8): the {@link NormalizedSource} stream with cv-tagged data frames, plus the connection-level {@link ProgressFrame} channel and the named-mutator upstream. The client buffers data frames by cv and applies all cv ≤ cvMin as one coherent release (§8.5).

export interface OptimisticSource {
    registerQuery(queryId: QueryId, remote: RemoteQuery): void;
    unregisterQuery(queryId: QueryId): void;
    /** Ship one named-mutator invocation upstream (§8.1). Confirmation rides the progress frames. */
    pushMutation(envelope: MutationEnvelope): Promise<void>;
    onNormalized(handler: (queryId: QueryId, event: NormalizedEvent) => void): void;
    onProgress(handler: (frame: ProgressFrame) => void): void;
    /** Optional: the backend hands its OWN typed per-table schemas so the source validates each
     *  server `hello` against them and rejects a schema skew (CRIT#4); see {@link NormalizedSource}. */
    expectClientSchema?(tables: NormalizedTableSchema[]): void;
    /** Optional: fired when the server restarts (a transport that can detect it, e.g. via a daemon
     *  boot id). The backend resets its `cv` watermark so the server's reset `cv` sequence is
     *  accepted rather than dropped as stale. In-process sources never restart and omit it. */
    onRestart?(handler: () => void): void;
    /** Optional (Slice H-v): the channel's {@link MutationOutcomeFrame} stream — the room deopt
     *  handshake's client half. **OUT-OF-BAND BY DESIGN**: the frame carries no `cv` and the source
     *  MUST dispatch it immediately on arrival, never behind the cv buffer — a deopt has to migrate
     *  its pending entry BEFORE the buffered lmid release that would otherwise retire it as a
     *  success (and the §7.3 hold-back trigger, keyed on the entry's confirming domain, would then
     *  park its staged writes the wrong way). Sources whose authority never deopts (the in-process
     *  native source, a plain daemon) omit it. */
    onMutationOutcome?(handler: (frame: MutationOutcomeFrame) => void): void;
    /** Optional (Slice H-v, the §7.5 rule-3 crash-window closer): fired when the transport
     *  RE-establishes its session (reconnect → re-`init`), BEFORE any post-reconnect frame is
     *  processed — the ordering is load-bearing: the re-subscribed lmid stream's fresh snapshot may
     *  cover a mid whose `mutationOutcome` frame died with the old socket, and once that release
     *  retires the entry as an apparent success there is nothing left to re-send. The backend
     *  re-sends this domain's unconfirmed pending envelopes with their ORIGINAL mids, in mid order;
     *  the source may DEFER their delivery until the session is re-authorized (a room's
     *  `pushMutation` requires the lease-token subscribe's subject). Idempotent under the domain's
     *  own ledger — a processed mid dedups silently (silence + lmid coverage ⇒ applied), a
     *  non-applied mid is re-answered from the authority's recorded-outcome map (resolving even an
     *  already-retired entry through the handshake's not-found arm). Distinct from
     *  {@link onRestart} (a NEW server incarnation): a same-incarnation socket drop re-syncs
     *  without restarting, and envelopes sent into the dead socket are exactly what this recovers.
     *  In-process sources never drop a session and omit it. */
    onResync?(handler: () => void): void;
}

PendingInspect

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:475 · Supporting declarations

One pending mutation, as the timeline sees it (DEBUG-TOOLS-BROWSER-DESIGN §4.1).

export interface PendingInspect {
    /** Stable identity across snapshots: `m:<mid>` once a mid is assigned, else `f:<foldKey>` for an
     *  un-flushed folded entry (the mid is dealt at flush, FOLDED-MUTATIONS-DESIGN §4.1). */
    key: string;
    /** The wire mutation id, or `null` for an un-flushed fold. */
    mid: number | null;
    name: string;
    args: unknown;
    /** Tables this mutator touched at its last (re)invocation — the pending-axis basis (§7.2). */
    tables: string[];
    /** The pk-granular write-set captured at this entry's LAST invocation (RINDLE-REALTIME-QUERY-
     *  ENABLEMENT-DESIGN.md §3.2 #1), flattened from the {@link WriteSet} map for inspection — one
     *  entry per `(table, pk)` currently held. Pure capture; no routing consumer yet. */
    writes: WriteRecord[];
    /** The read-log captured at this entry's LAST *recorded* invocation (§3.2 #2). Empty for a
     *  folded entry — the read TRAP arms there, not recording (see {@link PendingMutation.reads}). */
    reads: ReadLog;
    /** Present iff this entry is a folded (debounced) write. */
    fold?: FoldInspect;
}

PersistChannel

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:102 · Supporting declarations

export interface PersistChannel {
    post(msg: unknown): void;
    onMessage(handler: (msg: unknown) => void): void;
    close(): void;
}

PersistDb

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:89 · Supporting declarations

The narrow storage surface the layer needs (§3.1): two fixed stores, rows + meta. Each method is one atomic IDB transaction; a resolved promise means the txn COMPLETED — the P1 ("persist-then-broadcast") anchor.

export interface PersistDb {
    getMeta(): Promise<PersistMeta | undefined>;
    putMeta(meta: PersistMeta): Promise<void>;
    getAllRows(): Promise<StoredRow[]>;
    /** Apply one commit batch atomically: `row === null` deletes, else puts. */
    putBatch(batch: RowState[]): Promise<void>;
    /** The P7 gate: clear `rows` and write `meta` in ONE transaction (never a half state). */
    reset(meta: PersistMeta): Promise<void>;
    /** Delete specific records (the leader's stale-table sweep, §3.1). */
    deleteRows(keys: Array<{
        table: string;
        pkKey: string;
    }>): Promise<void>;
    close(): void;
}

PersistEnv

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:108 · Supporting declarations

export interface PersistEnv {
    /** Open (creating if needed) the database. Resolves `null` when storage is unavailable —
     *  the layer degrades to broadcast-only (§3.2). */
    openDatabase(name: string): Promise<PersistDb | null>;
    deleteDatabase(name: string): Promise<void>;
    /** Open the tab-coherence channel; `null` when BroadcastChannel is unavailable (single-tab). */
    createChannel(name: string): PersistChannel | null;
    /** Queue for the exclusive leadership lock (§4.1): `onAcquired`'s promise HOLDS the lock until
     *  it resolves; `signal` aborts a still-queued request. When Web Locks are unavailable, grant
     *  immediately ONLY if the runtime is provably single-context (no channel) — see
     *  {@link leaderElectionUnavailable}. */
    requestLock(name: string, signal: AbortSignal, onAcquired: () => Promise<void>): void;
    /** The runtime has tabs (a channel) but NO exclusive lock (Firefox <96, Safari 15.1–15.3,
     *  Node): every context would self-promote into concurrent leaders over one database — P2
     *  violated, permanent divergence. When set, the layer runs INERT: local tables still work,
     *  session-scoped (the 201 baseline), with no persistence and no cross-tab replication. */
    leaderElectionUnavailable?: boolean;
    /** `navigator.storage.persist()` (§3.2), best-effort. */
    requestPersistentStorage?(): void;
}

PersistLocalOptions

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:31 · Supporting declarations

export interface PersistLocalOptions {
    /** The storage identity (§3.2): one IDB database per (origin, user). NOT the mutator principal —
     *  this is fixed for the client's lifetime; an anonymous mode passes its own sentinel (`"anon"`). */
    user: string;
    /** Call `navigator.storage.persist()` to resist eviction (§3.2). Default false — it can prompt. */
    requestPersistentStorage?: boolean;
    /** Reports storage-operation failures without rejecting local writes. Default `console.error`.
     *  Missing browser APIs can warn and disable a feature instead; this hook does not receive
     *  every channel failure. Persistence and cross-tab delivery remain best effort. */
    onError?: (e: Error) => void;
    /** Test seam: a fake IDB/locks/channel environment. Defaults to the browser globals. */
    env?: PersistEnv;
}

PersistMeta

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:129 · Supporting declarations

export interface PersistMeta {
    schemaHash: string;
    epoch: number;
}

ProgressFrame

InterfaceDeclaration · Source: packages/client/src/types.ts:275 · Supporting declarations

The connection-level progress frame (§8.6): advances the coherent-apply release point (cvMin). A pure release signal — mutation confirmation does NOT ride it: lmid is a row in {@link CLIENT_MUTATIONS_TABLE}, delivered through the client's own per-client system query ({@link LMID_QUERY_NAME}) like any data, so it is released by the same cvMin as the commit's effects (transactionally coherent by construction).

export interface ProgressFrame {
    cvMin: number;
}

QueryArg

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:84 · Supporting declarations

What {@link IsoTx.query} accepts: a query handle whose .ast() lowers to the wire {@link Ast} — exactly what the typed query builder produces (newQueryBuilder(schema).<table>…, the tier-agnostic server-scope builder both tiers can construct because it performs no I/O). Structural so the seam need not carry the builder's heavy generics.

export type QueryArg = {
    ast(): Ast;
};

QueryResultRow

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:78 · Supporting declarations

A row returned by a {@link QueryEffect}: column name → cell, plus each materialized relationship name → its nested row(s) — an array (a plural relationship) or a single row / null (a .one() relationship). Recursive. Presented identically on both tiers (a view.data row of the same query).

export type QueryResultRow = {
    [key: string]: WireValue | QueryResultRow | QueryResultRow[];
};

ReadLog

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:238 · Supporting declarations

The read-log captured over ONE mutator invocation when recording is armed (RINDLE-REALTIME- QUERY-ENABLEMENT-DESIGN.md §3.2 #2): every point read (reads — the public tx.get/tx.row and, since H-ii, the keyed writers' internal pre-existence probes) plus every resolved query AST (queries, from tx.query). A SIBLING of the folded read TRAP (FoldReadError below) — the trap arms on the folded path and throws before any read completes (recording never runs there); recording arms on the ordinary (non-folded) prediction run and never throws. Pure capture for devtools/inspection (the §3 routing derivation it once fed was removed by 302-ROOM-STORE-SEPARATION-DESIGN.md §5 — mutators DECLARE their domain now).

export interface ReadLog {
    reads: ReadRecord[];
    queries: Ast[];
}

ReadOutcome

TypeAliasDeclaration · Source: packages/optimistic/src/backend.ts:219 · Supporting declarations

Whether a recorded point read (tx.get/tx.row) found a row.

export type ReadOutcome = "present" | "absent";

ReadRecord

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:224 · Supporting declarations

One recorded point read, pk-granular (§3.2 #2). Recording-mode only — see {@link ReadLog}. Since H-ii this covers BOTH the public reads (tx.get/tx.row) and the keyed writers' internal pre-existence probes (§3.2 #3 — see the rawGet note in {@link trackingTx}).

export interface ReadRecord {
    table: string;
    pk: WireValue[];
    outcome: ReadOutcome;
}

RealtimeAnomaly

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:170 · Supporting declarations

A loud realtime lease anomaly (always ALSO console.error'd).

export interface RealtimeAnomaly {
    kind: RealtimeAnomalyKind;
    name: string;
    args: unknown;
    message: string;
}

RealtimeAnomalyKind

TypeAliasDeclaration · Source: packages/optimistic/src/client.ts:150 · Supporting declarations

export type RealtimeAnomalyKind = 
/** A re-lease (renewal / reconnect re-resolution) came back WITHOUT a realtime block AND
 *  without a §4.2 fence — the query is no longer room-served but the server gave nothing to
 *  downgrade behind (a legacy/pre-I-v server, or `lifecycle.drainRoom` unconfigured). Surfaced
 *  loudly; a reply WITH a `realtimeFence` takes the graceful I-v dance instead. */
"downgrade"
/** The I-v ghost is STUCK (§7.5): its watermark fence cleared but sent room-domain mids never
 *  resolved (sent-but-undelivered when the socket died — undecidable in general). The ghost
 *  holds — no timeout-retire is invented — and the mids are named once, actionably. */
 | "downgrade-stuck"
/** A lease named a DIFFERENT `sourceKey` than the query's live room sub — surfaced loudly, no
 *  re-attach. Deliberately NOT composed from demote+upgrade (deferred to §7.6's rare-case
 *  follow-up): a sourceKey-change reply carries a realtime block for the NEW room but NO
 *  fence for the OLD one, and without `finalFlushSeq` the old slice cannot be ghosted soundly. */
 | "source-key-changed"
/** The lease POST failed or the room attach threw. The initial-materialize case fails OPEN to
 *  the daemon path (indistinguishable from an unlabeled query's recovery). */
 | "lease-failed";

RealtimeClientOptions

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:179 · Supporting declarations

Rindle Realtime client knobs (Slice G-v). All optional — an app with no labeled queries never touches any of this.

export interface RealtimeClientOptions {
    /** The DECLARED room mutators (302 §5: declared, not derived). A mutator named here routes to
     *  the attached room — it stages onto the room's own tables and ships on the room socket —
     *  whenever exactly ONE room is attached; solo (no room) it takes the ordinary daemon path,
     *  and with several rooms attached it routes daemon too (explicit multi-room binding is a
     *  later slice). Every mutator NOT named here is a daemon mutator. A misdeclaration fails
     *  SOFT (302 §5.1): the write lands on the other authority's tables, so the view just stops
     *  feeling instant until the echo relays it — never a divergence. An explicit top-level
     *  `domainPolicy` overrides this entirely. */
    mutators?: readonly string[];
    /** Build the ROOM ws transport for a lease's `realtime.wsEndpoint`. Default
     *  `(endpoint) => new WsTransport(endpoint)`. Injectable for tests / custom ws impls. */
    transport?: (endpoint: string) => Transport;
    /** Loud anomaly surface — see {@link RealtimeAnomaly}. Every anomaly is also `console.error`'d. */
    onAnomaly?: (anomaly: RealtimeAnomaly) => void;
    /** How long before a room lease's `exp` the proactive token renewal fires (default 30s). The
     *  renewal is a FRESH lease through the app query route (renewal-as-reauthorization), and the
     *  live room sub proactively re-subscribes with the fresh token so the shell's TTL backstop
     *  never fires on a healthy session. */
    renewMarginMs?: number;
}

RealtimeInspect

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:203 · Supporting declarations

Read-only realtime bookkeeping snapshot ({@link RindleClient.__realtimeInspect}) — test/devtools introspection, mirroring the backend's __inspect convention.

export interface RealtimeInspect {
    rooms: Record<string, {
        wsEndpoint: string;
        /** The room's OWNED tables (302 §2): wire table → its namespaced engine table — read back
         *  from the BACKEND's registry (`backend.roomTablesFor`, the one source of truth; the
         *  client keeps no shadow copy). */
        promoted: Record<string, string>;
        /** Live room-retained queries on this room, by remote key. */
        queries: Record<string, {
            name: string;
            sourceQid: QueryId;
            exp: number;
            refCount: number;
        }>;
    }>;
}

RealtimeLeaseBlock

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:82 · Supporting declarations

The room-serve block on a query lease (mirror of the api-server's QueryLeaseRealtime).

export interface RealtimeLeaseBlock {
    /** The store's gate/domain key for this room source (`connectSource`) — `"room:<profile>/<key>"`. */
    sourceKey: string;
    /** Where the ROOM ws opens — the lease's DEDICATED field. Never confuse it with the TOP-LEVEL
     *  `wsEndpoint` (the read-router's whole-DAEMON-session migration signal). */
    wsEndpoint: string;
    /** The room shell's self-authorizing signed lease (seals the APPROVED query AST) — presented as
     *  the room subscribe's `leaseToken`. */
    roomToken: string;
    /** Token expiry (ms epoch) — the renewal clock (renewal = a fresh lease through the app route). */
    exp: number;
    doc: string;
    tables: RealtimeLeaseTableSpec[];
}

RealtimeLeaseTableSpec

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:73 · Supporting declarations

One footprint table's spec on the lease wire (mirror of the api-server's RoomTableSpec). footprintWhere (H-iii lease-wire flip) is the EXACT footprint-membership predicate from the ONE unified compiler (compileRoomScopeSpecs — the same output the boot wire ships the room gate): present only for an exact footprint ROOT (lossless row-local extraction; the vacuous-true empty AND for an unconstrained one), ABSENT for child/correlated tables. It feeds the §3 router's pk-membership read proof (OptimisticBackend's routing table) — never authorization.

export interface RealtimeLeaseTableSpec {
    table: string;
    footprintWhere?: Condition;
    writable: {
        kind: "none";
    } | {
        kind: "predicate";
        where?: Condition;
        joinKeyCols: string[];
    };
}

resetStableClientID

FunctionDeclaration · Source: packages/optimistic/src/client-id.ts:52 · Supporting declarations

Clear the persisted client identity so the next page load starts a fresh mid/lmid stream. Intended for dev recovery after out-of-band server state loss, not for normal operation.

export declare function resetStableClientID(): void;

ResultType

TypeAliasDeclaration · Source: packages/client/src/types.ts:155 · Supporting declarations

A query's SERVER-CHANNEL state, surfaced on its {@link ArrayView } (FOLDED-MUTATIONS-DESIGN §7 — formerly conflated with pending-ness, OPTIMISTIC-WRITES-DESIGN.md §6):

  • unknown — not hydrated: the server has not produced a first result for this query yet;
  • complete — the server has answered. STAYS complete while a local mutation is pending (the prediction is the client's best current answer); reversion on rejection is an event (onRejected), not a downgrade of completeness;
  • error — RESERVED for a future server-side, query-level error signal (see designs/101-QUERY-ERRORS-DESIGN.md); no longer produced by a pending mutation. "Is a prediction pending here?" is now a separate reactive axis (the backend's pending(qid) / onPending), not folded into this type. A backend with no server lifecycle (the in-process engine) leaves every view complete.
export type ResultType = "unknown" | "complete" | "error";

RindleClient

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:315 · Supporting declarations

export interface RindleClient<S extends ColsMap, R extends ClientRegistry> {
    store: Store<S>;
    backend: OptimisticBackend<S>;
    /** Retain a named query for navigation/prefetch. By default waits for server authority; pass
     *  `{ until: "present" }` to continue as soon as the local view has a result. */
    ensure<Q extends AnyQuery>(query: Q, options?: EnsureQueryOptions): Promise<void>;
    /** Call `mutate.foo(args)` for a normal optimistic write, or `mutate.foo.folded(opts, args)` for a
     *  debounced, last-value-wins folded write (FOLDED-MUTATIONS-DESIGN §3). */
    mutate: {
        [K in keyof R]: MutateFn<Parameters<R[K]>[1]>;
    };
    /** Assign IDs and enqueue every outstanding fold immediately. Also called on
     *  `beforeunload`/`pagehide` as a best-effort flush; neither path confirms delivery. */
    flushFolds(): void;
    clientID: string;
    /** Release retained prefetch queries, transports, and lifecycle listeners; close local persistence.
     *  Flushes folded arguments into the queue first; does not await server confirmation. */
    close(): void;
    /** Read-only realtime bookkeeping snapshot (rooms, promoted tables + their client-held
     *  `joinKeyCols`, live room queries) — the `__inspect`-convention test/devtools hook. */
    __realtimeInspect(): RealtimeInspect;
}

RindleClientOptions

InterfaceDeclaration · Source: packages/optimistic/src/client.ts:237 · Supporting declarations

export interface RindleClientOptions<S extends ColsMap, R extends ClientRegistry> {
    schema: Schema<S>;
    /** The PREDICTED mutators (the API server holds the authoritative twins by name). */
    mutators: R;
    /** The acting principal for a shared (generator) mutator's `ctx.user` — the local identity the
     *  optimistic prediction writes under (the server injects its OWN authenticated user for the
     *  authoritative run). Re-read on each run, including replay. Keep this identity stable for
     *  the client's lifetime and recreate the client on account changes. */
    user?: () => string;
    /** The app API server: named queries resolve to leases here, mutations push here. */
    api: {
        url: string;
        routes?: {
            query?: string;
            mutate?: string;
        };
        /** Extra headers per request (auth). A function is re-evaluated per call. */
        headers?: HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
        fetch?: typeof fetch;
    };
    /** Optional subscription transport override. Omit it in the normal unified setup: the first
     *  query lease carries `wsEndpoint` + a fresh affinity ticket and opens the transport lazily.
     *  - `{ wsUrl }` — a static endpoint (single daemon), opened eagerly; in a routed deploy this is
     *    the SSR-injected bootstrap endpoint (READ-ROUTER-DESIGN.md §2.4). A routed lease naming a
     *    different follower migrates the connection there.
     *  - `{ wsUrl }` omitted (or this whole option omitted) — pure-lazy: the first lease's
     *    `wsEndpoint` opens the connection (a
     *    routed SPA with no SSR bootstrap).
     *  - `{ transport }` — a pre-built transport (tests / in-process); fixed, no migration.
     *
     *  With a fixed `wsUrl`, set `affinity: true` to opt into FOLLOWER-AFFINITY mode (design §2).
     *  With no daemon option, a lease that returns an affinity ticket enables the same mode
     *  automatically before opening its socket. The ticket is persisted per-tab and forwarded on
     *  later leases so both legs pin the same nearby follower. Ignored for `{ transport }`. */
    daemon?: {
        wsUrl?: string;
        affinity?: boolean;
    } | {
        transport: Transport;
    };
    /** Stable client identity. Default: a per-origin base (localStorage) plus per-tab and per-instance
     *  suffixes, so each tab — and each client instance within a tab — gets its own mid sequence yet a
     *  reload keeps it; falls back to a fresh random id when web storage is unavailable. Pass a value
     *  to override. */
    clientID?: string;
    /** A policy rejection's reason (the prediction's snap-back rides the lmid release). Fires for
     *  BOTH planes since H-v: the HTTP mutate route's per-envelope rejections AND a room's
     *  `mutationOutcome {kind:"rejected"}` frames — one surface, whichever authority said no. */
    onRejected?: (envelope: MutationEnvelope, reason: string) => void;
    /** A failed mutate FLUSH — the transport/authority leg, not a policy verdict: the batch is
     *  retried with backoff and nothing has been confirmed yet, so the pending mutations stay
     *  predicted and the queue is head-of-line blocked until it succeeds. Fires once per attempt.
     *  This is the twin of {@link onRejected}: `onRejected` is "the authority said no" (final,
     *  lmid already advanced), this is "the authority never answered" (retrying).
     *
     *  LOUD by contract — every attempt reaching this hook is ALSO `console.error`'d (backed off
     *  to attempts 1, 2, 4, 8, … so a long outage doesn't flood the console), because an
     *  indefinitely retried flush is indistinguishable from a hung app if it stays silent. */
    onMutationError?: (err: unknown, attempt: number) => void;
    /** Persist `local: true` tables across reloads and keep them live-coherent across tabs
     *  (`207-LOCAL-TABLE-PERSISTENCE-DESIGN.md`). `user` is the storage identity (§3.2) — one IDB
     *  database per (origin, user); pass a sentinel like `"anon"` for a signed-out mode. When set,
     *  `createRindleClient` awaits the initial restore attempt before returning. Storage failures
     *  can leave tables empty while local writes remain usable. Close the old client before calling
     *  `deleteLocalPersistence(user)` at logout; deletion is never implicit.
     *  Per-table opt-out: declare `table(name, { local: "session" })` for local state that must stay
     *  ephemeral and per-tab (e.g. selection) even with persistence on (§5.4). */
    persistLocal?: PersistLocalOptions;
    queue?: {
        maxBatch?: number;
        retryDelayMs?: (attempt: number) => number;
    };
    /** Explicitly selects a mutation's confirming stream. Its domain supplies the mutation ID,
     *  transport, and confirmation watermark. Returning `undefined` selects `"daemon"`.
     *  If this policy is omitted, `realtime.mutators` supplies the declared room routing policy;
     *  without either declaration, mutations route to the daemon. Routes are not inferred from
     *  reads or writes. A room deopt can re-enqueue the mutation on the daemon stream. */
    domainPolicy?: (name: string, args: unknown) => string | undefined;
    /** Rindle Realtime client knobs (G-v resolve-then-register) — see {@link RealtimeClientOptions}. */
    realtime?: RealtimeClientOptions;
    /** Development-only recovery knobs. Keep off in production: a mutation gap means state loss
     *  or two writers sharing a clientID, and should be investigated. */
    dev?: {
        /** On a mutation-gap response, clear the persisted clientID and hard reload the page. This
         *  recovers from dev DB wipes while making the reset visible to the developer. */
        resetOnMutationGap?: boolean;
    };
}

ROOM_CLIENT_MUTATIONS_TABLE

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:33 · Supporting declarations

§7.1 domain-scoped room ledger: (doc, client_id, last_mutation_id), PK (doc, client_id) — the FIRST daemon-carried room-lmid path (the named invariant's enforcement point).

export declare const ROOM_CLIENT_MUTATIONS_TABLE = "_rindle_room_client_mutations";

ROOM_MUTATION_OUTCOMES_TABLE

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:37 · Supporting declarations

§3.3 durable outcome rows: (doc, client_id, mid, kind, reason, name, args), PK (doc, client_id, mid) — the H-iv-b mutationOutcome frame's durable twin (Slice I-ii co-commits one per NON-applied mid; an absent row under a covering lmid means applied).

export declare const ROOM_MUTATION_OUTCOMES_TABLE = "_rindle_room_mutation_outcomes";

ROOM_WATERMARK_TABLE

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:30 · Supporting declarations

§4.2 downgrade fence: (doc, flush_seq), PK doc — monotone, co-committed per room flush.

export declare const ROOM_WATERMARK_TABLE = "_rindle_room_watermark";

roomDomainKey

FunctionDeclaration · Source: packages/optimistic/src/system-streams.ts:102 · Supporting declarations

The domain/gate key a room doc's daemon-carried confirms fold into — the SAME key the lease wire mints for the room source (api-server: sourceKey = "room:" + doc), so a daemon-carried lmid and a live room socket's lmid stream land on ONE watermark entry.

export declare function roomDomainKey(doc: string): string;

RowState

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:142 · Supporting declarations

The replication unit everywhere (§4.3): idempotent full-row state. row: null = tombstone (live protocol only — removes DELETE the IDB record; there are no persisted tombstones).

export interface RowState {
    table: string;
    pkKey: string;
    row: WireValue[] | null;
}

SCOPE_SESSIONS_TABLE

VariableDeclaration · Source: packages/optimistic/src/system-streams.ts:28 · Supporting declarations

§4.1 occupancy: (scope, client_id, expires_at), PK (scope, client_id). The row delta IS the upgrade doorbell (Slice I-iv reacts; I-iii only folds the map).

export declare const SCOPE_SESSIONS_TABLE = "_rindle_scope_sessions";

ScopeSessionsEvent

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:388 · Supporting declarations

One I-iv doorbell event ({@link OptimisticBackend.onScopeSessions}, §4.1): a release folded scope-session rows for scope, and others is the count of OTHER clients' unexpired sessions there — {@link OptimisticBackend.otherScopeSessions} evaluated at fold time (the same one rule, on the injectable {@link FoldClock}, so a virtual-clock harness gets deterministic verdicts). The consumer (client.ts) triggers its one debounced re-lease on the 0→≥1 transition; expired and own-clientID rows never count, so a solo client's own row can never ring its own bell.

export interface ScopeSessionsEvent {
    scope: string;
    others: number;
}

SharedMutator

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:152 · Supporting declarations

A generator (isomorphic) mutator, shared verbatim by both tiers: the client trusts typed args, the server parses untrusted args into Args before invoking.

export type SharedMutator<Args, Ctx extends MutatorCtx = MutatorCtx> = (tx: IsoTx, args: Args, ctx: Ctx) => MutationGen;

stableClientID

FunctionDeclaration · Source: packages/optimistic/src/client-id.ts:109 · Supporting declarations

The connection's stable clientID. A per-origin base (localStorage) keeps one logical identity across reloads; a per-tab suffix (sessionStorage) gives each tab its OWN mid/lmid stream; a per-load instance suffix keeps two clients constructed in ONE tab from sharing that stream. Falls back to a fresh random id when web storage is unavailable.

Note: an explicit "duplicate tab" copies sessionStorage, so the clone briefly shares its source's suffix until reloaded — the one residual case of the shared-clientID collision.

export declare function stableClientID(): string;

StoredRow

InterfaceDeclaration · Source: packages/optimistic/src/local-persist.ts:134 · Supporting declarations

export interface StoredRow {
    table: string;
    pkKey: string;
    row: WireValue[];
}

SystemStreamSpec

InterfaceDeclaration · Source: packages/optimistic/src/system-streams.ts:85 · Supporting declarations

What a system retain declares about itself (OptimisticBackend.retainSystemQuery): which system table its frames carry, and the scope/doc its minted predicate was scoped to. The fold filters rows against this spec (AND against the client's own clientID) as DEFENSE IN DEPTH — the server predicate is the tight path, but a server that couldn't scope (no clientId on the lease request) legitimately delivers other clients' rows, and a confused server must never invent verdicts for us.

export interface SystemStreamSpec {
    table: SystemStreamTable;
    /** The §4.1 occupancy scope the doorbell was minted for (`_rindle_scope_sessions` only). */
    scope?: string;
    /** The room doc the fence entry was minted for (the three room tables). */
    doc?: string;
}

SystemStreamTable

TypeAliasDeclaration · Source: packages/optimistic/src/system-streams.ts:73 · Supporting declarations

The tables a system retain may serve — the fold category discriminant.

export type SystemStreamTable = typeof SCOPE_SESSIONS_TABLE | typeof ROOM_WATERMARK_TABLE | typeof ROOM_CLIENT_MUTATIONS_TABLE | typeof ROOM_MUTATION_OUTCOMES_TABLE;

WriteRecord

InterfaceDeclaration · Source: packages/optimistic/src/backend.ts:198 · Supporting declarations

One captured write, pk-granular (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §3.2 #1: "the writers already receive (table, row)... this is pure capture, no semantic change"). row is the post-write image (positional wire cells, schema column order); undefined for a remove — no row survives it, and row === undefined stays the remove marker.

oldRow is the PRE-IMAGE, captured for a remove AND an edit (H-ii): the full-width row read via tx.get immediately BEFORE the write staged — read-your-writes, so a write to the same pk earlier in the SAME invocation shows through — falling back to the caller's asserted old row when the pk is not txn-visible (a raw remove/edit of an absent row; incl. the pk-MOVING raw edit, whose record is keyed by the NEW pk yet whose pre-image is the caller's OLD row). A captured remove/edit thus always carries a full-width pre-image — with ONE exception: a record that collapses to a (re-)insert has none, see the matrix.

Coalescing within one invocation is last-write-wins per pk on row (the final image matches the engine head's own semantics for that pk) with oldRow pinned to the TXN-ENTRY BASE — the H-ii matrix:

  • edit-after-add / edit-after-remove: the record collapses to a (re-)insert — post-image only, NO oldRow (the pk did not pre-exist this invocation's base; presence hold-back uses row).
  • edit-after-edit: keeps the FIRST pre-image (the txn-entry base — the chain nets to ONE edit from the base to the final image).
  • remove-after-edit / remove-after-remove: keeps the ORIGINAL pre-image (the first write's captured base), NOT the edited transient — the net effect is a remove of the row the external world last knew.
  • remove-after-add: keeps the txn-visible pre-image (the transient added row — the pk had no base, and this is the only truthful full-width row there is; G-iii pinned it).
  • add-after-remove (a re-insert): drops oldRow (presence hold-back uses row). Across rebase re-invocations the write-set is union-never-shrink ({@link mergeWriteSet}): a re-run that no-ops keeps the prior record — and its pre-image — intact.
export interface WriteRecord {
    table: string;
    pk: WireValue[];
    row: WireValue[] | undefined;
    oldRow?: WireValue[];
}

WriteSet

TypeAliasDeclaration · Source: packages/optimistic/src/backend.ts:216 · Supporting declarations

The pk-granular write-set captured over ONE mutator invocation: table → pk-key (a stable-JSON encoding of the pk cells, {@link stableJson}) → that pk's write, LAST-WRITE-WINS within the invocation — an add-then-edit or edit-then-edit of the SAME pk collapses to its final image, matching the engine head's own semantics for that pk. Chosen (over a flat array) because the later routing proof needs "is pk P in the writable scope" / "did we already see this pk in this invocation" as cheap lookups, and rebase re-invocation needs to MERGE a fresh write-set into an accumulated one ({@link mergeWriteSet}) — both are Map operations, not scans.

touched (the pre-existing table-granular Set<string> the pending axis reads, §7.2) is exactly new Set(writeSet.keys()) — derived from this, never separately populated, so the two can never drift.

export type WriteSet = Map<string, Map<string, WriteRecord>>;

YieldEffect

TypeAliasDeclaration · Source: packages/client/src/mutation-ops.ts:95 · Supporting declarations

Everything a generator mutator may yield: a write {@link MutationOp}, a point {@link ReadEffect}, a full-query {@link QueryEffect}, or a {@link BatchEffect} fan-out.

export type YieldEffect = MutationOp | ReadEffect | BatchEffect | QueryEffect;