@rindle/optimistic 0.4.4 → 0.6.3
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/README.md +1 -1
- package/dist/backend.d.ts +696 -27
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +1632 -204
- package/dist/backend.js.map +1 -1
- package/dist/client-id.d.ts +5 -0
- package/dist/client-id.d.ts.map +1 -1
- package/dist/client-id.js +45 -0
- package/dist/client-id.js.map +1 -1
- package/dist/client.d.ts +169 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +887 -6
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/system-streams.d.ts +62 -0
- package/dist/system-streams.d.ts.map +1 -0
- package/dist/system-streams.js +107 -0
- package/dist/system-streams.js.map +1 -0
- package/package.json +6 -6
- package/src/backend.ts +1926 -203
- package/src/client-id.ts +44 -0
- package/src/client.ts +1239 -13
- package/src/index.ts +38 -1
- package/src/system-streams.ts +140 -0
package/dist/backend.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { Ast, Backend, BackendDevObserver, ChangeEvent, ColsMap, IsoTx, KeyedRow, Mutation, MutationGen, MutatorCtx, OptimisticSource, QueryArg, QueryId, QueryResultRow, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
|
|
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";
|
|
2
|
+
import { type SystemStreamSpec } from "./system-streams.ts";
|
|
3
|
+
export type { SystemStreamSpec, SystemStreamTable } from "./system-streams.ts";
|
|
2
4
|
/** A keyed row: column name → cell. The ergonomic shape — column names are validated against the
|
|
3
5
|
* schema at runtime, so a typo throws immediately with the valid names. Re-exported from
|
|
4
6
|
* `@rindle/client` (the leaf both tiers share). */
|
|
@@ -52,6 +54,75 @@ export type ClientMutator = ((tx: MutationTx, args: never) => void) | ((tx: IsoT
|
|
|
52
54
|
* twin shares names (and possibly code), never the wire. */
|
|
53
55
|
export type ClientRegistry = Record<string, ClientMutator>;
|
|
54
56
|
export type { ResultType };
|
|
57
|
+
/** One captured write, pk-granular (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §3.2 #1: "the
|
|
58
|
+
* writers already receive `(table, row)`... this is pure capture, no semantic change"). `row` is
|
|
59
|
+
* the post-write image (positional wire cells, schema column order); `undefined` for a `remove` —
|
|
60
|
+
* no row survives it, and `row === undefined` stays the remove marker.
|
|
61
|
+
*
|
|
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 through — falling 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.
|
|
69
|
+
*
|
|
70
|
+
* Coalescing within one invocation is last-write-wins per pk on `row` (the final image matches
|
|
71
|
+
* the engine head's own semantics for that pk) with `oldRow` pinned to the TXN-ENTRY BASE — the
|
|
72
|
+
* H-ii matrix:
|
|
73
|
+
* - edit-after-add / edit-after-remove: the record collapses to a (re-)insert — post-image
|
|
74
|
+
* only, NO `oldRow` (the pk did not pre-exist this invocation's base; presence hold-back
|
|
75
|
+
* uses `row`).
|
|
76
|
+
* - edit-after-edit: keeps the FIRST pre-image (the txn-entry base — the chain nets to ONE
|
|
77
|
+
* edit from the base to the final image).
|
|
78
|
+
* - remove-after-edit / remove-after-remove: keeps the ORIGINAL pre-image (the first write's
|
|
79
|
+
* captured base), NOT the edited transient — the net effect is a remove of the row the
|
|
80
|
+
* external world last knew.
|
|
81
|
+
* - remove-after-add: keeps the txn-visible pre-image (the transient added row — the pk had
|
|
82
|
+
* no base, and this is the only truthful full-width row there is; G-iii pinned it).
|
|
83
|
+
* - add-after-remove (a re-insert): drops `oldRow` (presence hold-back uses `row`).
|
|
84
|
+
* Across rebase re-invocations the write-set is union-never-shrink ({@link mergeWriteSet}): a
|
|
85
|
+
* re-run that no-ops keeps the prior record — and its pre-image — intact. */
|
|
86
|
+
export interface WriteRecord {
|
|
87
|
+
table: string;
|
|
88
|
+
pk: WireValue[];
|
|
89
|
+
row: WireValue[] | undefined;
|
|
90
|
+
oldRow?: WireValue[];
|
|
91
|
+
}
|
|
92
|
+
/** The pk-granular write-set captured over ONE mutator invocation: table → pk-key (a stable-JSON
|
|
93
|
+
* encoding of the pk cells, {@link stableJson}) → that pk's write, LAST-WRITE-WINS within the
|
|
94
|
+
* invocation — an add-then-edit or edit-then-edit of the SAME pk collapses to its final image,
|
|
95
|
+
* matching the engine head's own semantics for that pk. Chosen (over a flat array) because the
|
|
96
|
+
* later routing proof needs "is pk P in the writable scope" / "did we already see this pk in this
|
|
97
|
+
* invocation" as cheap lookups, and rebase re-invocation needs to MERGE a fresh write-set into an
|
|
98
|
+
* accumulated one ({@link mergeWriteSet}) — both are Map operations, not scans.
|
|
99
|
+
*
|
|
100
|
+
* `touched` (the pre-existing table-granular `Set<string>` the pending axis reads, §7.2) is
|
|
101
|
+
* exactly `new Set(writeSet.keys())` — derived from this, never separately populated, so the two
|
|
102
|
+
* can never drift. */
|
|
103
|
+
export type WriteSet = Map<string, Map<string, WriteRecord>>;
|
|
104
|
+
/** Whether a recorded point read (`tx.get`/`tx.row`) found a row. */
|
|
105
|
+
export type ReadOutcome = "present" | "absent";
|
|
106
|
+
/** One recorded point read, pk-granular (§3.2 #2). Recording-mode only — see {@link ReadLog}.
|
|
107
|
+
* Since H-ii this covers BOTH the public reads (`tx.get`/`tx.row`) and the keyed writers'
|
|
108
|
+
* internal pre-existence probes (§3.2 #3 — see the `rawGet` note in {@link trackingTx}). */
|
|
109
|
+
export interface ReadRecord {
|
|
110
|
+
table: string;
|
|
111
|
+
pk: WireValue[];
|
|
112
|
+
outcome: ReadOutcome;
|
|
113
|
+
}
|
|
114
|
+
/** The read-log captured over ONE mutator invocation when recording is armed (RINDLE-REALTIME-
|
|
115
|
+
* QUERY-ENABLEMENT-DESIGN.md §3.2 #2): every point read (`reads` — the public `tx.get`/`tx.row`
|
|
116
|
+
* and, since H-ii, the keyed writers' internal pre-existence probes) plus every resolved query AST
|
|
117
|
+
* (`queries`, from `tx.query`). A SIBLING of the folded read TRAP (`FoldReadError` below) — the
|
|
118
|
+
* trap arms on the folded path and throws before any read completes (recording never runs there);
|
|
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). */
|
|
122
|
+
export interface ReadLog {
|
|
123
|
+
reads: ReadRecord[];
|
|
124
|
+
queries: Ast[];
|
|
125
|
+
}
|
|
55
126
|
/** A virtual-clock seam for the fold debounce/maxWait timers (FOLDED-MUTATIONS-DESIGN §9): the
|
|
56
127
|
* oracle injects a deterministic scheduler; production defaults to real timers + `Date.now`. */
|
|
57
128
|
export interface FoldClock {
|
|
@@ -69,6 +140,15 @@ export interface FoldOptions {
|
|
|
69
140
|
/** Hard cap so a never-idle drag still persists periodically (trailing throttle). Unbounded if
|
|
70
141
|
* omitted — an idle gap of `debounceMs` is then the only thing that flushes. */
|
|
71
142
|
maxWaitMs?: number;
|
|
143
|
+
/** §9.3 room-aware cadence. When this fold's write ROUTES INTO A ROOM (a collaborator is live on
|
|
144
|
+
* the shared head), flush at this (short) interval instead of `debounceMs`/`maxWaitMs`, so the
|
|
145
|
+
* intermediate frames STREAM to the room rather than collapsing to last-value-wins — the pen is
|
|
146
|
+
* watched, so its growth matters. OFF the room (solo / daemon-served) this is ignored and the
|
|
147
|
+
* caller's `debounceMs` collapse governs. `0` ⇒ per-frame (never coalesce while in a room); a
|
|
148
|
+
* small value (e.g. 40ms) animates while still capping the write rate. Absent ⇒ same cadence
|
|
149
|
+
* room or not (today's behavior). The room decision is probed from the write-set at the fold
|
|
150
|
+
* window's first invoke; it stays fixed for that window. */
|
|
151
|
+
roomDebounceMs?: number;
|
|
72
152
|
/** Keep deferring across overlapping non-fold writes for maximum economy, accepting the §4.2
|
|
73
153
|
* read-dependent reorder snap. Default `false` (flush-on-enqueue — correct-and-boring). */
|
|
74
154
|
deferAcrossWrites?: boolean;
|
|
@@ -80,6 +160,39 @@ export interface FoldHandle {
|
|
|
80
160
|
flush(): void;
|
|
81
161
|
readonly mid: Promise<number>;
|
|
82
162
|
}
|
|
163
|
+
/** One I-iv doorbell event ({@link OptimisticBackend.onScopeSessions}, §4.1): a release folded
|
|
164
|
+
* scope-session rows for `scope`, and `others` is the count of OTHER clients' unexpired sessions
|
|
165
|
+
* there — {@link OptimisticBackend.otherScopeSessions} evaluated at fold time (the same one rule,
|
|
166
|
+
* on the injectable {@link FoldClock}, so a virtual-clock harness gets deterministic verdicts).
|
|
167
|
+
* The consumer (client.ts) triggers its one debounced re-lease on the 0→≥1 transition; expired
|
|
168
|
+
* and own-clientID rows never count, so a solo client's own row can never ring its own bell. */
|
|
169
|
+
export interface ScopeSessionsEvent {
|
|
170
|
+
scope: string;
|
|
171
|
+
others: number;
|
|
172
|
+
}
|
|
173
|
+
/** The I-v stuck-downgrade event ({@link OptimisticBackend.onDowngradeStuck}): the ghost's fence
|
|
174
|
+
* is satisfied but these SENT room-domain mids never resolved (an entry that never reached the
|
|
175
|
+
* room — sent-but-undelivered when the socket died — is undecidable in general, §7.5). The ghost
|
|
176
|
+
* HOLDS (fail LOUD, never silent; no timeout-retire is invented) and the mids are surfaced once,
|
|
177
|
+
* actionably. */
|
|
178
|
+
export interface DowngradeStuckEvent {
|
|
179
|
+
sourceKey: string;
|
|
180
|
+
doc: string;
|
|
181
|
+
mids: number[];
|
|
182
|
+
}
|
|
183
|
+
/** The 302 §6.1 context-coverage event ({@link OptimisticBackend.onRoomContextJoin}): a view
|
|
184
|
+
* swapping onto room `sourceKey`'s namespaced tables still references `tables` the room does NOT
|
|
185
|
+
* own — those refs keep reading the PLAIN daemon tables (the client-side join across kinds), and
|
|
186
|
+
* the room's relayed copies of them are dropped by design (§6). Whether a daemon subscription
|
|
187
|
+
* covers the joined rows is unknowable here, so the condition is surfaced ONCE per view: without
|
|
188
|
+
* coverage the join renders silently empty for the whole room session. Fired at swap-in — a view
|
|
189
|
+
* whose every referenced table is room-owned (every in-repo app today) never fires it. */
|
|
190
|
+
export interface RoomContextJoinEvent {
|
|
191
|
+
sourceKey: string;
|
|
192
|
+
name: string;
|
|
193
|
+
args: unknown;
|
|
194
|
+
tables: string[];
|
|
195
|
+
}
|
|
83
196
|
export interface OptimisticBackendOptions {
|
|
84
197
|
/** Stable per-client identity for the upstream envelopes (§8.1). */
|
|
85
198
|
clientID: string;
|
|
@@ -93,6 +206,22 @@ export interface OptimisticBackendOptions {
|
|
|
93
206
|
/** Virtual-clock seam for the fold debounce timers (FOLDED-MUTATIONS-DESIGN §9). Defaults to
|
|
94
207
|
* real `setTimeout`/`clearTimeout`/`Date.now`; the fold oracle injects a deterministic clock. */
|
|
95
208
|
clock?: FoldClock;
|
|
209
|
+
/** The DECLARED confirming stream per mutation (302 §5: declared, not derived — there is no
|
|
210
|
+
* routing proof). A policy returning a string pins that domain verbatim: the mutation stages
|
|
211
|
+
* onto that room's namespaced tables and ships on its channel. Returning `undefined` (or
|
|
212
|
+
* configuring no policy) means `"daemon"`. The client layer builds this from the app's declared
|
|
213
|
+
* realtime mutators + the currently attached rooms; a misdeclaration fails SOFT (302 §5.1) —
|
|
214
|
+
* the write lands on the other authority's tables and the view simply stops feeling instant
|
|
215
|
+
* until the echo relays it. */
|
|
216
|
+
domainPolicy?: (name: string, args: unknown) => string | undefined;
|
|
217
|
+
/** A FINAL (authz/validation) mutation rejection's reason surface — the room plane's twin of the
|
|
218
|
+
* HTTP mutate route's `onRejected` (H-v; the H-iv-b `mutationOutcome {kind:"rejected"}` frame).
|
|
219
|
+
* The prediction's snap-back is NOT this callback's job: the room burns the mid and its lmid
|
|
220
|
+
* release drops the entry exactly as a daemon-path rejection does (processed-as-no-op) — this
|
|
221
|
+
* is where the REASON reaches the app, same contract as the queue's callback. Also invoked when
|
|
222
|
+
* a DEOPT's fresh re-invocation (the already-retired arm) throws — that mutation is dead on the
|
|
223
|
+
* current base with no stream left to confirm it, the closest thing to a rejection there is. */
|
|
224
|
+
onRejected?: (envelope: MutationEnvelope, reason: string) => void;
|
|
96
225
|
}
|
|
97
226
|
/** One folded entry's debounce window, for the timeline's fold drill-down (§4.1). */
|
|
98
227
|
export interface FoldInspect {
|
|
@@ -115,6 +244,13 @@ export interface PendingInspect {
|
|
|
115
244
|
args: unknown;
|
|
116
245
|
/** Tables this mutator touched at its last (re)invocation — the pending-axis basis (§7.2). */
|
|
117
246
|
tables: string[];
|
|
247
|
+
/** The pk-granular write-set captured at this entry's LAST invocation (RINDLE-REALTIME-QUERY-
|
|
248
|
+
* ENABLEMENT-DESIGN.md §3.2 #1), flattened from the {@link WriteSet} map for inspection — one
|
|
249
|
+
* entry per `(table, pk)` currently held. Pure capture; no routing consumer yet. */
|
|
250
|
+
writes: WriteRecord[];
|
|
251
|
+
/** The read-log captured at this entry's LAST *recorded* invocation (§3.2 #2). Empty for a
|
|
252
|
+
* folded entry — the read TRAP arms there, not recording (see {@link PendingMutation.reads}). */
|
|
253
|
+
reads: ReadLog;
|
|
118
254
|
/** Present iff this entry is a folded (debounced) write. */
|
|
119
255
|
fold?: FoldInspect;
|
|
120
256
|
}
|
|
@@ -156,6 +292,9 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
156
292
|
* its width check. */
|
|
157
293
|
private readonly colCounts;
|
|
158
294
|
private readonly colIndex;
|
|
295
|
+
/** Per-table pk column indices — held so `connectSource` can build a fresh per-source
|
|
296
|
+
* `NormalizedSync` with the same layout the daemon's uses. */
|
|
297
|
+
private readonly pkCols;
|
|
159
298
|
/** The client's OWN typed per-table schemas + the reserved lmid table — the fixed base
|
|
160
299
|
* of the expected-schema set (CRIT#4 validation). Synthetic agg tables are appended as
|
|
161
300
|
* queries arrive (`ensureSyntheticTables`). */
|
|
@@ -175,6 +314,12 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
175
314
|
private readonly overlay;
|
|
176
315
|
private handler;
|
|
177
316
|
private catchUpQids;
|
|
317
|
+
/** Newly-hydrated qids whose reconcile ACTUALLY emitted a (catch-up-stamped) batch — recorded by the
|
|
318
|
+
* local-event forwarder alongside {@link catchUpQids}. After the reconcile, any newly-hydrated qid
|
|
319
|
+
* NOT in here folded nothing (0 rows, or its result already present via a sibling → 0 net muts, or
|
|
320
|
+
* the reconcile was skipped), so `onProgress` sends it an explicit empty catch-up — else its SSR
|
|
321
|
+
* seed would never retire (the view freezes). Non-null only for the reconcile's duration. */
|
|
322
|
+
private catchUpEmitted;
|
|
178
323
|
/** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
|
|
179
324
|
* local engine's `dispatch` brackets so the Store folds every affected view before notifying any
|
|
180
325
|
* subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
|
|
@@ -182,18 +327,114 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
182
327
|
private boundaryHandler;
|
|
183
328
|
private readonly devObservers;
|
|
184
329
|
private pendingMutations;
|
|
330
|
+
/** The next mid to deal, PER DOMAIN (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.1): a client
|
|
331
|
+
* writing through room + daemon concurrently must not alias one lmid counter. Seeded with the
|
|
332
|
+
* `"daemon"` stream at 1; a domain absent from the map starts at 1. In the single-domain
|
|
333
|
+
* configuration only `"daemon"` is ever touched, so the sequence is byte-for-byte as before. */
|
|
185
334
|
private nextMid;
|
|
335
|
+
/** The client-global deal counter behind {@link PendingMutation.seq}: one sequence across ALL
|
|
336
|
+
* domains, bumped whenever any domain's mid is dealt. The replay order (mids are per-domain and
|
|
337
|
+
* incomparable across domains — see the `seq` field doc). */
|
|
338
|
+
private dealSeq;
|
|
339
|
+
/** The explicit confirming-stream override (§7.1/§3) — see
|
|
340
|
+
* {@link OptimisticBackendOptions.domainPolicy}. `undefined` from it ⇒ H-iii derivation. */
|
|
341
|
+
private readonly domainPolicy;
|
|
342
|
+
/** The final-rejection reason surface ({@link OptimisticBackendOptions.onRejected}). */
|
|
343
|
+
private readonly rejectedHandler;
|
|
344
|
+
/** Processed `(domain, mid)` outcome frames (H-v) — the deopt handshake's idempotence guard: a
|
|
345
|
+
* duplicate frame (the original plus a reconnect re-send's re-answer, or two re-answers across
|
|
346
|
+
* two reconnects) must not double-invoke. Needed precisely because a deopt frame can arrive for
|
|
347
|
+
* an ALREADY-RETIRED mid (the replay gotcha) — "no matching entry" alone cannot distinguish
|
|
348
|
+
* "handle it fresh" from "already handled". Per-domain FIFO, capped like the shell's
|
|
349
|
+
* recorded-outcome map ({@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}); past the cap a duplicate of
|
|
350
|
+
* an evicted mid would be re-processed — the same bounded-window trade the shell makes, and it
|
|
351
|
+
* takes 512 interleaving non-applied outcomes on one domain to open it. */
|
|
352
|
+
private readonly outcomesProcessed;
|
|
353
|
+
/** THE room-table registry (302 §2 — one source per table): per connected room `sourceKey`, the
|
|
354
|
+
* wire-table → engine-table map for the tables that room OWNS (its writable scope). Written by
|
|
355
|
+
* {@link registerRoomTables} (same breath as the engine registration); read by the gate's
|
|
356
|
+
* release rename/filter, the mutator staging map, the view swap ({@link processSwapIns}), and
|
|
357
|
+
* the client's `__realtimeInspect` bookkeeping. The record outlives a downgrade's disconnect —
|
|
358
|
+
* the ghost's views still read the engine tables — and drops at {@link dropGhost} (or the last
|
|
359
|
+
* clean release via {@link unregisterRoomTables}). */
|
|
360
|
+
private readonly roomTables;
|
|
361
|
+
/** Local view qids currently REGISTERED on a room's namespaced tables (302 §4 swap-in), →
|
|
362
|
+
* their sourceKey. Set by {@link processSwapIns}; cleared by the swap-back ({@link dropGhost})
|
|
363
|
+
* and view teardown. The original AST stays in {@link asts} throughout — the swap re-registers
|
|
364
|
+
* only the ENGINE query. */
|
|
365
|
+
private readonly roomSwappedViews;
|
|
366
|
+
/** Room subs whose FIRST snapshot released in the current release — their views swap onto the
|
|
367
|
+
* room tables at the release tail ({@link processSwapIns}), strictly AFTER the reconcile folded
|
|
368
|
+
* the snapshot into those tables (swapping earlier would hydrate the view EMPTY, a flash). */
|
|
369
|
+
private readonly pendingSwapIns;
|
|
186
370
|
/** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
|
|
187
371
|
* (FOLDED-MUTATIONS-DESIGN §8). Insertion order is creation order (the drain/flush tiebreak). */
|
|
188
372
|
private readonly folds;
|
|
189
373
|
/** The fold debounce clock (real timers by default; the oracle injects a virtual one). */
|
|
190
374
|
private readonly clock;
|
|
191
|
-
/** The high-water confirmed mutation id,
|
|
192
|
-
*
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
375
|
+
/** The high-water confirmed mutation id, PER DOMAIN (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md
|
|
376
|
+
* §7.2 per-domain confirm-drop): an entry with `mid <= watermark[entry.domain]` has been
|
|
377
|
+
* confirmed. The `"daemon"` domain is folded from the lmid system query's RELEASED ops
|
|
378
|
+
* (lmid-as-data) — never from a frame; a room domain will fold from its own lmid stream (later
|
|
379
|
+
* slice). Seeded with `"daemon"` at 0; the daemon scalar `confirmedLmid` (devtools) is
|
|
380
|
+
* `watermark.get("daemon")`. */
|
|
381
|
+
private watermark;
|
|
382
|
+
/** The per-source coherence gates (§5.1), by source key. Seeded with the daemon gate at
|
|
383
|
+
* construction; a room channel attaches later (`connectSource`). Single-domain: one entry,
|
|
384
|
+
* and every gate-generalized path degenerates to the old single-buffer code. NOT the same
|
|
385
|
+
* space as {@link watermark}/{@link nextMid}: a DOMAIN can confirm with no gate connected
|
|
386
|
+
* (the `__testRelease` seam); a gate's `key` names the domain its lmid stream folds into. */
|
|
387
|
+
private readonly gates;
|
|
388
|
+
/** The daemon's gate — the always-present channel (constructor-attached). The devtools
|
|
389
|
+
* scalars (`__inspect`) read it directly; its `sync` IS {@link sync} (the agg overlay and
|
|
390
|
+
* synthetic tables are daemon-tracked by design). */
|
|
391
|
+
private readonly daemonGate;
|
|
392
|
+
/** System retains by source qid ({@link retainSystemQuery}): a subscription with NO store view
|
|
393
|
+
* and NO user-visible table — its frames buffer on its gate exactly like {@link LMID_QID}'s and
|
|
394
|
+
* fold at RELEASE time ({@link foldSystemFrames}), never entering the sync layer or the local
|
|
395
|
+
* engine. The spec names which system table the qid serves and the scope/doc it was minted for
|
|
396
|
+
* (the fold's row filter). Empty on every non-lifecycle client — every partition below is then
|
|
397
|
+
* a structural no-op and the release path is byte-identical to before. */
|
|
398
|
+
private readonly systemQids;
|
|
399
|
+
/** The §4.2 fence state: room doc → highest `flush_seq` delivered through the daemon plane
|
|
400
|
+
* (monotone max-fold; a remove never regresses it). Slice I-v's ghost-drop consumer — I-iii
|
|
401
|
+
* only maintains + exposes it (`__inspectDomains().lifecycle`). */
|
|
402
|
+
private readonly roomWatermarks;
|
|
403
|
+
/** The §4.1 occupancy state: scope → (client_id → expires_at) from the doorbell stream. Slice
|
|
404
|
+
* I-iv's doorbell consumer (the 1→2 re-lease reaction) — I-iii only maintains + exposes it.
|
|
405
|
+
* A snapshot REPLACES the scope's map (authoritative re-hydrate); a batch folds add/edit/remove
|
|
406
|
+
* incrementally (the age-out sweep's deletes arrive as removes). */
|
|
407
|
+
private readonly scopeSessions;
|
|
408
|
+
/** The I-iv doorbell event sink ({@link onScopeSessions}) — fired once per scope a release's
|
|
409
|
+
* scope-session fold touched, AFTER the whole release applied. Default no-op: a client that
|
|
410
|
+
* never registers (no lifecycle plane) pays nothing. */
|
|
411
|
+
private scopeSessionsHandler;
|
|
412
|
+
/** Deferred old-channel row GC for in-flight upgrade retargets ({@link retargetRemoteQuery}):
|
|
413
|
+
* sub sourceQid → the channel it left. The rows the OLD gate's sync holds for the qid stay
|
|
414
|
+
* visible (merge: daemon tier) until the sub's first snapshot RELEASES on its new room channel
|
|
415
|
+
* ({@link flushRetargetGc}) — dropping them at retarget time would emit net removes ahead of
|
|
416
|
+
* the room's re-adds, the flicker the two-phase cutover exists to avoid. Doubles as the
|
|
417
|
+
* wrong-channel GRACE window in {@link onFrame}: a frame already in flight from the old
|
|
418
|
+
* channel when the sub moved is stale, not a wiring bug. Empty on every non-upgrade client —
|
|
419
|
+
* every consultation below is then a structural no-op. */
|
|
420
|
+
private readonly pendingRetargetGc;
|
|
421
|
+
/** The §4.2 GHOSTS (Slice I-v): demoted room sources awaiting their watermark fence, by
|
|
422
|
+
* sourceKey. Written only by {@link demoteRoomSource}; evaluated after every release
|
|
423
|
+
* ({@link evaluateGhosts}) and dropped by {@link dropGhost} once the fence clears with no
|
|
424
|
+
* sent room-domain pending left. Empty on every non-downgrade client — the per-release
|
|
425
|
+
* evaluation is then a structural no-op. */
|
|
426
|
+
private readonly ghosts;
|
|
427
|
+
/** The I-v stuck-downgrade surface ({@link onDowngradeStuck}) — fired AT MOST ONCE per ghost
|
|
428
|
+
* when its fence is satisfied but sent room-domain mids remain unresolved (§7.5: they retire
|
|
429
|
+
* only through outcome resolution; the ghost holds rather than inventing a timeout-retire).
|
|
430
|
+
* Default no-op. */
|
|
431
|
+
private downgradeStuckHandler;
|
|
432
|
+
/** The 302 §6.1 context-coverage surface ({@link onRoomContextJoin}) — fired at most once per
|
|
433
|
+
* view ({@link contextJoinWarned}), at swap-in, when its AST references tables the room does
|
|
434
|
+
* not own. Default no-op. */
|
|
435
|
+
private roomContextJoinHandler;
|
|
436
|
+
/** Views the context-coverage event already fired for (once per view; cleared on teardown). */
|
|
437
|
+
private readonly contextJoinWarned;
|
|
197
438
|
private readonly asts;
|
|
198
439
|
/** Per query: the base tables its result can draw from (from the AST tree). */
|
|
199
440
|
private readonly queryTables;
|
|
@@ -215,7 +456,40 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
215
456
|
* {@link writeLocal} invokes it post-commit; {@link applyLocalReplica} deliberately does not. */
|
|
216
457
|
private localWriteObserver;
|
|
217
458
|
constructor(schema: Schema<S>, source: OptimisticSource, registry: ClientRegistry, opts: OptimisticBackendOptions);
|
|
218
|
-
|
|
459
|
+
/** Wire one authority channel into its own coherence gate (§5.1): every frame the channel
|
|
460
|
+
* delivers buffers on THIS gate's cv timeline, its progress frames release THIS buffer, its
|
|
461
|
+
* restart resets THIS gate alone, and its reserved lmid stream folds into `watermark[key]`.
|
|
462
|
+
* Validates each server hello against our OWN typed schema → reject a schema skew (CRIT#4);
|
|
463
|
+
* the reserved lmid table is part of the expected set so the system query's hello passes, and
|
|
464
|
+
* synthetic agg tables join the set as queries register them. */
|
|
465
|
+
private attachGate;
|
|
466
|
+
/** Attach a SECOND authority channel (§5.1) — the seam Slice G's room upgrade calls with the
|
|
467
|
+
* ws-backed room feed. Rooms speak the daemon protocol verbatim (§2.4: the client cannot tell
|
|
468
|
+
* a room from the daemon), so the argument is a full {@link OptimisticSource} — exactly what
|
|
469
|
+
* `@rindle/remote` builds from `{roomUrl, leaseToken}`. The channel buffers/releases on its
|
|
470
|
+
* own cv timeline (an independent §5.1 gate: coherent within, eventual across) and its
|
|
471
|
+
* reserved lmid stream folds into `watermark[sourceKey]` — so `sourceKey` must equal the
|
|
472
|
+
* `domainPolicy` name for the mutations this authority confirms. The converse is NOT required:
|
|
473
|
+
* a domain may exist with no connected gate (`__testRelease` drives confirms gate-less); the
|
|
474
|
+
* live production path stays daemon-only until G calls this. */
|
|
475
|
+
connectSource(sourceKey: string, source: OptimisticSource): void;
|
|
476
|
+
/** Register the tables room `sourceKey` OWNS (its writable scope — 302 §2): each wire table
|
|
477
|
+
* gets its own namespaced ENGINE table (`{@link roomEngineTable}`), an ordinary tracked table
|
|
478
|
+
* whose sole authority is the room channel. From here on the channel's released deltas rename
|
|
479
|
+
* into these tables (wire tables outside the map are DROPPED — context stays daemon-owned,
|
|
480
|
+
* 302 §6), room-domain mutators stage onto them, and a room-homed view swaps onto them once
|
|
481
|
+
* the room sub hydrates ({@link processSwapIns}). Idempotent per (sourceKey, table); a wire
|
|
482
|
+
* table unknown to the schema is skipped (nothing to hold rows for). */
|
|
483
|
+
registerRoomTables(sourceKey: string, tables: readonly string[]): void;
|
|
484
|
+
/** The wire-table → engine-table map for room `sourceKey`'s owned tables (empty when none) —
|
|
485
|
+
* the client's idempotence check and `__realtimeInspect` read THIS record (one source of
|
|
486
|
+
* truth; the client keeps no shadow copy). */
|
|
487
|
+
roomTablesFor(sourceKey: string): ReadonlyMap<string, string>;
|
|
488
|
+
/** `channel` (G-iii registration-time routing) names the authority channel the remote sub
|
|
489
|
+
* registers on — a `connectSource`d gate key; default `"daemon"` (every existing caller is
|
|
490
|
+
* byte-identical). Slice G-v threads the lease's `realtime.sourceKey` here. Validated FIRST
|
|
491
|
+
* (like the E3 check below): a bad channel must throw before any per-query state is recorded. */
|
|
492
|
+
registerQuery(qid: QueryId, ast: Ast, remote?: RemoteQuery, channel?: string): void;
|
|
219
493
|
/** Register every synthetic aggregate table `ast` needs that we haven't seen yet: on the
|
|
220
494
|
* local engine (which auto-tracks it for the optimistic rebase loop), on `NormalizedSync`
|
|
221
495
|
* (so its rows refcount/GC by group key), and into the source's expected-schema set (so
|
|
@@ -229,8 +503,140 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
229
503
|
* `unregisterTable` frees it (the engine refuses otherwise). */
|
|
230
504
|
private releaseSyntheticTables;
|
|
231
505
|
unregisterQuery(qid: QueryId): void;
|
|
232
|
-
|
|
506
|
+
/** `channel` as in {@link registerQuery} (G-iii): the gate the remote sub registers on; default
|
|
507
|
+
* `"daemon"`. This is the split-retain seam G-v's resolve-then-register drives — resolve the
|
|
508
|
+
* lease, learn `realtime.sourceKey`, `connectSource` it, then retain the query on that channel.
|
|
509
|
+
* Validated FIRST so a bad channel throws before any synthetic-table refcount moves. */
|
|
510
|
+
retainRemoteQuery(qid: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast, channel?: string): void;
|
|
233
511
|
releaseRemoteQuery(qid: QueryId): void;
|
|
512
|
+
/** The Slice I-iv upgrade retarget (§4.1 "Retarget" / the doorbell reaction): move a LIVE
|
|
513
|
+
* (name, args) sub — every retain of it and every local view it feeds, wholesale — from the
|
|
514
|
+
* channel it lives on onto `sourceKey`'s (already-`connectSource`d, already-promoted) room
|
|
515
|
+
* channel, WITHOUT the view ever dropping its rows. Returns the sub's wire `sourceQid` (the
|
|
516
|
+
* identity the client's renewal loop re-subscribes with).
|
|
517
|
+
*
|
|
518
|
+
* Why a dedicated primitive: the one-channel-per-(name,args) invariant ({@link retainRemote}'s
|
|
519
|
+
* loud throw) is correct — a sub's frames must never split across two cv timelines — so the
|
|
520
|
+
* upgrade cannot simply retain a second sub on the room and release the daemon one; and the
|
|
521
|
+
* naive release-then-retain order GCs the daemon sync's rows synchronously (net removes emit,
|
|
522
|
+
* the view flashes empty) a full ws round trip before the room's seq-0 snapshot refills it.
|
|
523
|
+
* The cutover is therefore TWO-PHASE around the room's first release:
|
|
524
|
+
*
|
|
525
|
+
* 1. NOW (here): unsubscribe the old channel's wire sub, sweep its still-buffered frames for
|
|
526
|
+
* this qid (their cv timeline continues without the sub — the hello-supersession
|
|
527
|
+
* precedent), flip `sub.channel`, re-arm `sub.hydrated` (the room's own snapshot is the
|
|
528
|
+
* cutover point), and register on the room source (its resolver presents the handed
|
|
529
|
+
* roomToken). The old gate's SYNC rows are deliberately NOT dropped: they keep the view's
|
|
530
|
+
* plain tables populated through the window — the view still reads them until the swap.
|
|
531
|
+
* 2. AT THE ROOM'S FIRST RELEASED SNAPSHOT: the reconcile folds the snapshot into the room's
|
|
532
|
+
* namespaced tables, the release tail SWAPS every local view onto them (302 §4.1,
|
|
533
|
+
* {@link processSwapIns} — the accepted-flash boundary), and {@link flushRetargetGc}'s
|
|
534
|
+
* deferred `dropQuery`+reconcile on the OLD gate then GCs the plain-table rows the sub
|
|
535
|
+
* alone referenced — invisible to the swapped views.
|
|
536
|
+
*
|
|
537
|
+
* Idempotent per target channel: a sub already on `sourceKey` returns immediately (the
|
|
538
|
+
* double-doorbell / re-entrancy guard — one retarget per (query, sourceKey)). Validates before
|
|
539
|
+
* mutating: a throw here leaves the sub fully daemon-attached (the client's fail-open). */
|
|
540
|
+
retargetRemoteQuery(remote: RemoteQuery, sourceKey: string): QueryId;
|
|
541
|
+
/** Phase 2 of {@link retargetRemoteQuery}, run at the end of every gate release: once a
|
|
542
|
+
* retargeted sub's first snapshot has RELEASED on its new channel (`sub.hydrated` re-armed at
|
|
543
|
+
* retarget, re-set by {@link markSubHydrated} inside this very release), drop the qid's rows
|
|
544
|
+
* from the OLD gate's sync and reconcile them out — after the room's rows are already applied,
|
|
545
|
+
* so the winner flip is value-equal (net-zero; see the phase table above). A sub torn down
|
|
546
|
+
* mid-window was already swept by `releaseRemoteQuery`/`unregisterQuery` (which delete the
|
|
547
|
+
* record); a vanished record here is pruned defensively. */
|
|
548
|
+
private flushRetargetGc;
|
|
549
|
+
/** The I-v downgrade orchestration primitive (§4.2/§7.4, re-expressed by 302 §4.2 as the
|
|
550
|
+
* SWAP-BACK GATE): retire room `sourceKey` behind the watermark fence. The caller has ALREADY
|
|
551
|
+
* retargeted every live sub off the channel ({@link retargetRemoteQuery} room→daemon —
|
|
552
|
+
* validated loudly below) and holds the fence from the api-server's downgrade response
|
|
553
|
+
* (`finalFlushSeq` = the room's last COMMITTED flush seq; `doc` keys the §4.2 watermark fold,
|
|
554
|
+
* {@link roomWatermarks}). Steps, in order:
|
|
555
|
+
*
|
|
556
|
+
* 1. **Disconnect** the channel ({@link disconnectSource}): handlers detached, gate + buffer
|
|
557
|
+
* dropped. `nextMid`/`watermark`/processed-outcomes for the domain are KEPT FOREVER (§7.1:
|
|
558
|
+
* an assigned mid pins its domain; a later re-upgrade of the same doc continues the
|
|
559
|
+
* sequence — {@link connectSource} attaches a fresh gate and the lmid snapshot max-folds
|
|
560
|
+
* into the surviving watermark). Disconnecting BEFORE the daemon sub's first release is
|
|
561
|
+
* load-bearing: it makes {@link flushRetargetGc}'s deferred old-channel GC a no-op (gate
|
|
562
|
+
* gone ⇒ record deleted, nothing dropped). The room's namespaced tables — and the views
|
|
563
|
+
* swapped onto them — deliberately stay: frozen at the room's last state, they keep the
|
|
564
|
+
* document visible while the falling-back follower may still lack the final flush.
|
|
565
|
+
* Swapping back earlier would show its pre-flush images — the regression §4.2 prevents.
|
|
566
|
+
* 2. **Ghost + first evaluation**: the record joins {@link ghosts} and is evaluated once
|
|
567
|
+
* immediately — `finalFlushSeq === 0` (a never-flushed room) with no room-domain pending
|
|
568
|
+
* drops on the spot, the single-daemon first-frame case.
|
|
569
|
+
*
|
|
570
|
+
* In-flight discipline (§7.5): entries with `mid !== null` on `sourceKey` stay PINNED (rule
|
|
571
|
+
* 2 — never re-route a sent mutation); their resolution arrives via the daemon-carried
|
|
572
|
+
* ledger+outcome folds (I-iii) and blocks the drop until then. Idempotent per sourceKey (a
|
|
573
|
+
* second labeled query sharing the room demotes into the existing ghost). */
|
|
574
|
+
demoteRoomSource(sourceKey: string, doc: string, finalFlushSeq: number): void;
|
|
575
|
+
/** Detach one connected room channel (Slice I-v step 3): the source's handlers are replaced
|
|
576
|
+
* with no-ops (the {@link OptimisticSource} handler seam is single-registration, so this IS
|
|
577
|
+
* the detach — a late frame from a dying socket can no longer touch any bookkeeping), its
|
|
578
|
+
* reserved lmid sub is unregistered, and the gate — buffer, per-source sync, cv watermark —
|
|
579
|
+
* is dropped from {@link gates}. The DOMAIN state deliberately survives forever:
|
|
580
|
+
* `nextMid[sourceKey]`, `watermark[sourceKey]`, and the processed-outcome set are untouched
|
|
581
|
+
* (§7.1 — an assigned mid pins its domain; a re-upgrade must continue, never restart, the mid
|
|
582
|
+
* sequence; {@link connectSource} then attaches a fresh gate whose lmid snapshot max-folds
|
|
583
|
+
* into the surviving watermark via {@link foldConfirm}). Closing the underlying transport is
|
|
584
|
+
* the caller's job. Idempotent (a missing gate is a no-op). */
|
|
585
|
+
disconnectSource(sourceKey: string): void;
|
|
586
|
+
/** Register the I-v stuck-downgrade sink — see {@link DowngradeStuckEvent}. One handler (a
|
|
587
|
+
* later registration replaces it, the {@link onScopeSessions} convention); client.ts maps it
|
|
588
|
+
* onto the loud anomaly surface. */
|
|
589
|
+
onDowngradeStuck(handler: (event: DowngradeStuckEvent) => void): void;
|
|
590
|
+
/** Register the 302 §6.1 context-coverage sink — see {@link RoomContextJoinEvent}. One handler
|
|
591
|
+
* (a later registration replaces it, the {@link onScopeSessions} convention); client.ts maps
|
|
592
|
+
* it onto the loud anomaly surface. */
|
|
593
|
+
onRoomContextJoin(handler: (event: RoomContextJoinEvent) => void): void;
|
|
594
|
+
/** The I-v ghost-drop watcher (§4.2), run after every applied release ({@link applyRelease} —
|
|
595
|
+
* the seam where {@link roomWatermarks} has just folded and the confirm-drop has just run) and
|
|
596
|
+
* once at demote time. For each ghost: the fence must be satisfied
|
|
597
|
+
* (`roomWatermarks[doc] ≥ finalFlushSeq`; 0 is trivially satisfied) AND no SENT room-domain
|
|
598
|
+
* pending may remain (§7.5 — such entries resolve only through the daemon-carried
|
|
599
|
+
* outcome/ledger folds; an entry that never reached the room is undecidable, so the ghost
|
|
600
|
+
* HOLDS and the stuck event fires exactly once, naming the mids). Both satisfied ⇒
|
|
601
|
+
* {@link dropGhost}. */
|
|
602
|
+
private evaluateGhosts;
|
|
603
|
+
/** Drop one cleared ghost — the 302 §4.2 SWAP-BACK: under the fence the daemon tables are
|
|
604
|
+
* value-equal-or-ahead of the room's final state, so (1) every view swapped onto the room's
|
|
605
|
+
* namespaced tables re-registers on its ORIGINAL (daemon-table) AST — visually a no-op, the
|
|
606
|
+
* Store folds the re-hello as an in-place reset; (2) the namespaced tables unregister (no
|
|
607
|
+
* reader is left after the swap); (3) ONE daemon reconcile re-invokes the pending set so any
|
|
608
|
+
* entry whose writes had staged onto the now-gone room tables re-stages onto the daemon tables
|
|
609
|
+
* (its domain policy stopped naming the dead room when the client dropped it). The whole drop
|
|
610
|
+
* runs under one commit boundary so the swap and the re-staged predictions notify as ONE step.
|
|
611
|
+
* After this, a FUTURE upgrade of the same doc registers again from scratch. */
|
|
612
|
+
private dropGhost;
|
|
613
|
+
/** Unregister room `sourceKey`'s namespaced engine tables and drop the {@link roomTables}
|
|
614
|
+
* record. Callers must have no view registered on them (the engine refuses otherwise —
|
|
615
|
+
* loud by design). No-op for an unknown sourceKey. */
|
|
616
|
+
unregisterRoomTables(sourceKey: string): void;
|
|
617
|
+
/** Retain one minted SYSTEM subscription (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §4, Slice
|
|
618
|
+
* I-iii): a wire sub with NO store view and NO user-visible table. Registered through the same
|
|
619
|
+
* {@link RemoteSub} bookkeeping as any remote retain — so qid→channel ownership, the overflow
|
|
620
|
+
* re-subscribe, and refcounted release all work unchanged — but with an EMPTY `localQids` set
|
|
621
|
+
* (no hydration/resultType coupling) and a {@link systemQids} record telling the release path
|
|
622
|
+
* which system table this qid's frames carry (`spec.table`) and which scope/doc it was minted
|
|
623
|
+
* for (the fold's row filter). Its frames then buffer on the channel's gate exactly like
|
|
624
|
+
* {@link LMID_QID}'s and fold at RELEASE time in {@link foldSystemFrames} — riding the SAME
|
|
625
|
+
* buffered cv path as the data they co-committed with (fence coherence: an out-of-band
|
|
626
|
+
* shortcut would break I-ii's co-commit ordering guarantee).
|
|
627
|
+
*
|
|
628
|
+
* `channel` defaults to `"daemon"` — the system tables live in the DAEMON store (that is the
|
|
629
|
+
* point: outcome/ledger/watermark rows must be readable with no room socket alive, §7.1
|
|
630
|
+
* "load-bearing for §7.5"). Idempotence per (table, scope/doc) is the CALLER's job (client.ts
|
|
631
|
+
* keys its retains on exactly that); a duplicate retain of the SAME remote identity refcounts
|
|
632
|
+
* like any sub. */
|
|
633
|
+
retainSystemQuery(retainQid: QueryId, remote: RemoteQuery, spec: SystemStreamSpec, channel?: string): void;
|
|
634
|
+
/** Release a {@link retainSystemQuery} retain. Refcounted like any sub; the LAST release
|
|
635
|
+
* unregisters from the owning channel, sweeps its buffered frames, and drops the
|
|
636
|
+
* {@link systemQids} record. The folded lifecycle STATE (`roomWatermarks`/`scopeSessions`/
|
|
637
|
+
* processed outcomes) deliberately survives — the fence is monotone truth about the store, not
|
|
638
|
+
* about the subscription (a re-retained fence must not forget a cleared watermark). */
|
|
639
|
+
releaseSystemQuery(retainQid: QueryId): void;
|
|
234
640
|
/** Raw CRUD has no optimistic story (§9 replaces it with named mutators). Register a
|
|
235
641
|
* mutator — even a trivial one — and `invoke` it. */
|
|
236
642
|
mutate(_mutations: Mutation[]): Promise<void>;
|
|
@@ -276,10 +682,51 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
276
682
|
* state (read-your-writes), the SAME body the API server drives asynchronously. `ctx.user` is
|
|
277
683
|
* the acting principal (re-read per invoke, stable across a rebase re-invoke). */
|
|
278
684
|
private runMutator;
|
|
685
|
+
/** Deal the next wire mid from `domain`'s ledger (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md
|
|
686
|
+
* §7.1) and advance that counter. A domain absent from the map starts at 1. Per-domain, so a
|
|
687
|
+
* client writing through room + daemon concurrently keeps two gapless, non-aliasing sequences.
|
|
688
|
+
* The client-global `seq` is stamped in the same breath — the ONE cross-domain total order
|
|
689
|
+
* (confirmation order is per-domain; replay order is client-global). Bundled here so no call
|
|
690
|
+
* site can deal a mid without its seq. ONE caller discards the seq deliberately: the H-v deopt
|
|
691
|
+
* flip ({@link handleMutationOutcome}) keeps the entry's ORIGINAL seq — its replay position —
|
|
692
|
+
* and takes only the fresh mid (the dealSeq bump is harmless: seq consumers order, never
|
|
693
|
+
* count). */
|
|
694
|
+
private dealMid;
|
|
695
|
+
/** The declared confirming stream for one invocation: the `domainPolicy`'s verdict, `"daemon"`
|
|
696
|
+
* when it abstains. Resolved BEFORE the prediction runs — the domain picks the staging map
|
|
697
|
+
* (a room domain stages its owned tables onto the room's namespaced twins). */
|
|
698
|
+
private resolveDomain;
|
|
699
|
+
/** The staging table map for a `domain`-routed prediction ({@link trackingTx}'s `stage`):
|
|
700
|
+
* wire table → the room's namespaced engine table for the tables the room owns; identity for
|
|
701
|
+
* everything else (including the whole map for the daemon domain). */
|
|
702
|
+
private stagingMap;
|
|
703
|
+
/** The PLAIN (daemon-homed) engine AST for `ast` — aggregate relationships rewritten to their
|
|
704
|
+
* synthetic `__agg_*` reads, no room renames. The ONE form every non-swapped engine
|
|
705
|
+
* registration uses ({@link registerQuery}, {@link dropGhost}'s swap-back) and the base the
|
|
706
|
+
* swap-in renames ({@link processSwapIns}). */
|
|
707
|
+
private plainEngineAst;
|
|
708
|
+
/** Mutator names the cross-authority warn below already fired for (once per name). */
|
|
709
|
+
private readonly warnedCrossAuthority;
|
|
710
|
+
/** 302 §5.1 dev-time guard: a room-DECLARED mutator wrote tables the room does not own. Those
|
|
711
|
+
* writes staged onto the PLAIN daemon tables (the staging map covers only owned tables), but
|
|
712
|
+
* the entry confirms on the ROOM stream — and only the room's OWNED tables flush back to the
|
|
713
|
+
* daemon, so nothing upstream ever echoes them: once the room confirm retires the entry, the
|
|
714
|
+
* next release's whole-store rewind reverts them for good. The first-party room shell refuses
|
|
715
|
+
* such a mutation (the §3.3 deopt/reject backstop re-routes it to the daemon), so this warns
|
|
716
|
+
* for the shapes where that backstop may be absent (a BYO relay) — loud, once, soft (§5.1:
|
|
717
|
+
* misdeclarations never throw). */
|
|
718
|
+
private warnCrossAuthorityWrites;
|
|
279
719
|
/** Run the named client mutator optimistically: the prediction applies to the live
|
|
280
720
|
* engine now (affected views update synchronously), `(mid, name, args)` joins the
|
|
281
721
|
* pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
|
|
282
722
|
invoke(name: string, args: unknown): number;
|
|
723
|
+
/** {@link invoke} with an optional PINNED confirming domain (H-v): the deopt handshake's
|
|
724
|
+
* already-retired arm re-invokes the frame's echoed `(name, args)` as a FRESH invocation pinned
|
|
725
|
+
* to `"daemon"` — an honest re-prediction on the current base, never derived (`pin` bypasses
|
|
726
|
+
* {@link resolveDomain} entirely, so the router never runs and no Q6 counter moves). Every
|
|
727
|
+
* other step is `invoke` verbatim: prediction now, capture, drainOverlapping, mid dealt from
|
|
728
|
+
* the pinned domain's ledger, envelope on its channel. */
|
|
729
|
+
private invokeWith;
|
|
283
730
|
/** Run a FOLDED invoke (FOLDED-MUTATIONS-DESIGN §8): apply the prediction to the live engine now
|
|
284
731
|
* (like `invoke`), but collapse a run of same-key invokes into ONE pending entry whose `args`
|
|
285
732
|
* are overwritten in place, debounce the server write, and ship only the last value. The `mid`
|
|
@@ -294,6 +741,66 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
294
741
|
* by construction), stamp the entry, ship the envelope with the LATEST args, resolve the handle.
|
|
295
742
|
* The entry stays on `pendingMutations` (now with a real mid) until the lmid release confirms it. */
|
|
296
743
|
private flushFold;
|
|
744
|
+
/** The transport a `domain`-confirmed mutation ships on (§7.5 sent-pins-domain: only the
|
|
745
|
+
* domain's own authority can confirm it, so its channel is the only correct transport). A
|
|
746
|
+
* domain with NO connected gate ships on the daemon channel — the gate-less configurations
|
|
747
|
+
* (`__testRelease`-driven tests) and today's entire live path resolve `"daemon"` anyway. */
|
|
748
|
+
private channelFor;
|
|
749
|
+
/** The gate a channel-keyed retain registers through (G-iii registration-time routing). The
|
|
750
|
+
* channel MUST already be connected (`connectSource`; the daemon is constructor-attached) —
|
|
751
|
+
* loud by design: a typo'd or not-yet-connected sourceKey must throw at retain time, never
|
|
752
|
+
* silently register on the daemon and split the query's frames across channels. */
|
|
753
|
+
private requireGate;
|
|
754
|
+
/** The channel that owns `sourceQid` — {@link RemoteSub.channel}, the ONE source of truth for
|
|
755
|
+
* qid routing (G-iii). `undefined` when no sub owns the qid (a harness-delivered raw feed, or
|
|
756
|
+
* a just-released sub): such frames buffer on whatever gate they arrive at. */
|
|
757
|
+
private channelOf;
|
|
758
|
+
/** Record `(domain, mid)` as processed; `false` if it already was (a duplicate frame —
|
|
759
|
+
* ignore it). FIFO-capped per domain ({@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}). */
|
|
760
|
+
private markOutcomeProcessed;
|
|
761
|
+
/** One `mutationOutcome` frame from `domain`'s channel (H-v — the §3.3 handshake's client
|
|
762
|
+
* half). The frame arrives OUT-OF-BAND (see {@link attachGate}); the state machine:
|
|
763
|
+
*
|
|
764
|
+
* 1. `mid` never issued on `domain` ⇒ ignore (a confused/foreign frame must not invent work).
|
|
765
|
+
* 2. `(domain, mid)` already processed ⇒ ignore — idempotence under duplicate frames (the
|
|
766
|
+
* original + a re-send's re-answer; a deopt for a mid whose entry ALREADY FLIPPED also
|
|
767
|
+
* lands here harmlessly on its second frame).
|
|
768
|
+
* 3. `kind:"rejected"` ⇒ FINAL. Surface the reason through {@link rejectedHandler} (room-plane
|
|
769
|
+
* parity with the HTTP queue's callback) and STOP — the drop + snap-back is the EXISTING
|
|
770
|
+
* failed-mutation machinery: the room burnt the mid, its lmid release retires the entry
|
|
771
|
+
* per-domain and the reconcile rewinds the prediction, exactly the daemon path's
|
|
772
|
+
* processed-as-no-op rejection. No new drop path.
|
|
773
|
+
* 4. `kind:"deopt"`, entry found (pending `(domain, mid)`) ⇒ FLIP IN PLACE: `domain` becomes
|
|
774
|
+
* `"daemon"`, a fresh daemon mid is dealt and the envelope ships NOW on the daemon channel
|
|
775
|
+
* ("deal-and-send-now" — the conforming §3.3 re-enqueue: there is no flush machinery for
|
|
776
|
+
* non-fold entries, so the design's "mid: null until the daemon flush" is satisfied
|
|
777
|
+
* momentarily inside this call). THE ENTRY'S `seq` IS KEPT — settled (§5.3, commit
|
|
778
|
+
* 68141096): `seq` is the client-global REPLAY order; re-sequencing would move the entry's
|
|
779
|
+
* overlay position and change read-dependent SIBLINGS' replay base. Everything else stays
|
|
780
|
+
* (writes/reads/touched/touchedSources/writeSources — union-never-shrink), the prediction
|
|
781
|
+
* stays applied (the entry never leaves `pendingMutations`, so no rewind fires), and the
|
|
782
|
+
* router does NOT re-run nor does `drainOverlapping` (§3.3 re-enqueues, never re-derives;
|
|
783
|
+
* any open overlapping fold was invoked later and flushes later with a larger mid).
|
|
784
|
+
* 5. `kind:"deopt"`, entry NOT found ⇒ the burnt-mid confirm won the race, or the frame is a
|
|
785
|
+
* replay re-answer for an entry a previous session retired (the replay gotcha): re-invoke
|
|
786
|
+
* the frame's echoed `name`/`args` as a FRESH invocation PINNED to `"daemon"` — an honest
|
|
787
|
+
* re-prediction on the current base, never a derived route ({@link invokeWith}). A frame
|
|
788
|
+
* without `name` (not self-contained) has nothing to re-invoke and is dropped; a re-invoke
|
|
789
|
+
* that THROWS (the base moved from under it) is surfaced through {@link rejectedHandler} —
|
|
790
|
+
* the mutation is dead with no stream left to confirm it.
|
|
791
|
+
*
|
|
792
|
+
* A `"deopt"` bump joins the Q6 routing counters either way (`routing.reasons.deopt`) —
|
|
793
|
+
* derived-and-deopted routes are visible beside derived successes. */
|
|
794
|
+
private handleMutationOutcome;
|
|
795
|
+
/** §7.5 rule 3 (H-v): re-send `domain`'s unconfirmed pending envelopes with their ORIGINAL
|
|
796
|
+
* mids, in mid order, on the domain's own channel. Folds with `mid === null` are excluded —
|
|
797
|
+
* nothing was ever sent for them (the flush deals their mid). Envelopes are reconstructed from
|
|
798
|
+
* the pending entries exactly as `invoke` shipped them (`clientID`/`mid`/`name`/`args` —
|
|
799
|
+
* entries carry everything the wire needs). Idempotent under the domain's ledger: an APPLIED
|
|
800
|
+
* mid dedups silently and its lmid coverage retires the entry; a NON-APPLIED mid is re-answered
|
|
801
|
+
* from the shell's recorded-outcome map into {@link handleMutationOutcome}. Confirmed entries
|
|
802
|
+
* are already gone from `pendingMutations`, so no filter against the watermark is needed. */
|
|
803
|
+
private resendPending;
|
|
297
804
|
/** Drain every outstanding fold immediately (FOLDED-MUTATIONS-DESIGN §3): the explicit
|
|
298
805
|
* `app.flushFolds()` and the `beforeunload`/`close` hook. Creation (insertion) order. */
|
|
299
806
|
flushFolds(): void;
|
|
@@ -326,17 +833,159 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
326
833
|
* §4.1). Built fresh per call from state the backend already holds — no new instrumentation, no
|
|
327
834
|
* mutation. Only ever called by `@rindle/devtools` (imported in dev). */
|
|
328
835
|
__inspect(): OptimisticInspect;
|
|
836
|
+
/** Test-only per-domain ledger snapshot (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.1/§8.5).
|
|
837
|
+
* Kept separate from {@link __inspect} so the devtools `OptimisticInspect` mirror stays byte-for-
|
|
838
|
+
* byte identical (daemon-scalar-only). Exposes the per-domain `nextMid`/`watermark` maps plus each
|
|
839
|
+
* pending entry's confirming domain — the axes the §8.5 ledger-isolation assertion checks. */
|
|
840
|
+
__inspectDomains(): {
|
|
841
|
+
nextMid: Record<string, number>;
|
|
842
|
+
watermark: Record<string, number>;
|
|
843
|
+
/** Per connected CHANNEL (§5.1): its release watermark + buffered-frame depth — the axis the
|
|
844
|
+
* gate-isolation assertions read (one source's laggy cvMin must never move the other's). */
|
|
845
|
+
gates: Record<string, {
|
|
846
|
+
appliedCv: number;
|
|
847
|
+
bufferedFrames: number;
|
|
848
|
+
}>;
|
|
849
|
+
/** Per connected/registered room: its wire-table → engine-table map (302 §2) and which local
|
|
850
|
+
* view qids are currently swapped onto it (302 §4). */
|
|
851
|
+
roomTables: Record<string, Record<string, string>>;
|
|
852
|
+
swappedViews: Record<number, string>;
|
|
853
|
+
/** The §4 lifecycle plane's folded state (Slice I-iii introspection): the per-doc §4.2 fence
|
|
854
|
+
* value (`roomWatermarks`, I-v's ghost-drop input), the per-scope §4.1 occupancy map
|
|
855
|
+
* (`scopeSessions`: scope → client_id → expires_at, I-iv's doorbell input), and the live
|
|
856
|
+
* I-v ghosts (demoted room sources still awaiting their swap-back fence). */
|
|
857
|
+
lifecycle: {
|
|
858
|
+
roomWatermarks: Record<string, number>;
|
|
859
|
+
scopeSessions: Record<string, Record<string, number>>;
|
|
860
|
+
ghosts: Record<string, {
|
|
861
|
+
doc: string;
|
|
862
|
+
finalFlushSeq: number;
|
|
863
|
+
}>;
|
|
864
|
+
};
|
|
865
|
+
pending: {
|
|
866
|
+
mid: number | null;
|
|
867
|
+
seq: number | null;
|
|
868
|
+
name: string;
|
|
869
|
+
domain: string;
|
|
870
|
+
}[];
|
|
871
|
+
};
|
|
329
872
|
/** Recompute the pending axis for every query and fire `onPending` on transitions only. Called
|
|
330
873
|
* from the two points that move the pending set: invoke/invokeFolded (add) and the confirm-drop
|
|
331
874
|
* (remove) — exactly where `:359`/`:468` used to flip ResultType (§7.3). */
|
|
332
875
|
private refreshPending;
|
|
333
|
-
private
|
|
876
|
+
private onFrame;
|
|
334
877
|
private emitServerDelta;
|
|
335
878
|
private localQidsForSource;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
*
|
|
879
|
+
/** One gate's release (§5.1 release gate): compute the coherent delta from THIS gate's cv-buffer,
|
|
880
|
+
* then apply it against the gate's source/domain. Split into {@link computeRelease} (buffer →
|
|
881
|
+
* delta, lmid → watermark) and {@link applyRelease} (per-source confirm-drop + reconcile) —
|
|
882
|
+
* N independent gates all feed the ONE apply half; {@link __testRelease} drives it directly. */
|
|
883
|
+
private onGateProgress;
|
|
884
|
+
/** Compute one coherent release from ONE gate's cv-buffer (§5.1) — gate-scoped: its buffer, its
|
|
885
|
+
* cvMin timeline. Take every buffered frame at `cv ≤ cvMin`, in (cv, arrival) order, and fold
|
|
886
|
+
* it: the lmid system-query frame advances `watermark[gate.key]` (via {@link foldLmidOps} — the
|
|
887
|
+
* daemon stream folds "daemon", a room stream folds its own domain); data frames fold through
|
|
888
|
+
* this SOURCE's cross-query refcount into ONE net base delta — the §1.3 `D`. Returns that delta
|
|
889
|
+
* plus the set of local views this release JUST hydrated (so their reconcile batch phases as a
|
|
890
|
+
* `snapshot`). Mutates the gate's buffer/`appliedCv`, hydration, and the gate's domain
|
|
891
|
+
* watermark; the pending set and the reconcile are {@link applyRelease}'s job. */
|
|
892
|
+
private computeRelease;
|
|
893
|
+
/** Apply one released delta against `sourceKey`'s domain (§7.2 per-domain confirm-drop + the §1.3
|
|
894
|
+
* reconcile cycle). `watermarkUpdate`, when given, advances `watermark[sourceKey]` first — the
|
|
895
|
+
* hook a per-source lmid confirm rides on (the daemon path folds its watermark in
|
|
896
|
+
* {@link computeRelease} and passes `undefined`). Then: drop every pending entry its OWN domain's
|
|
897
|
+
* watermark now covers (a room confirm can never retire a daemon entry, and vice-versa — the §7.1
|
|
898
|
+
* ledger-collision fix), and run the reconcile cycle against `sourceKey` when the base delta or the
|
|
899
|
+
* pending set changed. `newlyHydrated` stamps the initial-hydration batch as a catch-up. */
|
|
900
|
+
private applyRelease;
|
|
901
|
+
/** Test-only per-source release seam (RINDLE-REALTIME-QUERY-ENABLEMENT-DESIGN.md §7.2/§8.5): drive
|
|
902
|
+
* {@link applyRelease} for `sourceKey` directly — an explicit `watermarkUpdate` (a simulated lmid
|
|
903
|
+
* confirm for that domain) and `deltas` (a coherent base delta), with no real gate. Lets a harness
|
|
904
|
+
* exercise a room-domain confirm before the real second lmid stream / per-source gate is wired
|
|
905
|
+
* (E-iii-b/c). The `__`-prefix marks it a test hook, alongside {@link __inspect}. */
|
|
906
|
+
__testRelease(sourceKey: string, deltas: Mutation[], watermarkUpdate?: number): void;
|
|
907
|
+
/** Swap every view of each just-hydrated ROOM sub onto the room's namespaced tables (302 §4.1):
|
|
908
|
+
* re-register the local engine query with the AST's room-owned table references renamed
|
|
909
|
+
* ({@link remapAstTables}); the Store folds the re-hello as an in-place reset, so the caller's
|
|
910
|
+
* view reference survives and subscribers see ONE transition. Runs at the applyRelease tail —
|
|
911
|
+
* the reconcile has already folded the sub's snapshot into the room tables, so the swapped
|
|
912
|
+
* view hydrates straight to the room state (swapping earlier would flash it empty). The
|
|
913
|
+
* ORIGINAL ast stays in {@link asts}; the swap-back ({@link dropGhost}) re-registers it.
|
|
914
|
+
*
|
|
915
|
+
* This is the accepted-flash boundary (302 §4.1/§7.1): the room's copy may be behind the
|
|
916
|
+
* daemon rows the view showed a moment ago — accepted by decision, revisit on a real
|
|
917
|
+
* two-region deploy. */
|
|
918
|
+
private processSwapIns;
|
|
919
|
+
/** Fold `domain`'s lmid system query's released ops (lmid-as-data): the one row's
|
|
920
|
+
* `last_mutation_id` cell is this client's confirmed high-water mid in that domain — it advances
|
|
921
|
+
* `watermark[domain]` and, on a fresh session ahead of our issued mids, `nextMid[domain]`. The
|
|
922
|
+
* daemon stream folds `"daemon"`; a room stream folds its own `"room:doc:X"`; the daemon-carried
|
|
923
|
+
* §7.1 ledger rows fold through the same {@link foldConfirm} core (Slice I-iii). */
|
|
339
924
|
private foldLmidOps;
|
|
925
|
+
/** THE one confirm fold (§7.1/§7.2): advance `watermark[domain]` to `lmid` (monotone max) and,
|
|
926
|
+
* on a fresh session ahead of our issued mids, adopt `nextMid[domain]`. Shared verbatim by the
|
|
927
|
+
* per-channel lmid system query ({@link foldLmidOps}) and the daemon-carried room-ledger rows
|
|
928
|
+
* ({@link foldSystemFrames} — one core so the two paths cannot drift). */
|
|
929
|
+
private foldConfirm;
|
|
930
|
+
/** Fold one release's SYSTEM frames in a FIXED category order — the order is STRUCTURAL (one
|
|
931
|
+
* function, categories in sequence), because it is the client half of THE NAMED INVARIANT
|
|
932
|
+
* (§3.3's shipped note; documented above {@link handleMutationOutcome}): **never retire a
|
|
933
|
+
* room-domain entry off a daemon-carried lmid without outcome resolution.**
|
|
934
|
+
*
|
|
935
|
+
* 1. **outcome rows** (`_rindle_room_mutation_outcomes`) — each row for OUR clientID is
|
|
936
|
+
* synthesized into a {@link MutationOutcomeFrame} and routed through
|
|
937
|
+
* {@link handleMutationOutcome}, the SAME H-v state machine the room socket's frames use
|
|
938
|
+
* (one verdict path: frames and rows cannot drift). A deopt flips its pending entry to
|
|
939
|
+
* the daemon IN PLACE (keep-seq, deal-and-send-now); a rejection surfaces + stays for the
|
|
940
|
+
* ordinary burnt-mid retire; a duplicate (frame already seen, or the row re-delivered) is
|
|
941
|
+
* absorbed by the processed set — which doubles as the resolved-verdict memory across
|
|
942
|
+
* releases (per-domain FIFO, {@link MAX_PROCESSED_OUTCOMES_PER_DOMAIN}, mirroring the
|
|
943
|
+
* shell's recorded-outcome cap).
|
|
944
|
+
* 2. **room-ledger rows** (`_rindle_room_client_mutations`) — the FIRST daemon-carried
|
|
945
|
+
* room-lmid path: OUR row's `last_mutation_id` folds into `watermark[room:<doc>]` via
|
|
946
|
+
* {@link foldConfirm}. Because step 1 ALREADY resolved every non-applied verdict this
|
|
947
|
+
* release carries (and earlier releases' verdicts were resolved at their own release),
|
|
948
|
+
* the confirm-drop that follows in {@link applyRelease} retires only entries whose
|
|
949
|
+
* outcome is resolution-by-absence — which I-ii's co-commit atomicity defines as APPLIED
|
|
950
|
+
* (a room flush co-commits the ledger row and every non-applied mid's outcome row in ONE
|
|
951
|
+
* daemon transaction, so a covering lmid without a row IS the applied verdict).
|
|
952
|
+
* Processing this category before step 1 is the violation, in two proven directions
|
|
953
|
+
* (each run break→fail→revert against `test/system_streams.test.ts`): (a) the ledger's
|
|
954
|
+
* fresh-session `nextMid` ADOPTION must not run before historical outcome rows are
|
|
955
|
+
* judged — adopted-first, a previous session's retained deopt row passes the
|
|
956
|
+
* "never-issued" guard and spuriously re-invokes a mutation that session already handled
|
|
957
|
+
* (a double-apply); (b) the RETIRE must not precede resolution — it does not BECAUSE the
|
|
958
|
+
* confirm-drop runs in {@link applyRelease}, strictly after this whole function. That
|
|
959
|
+
* deferral is load-bearing: an "optimization" retiring inline with the watermark fold
|
|
960
|
+
* retires a deopted entry as a silent success (the exact lost-write H-v exists to
|
|
961
|
+
* prevent) and mis-attributes a rejected row's reason.
|
|
962
|
+
* 3. **watermark rows** (`_rindle_room_watermark`) — the §4.2 fence value, max-folded per
|
|
963
|
+
* doc ({@link roomWatermarks}); I-v's ghost-drop consumer, no reaction here.
|
|
964
|
+
* 4. **scope-session rows** (`_rindle_scope_sessions`) — the §4.1 occupancy map
|
|
965
|
+
* ({@link scopeSessions}); I-iv's doorbell consumer, no reaction here.
|
|
966
|
+
*
|
|
967
|
+
* Ordinary data ops fold AFTER all of these (the caller's main loop) — outcome/ledger state
|
|
968
|
+
* must be in place before {@link applyRelease}'s confirm-drop + reconcile consume the release.
|
|
969
|
+
* Every row is filtered against the retain's {@link SystemStreamSpec} scope/doc AND (for the
|
|
970
|
+
* client-keyed tables) our own `clientID` — defense in depth: the server predicate may have
|
|
971
|
+
* been minted doc-only (no `clientId` at lease time), so other clients' rows are expected and
|
|
972
|
+
* must be ignored, and a row for a doc this retain was not minted for is never folded.
|
|
973
|
+
*
|
|
974
|
+
* Returns the scopes category 4 touched (snapshot or ops) — the I-iv doorbell events' input;
|
|
975
|
+
* `null` when none (every non-lifecycle release). The events themselves fire from
|
|
976
|
+
* `onGateProgress` AFTER the release applies, never from inside the fold. */
|
|
977
|
+
private foldSystemFrames;
|
|
978
|
+
/** The I-iv occupancy count — THE one rule (§4.1/D7): unexpired (`expires_at >` the fold
|
|
979
|
+
* clock's now) sessions under `scope` from OTHER clientIDs. Shared by the doorbell events
|
|
980
|
+
* ({@link onGateProgress}) and the client's registration-time check (a doorbell that folded
|
|
981
|
+
* BEFORE a candidate registered must still be able to trigger it) so the two can never
|
|
982
|
+
* disagree. Own-clientID rows never count — a solo client cannot ring its own bell — and
|
|
983
|
+
* expiry is judged on the injectable {@link FoldClock} (deterministic in a virtual-clock
|
|
984
|
+
* harness, the folded-oracle discipline). */
|
|
985
|
+
otherScopeSessions(scope: string): number;
|
|
986
|
+
/** Register the I-iv doorbell event sink — see {@link ScopeSessionsEvent}. One handler (a later
|
|
987
|
+
* registration replaces it, the {@link onLocalWrite} convention); client.ts is the consumer. */
|
|
988
|
+
onScopeSessions(handler: (event: ScopeSessionsEvent) => void): void;
|
|
340
989
|
/** One §1.3 reconcile cycle: rewind the optimistic layer and fold the coherent SERVER
|
|
341
990
|
* delta into BOTH head AND the `sync` baseline (`serverBatchBegin`), re-invoke every
|
|
342
991
|
* still-pending mutator to re-stage the optimistic layer (the rewind un-applied it), then
|
|
@@ -344,29 +993,49 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
344
993
|
* boundary — `onProgress` releases and `unregisterQuery`'s GC both go through here so head
|
|
345
994
|
* and sync never diverge (the §1.2 invariant; CRIT#2). */
|
|
346
995
|
private runReconcileCycle;
|
|
347
|
-
/**
|
|
348
|
-
* sequence reset, so previously-released `cv`s no longer bound the new stream. The
|
|
349
|
-
* already re-subscribed every query (reconnect → resync); drop
|
|
350
|
-
* watermark so the fresh, low-`cv` snapshots are RELEASED instead of dropped as stale
|
|
351
|
-
* (`
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
*
|
|
358
|
-
*
|
|
996
|
+
/** ONE channel's authority restarted (a new boot id): it lost all materialization + `cv` state
|
|
997
|
+
* and its `cv` sequence reset, so previously-released `cv`s no longer bound the new stream. The
|
|
998
|
+
* source has already re-subscribed every query (reconnect → resync); drop THIS gate's buffer
|
|
999
|
+
* and `cv` watermark so the fresh, low-`cv` snapshots are RELEASED instead of dropped as stale
|
|
1000
|
+
* (`onFrame`/`computeRelease` gate on `appliedCv`). The OTHER gates are untouched — an
|
|
1001
|
+
* authority restart is per-channel (§5.1). Pending optimistic mutations stay put — they
|
|
1002
|
+
* re-apply on the next reconcile, and the channel's lmid system query's fresh snapshot restores
|
|
1003
|
+
* its domain's confirmation watermark. */
|
|
1004
|
+
private resetGate;
|
|
1005
|
+
/** The §8.5 escape: ONE gate's buffer outgrew its cap (a pinned `cvMin` under churn on that
|
|
1006
|
+
* channel). Drop everything it buffered and re-register every query on that source — the fresh
|
|
1007
|
+
* snapshots arrive as ordinary frames and the next release re-hydrates via the footprint diff
|
|
1008
|
+
* (the §5.3 path); still-pending optimism re-applies in that cycle. The other gates' buffers
|
|
1009
|
+
* and subscriptions are untouched. */
|
|
359
1010
|
private overflow;
|
|
360
1011
|
private setResultType;
|
|
361
1012
|
/** Recompute a query's server-channel state from hydration alone (§7): a pending mutation no
|
|
362
1013
|
* longer affects it. Used when a remote sub attaches to or hydrates a local view. */
|
|
363
1014
|
private recomputeResultType;
|
|
364
1015
|
/** A remote sub's first snapshot landed: mark it (and every local view it feeds) hydrated, then
|
|
365
|
-
* lift those views out of `unknown` (loading).
|
|
366
|
-
*
|
|
1016
|
+
* lift those views out of `unknown` (loading). A ROOM sub's hydration additionally queues the
|
|
1017
|
+
* 302 §4.1 swap-in — performed at the applyRelease TAIL ({@link processSwapIns}), once the
|
|
1018
|
+
* reconcile has folded this snapshot into the room tables. Idempotent — a re-hydrate snapshot
|
|
1019
|
+
* re-marks harmlessly; a source qid with no sub (the lmid system query) is a no-op. */
|
|
367
1020
|
private markSubHydrated;
|
|
1021
|
+
/** `channel` (G-iii registration-time routing): the gate the sub registers on — the qid's
|
|
1022
|
+
* ownership is fixed HERE, at retain time (no lazy claim; `onFrame` only asserts it). Default
|
|
1023
|
+
* `"daemon"`, so every channel-less caller is byte-identical to before. */
|
|
368
1024
|
private retainRemote;
|
|
369
1025
|
private releaseRemote;
|
|
370
1026
|
private addServerDependencyTables;
|
|
371
1027
|
}
|
|
1028
|
+
/** The namespaced ENGINE table backing wire `table` for room `sourceKey` (302 §2: `room_deck` ≠
|
|
1029
|
+
* `deck` — one authority per table). `@` appears in no schema table name — ENFORCED by
|
|
1030
|
+
* `createSchema`/`extendSchema`'s addTableMeta ban (packages/client/src/schema.ts), so the name
|
|
1031
|
+
* cannot collide with a real table. */
|
|
1032
|
+
export declare function roomEngineTable(table: string, sourceKey: string): string;
|
|
1033
|
+
/** Rename every TABLE reference in a query AST through `map` (302 §2 point 3 — the room-homed
|
|
1034
|
+
* view's rewrite): the root `table`, every `related` subquery, every `correlatedSubquery`
|
|
1035
|
+
* (EXISTS) condition — walking the KNOWN wire-AST shape, never a blind key scan: `start.row` is
|
|
1036
|
+
* keyed by COLUMN name (a schema column literally named `table` must keep its bound value), and
|
|
1037
|
+
* the same goes for any future column-keyed record. Tables absent from the map keep their name —
|
|
1038
|
+
* that is the client-side join across kinds (a room table joined to daemon-owned context,
|
|
1039
|
+
* 201-style). Structural clone; the input AST is never mutated. */
|
|
1040
|
+
export declare function remapAstTables(ast: Ast, map: ReadonlyMap<string, string>): Ast;
|
|
372
1041
|
//# sourceMappingURL=backend.d.ts.map
|