@rindle/optimistic 0.5.0 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/backend.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import type { Ast, Backend, BackendDevObserver, ChangeEvent, ColsMap, Condition, IsoTx, KeyedRow, Mutation, MutationEnvelope, MutationGen, MutatorCtx, OptimisticSource, QueryArg, QueryId, QueryResultRow, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
2
- import { type WritableDescriptor } from "@rindle/wasm";
1
+ import type { Ast, Backend, BackendDevObserver, ChangeEvent, ColsMap, IsoTx, KeyedRow, Mutation, MutationEnvelope, MutationGen, MutatorCtx, OptimisticSource, QueryArg, QueryId, QueryResultRow, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
3
2
  import { type SystemStreamSpec } from "./system-streams.ts";
4
3
  export type { SystemStreamSpec, SystemStreamTable } from "./system-streams.ts";
5
4
  /** A keyed row: column name → cell. The ergonomic shape — column names are validated against the
@@ -60,16 +59,13 @@ export type { ResultType };
60
59
  * the post-write image (positional wire cells, schema column order); `undefined` for a `remove` —
61
60
  * no row survives it, and `row === undefined` stays the remove marker.
62
61
  *
63
- * `oldRow` is the PRE-IMAGE, captured for a `remove` (G-iii, §7.3 tombstone) AND since H-ii
64
- * an `edit` (it feeds H-iii's join-key no-change rule, old vs new join-key cells, and mirrors the
65
- * engine, which routes an Edit by its OLD rowthe H-i handoff): the full-width row read via
66
- * `tx.get` immediately BEFORE the write staged read-your-writes, so a write to the same pk
67
- * earlier in the SAME invocation shows through falling back to the caller's asserted old row
68
- * when the pk is not txn-visible (a raw remove/edit of an absent row; incl. the pk-MOVING raw
69
- * edit, whose record is keyed by the NEW pk yet whose pre-image is the caller's OLD row — the row
70
- * H-i's `provenanceOf` routes by). A captured remove/edit thus always carries a full-width
71
- * pre-image (the engine width-checks `holdBackAbsent`; Slice H evaluates writable predicates on
72
- * it) — with ONE exception: a record that collapses to a (re-)insert has none, see the matrix.
62
+ * `oldRow` is the PRE-IMAGE, captured for a `remove` AND an `edit` (H-ii): the full-width row
63
+ * read via `tx.get` immediately BEFORE the write staged — read-your-writes, so a write to the
64
+ * same pk earlier in the SAME invocation shows throughfalling back to the caller's asserted
65
+ * old row when the pk is not txn-visible (a raw remove/edit of an absent row; incl. the
66
+ * pk-MOVING raw edit, whose record is keyed by the NEW pk yet whose pre-image is the caller's
67
+ * OLD row). A captured remove/edit thus always carries a full-width pre-image — with ONE
68
+ * exception: a record that collapses to a (re-)insert has none, see the matrix.
73
69
  *
74
70
  * Coalescing within one invocation is last-write-wins per pk on `row` (the final image matches
75
71
  * the engine head's own semantics for that pk) with `oldRow` pinned to the TXN-ENTRY BASE — the
@@ -114,119 +110,19 @@ export interface ReadRecord {
114
110
  table: string;
115
111
  pk: WireValue[];
116
112
  outcome: ReadOutcome;
117
- /** Per-read provenance (H-ii, §3.2 #3): the engine's `provenanceOf` answer AT THE MOMENT OF THE
118
- * READ — which source ("daemon" / a room key) served the row the mutator saw. Live pre-commit
119
- * state: an open txn's staged writes are not consulted (H-i), so a PRESENT read of a pk this
120
- * txn itself created reports `undefined` — recorded honestly, key omitted (the router treats
121
- * self-reads via write-set membership, not provenance). ABSENT reads carry no source (the
122
- * mutator saw no row; there is no full-width row to probe with — a pk this txn staged-removed
123
- * thus also records `undefined`, again a write-set-membership case for the router). Probed only
124
- * on a PROMOTED table ({@link promotedTables}): a never-promoted (Collapsed) table is
125
- * daemon-owned by construction, so a pure single-domain app pays zero wasm calls per read and
126
- * `undefined` is still correct (everything is daemon-owned). One probe call per recorded
127
- * present read, at read time. */
128
- source?: string;
129
113
  }
130
114
  /** The read-log captured over ONE mutator invocation when recording is armed (RINDLE-REALTIME-
131
115
  * QUERY-ENABLEMENT-DESIGN.md §3.2 #2): every point read (`reads` — the public `tx.get`/`tx.row`
132
116
  * and, since H-ii, the keyed writers' internal pre-existence probes) plus every resolved query AST
133
117
  * (`queries`, from `tx.query`). A SIBLING of the folded read TRAP (`FoldReadError` below) — the
134
118
  * trap arms on the folded path and throws before any read completes (recording never runs there);
135
- * recording arms on the ordinary (non-folded) prediction run and never throws. Pure capture: which
136
- * row/predicate a read is actually PROVEN covered by (§3.1's table) is a later slice's job. */
119
+ * recording arms on the ordinary (non-folded) prediction run and never throws. Pure capture for
120
+ * devtools/inspection (the §3 routing derivation it once fed was removed by
121
+ * 302-ROOM-STORE-SEPARATION-DESIGN.md §5 — mutators DECLARE their domain now). */
137
122
  export interface ReadLog {
138
123
  reads: ReadRecord[];
139
124
  queries: Ast[];
140
125
  }
141
- /** One promoted `(sourceKey, table)`'s client-held routing spec — THE per-table routing record the
142
- * §3 prove-or-slow-path router reads (H-iii). Recorded by {@link OptimisticBackend.promoteRoomTable}
143
- * in the same breath as the engine attach, from the lease's `RealtimeLeaseTableSpec` (the
144
- * api-server's `RoomTableSpec`, one compiler with the boot wire).
145
- *
146
- * - `writable`: whether the room may write this table AT ALL — `true` iff the registered engine
147
- * {@link WritableDescriptor} is not `none` (a context/"followed" table is `none`; the daemon
148
- * stays write-authoritative for it, §2.2). Rule #1 of the write proof.
149
- * - `joinKeyCols`: the correlation columns the footprint binds on this table — the "room mutators
150
- * never change join keys" input (an edit with a moved join-key cell would re-parent the row
151
- * across the footprint's correlation and is refused room-routing). Deliberately client-side —
152
- * they never cross the wasm {@link WritableDescriptor} ABI (the room GATE enforces them
153
- * engine-side, H-iv).
154
- * - `where`: the lease's row-local writable predicate (advisory here — the router evaluates the
155
- * ENGINE's compiled copy via `writableMatches`, never this one; kept for introspection parity
156
- * with the wire).
157
- * - `footprintWhere`: the EXACT footprint-membership predicate (H-iv-b: root-node-only, lossless
158
- * extraction; the vacuous-true `{type:"and",conditions:[]}` for an exact unconstrained root;
159
- * ABSENT for child/correlated tables). The read proof's pk-membership test input — see
160
- * {@link OptimisticBackend.deriveDomain}. */
161
- export interface RoomTableRouting {
162
- writable: boolean;
163
- joinKeyCols: string[];
164
- where?: Condition;
165
- footprintWhere?: Condition;
166
- }
167
- /** The per-table spec {@link OptimisticBackend.promoteRoomTable} receives beside the engine
168
- * descriptor (H-iii spec plumbing) — the wire-facing half of {@link RoomTableRouting}
169
- * (`writable` is derived from the descriptor's kind, never passed separately). */
170
- export interface RoomTableRoutingSpec {
171
- joinKeyCols?: readonly string[];
172
- where?: Condition;
173
- footprintWhere?: Condition;
174
- }
175
- /** The §3 router's Q6 measurement hook ({@link OptimisticBackend.__inspectDomains}`().routing`):
176
- * how often derivation succeeded (per derived room) and why it fell back to the daemon (per
177
- * failure reason). `reasons` counts PER-CANDIDATE proof failures plus the terminal reasons
178
- * (`no-candidates` / `tx-query` / `ambiguous`) — with multiple candidate rooms one derivation can
179
- * bump several reason counters (one per failing candidate), and a candidate's failure is counted
180
- * even when a sibling candidate ultimately proved. Explicit-policy pins bump NOTHING (the router
181
- * never ran). */
182
- export interface RoutingInspect {
183
- /** Successful DERIVED routes, per room key. */
184
- derived: Record<string, number>;
185
- /** Derivation failures, per {@link RoutingFailureReason}. */
186
- reasons: Record<string, number>;
187
- }
188
- /** Why a §3 derivation (or one candidate's proof) fell to the daemon — the {@link RoutingInspect}
189
- * counter keys. */
190
- export type RoutingFailureReason =
191
- /** No connected room gate with a promoted table — the single-domain fast path. */
192
- "no-candidates"
193
- /** The invocation ran `tx.query` — a declarative read is not room-provable client-side
194
- * (rindle-cover is native-only BY DESIGN), so the whole derivation fails unconditionally. */
195
- | "tx-query"
196
- /** Two or more candidate rooms both proved. Principled disambiguation is a §9.2 (multi-room
197
- * clients + budgets) NON-goal, deferred past Slice J; routing slow is always sound (the §3.3
198
- * gate is the contract). Reachable essentially only for zero-write mutations — real writes
199
- * can't be writable in two rooms (§2.2 exactly-one-writer per doc). */
200
- | "ambiguous"
201
- /** A write touched a table not promoted for the candidate, or promoted `writable: none`
202
- * (context) — the room may not write it at all. */
203
- | "write-unwritable-table"
204
- /** `writableMatches` refused the write's post-image (add/edit) or pre-image (remove). */
205
- | "write-scope-miss"
206
- /** An edit moved a join-key cell (old vs new differ on a `joinKeyCols` column). */
207
- | "write-join-key-change"
208
- /** The engine's provenance for the write's probe row names a DIFFERENT room. */
209
- | "write-cross-room-provenance"
210
- /** A recorded read was served by a DIFFERENT room. */
211
- | "read-cross-room"
212
- /** A read needed the pk-membership test but the table has no `footprintWhere` (child /
213
- * correlated / inexact extraction / never-promoted table). */
214
- | "read-no-footprint-where"
215
- /** `footprintWhere` references a non-pk column — not decidable from the key alone. */
216
- | "read-not-key-decidable"
217
- /** `footprintWhere` is key-decidable but outside the deliberately-minimal client evaluator
218
- * (an op other than `=`/`!=`, a null/type-mixed comparison, column-vs-column, …). */
219
- | "read-not-evaluable"
220
- /** `footprintWhere` evaluated FALSE on the read's pk — the room provably never holds it. */
221
- | "read-outside-footprint"
222
- /** NOT a derivation failure (H-v): a routed mutation came back `mutationOutcome
223
- * {kind:"deopt"}` — the room GATE refused it at commit and the client re-enqueued it onto the
224
- * daemon. Counted per processed deopt frame so Q6's picture is complete: derived-and-deopted
225
- * routes are visible beside derived successes (a rising `deopt` count against a rising
226
- * `derived` count means the client proof and the gate disagree — a compiler/evaluator skew
227
- * worth investigating; each one costs a room round-trip + a burnt room mid, never
228
- * correctness). */
229
- | "deopt";
230
126
  /** A virtual-clock seam for the fold debounce/maxWait timers (FOLDED-MUTATIONS-DESIGN §9): the
231
127
  * oracle injects a deterministic scheduler; production defaults to real timers + `Date.now`. */
232
128
  export interface FoldClock {
@@ -297,14 +193,13 @@ export interface OptimisticBackendOptions {
297
193
  /** Virtual-clock seam for the fold debounce timers (FOLDED-MUTATIONS-DESIGN §9). Defaults to
298
194
  * real `setTimeout`/`clearTimeout`/`Date.now`; the fold oracle injects a deterministic clock. */
299
195
  clock?: FoldClock;
300
- /** The explicit confirming-stream OVERRIDE (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.1 /
301
- * §3). Since H-iii the live default is the §3 prove-or-slow-path DERIVATION
302
- * ({@link OptimisticBackend.deriveDomain}): a configured policy returning a string pins that
303
- * domain VERBATIM — no proof runs (the test suites and the dual-source e2e pin by name);
304
- * returning `undefined` (or configuring no policy) derives the domain from the prediction run's
305
- * write/read capture against the connected room gates' routing table. With no room gate
306
- * connected the derivation short-circuits to `"daemon"` — every single-domain app is
307
- * byte-for-byte as before. */
196
+ /** The DECLARED confirming stream per mutation (302 §5: declared, not derived — there is no
197
+ * routing proof). A policy returning a string pins that domain verbatim: the mutation stages
198
+ * onto that room's namespaced tables and ships on its channel. Returning `undefined` (or
199
+ * configuring no policy) means `"daemon"`. The client layer builds this from the app's declared
200
+ * realtime mutators + the currently attached rooms; a misdeclaration fails SOFT (302 §5.1) —
201
+ * the write lands on the other authority's tables and the view simply stops feeling instant
202
+ * until the echo relays it. */
308
203
  domainPolicy?: (name: string, args: unknown) => string | undefined;
309
204
  /** A FINAL (authz/validation) mutation rejection's reason surface — the room plane's twin of the
310
205
  * HTTP mutate route's `onRejected` (H-v; the H-iv-b `mutationOutcome {kind:"rejected"}` frame).
@@ -442,15 +337,23 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
442
337
  * an evicted mid would be re-processed — the same bounded-window trade the shell makes, and it
443
338
  * takes 512 interleaving non-applied outcomes on one domain to open it. */
444
339
  private readonly outcomesProcessed;
445
- /** THE routing table (H-iii §3): per connected room `sourceKey` → per promoted table → its
446
- * client-held {@link RoomTableRouting}. Written ONLY by {@link promoteRoomTable} (same breath
447
- * as the engine attach + {@link promotedTables}); read by {@link deriveDomain} and — via
448
- * {@link roomTablesFor} the client's `__realtimeInspect` bookkeeping. */
449
- private readonly roomRouting;
450
- /** Q6 counters ({@link RoutingInspect}): successful derivations per room. */
451
- private readonly routingDerived;
452
- /** Q6 counters ({@link RoutingInspect}): derivation failures per {@link RoutingFailureReason}. */
453
- private readonly routingReasons;
340
+ /** THE room-table registry (302 §2 — one source per table): per connected room `sourceKey`, the
341
+ * wire-table engine-table map for the tables that room OWNS (its writable scope). Written by
342
+ * {@link registerRoomTables} (same breath as the engine registration); read by the gate's
343
+ * release rename/filter, the mutator staging map, the view swap ({@link processSwapIns}), and
344
+ * the client's `__realtimeInspect` bookkeeping. The record outlives a downgrade's disconnect —
345
+ * the ghost's views still read the engine tables — and drops at {@link dropGhost} (or the last
346
+ * clean release via {@link unregisterRoomTables}). */
347
+ private readonly roomTables;
348
+ /** Local view qids currently REGISTERED on a room's namespaced tables (302 §4 swap-in), →
349
+ * their sourceKey. Set by {@link processSwapIns}; cleared by the swap-back ({@link dropGhost})
350
+ * and view teardown. The original AST stays in {@link asts} throughout — the swap re-registers
351
+ * only the ENGINE query. */
352
+ private readonly roomSwappedViews;
353
+ /** Room subs whose FIRST snapshot released in the current release — their views swap onto the
354
+ * room tables at the release tail ({@link processSwapIns}), strictly AFTER the reconcile folded
355
+ * the snapshot into those tables (swapping earlier would hydrate the view EMPTY, a flash). */
356
+ private readonly pendingSwapIns;
454
357
  /** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
455
358
  * (FOLDED-MUTATIONS-DESIGN §8). Insertion order is creation order (the drain/flush tiebreak). */
456
359
  private readonly folds;
@@ -473,45 +376,6 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
473
376
  * scalars (`__inspect`) read it directly; its `sync` IS {@link sync} (the agg overlay and
474
377
  * synthetic tables are daemon-tracked by design). */
475
378
  private readonly daemonGate;
476
- /** Tables promoted to a MERGED multi-source engine (a room source attached). Written by the one
477
- * promotion seam ({@link promoteRoomTable}; the test-named {@link __addRoomSource} and Slice
478
- * G-v's lease-driven promotion from `RoomTableSpec[]` both flow through it). Read by the §7.3
479
- * hold-back trigger in
480
- * {@link applyRelease}: parking on a never-promoted (Collapsed) table is SKIPPED — inert anyway
481
- * today (`rewind_collapsed` never consults `held_back`), and a live hazard if the table later
482
- * promotes (a stale Collapsed-era entry turns overlay-first-visible and pins forever). Slice H's
483
- * prove-or-slow-path routing keeps the real scenario — a room-confirmed mutation writing a
484
- * Collapsed table — impossible; this gate covers the window until then. */
485
- private readonly promotedTables;
486
- /** The §301 pin registry ({@link EchoFencePin}): every parked §7.3 hold-back's delivery-fence
487
- * inputs, keyed `${table}\0${sourceKey}\0${pkKey}`. Written in the same breath as the
488
- * `holdBack`/`holdBackAbsent` park; read by {@link dropEchoFencePins} at the tail of every
489
- * release. Empty on every single-domain client — the drop pass is then a structural no-op. */
490
- private readonly pins;
491
- /** The §301 direction-A fence input: per room domain, the highest lmid the DAEMON-CARRIED
492
- * ledger rows have folded ({@link foldSystemFrames} step 2 — and ONLY that path: the room
493
- * socket's own lmid stream confirms long before the flush reaches the daemon, so it folds
494
- * into the shared {@link watermark} but never here, 301 §1.1). When this covers a pin's mid,
495
- * the same coherent release (or an earlier one) folded the flush data that carried it into
496
- * the daemon baseline — the fence. */
497
- private readonly daemonCarriedLmid;
498
- /** The §301 §2.4 boot rule's inputs: the daemon boot ids this client has OBSERVED, in order
499
- * (id → ordinal), plus the current one. Boot ids are opaque — ordering is the client's own
500
- * observation ({@link OptimisticSource.onBootId} on the daemon gate). Direction-B pins stamp
501
- * the current boot; a room advertising a LATER-observed boot proves absorption (its
502
- * re-snapshot came from a daemon state that durably includes the confirmed write). */
503
- private readonly daemonBootOrdinals;
504
- private daemonBootId;
505
- /** The per-read provenance probe `invoke` hands `trackingTx` when recording is armed (H-ii,
506
- * §3.2 #3): the engine's `provenanceOf` — the VISIBLE overlay-first winner for the row's pk,
507
- * read from LIVE pre-commit state (an open txn's staged writes are not consulted; H-i). Gated
508
- * on PER-TABLE {@link promotedTables} membership — the cheapest existing signal, and the
509
- * correct one: provenance is an ENGINE-merge question, not a channel question ({@link gates}
510
- * can hold a connected-but-unpromoted room feed, and the oracle harness promotes with no gate),
511
- * and a never-promoted (Collapsed) table is daemon-owned by construction. So a pure
512
- * single-domain app pays ZERO wasm calls per recorded read; the recorded `source` is then
513
- * `undefined`, which is also correct — everything is daemon-owned (see {@link ReadRecord}). */
514
- private readonly readProvenance;
515
379
  /** System retains by source qid ({@link retainSystemQuery}): a subscription with NO store view
516
380
  * and NO user-visible table — its frames buffer on its gate exactly like {@link LMID_QID}'s and
517
381
  * fold at RELEASE time ({@link foldSystemFrames}), never entering the sync layer or the local
@@ -590,6 +454,18 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
590
454
  * a domain may exist with no connected gate (`__testRelease` drives confirms gate-less); the
591
455
  * live production path stays daemon-only until G calls this. */
592
456
  connectSource(sourceKey: string, source: OptimisticSource): void;
457
+ /** Register the tables room `sourceKey` OWNS (its writable scope — 302 §2): each wire table
458
+ * gets its own namespaced ENGINE table (`{@link roomEngineTable}`), an ordinary tracked table
459
+ * whose sole authority is the room channel. From here on the channel's released deltas rename
460
+ * into these tables (wire tables outside the map are DROPPED — context stays daemon-owned,
461
+ * 302 §6), room-domain mutators stage onto them, and a room-homed view swaps onto them once
462
+ * the room sub hydrates ({@link processSwapIns}). Idempotent per (sourceKey, table); a wire
463
+ * table unknown to the schema is skipped (nothing to hold rows for). */
464
+ registerRoomTables(sourceKey: string, tables: readonly string[]): void;
465
+ /** The wire-table → engine-table map for room `sourceKey`'s owned tables (empty when none) —
466
+ * the client's idempotence check and `__realtimeInspect` read THIS record (one source of
467
+ * truth; the client keeps no shadow copy). */
468
+ roomTablesFor(sourceKey: string): ReadonlyMap<string, string>;
593
469
  /** `channel` (G-iii registration-time routing) names the authority channel the remote sub
594
470
  * registers on — a `connectSource`d gate key; default `"daemon"` (every existing caller is
595
471
  * byte-identical). Slice G-v threads the lease's `realtime.sourceKey` here. Validated FIRST
@@ -632,17 +508,15 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
632
508
  * precedent), flip `sub.channel`, re-arm `sub.hydrated` (the room's own snapshot is the
633
509
  * cutover point), and register on the room source (its resolver presents the handed
634
510
  * roomToken). The old gate's SYNC rows are deliberately NOT dropped: they keep the view's
635
- * rows visible through the window (merge: the daemon-tier holder). Local views keep their
636
- * `hydrated` membership the view stays `complete` on its authoritative daemon rows.
637
- * 2. AT THE ROOM'S FIRST RELEASED SNAPSHOT ({@link flushRetargetGc}): the room source now
638
- * holds the footprint rows (folded via `serverBatchBegin(sourceKey)` value-equal rows
639
- * re-hydrate as a net-zero diff, RT §3.4 per channel), so the deferred
640
- * `dropQuery`+reconcile on the OLD gate flips each pk's winner daemon→room value-equal:
641
- * net-zero again. No emission carries a disappearance at any point.
511
+ * plain tables populated through the window the view still reads them until the swap.
512
+ * 2. AT THE ROOM'S FIRST RELEASED SNAPSHOT: the reconcile folds the snapshot into the room's
513
+ * namespaced tables, the release tail SWAPS every local view onto them (302 §4.1,
514
+ * {@link processSwapIns} the accepted-flash boundary), and {@link flushRetargetGc}'s
515
+ * deferred `dropQuery`+reconcile on the OLD gate then GCs the plain-table rows the sub
516
+ * alone referenced invisible to the swapped views.
642
517
  *
643
518
  * Idempotent per target channel: a sub already on `sourceKey` returns immediately (the
644
- * double-doorbell / re-entrancy guard — one retarget per (query, sourceKey), mirroring
645
- * {@link promoteRoomTable}'s caller-side per-(sourceKey, table) idempotence). Validates before
519
+ * double-doorbell / re-entrancy guard — one retarget per (query, sourceKey)). Validates before
646
520
  * mutating: a throw here leaves the sub fully daemon-attached (the client's fail-open). */
647
521
  retargetRemoteQuery(remote: RemoteQuery, sourceKey: string): QueryId;
648
522
  /** Phase 2 of {@link retargetRemoteQuery}, run at the end of every gate release: once a
@@ -653,35 +527,24 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
653
527
  * mid-window was already swept by `releaseRemoteQuery`/`unregisterQuery` (which delete the
654
528
  * record); a vanished record here is pruned defensively. */
655
529
  private flushRetargetGc;
656
- /** The I-v downgrade orchestration primitive (§4.2/§7.4): retire room `sourceKey` behind the
657
- * watermark fence. The caller has ALREADY retargeted every live sub off the channel
658
- * ({@link retargetRemoteQuery} room→daemon — validated loudly below) and holds the fence from
659
- * the api-server's downgrade response (`finalFlushSeq` = the room's last COMMITTED flush seq;
660
- * `doc` keys the §4.2 watermark fold, {@link roomWatermarks}). Steps, in order:
530
+ /** The I-v downgrade orchestration primitive (§4.2/§7.4, re-expressed by 302 §4.2 as the
531
+ * SWAP-BACK GATE): retire room `sourceKey` behind the watermark fence. The caller has ALREADY
532
+ * retargeted every live sub off the channel ({@link retargetRemoteQuery} room→daemon —
533
+ * validated loudly below) and holds the fence from the api-server's downgrade response
534
+ * (`finalFlushSeq` = the room's last COMMITTED flush seq; `doc` keys the §4.2 watermark fold,
535
+ * {@link roomWatermarks}). Steps, in order:
661
536
  *
662
- * 1. **De-candidacy NOW**: the sourceKey's {@link roomRouting} entries are removed, so
663
- * {@link deriveDomain} stops proposing the room (zero candidates ⇒ `"daemon"`). Unsent
664
- * pending (`mid === null`) re-routes for free — a fold's flush re-derives from the live
665
- * candidate set (§7.5 rule 1). The engine's frozen scope still ANSWERS (`writableMatches`
666
- * / `provenanceOf` — freezing gates serving, not the scope definition); routing is
667
- * TS-gated here.
668
- * 2. **Freeze** the WRITABLE promoted tables (`freezeSource`): the room slice becomes the
669
- * §4.2 ghost — wins-if-present in the merge (D4), so its rows (at-or-ahead of the daemon
670
- * until the flush echoes) stay visible while the slice accepts nothing. Context tables
671
- * (`writable: false`) are deliberately NOT frozen: the daemon is authoritative for them
672
- * LIVE (§5.2's context tier) and freezing would invert that, pinning a possibly-BEHIND
673
- * relayed copy over fresher daemon rows; unfrozen they keep exactly the live tiering.
674
- * 3. **Disconnect** the channel ({@link disconnectSource}): handlers detached, gate + buffer
537
+ * 1. **Disconnect** the channel ({@link disconnectSource}): handlers detached, gate + buffer
675
538
  * dropped. `nextMid`/`watermark`/processed-outcomes for the domain are KEPT FOREVER (§7.1:
676
539
  * an assigned mid pins its domain; a later re-upgrade of the same doc continues the
677
540
  * sequence — {@link connectSource} attaches a fresh gate and the lmid snapshot max-folds
678
541
  * into the surviving watermark). Disconnecting BEFORE the daemon sub's first release is
679
542
  * load-bearing: it makes {@link flushRetargetGc}'s deferred old-channel GC a no-op (gate
680
- * gone ⇒ record deleted, nothing dropped) running that GC would rewind the room slice's
681
- * rows at daemon hydration, i.e. BEFORE the fence, surfacing a lagging follower's stale
682
- * images (the exact regression §4.2 exists to prevent). The ghost's rows leave only
683
- * through {@link dropGhost}'s `removeRoomSource`, value-equal under the fence.
684
- * 4. **Ghost + first evaluation**: the record joins {@link ghosts} and is evaluated once
543
+ * gone ⇒ record deleted, nothing dropped). The room's namespaced tables and the views
544
+ * swapped onto them deliberately stay: frozen at the room's last state, they keep the
545
+ * document visible while the falling-back follower may still lack the final flush.
546
+ * Swapping back earlier would show its pre-flush images the regression §4.2 prevents.
547
+ * 2. **Ghost + first evaluation**: the record joins {@link ghosts} and is evaluated once
685
548
  * immediately — `finalFlushSeq === 0` (a never-flushed room) with no room-domain pending
686
549
  * drops on the spot, the single-daemon first-frame case.
687
550
  *
@@ -714,16 +577,20 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
714
577
  * HOLDS and the stuck event fires exactly once, naming the mids). Both satisfied ⇒
715
578
  * {@link dropGhost}. */
716
579
  private evaluateGhosts;
717
- /** Drop one cleared ghost (§4.2 end state): detach every promoted table's room source (D2
718
- * stay-merged `removeRoomSource` reconciles each held pk to daemon-or-vanish, value-equal
719
- * under the fence, so visually a no-op), release the {@link promotedTables} record for tables
720
- * no OTHER room/ghost still holds, then run ONE daemon reconcile so entries whose writes had
721
- * staged onto the ghost slice re-invoke and re-stage by provenance onto the daemon slice (the
722
- * removal took their staged copies with the tree; provenance now answers daemon). The whole
723
- * drop runs under one commit boundary so the removal's compensating deltas and the re-staged
724
- * predictions notify as ONE step. After this, a FUTURE upgrade of the same doc promotes
725
- * again from scratch the round trip is pinned by the §8.5 lanes. */
580
+ /** Drop one cleared ghost — the 302 §4.2 SWAP-BACK: under the fence the daemon tables are
581
+ * value-equal-or-ahead of the room's final state, so (1) every view swapped onto the room's
582
+ * namespaced tables re-registers on its ORIGINAL (daemon-table) AST — visually a no-op, the
583
+ * Store folds the re-hello as an in-place reset; (2) the namespaced tables unregister (no
584
+ * reader is left after the swap); (3) ONE daemon reconcile re-invokes the pending set so any
585
+ * entry whose writes had staged onto the now-gone room tables re-stages onto the daemon tables
586
+ * (its domain policy stopped naming the dead room when the client dropped it). The whole drop
587
+ * runs under one commit boundary so the swap and the re-staged predictions notify as ONE step.
588
+ * After this, a FUTURE upgrade of the same doc registers again from scratch. */
726
589
  private dropGhost;
590
+ /** Unregister room `sourceKey`'s namespaced engine tables and drop the {@link roomTables}
591
+ * record. Callers must have no view registered on them (the engine refuses otherwise —
592
+ * loud by design). No-op for an unknown sourceKey. */
593
+ unregisterRoomTables(sourceKey: string): void;
727
594
  /** Retain one minted SYSTEM subscription (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §4, Slice
728
595
  * I-iii): a wire sub with NO store view and NO user-visible table. Registered through the same
729
596
  * {@link RemoteSub} bookkeeping as any remote retain — so qid→channel ownership, the overflow
@@ -802,30 +669,30 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
802
669
  * and takes only the fresh mid (the dealSeq bump is harmless: seq consumers order, never
803
670
  * count). */
804
671
  private dealMid;
805
- /** The route decision for one invocation (invoke, and again at fold flush): the explicit
806
- * `domainPolicy` is the OVERRIDE a string pins that domain verbatim, no proof runs;
807
- * `undefined` (or no policy configured) derives per §3. */
672
+ /** The declared confirming stream for one invocation: the `domainPolicy`'s verdict, `"daemon"`
673
+ * when it abstains. Resolved BEFORE the prediction runs the domain picks the staging map
674
+ * (a room domain stages its owned tables onto the room's namespaced twins). */
808
675
  private resolveDomain;
809
- /** §9.3: does this fold's write-set route into a room (vs the daemon)? Used ONLY to pick the
810
- * {@link FoldOptions.roomDebounceMs} cadence at a fold window's start. It probes
811
- * {@link resolveDomain} but RESTORES the Q6 routing diagnostics afterward the authoritative
812
- * route and its single counter bump belong to the flush ({@link flushFold}), not to this
813
- * interval hint. A fold's reads are provably empty (the read-trap arms on that path), so the
814
- * write-set alone decides. */
815
- private routesToRoom;
816
- /** Count one derivation failure ({@link RoutingInspect}). Returns `false` so the per-candidate
817
- * proof's call sites read `return this.failDerivation(...)`. */
818
- private failDerivation;
819
- /** The §3 derivation. Candidates are the connected room gates with at least one promoted table
820
- * (a promoted table with no gate cannot confirm; a gate with no promoted table holds no data to
821
- * prove against). Zero candidates `"daemon"` (the single-domain fast path). The proof runs
822
- * per candidate and EXACTLY ONE must survivetwo rooms both proving routes slow (principled
823
- * disambiguation is a §9.2 multi-room NON-goal, deferred past Slice J; slow is always sound). */
824
- private deriveDomain;
825
- /** One candidate room's §3 proof over the captured write-set + read-log. Every write must pass
826
- * ALL the write rules; every read must pass ONE of the read rules. First failure wins (and is
827
- * counted); order is deterministic (write-set map order, then read-log order). */
828
- private provesRoom;
676
+ /** The staging table map for a `domain`-routed prediction ({@link trackingTx}'s `stage`):
677
+ * wire table the room's namespaced engine table for the tables the room owns; identity for
678
+ * everything else (including the whole map for the daemon domain). */
679
+ private stagingMap;
680
+ /** The PLAIN (daemon-homed) engine AST for `ast` aggregate relationships rewritten to their
681
+ * synthetic `__agg_*` reads, no room renames. The ONE form every non-swapped engine
682
+ * registration uses ({@link registerQuery}, {@link dropGhost}'s swap-back) and the base the
683
+ * swap-in renames ({@link processSwapIns}). */
684
+ private plainEngineAst;
685
+ /** Mutator names the cross-authority warn below already fired for (once per name). */
686
+ private readonly warnedCrossAuthority;
687
+ /** 302 §5.1 dev-time guard: a room-DECLARED mutator wrote tables the room does not own. Those
688
+ * writes staged onto the PLAIN daemon tables (the staging map covers only owned tables), but
689
+ * the entry confirms on the ROOM streamand only the room's OWNED tables flush back to the
690
+ * daemon, so nothing upstream ever echoes them: once the room confirm retires the entry, the
691
+ * next release's whole-store rewind reverts them for good. The first-party room shell refuses
692
+ * such a mutation (the §3.3 deopt/reject backstop re-routes it to the daemon), so this warns
693
+ * for the shapes where that backstop may be absent (a BYO relay) loud, once, soft (§5.1:
694
+ * misdeclarations never throw). */
695
+ private warnCrossAuthorityWrites;
829
696
  /** Run the named client mutator optimistically: the prediction applies to the live
830
697
  * engine now (affected views update synchronously), `(mid, name, args)` joins the
831
698
  * pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
@@ -951,54 +818,32 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
951
818
  nextMid: Record<string, number>;
952
819
  watermark: Record<string, number>;
953
820
  /** Per connected CHANNEL (§5.1): its release watermark + buffered-frame depth — the axis the
954
- * gate-isolation assertions read (one source's laggy cvMin must never move the other's)
955
- * plus the §301 upstream-absorption advert last recorded from its progress frames (a ROOM
956
- * gate with a new shell; absent otherwise). */
821
+ * gate-isolation assertions read (one source's laggy cvMin must never move the other's). */
957
822
  gates: Record<string, {
958
823
  appliedCv: number;
959
824
  bufferedFrames: number;
960
- upstreamCv?: number;
961
- upstreamBoot?: string;
962
825
  }>;
963
- /** The §3 router's counters (H-iii) Q6's measurement hook: how often derivation succeeded
964
- * (per derived room) and why it fell to the daemon (per reason). See {@link RoutingInspect}
965
- * for the counting discipline (per-candidate failures; pins bump nothing). */
966
- routing: RoutingInspect;
826
+ /** Per connected/registered room: its wire-table engine-table map (302 §2) and which local
827
+ * view qids are currently swapped onto it (302 §4). */
828
+ roomTables: Record<string, Record<string, string>>;
829
+ swappedViews: Record<number, string>;
967
830
  /** The §4 lifecycle plane's folded state (Slice I-iii introspection): the per-doc §4.2 fence
968
831
  * value (`roomWatermarks`, I-v's ghost-drop input), the per-scope §4.1 occupancy map
969
832
  * (`scopeSessions`: scope → client_id → expires_at, I-iv's doorbell input), and the live
970
- * I-v ghosts (demoted room sources still awaiting their fence). */
833
+ * I-v ghosts (demoted room sources still awaiting their swap-back fence). */
971
834
  lifecycle: {
972
835
  roomWatermarks: Record<string, number>;
973
836
  scopeSessions: Record<string, Record<string, number>>;
974
837
  ghosts: Record<string, {
975
838
  doc: string;
976
839
  finalFlushSeq: number;
977
- tables: string[];
978
840
  }>;
979
841
  };
980
- /** The live §301 pin registry (`301-ECHO-FENCE-DESIGN.md` §2.5 — the devtools/Q6 surface):
981
- * every parked hold-back's fence inputs plus its tripwire state, keyed
982
- * `${table}\0${sourceKey}\0${pkKey}`. Pruned lazily, so an entry here may briefly outlive
983
- * its engine pin (never the reverse). */
984
- pins: Record<string, {
985
- table: string;
986
- sourceKey: string;
987
- domain: string;
988
- mid: number;
989
- daemonBoot?: string;
990
- daemonCv?: number;
991
- tripwired: boolean;
992
- }>;
993
- /** The §301 direction-A fence map: per room domain, the highest DAEMON-CARRIED ledger lmid. */
994
- daemonCarriedLmid: Record<string, number>;
995
842
  pending: {
996
843
  mid: number | null;
997
844
  seq: number | null;
998
845
  name: string;
999
846
  domain: string;
1000
- touchedSources: string[];
1001
- writeSources: Record<string, string>;
1002
847
  }[];
1003
848
  };
1004
849
  /** Recompute the pending axis for every query and fire `onPending` on transitions only. Called
@@ -1036,50 +881,18 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
1036
881
  * exercise a room-domain confirm before the real second lmid stream / per-source gate is wired
1037
882
  * (E-iii-b/c). The `__`-prefix marks it a test hook, alongside {@link __inspect}. */
1038
883
  __testRelease(sourceKey: string, deltas: Mutation[], watermarkUpdate?: number): void;
1039
- /** Register (or overwrite a later cross-slice confirm re-pinning the same slice-pk matches
1040
- * the engine's `held_back.insert`) one {@link EchoFencePin}, in the same breath as the
1041
- * engine park (§2.1). `p` is the confirmed entry whose write staged onto `sourceKey`; its
1042
- * `mid` is non-null by the caller's confirm filter. */
1043
- private registerPin;
1044
- /** The §2.3 drop pass, run at the tail of every applied release: prune registry entries whose
1045
- * engine pin is already gone (the rewind's state-match fallback or a whole-source removal beat
1046
- * the fence — benign), drop every pin whose fence cleared (one engine drop each, delivered on
1047
- * the ordinary event stream, bracketed as ONE notification commit), and tripwire the rest. */
1048
- private dropEchoFencePins;
1049
- /** Has `pin`'s delivery fence provably closed its confirm→echo window? (§2.3/§2.4.) */
1050
- private pinFenceCleared;
1051
- /** The §2.5 stuck-pin tripwire, in the scopesHash spirit: log ONCE per pin when its slice's
1052
- * baseline row has CHANGED VALUE since park while the pin still holds — the suspicious state
1053
- * that precedes every forever-pin (a fence-less pairing, a fence bug). Never drops anything. */
1054
- private maybeTripwirePin;
1055
- /** Record one observed daemon boot id (§2.4): first observation of an id assigns the next
1056
- * ordinal (the client's own total order over opaque boot ids); every call refreshes the
1057
- * current-boot stamp for direction-B parks. */
1058
- private observeDaemonBoot;
1059
- /** Promote `table` to a MERGED multi-source engine with room `sourceKey`'s per-row writable
1060
- * scope (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §5.2) — THE one promotion seam. The engine
1061
- * attach and the {@link promotedTables} record move in the same breath: the §7.3 hold-back
1062
- * trigger in {@link applyRelease} parks ONLY on promoted tables, so a promotion that bypassed
1063
- * the record would silently disable the echo hold-back for that table (and a record without the
1064
- * engine attach would park onto a slice that doesn't exist). Recorded AFTER the engine accepts —
1065
- * a rejected descriptor/table must not leave a phantom promotion. Slice G-v's client drives this
1066
- * from the lease's `realtime.tables` (`RoomTableSpec` → {@link WritableDescriptor}); idempotence
1067
- * per `(sourceKey, table)` is the CALLER's job (the engine refuses a duplicate room).
884
+ /** Swap every view of each just-hydrated ROOM sub onto the room's namespaced tables (302 §4.1):
885
+ * re-register the local engine query with the AST's room-owned table references renamed
886
+ * ({@link remapAstTables}); the Store folds the re-hello as an in-place reset, so the caller's
887
+ * view reference survives and subscribers see ONE transition. Runs at the applyRelease tail —
888
+ * the reconcile has already folded the sub's snapshot into the room tables, so the swapped
889
+ * view hydrates straight to the room state (swapping earlier would flash it empty). The
890
+ * ORIGINAL ast stays in {@link asts}; the swap-back ({@link dropGhost}) re-registers it.
1068
891
  *
1069
- * `spec` (H-iii) is the per-table ROUTING spec riding the same lease table block recorded
1070
- * into {@link roomRouting} (THE routing table the §3 router reads) in the same breath, with
1071
- * `writable` derived from the descriptor's kind (`none` = a context table the room may not
1072
- * write). Omitted (the E-iii-b harness alias below) ⇒ an empty spec: no join keys to guard, no
1073
- * `footprintWhere` (that room's reads then fail closed to the daemon unless self/room-served). */
1074
- promoteRoomTable(table: string, sourceKey: string, writable: WritableDescriptor, spec?: RoomTableRoutingSpec): void;
1075
- /** The recorded routing specs for `sourceKey`'s promoted tables (table → its
1076
- * {@link RoomTableRouting}) — read-only: the client's `__realtimeInspect`/idempotence
1077
- * bookkeeping reads THIS record instead of keeping its own shadow copy (one source of truth).
1078
- * Empty map when the room has promoted nothing. */
1079
- roomTablesFor(sourceKey: string): ReadonlyMap<string, RoomTableRouting>;
1080
- /** Test-named alias of {@link promoteRoomTable} (E-iii-b scaffolding — the multi-domain oracle
1081
- * and per-source-gate suites drive it). Pure delegation: ONE body, one `promotedTables` record. */
1082
- __addRoomSource(table: string, roomKey: string, writable: WritableDescriptor, spec?: RoomTableRoutingSpec): void;
892
+ * This is the accepted-flash boundary (302 §4.1/§7.1): the room's copy may be behind the
893
+ * daemon rows the view showed a moment ago accepted by decision, revisit on a real
894
+ * two-region deploy. */
895
+ private processSwapIns;
1083
896
  /** Fold `domain`'s lmid system query's released ops (lmid-as-data): the one row's
1084
897
  * `last_mutation_id` cell is this client's confirmed high-water mid in that domain — it advances
1085
898
  * `watermark[domain]` and, on a fresh session ahead of our issued mids, `nextMid[domain]`. The
@@ -1177,8 +990,10 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
1177
990
  * longer affects it. Used when a remote sub attaches to or hydrates a local view. */
1178
991
  private recomputeResultType;
1179
992
  /** A remote sub's first snapshot landed: mark it (and every local view it feeds) hydrated, then
1180
- * lift those views out of `unknown` (loading). Idempotent a re-hydrate snapshot re-marks
1181
- * harmlessly; a source qid with no sub (the lmid system query) is a no-op. */
993
+ * lift those views out of `unknown` (loading). A ROOM sub's hydration additionally queues the
994
+ * 302 §4.1 swap-in performed at the applyRelease TAIL ({@link processSwapIns}), once the
995
+ * reconcile has folded this snapshot into the room tables. Idempotent — a re-hydrate snapshot
996
+ * re-marks harmlessly; a source qid with no sub (the lmid system query) is a no-op. */
1182
997
  private markSubHydrated;
1183
998
  /** `channel` (G-iii registration-time routing): the gate the sub registers on — the qid's
1184
999
  * ownership is fixed HERE, at retain time (no lazy claim; `onFrame` only asserts it). Default
@@ -1187,19 +1002,17 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
1187
1002
  private releaseRemote;
1188
1003
  private addServerDependencyTables;
1189
1004
  }
1190
- /** Whether `cond` is decidable from the KEY ALONE: every column it references collected from
1191
- * BOTH operand positions (under-counting could over-claim decidability; over-counting only fails
1192
- * closed the H-iv-a discipline) is a pk column. A `correlatedSubquery` is never key-local. */
1193
- export declare function keyDecidable(cond: Condition, pkCols: ReadonlySet<string>): boolean;
1194
- /** Evaluate a key-decidable `footprintWhere` on a read's pk cells — H-iii's ONE client-side
1195
- * non-engine evaluator, DELIBERATELY MINIMAL (the TS evaluation caveat on the router block
1196
- * comment): `simple` `=`/`!=` between a pk COLUMN and a same-primitive-type NON-NULL literal,
1197
- * composed under and/or. The empty AND the compiler's vacuous-true emission for an exact
1198
- * unconstrained footprint root evaluates `true` (load-bearing: whole-table footprints keep
1199
- * provable reads); the empty OR is vacuous-false. Returns `undefined` = NOT EVALUABLE for
1200
- * anything else (other ops, null on either side, cross-type comparisons, column-vs-column,
1201
- * literal-vs-literal, non-primitive cells, correlated subqueries) the caller fails to the
1202
- * daemon. A non-evaluable node anywhere poisons the whole tree (no short-circuit past it):
1203
- * partial evaluation could otherwise claim a verdict the engine's semantics might contradict. */
1204
- export declare function evalFootprintOnPk(cond: Condition, pkCells: ReadonlyMap<string, WireValue>): boolean | undefined;
1005
+ /** The namespaced ENGINE table backing wire `table` for room `sourceKey` (302 §2: `room_deck`
1006
+ * `deck` one authority per table). `@` appears in no schema table name — ENFORCED by
1007
+ * `createSchema`/`extendSchema`'s addTableMeta ban (packages/client/src/schema.ts), so the name
1008
+ * cannot collide with a real table. */
1009
+ export declare function roomEngineTable(table: string, sourceKey: string): string;
1010
+ /** Rename every TABLE reference in a query AST through `map` (302 §2 point 3 the room-homed
1011
+ * view's rewrite): the root `table`, every `related` subquery, every `correlatedSubquery`
1012
+ * (EXISTS) condition walking the KNOWN wire-AST shape, never a blind key scan: `start.row` is
1013
+ * keyed by COLUMN name (a schema column literally named `table` must keep its bound value), and
1014
+ * the same goes for any future column-keyed record. Tables absent from the map keep their name —
1015
+ * that is the client-side join across kinds (a room table joined to daemon-owned context,
1016
+ * 201-style). Structural clone; the input AST is never mutated. */
1017
+ export declare function remapAstTables(ast: Ast, map: ReadonlyMap<string, string>): Ast;
1205
1018
  //# sourceMappingURL=backend.d.ts.map