@rindle/optimistic 0.1.6 → 0.3.1
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 +44 -2
- package/dist/backend.d.ts.map +1 -1
- package/dist/backend.js +165 -86
- package/dist/backend.js.map +1 -1
- package/dist/client-id.d.ts +3 -0
- package/dist/client-id.d.ts.map +1 -1
- package/dist/client-id.js +17 -0
- package/dist/client-id.js.map +1 -1
- package/dist/client.d.ts +7 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +46 -5
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/src/backend.ts +183 -83
- package/src/client-id.ts +16 -0
- package/src/client.ts +58 -5
- package/src/index.ts +3 -1
package/dist/backend.d.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
-
import type { Ast, Backend, ChangeEvent, ColsMap, Mutation, OptimisticSource, QueryId, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
|
|
1
|
+
import type { Ast, Backend, BackendDevObserver, ChangeEvent, ColsMap, Mutation, OptimisticSource, QueryId, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
|
|
2
2
|
/** A keyed row: column name → cell. The ergonomic shape — column names are validated
|
|
3
3
|
* against the schema at runtime, so a typo throws immediately with the valid names. */
|
|
4
4
|
export type KeyedRow = Record<string, WireValue>;
|
|
5
|
+
/** What `MutationTx.query` accepts: a query handle whose `.ast()` lowers to the wire
|
|
6
|
+
* {@link Ast} — exactly what the typed query builder (`store.query.<table>…` or
|
|
7
|
+
* `newQueryBuilder(schema).<table>…`) produces (203-MUTATOR-READS-DESIGN.md §9.1). Typed
|
|
8
|
+
* structurally so the `MutationTx` surface need not carry the builder's heavy generics. */
|
|
9
|
+
export type QueryArg = {
|
|
10
|
+
ast(): Ast;
|
|
11
|
+
};
|
|
12
|
+
/** A row returned by {@link MutationTx.query}: column name → cell, plus each materialized
|
|
13
|
+
* relationship name → its nested row(s) — an array (a plural relationship), or a single row /
|
|
14
|
+
* `null` (a `.one()` relationship). Recursive (nested rows share the shape). Presented
|
|
15
|
+
* identically to a `view.data` row of the same query (203-MUTATOR-READS-DESIGN.md §5.2). */
|
|
16
|
+
export type QueryResultRow = {
|
|
17
|
+
[key: string]: WireValue | QueryResultRow | QueryResultRow[];
|
|
18
|
+
};
|
|
5
19
|
/** The write handle a client mutator runs against (the client `MutationTx`, §4.2):
|
|
6
20
|
* reads see the current base + this transaction's own staged writes (§4.1).
|
|
7
21
|
*
|
|
@@ -20,6 +34,14 @@ export interface MutationTx {
|
|
|
20
34
|
upsert(table: string, row: KeyedRow): void;
|
|
21
35
|
/** Delete the row identified by the pk columns. A missing row is a NO-OP. */
|
|
22
36
|
delete(table: string, pk: KeyedRow): void;
|
|
37
|
+
/** Run a one-shot read query (`where`/`orderBy`/`limit`/join) over the state this
|
|
38
|
+
* mutator is mutating — it sees this transaction's own writes-so-far, the same
|
|
39
|
+
* read-your-writes contract as `get`/`row` (§4.1; 203-MUTATOR-READS-DESIGN.md §5.2).
|
|
40
|
+
* Synchronous; returns the matching rows in the query's order, each with its materialized
|
|
41
|
+
* relationship children nested by name (presented identically to a `view.data` row). Pass
|
|
42
|
+
* a query from the typed builder, e.g. `tx.query(q.issue.where("owner", "=", me))`.
|
|
43
|
+
* Refused inside a FOLDED mutator (a reading mutator is non-absorbing, §9.1). */
|
|
44
|
+
query(query: QueryArg): QueryResultRow[];
|
|
23
45
|
get(table: string, pk: WireValue[]): WireValue[] | undefined;
|
|
24
46
|
add(table: string, row: WireValue[]): void;
|
|
25
47
|
remove(table: string, row: WireValue[]): void;
|
|
@@ -147,6 +169,12 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
147
169
|
* pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
|
|
148
170
|
private readonly overlay;
|
|
149
171
|
private handler;
|
|
172
|
+
/** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
|
|
173
|
+
* local engine's `dispatch` brackets so the Store folds every affected view before notifying any
|
|
174
|
+
* subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
|
|
175
|
+
* local engine, so its commit brackets are this backend's commit brackets. */
|
|
176
|
+
private boundaryHandler;
|
|
177
|
+
private readonly devObservers;
|
|
150
178
|
private pendingMutations;
|
|
151
179
|
private nextMid;
|
|
152
180
|
/** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
|
|
@@ -192,7 +220,7 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
192
220
|
* `unregisterTable` frees it (the engine refuses otherwise). */
|
|
193
221
|
private releaseSyntheticTables;
|
|
194
222
|
unregisterQuery(qid: QueryId): void;
|
|
195
|
-
retainRemoteQuery(qid: QueryId, remote: RemoteQuery, localQueryId?: QueryId): void;
|
|
223
|
+
retainRemoteQuery(qid: QueryId, remote: RemoteQuery, localQueryId?: QueryId, ast?: Ast): void;
|
|
196
224
|
releaseRemoteQuery(qid: QueryId): void;
|
|
197
225
|
/** Raw CRUD has no optimistic story (§9 replaces it with named mutators). Register a
|
|
198
226
|
* mutator — even a trivial one — and `invoke` it. */
|
|
@@ -204,6 +232,17 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
204
232
|
* non-reentrant (A5), so a local write can never interleave with an open server cycle. */
|
|
205
233
|
writeLocal(mutations: Mutation[]): void;
|
|
206
234
|
onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void;
|
|
235
|
+
onCommitBoundary(handler: (phase: "begin" | "end") => void): void;
|
|
236
|
+
/** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
|
|
237
|
+
* notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
|
|
238
|
+
* and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
|
|
239
|
+
* logical mutation: a relationship-`count` view and the data view it counts must update together.
|
|
240
|
+
* The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
|
|
241
|
+
* outer pair and the Store flushes every affected view (data AND count) once, together, at the
|
|
242
|
+
* outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
|
|
243
|
+
* half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
|
|
244
|
+
* prediction never wedges the Store in deferred mode. */
|
|
245
|
+
private inOneCommit;
|
|
207
246
|
/** Run the named client mutator optimistically: the prediction applies to the live
|
|
208
247
|
* engine now (affected views update synchronously), `(mid, name, args)` joins the
|
|
209
248
|
* pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
|
|
@@ -240,6 +279,7 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
240
279
|
* A pending local mutation no longer moves it — see {@link pending}. */
|
|
241
280
|
resultType(qid: QueryId): ResultType;
|
|
242
281
|
onResultType(handler: (qid: QueryId, rt: ResultType) => void): void;
|
|
282
|
+
__attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void;
|
|
243
283
|
/** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
|
|
244
284
|
* pending here?" (FOLDED-MUTATIONS-DESIGN §7.2). Orthogonal to {@link resultType}; this is the
|
|
245
285
|
* same `queryTables ∩ pending.touched` computation that used to be smuggled into `unknown`. */
|
|
@@ -258,6 +298,8 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
|
|
|
258
298
|
* (remove) — exactly where `:359`/`:468` used to flip ResultType (§7.3). */
|
|
259
299
|
private refreshPending;
|
|
260
300
|
private onNormalized;
|
|
301
|
+
private emitServerDelta;
|
|
302
|
+
private localQidsForSource;
|
|
261
303
|
private onProgress;
|
|
262
304
|
/** Fold the lmid system query's released ops (lmid-as-data): the one row's
|
|
263
305
|
* `last_mutation_id` cell is this client's confirmed high-water mid. */
|
package/dist/backend.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EACV,GAAG,EACH,OAAO,EACP,WAAW,EACX,OAAO,EACP,QAAQ,EAIR,gBAAgB,EAEhB,OAAO,EACP,WAAW,EACX,UAAU,EACV,MAAM,EACN,SAAS,EACV,MAAM,gBAAgB,CAAC;AAMxB;wFACwF;AACxF,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEjD;;;;;kFAKkF;AAClF,MAAM,WAAW,UAAU;IAEzB,uEAAuE;IACvE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD,gFAAgF;IAChF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C;0FACsF;IACtF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,uFAAuF;IACvF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,6EAA6E;IAC7E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EACV,GAAG,EACH,OAAO,EACP,kBAAkB,EAClB,WAAW,EACX,OAAO,EACP,QAAQ,EAIR,gBAAgB,EAEhB,OAAO,EACP,WAAW,EACX,UAAU,EACV,MAAM,EACN,SAAS,EACV,MAAM,gBAAgB,CAAC;AAMxB;wFACwF;AACxF,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAEjD;;;4FAG4F;AAC5F,MAAM,MAAM,QAAQ,GAAG;IAAE,GAAG,IAAI,GAAG,CAAA;CAAE,CAAC;AAEtC;;;6FAG6F;AAC7F,MAAM,MAAM,cAAc,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,cAAc,EAAE,CAAA;CAAE,CAAC;AAE9F;;;;;kFAKkF;AAClF,MAAM,WAAW,UAAU;IAEzB,uEAAuE;IACvE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACvD,gFAAgF;IAChF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C;0FACsF;IACtF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,uFAAuF;IACvF,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC3C,6EAA6E;IAC7E,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,GAAG,IAAI,CAAC;IAC1C;;;;;;sFAMkF;IAClF,KAAK,CAAC,KAAK,EAAE,QAAQ,GAAG,cAAc,EAAE,CAAC;IAEzC,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,SAAS,EAAE,GAAG,SAAS,EAAE,GAAG,SAAS,CAAC;IAC7D,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAC9C,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;CACrE;AAED;sFACsF;AACtF,MAAM,MAAM,aAAa,GAAG,CAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,KAAK,KAAK,IAAI,CAAC;AAElE;6DAC6D;AAC7D,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAE3D,YAAY,EAAE,UAAU,EAAE,CAAC;AAkB3B;iGACiG;AACjG,MAAM,WAAW,SAAS;IACxB,UAAU,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAChD,YAAY,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;IACpC,GAAG,IAAI,MAAM,CAAC;CACf;AAED;oFACoF;AACpF,MAAM,WAAW,WAAW;IAC1B,wFAAwF;IACxF,GAAG,EAAE,OAAO,CAAC;IACb,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;qFACiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;gGAC4F;IAC5F,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;6FAE6F;AAC7F,MAAM,WAAW,UAAU;IACzB,KAAK,IAAI,IAAI,CAAC;IACd,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;CAC/B;AAiCD,MAAM,WAAW,wBAAwB;IACvC,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;sGACkG;IAClG,KAAK,CAAC,EAAE,SAAS,CAAC;CACnB;AASD,qFAAqF;AACrF,MAAM,WAAW,WAAW;IAC1B,4FAA4F;IAC5F,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,OAAO,CAAC;IAC3B,qGAAqG;IACrG,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,uFAAuF;AACvF,MAAM,WAAW,cAAc;IAC7B;6FACyF;IACzF,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,OAAO,CAAC;IACd,8FAA8F;IAC9F,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,yFAAyF;AACzF,MAAM,WAAW,iBAAiB;IAChC;uFACmF;IACnF,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,iGAAiG;IACjG,aAAa,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,wFAAwF;IACxF,cAAc,EAAE,MAAM,CAAC;IACvB,oGAAoG;IACpG,aAAa,EAAE,MAAM,EAAE,CAAC;CACzB;AAYD,qBAAa,iBAAiB,CAAC,CAAC,SAAS,OAAO,CAAE,YAAW,OAAO;IAClE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;IACvC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAiB;IACtC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;IAC1C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IACnC;;0FAEsF;IACtF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAc;IAC1C;;;;2BAIuB;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAY;IACtC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D;;oDAEgD;IAChD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAA0B;IAC3D;;2BAEuB;IACvB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IACtE;;oFAEgF;IAChF,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,gGAAgG;IAChG,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAgC;IAC/D;yFACqF;IACrF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoB;IAC5C,OAAO,CAAC,OAAO,CAAqD;IACpE;;;mFAG+E;IAC/E,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,OAAO,CAAK;IACpB;sGACkG;IAClG,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;IACvD,0FAA0F;IAC1F,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAY;IAClC;4DACwD;IACxD,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,MAAM,CAAuB;IACrC,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,SAAS,CAAK;IAEtB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,+EAA+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAmC;IAC/D,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgC;IAC3D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA8B;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA8B;IAC5D,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAA2C;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAkC;IAC9D;;8FAE0F;IAC1F,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,iBAAiB,CAAoD;IAC7E;kGAC8F;IAC9F,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA+B;IAC5D,OAAO,CAAC,cAAc,CAAsD;gBAG1E,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,cAAc,EACxB,IAAI,EAAE,wBAAwB;IAiChC,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI;IA8CjE;;;;wEAIoE;IACpE,OAAO,CAAC,qBAAqB;IAuB7B;;;;qEAIiE;IACjE,OAAO,CAAC,sBAAsB;IAqB9B,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAyBnC,iBAAiB,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,YAAY,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,EAAE,GAAG,GAAG,IAAI;IAK7F,kBAAkB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI;IAatC;0DACsD;IACtD,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7C;;;;+FAI2F;IAC3F,UAAU,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,IAAI;IAIvC,OAAO,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,KAAK,IAAI,GAAG,IAAI;IAI/D,gBAAgB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,KAAK,IAAI,GAAG,IAAI;IAMjE;;;;;;;;8DAQ0D;IAC1D,OAAO,CAAC,WAAW;IASnB;;sFAEkF;IAClF,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM;IAkC3C;;;oGAGgG;IAChG,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,OAAO,GAAG,UAAU;IAoExE;;;oGAGgG;IAChG,OAAO,CAAC,gBAAgB;IAQxB;;0GAEsG;IACtG,OAAO,CAAC,SAAS;IAWjB;8FAC0F;IAC1F,UAAU,IAAI,IAAI;IAIlB;;qGAEiG;IACjG,OAAO,CAAC,WAAW;IAMnB;;;;;mFAK+E;IAC/E,OAAO,CAAC,gBAAgB;IAuBxB;6EACyE;IACzE,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,UAAU;IAIpC,YAAY,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,UAAU,KAAK,IAAI,GAAG,IAAI;IAInE,4BAA4B,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,IAAI;IAStE;;oGAEgG;IAChG,OAAO,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO;IAM9B;yEACqE;IACrE,SAAS,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI;IAIlE,uGAAuG;IACvG,aAAa,IAAI,GAAG,CAAC,MAAM,CAAC;IAM5B;;8EAE0E;IAC1E,SAAS,IAAI,iBAAiB;IAiC9B;;iFAE6E;IAC7E,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,YAAY;IAsCpB,OAAO,CAAC,eAAe;IAOvB,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,UAAU;IA+ClB;6EACyE;IACzE,OAAO,CAAC,WAAW;IAsBnB;;;;;+DAK2D;IAC3D,OAAO,CAAC,iBAAiB;IAyDzB;;;;;;kCAM8B;IAC9B,OAAO,CAAC,eAAe;IAKvB;;;2FAGuF;IACvF,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,aAAa;IAMrB;0FACsF;IACtF,OAAO,CAAC,mBAAmB;IAI3B;;mFAE+E;IAC/E,OAAO,CAAC,eAAe;IAYvB,OAAO,CAAC,YAAY;IAyBpB,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,yBAAyB;CAkBlC"}
|
package/dist/backend.js
CHANGED
|
@@ -94,6 +94,12 @@ export class OptimisticBackend {
|
|
|
94
94
|
* pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
|
|
95
95
|
overlay = new AggOverlay();
|
|
96
96
|
handler = () => { };
|
|
97
|
+
/** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
|
|
98
|
+
* local engine's `dispatch` brackets so the Store folds every affected view before notifying any
|
|
99
|
+
* subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
|
|
100
|
+
* local engine, so its commit brackets are this backend's commit brackets. */
|
|
101
|
+
boundaryHandler = () => { };
|
|
102
|
+
devObservers = new Set();
|
|
97
103
|
pendingMutations = [];
|
|
98
104
|
nextMid = 1;
|
|
99
105
|
/** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
|
|
@@ -127,6 +133,9 @@ export class OptimisticBackend {
|
|
|
127
133
|
constructor(schema, source, registry, opts) {
|
|
128
134
|
this.local = new WasmBackend(schema);
|
|
129
135
|
this.local.onEvent((qid, ev) => this.handler(qid, ev));
|
|
136
|
+
// Forward the local engine's commit brackets up to the Store (cross-view-atomic notification):
|
|
137
|
+
// every data delta this backend emits comes from `this.local`, so its commit boundaries are ours.
|
|
138
|
+
this.local.onCommitBoundary((phase) => this.boundaryHandler(phase));
|
|
130
139
|
this.colCounts = colCountsFromSchema(schema);
|
|
131
140
|
this.colIndex = colIndexFromSchema(schema);
|
|
132
141
|
this.sync = new NormalizedSync(pkColsFromSchema(schema), this.colCounts);
|
|
@@ -279,11 +288,18 @@ export class OptimisticBackend {
|
|
|
279
288
|
this.pendingState.delete(qid); // §7.2 cache, keyed by the local materialized qid (a monotonic
|
|
280
289
|
// Store counter — re-materialize gets a fresh id, never this one again), so drop it on teardown.
|
|
281
290
|
}
|
|
282
|
-
retainRemoteQuery(qid, remote, localQueryId) {
|
|
291
|
+
retainRemoteQuery(qid, remote, localQueryId, ast) {
|
|
292
|
+
if (ast)
|
|
293
|
+
this.ensureSyntheticTables(qid, ast);
|
|
283
294
|
this.retainRemote(qid, remote, localQueryId);
|
|
284
295
|
}
|
|
285
296
|
releaseRemoteQuery(qid) {
|
|
286
297
|
const remoteQid = this.releaseRemote(qid);
|
|
298
|
+
this.releaseSyntheticTables(qid);
|
|
299
|
+
if (!this.queryTables.has(qid)) {
|
|
300
|
+
this.resultTypes.delete(qid);
|
|
301
|
+
this.hydrated.delete(qid);
|
|
302
|
+
}
|
|
287
303
|
if (remoteQid === undefined)
|
|
288
304
|
return;
|
|
289
305
|
this.buffer = this.buffer.filter((f) => f.qid !== remoteQid);
|
|
@@ -307,7 +323,28 @@ export class OptimisticBackend {
|
|
|
307
323
|
onEvent(handler) {
|
|
308
324
|
this.handler = handler;
|
|
309
325
|
}
|
|
326
|
+
onCommitBoundary(handler) {
|
|
327
|
+
this.boundaryHandler = handler;
|
|
328
|
+
}
|
|
310
329
|
// --- the named-mutator entry (§9) ----------------------------------------------
|
|
330
|
+
/** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
|
|
331
|
+
* notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
|
|
332
|
+
* and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
|
|
333
|
+
* logical mutation: a relationship-`count` view and the data view it counts must update together.
|
|
334
|
+
* The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
|
|
335
|
+
* outer pair and the Store flushes every affected view (data AND count) once, together, at the
|
|
336
|
+
* outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
|
|
337
|
+
* half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
|
|
338
|
+
* prediction never wedges the Store in deferred mode. */
|
|
339
|
+
inOneCommit(apply) {
|
|
340
|
+
this.boundaryHandler("begin");
|
|
341
|
+
try {
|
|
342
|
+
return apply();
|
|
343
|
+
}
|
|
344
|
+
finally {
|
|
345
|
+
this.boundaryHandler("end");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
311
348
|
/** Run the named client mutator optimistically: the prediction applies to the live
|
|
312
349
|
* engine now (affected views update synchronously), `(mid, name, args)` joins the
|
|
313
350
|
* pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
|
|
@@ -315,32 +352,36 @@ export class OptimisticBackend {
|
|
|
315
352
|
const mutator = this.registry[name];
|
|
316
353
|
if (!mutator)
|
|
317
354
|
throw new Error(`unknown client mutator: ${name}`);
|
|
318
|
-
//
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
355
|
+
// One commit boundary spans the prediction AND the `__agg`-head reconcile below, so their views
|
|
356
|
+
// (data + count) flush together rather than tearing across two engine commits.
|
|
357
|
+
return this.inOneCommit(() => {
|
|
358
|
+
// Apply the prediction FIRST. If the mutator throws (client-side validation, a bad read),
|
|
359
|
+
// the staged write is discarded (the wasm txn is a clean no-op until commit) and the throw
|
|
360
|
+
// propagates with NO mid consumed — a burnt mid is a permanent server-side gap that
|
|
361
|
+
// silently refuses every later mutation from this client (#10).
|
|
362
|
+
const touched = new Set();
|
|
363
|
+
const ops = [];
|
|
364
|
+
this.local.writeWith((tx) => {
|
|
365
|
+
mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), args);
|
|
366
|
+
});
|
|
367
|
+
// Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
|
|
368
|
+
// this write does, so wire order == local-apply order for any pair that can observe each other
|
|
369
|
+
// (a read-dependent write reading a folded cell sees the same value optimistically and on the
|
|
370
|
+
// wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
|
|
371
|
+
this.drainOverlapping(touched);
|
|
372
|
+
const mid = this.nextMid++;
|
|
373
|
+
this.pendingMutations.push({ mid, name, args, touched });
|
|
374
|
+
// The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
|
|
375
|
+
// the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
|
|
376
|
+
// delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
|
|
377
|
+
// touched group's head as the absolute `server_base ⊕ delta`.
|
|
378
|
+
for (const op of ops)
|
|
379
|
+
this.overlay.observe(op);
|
|
380
|
+
this.reconcileAggHead();
|
|
381
|
+
this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
|
|
382
|
+
void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
|
|
383
|
+
return mid;
|
|
326
384
|
});
|
|
327
|
-
// Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
|
|
328
|
-
// this write does, so wire order == local-apply order for any pair that can observe each other
|
|
329
|
-
// (a read-dependent write reading a folded cell sees the same value optimistically and on the
|
|
330
|
-
// wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
|
|
331
|
-
this.drainOverlapping(touched);
|
|
332
|
-
const mid = this.nextMid++;
|
|
333
|
-
this.pendingMutations.push({ mid, name, args, touched });
|
|
334
|
-
// The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
|
|
335
|
-
// the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
|
|
336
|
-
// delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
|
|
337
|
-
// touched group's head as the absolute `server_base ⊕ delta`.
|
|
338
|
-
for (const op of ops)
|
|
339
|
-
this.overlay.observe(op);
|
|
340
|
-
this.reconcileAggHead();
|
|
341
|
-
this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
|
|
342
|
-
void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
|
|
343
|
-
return mid;
|
|
344
385
|
}
|
|
345
386
|
/** Run a FOLDED invoke (FOLDED-MUTATIONS-DESIGN §8): apply the prediction to the live engine now
|
|
346
387
|
* (like `invoke`), but collapse a run of same-key invokes into ONE pending entry whose `args`
|
|
@@ -351,65 +392,69 @@ export class OptimisticBackend {
|
|
|
351
392
|
if (!mutator)
|
|
352
393
|
throw new Error(`unknown client mutator: ${name}`);
|
|
353
394
|
const foldKey = `${name}\0${stableJson(opts.key)}`;
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
throw new Error(`cannot fold "${name}": it reads state via tx.get/tx.row, so it is not absorbing — folded mutators must be last-writer-wins (FOLDED-MUTATIONS-DESIGN §5)`);
|
|
395
|
+
// One commit boundary spans the prediction AND the `__agg`-head reconcile (see {@link inOneCommit}),
|
|
396
|
+
// so a folded mutation's list view and count view flush together, never torn across two commits.
|
|
397
|
+
return this.inOneCommit(() => {
|
|
398
|
+
// Apply the prediction with the read trap armed (§5): a folded mutator that reads state to
|
|
399
|
+
// compute its write is non-absorbing and refused. A throw discards the staged write (clean
|
|
400
|
+
// no-op) and consumes no mid — exactly `invoke`'s guarantee.
|
|
401
|
+
const touched = new Set();
|
|
402
|
+
const ops = [];
|
|
403
|
+
try {
|
|
404
|
+
this.local.writeWith((tx) => {
|
|
405
|
+
mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops), true), args);
|
|
406
|
+
});
|
|
367
407
|
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
f
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
entry
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
f.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
408
|
+
catch (e) {
|
|
409
|
+
if (e instanceof FoldReadError) {
|
|
410
|
+
throw new Error(`cannot fold "${name}": it reads state via tx.get/tx.row, so it is not absorbing — folded mutators must be last-writer-wins (FOLDED-MUTATIONS-DESIGN §5)`);
|
|
411
|
+
}
|
|
412
|
+
throw e;
|
|
413
|
+
}
|
|
414
|
+
for (const op of ops)
|
|
415
|
+
this.overlay.observe(op);
|
|
416
|
+
this.reconcileAggHead();
|
|
417
|
+
const now = this.clock.now();
|
|
418
|
+
let f = this.folds.get(foldKey);
|
|
419
|
+
if (f) {
|
|
420
|
+
// Overwrite the single entry in place — the pending stack does NOT grow (§1 #2). The head
|
|
421
|
+
// already carries this new prediction (absorbing, last-wins on the cell); the entry holds
|
|
422
|
+
// only the LATEST args, which is what a rebase re-derives from and what the flush ships.
|
|
423
|
+
f.entry.args = args;
|
|
424
|
+
f.entry.touched = touched;
|
|
425
|
+
f.args = args;
|
|
426
|
+
this.clock.clearTimeout(f.timer);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
const entry = { mid: null, name, args, touched };
|
|
430
|
+
this.pendingMutations.push(entry);
|
|
431
|
+
let resolveMid;
|
|
432
|
+
const midPromise = new Promise((res) => (resolveMid = res));
|
|
433
|
+
f = {
|
|
434
|
+
entry,
|
|
435
|
+
args,
|
|
436
|
+
timer: undefined,
|
|
437
|
+
firstAt: now,
|
|
438
|
+
debounceMs: opts.debounceMs ?? DEFAULT_FOLD_DEBOUNCE_MS,
|
|
439
|
+
maxWaitMs: opts.maxWaitMs,
|
|
440
|
+
deferAcrossWrites: opts.deferAcrossWrites ?? false,
|
|
441
|
+
midPromise,
|
|
442
|
+
resolveMid,
|
|
443
|
+
};
|
|
444
|
+
this.folds.set(foldKey, f);
|
|
445
|
+
}
|
|
446
|
+
// (Re)arm the trailing debounce — unless the maxWaitMs cap is already due (a never-idle drag
|
|
447
|
+
// still persists periodically, §3/#4), in which case flush now instead of re-arming.
|
|
448
|
+
if (f.maxWaitMs !== undefined && now - f.firstAt >= f.maxWaitMs) {
|
|
449
|
+
this.flushFold(foldKey);
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
f.timer = this.clock.setTimeout(() => this.flushFold(foldKey), f.debounceMs);
|
|
453
|
+
}
|
|
454
|
+
this.refreshPending();
|
|
455
|
+
const handle = { flush: () => this.flushFold(foldKey), mid: f.midPromise };
|
|
456
|
+
return handle;
|
|
457
|
+
});
|
|
413
458
|
}
|
|
414
459
|
/** Flush-on-enqueue (§4.2): for each outstanding fold whose touched tables overlap `tables`,
|
|
415
460
|
* assign its mid NOW and ship it — in creation (insertion) order, so the wire stays gapless. A
|
|
@@ -493,6 +538,12 @@ export class OptimisticBackend {
|
|
|
493
538
|
onResultType(handler) {
|
|
494
539
|
this.resultTypeHandler = handler;
|
|
495
540
|
}
|
|
541
|
+
__attachDevtoolsServerDeltas(observer) {
|
|
542
|
+
this.devObservers.add(observer);
|
|
543
|
+
return () => {
|
|
544
|
+
this.devObservers.delete(observer);
|
|
545
|
+
};
|
|
546
|
+
}
|
|
496
547
|
// --- the pending AXIS (§7.2): orthogonal to ResultType -------------------------------
|
|
497
548
|
/** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
|
|
498
549
|
* pending here?" (FOLDED-MUTATIONS-DESIGN §7.2). Orthogonal to {@link resultType}; this is the
|
|
@@ -566,6 +617,8 @@ export class OptimisticBackend {
|
|
|
566
617
|
}
|
|
567
618
|
// --- the downstream stream (§8.5: buffer, then release coherently) ---------------
|
|
568
619
|
onNormalized(qid, ev) {
|
|
620
|
+
if (qid !== LMID_QID)
|
|
621
|
+
this.emitServerDelta(qid, ev);
|
|
569
622
|
if (ev.type === "hello") {
|
|
570
623
|
// A hello is a (re)subscribe = a NEW epoch. The column map below is mutated eagerly, but
|
|
571
624
|
// data frames are cv-buffered and drained later (onProgress). So any frame still buffered
|
|
@@ -606,6 +659,22 @@ export class OptimisticBackend {
|
|
|
606
659
|
if (this.buffer.length > this.bufferCap)
|
|
607
660
|
this.overflow();
|
|
608
661
|
}
|
|
662
|
+
emitServerDelta(sourceQid, ev) {
|
|
663
|
+
if (!this.devObservers.size)
|
|
664
|
+
return;
|
|
665
|
+
for (const qid of this.localQidsForSource(sourceQid)) {
|
|
666
|
+
for (const o of this.devObservers)
|
|
667
|
+
o.onServerDelta?.(qid, { format: "normalized", event: ev });
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
localQidsForSource(sourceQid) {
|
|
671
|
+
const key = this.sourceToRemote.get(sourceQid);
|
|
672
|
+
const sub = key ? this.remoteSubs.get(key) : undefined;
|
|
673
|
+
if (!sub)
|
|
674
|
+
return [sourceQid];
|
|
675
|
+
const localQids = [...sub.localQids.keys()];
|
|
676
|
+
return localQids.length ? localQids : [sourceQid];
|
|
677
|
+
}
|
|
609
678
|
onProgress(frame) {
|
|
610
679
|
// (1) Take every buffered frame at cv ≤ cvMin, in (cv, arrival) order, and fold it:
|
|
611
680
|
// lmid system-query frames advance `confirmedLmid`; data frames fold through the
|
|
@@ -776,8 +845,6 @@ export class OptimisticBackend {
|
|
|
776
845
|
/** Recompute a query's server-channel state from hydration alone (§7): a pending mutation no
|
|
777
846
|
* longer affects it. Used when a remote sub attaches to or hydrates a local view. */
|
|
778
847
|
recomputeResultType(qid) {
|
|
779
|
-
if (!this.queryTables.has(qid))
|
|
780
|
-
return;
|
|
781
848
|
this.setResultType(qid, this.hydrated.has(qid) ? "complete" : "unknown");
|
|
782
849
|
}
|
|
783
850
|
/** A remote sub's first snapshot landed: mark it (and every local view it feeds) hydrated, then
|
|
@@ -994,8 +1061,20 @@ function trackingTx(tx, touched, specs, localTables, onOp, trapReads = false) {
|
|
|
994
1061
|
const trapped = () => {
|
|
995
1062
|
throw new FoldReadError();
|
|
996
1063
|
};
|
|
1064
|
+
// One-shot query (203 §5.2): lower the builder to an AST, refuse any local-only table it
|
|
1065
|
+
// reads (M1 — same guard as `rawGet`), and run it over the engine's read-cache fork. The
|
|
1066
|
+
// wasm engine returns the rows already keyed by column name with their materialized
|
|
1067
|
+
// relationship children nested by name (`marshal::caught_node_to_js`), in the query's order —
|
|
1068
|
+
// identical in shape to `view.data` — so this is a pass-through.
|
|
1069
|
+
const runQuery = (q) => {
|
|
1070
|
+
const ast = q.ast();
|
|
1071
|
+
for (const t of collectTables(ast))
|
|
1072
|
+
assertNotLocal(t, "read");
|
|
1073
|
+
return tx.query(ast);
|
|
1074
|
+
};
|
|
997
1075
|
return {
|
|
998
1076
|
get: trapReads ? trapped : rawGet,
|
|
1077
|
+
query: trapReads ? trapped : runQuery,
|
|
999
1078
|
add,
|
|
1000
1079
|
remove,
|
|
1001
1080
|
edit,
|