@rindle/optimistic 0.2.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/backend.d.ts CHANGED
@@ -1,7 +1,22 @@
1
- import type { Ast, Backend, ChangeEvent, ColsMap, Mutation, OptimisticSource, QueryId, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
2
- /** A keyed row: column name → cell. The ergonomic shape — column names are validated
3
- * against the schema at runtime, so a typo throws immediately with the valid names. */
4
- export type KeyedRow = Record<string, WireValue>;
1
+ import type { Ast, Backend, BackendDevObserver, ChangeEvent, ColsMap, IsoTx, KeyedRow, Mutation, MutationGen, MutatorCtx, OptimisticSource, QueryId, RemoteQuery, ResultType, Schema, WireValue } from "@rindle/client";
2
+ /** A keyed row: column name → cell. The ergonomic shape — column names are validated against the
3
+ * schema at runtime, so a typo throws immediately with the valid names. Re-exported from
4
+ * `@rindle/client` (the leaf both tiers share). */
5
+ export type { KeyedRow };
6
+ /** What `MutationTx.query` accepts: a query handle whose `.ast()` lowers to the wire
7
+ * {@link Ast} — exactly what the typed query builder (`store.query.<table>…` or
8
+ * `newQueryBuilder(schema).<table>…`) produces (203-MUTATOR-READS-DESIGN.md §9.1). Typed
9
+ * structurally so the `MutationTx` surface need not carry the builder's heavy generics. */
10
+ export type QueryArg = {
11
+ ast(): Ast;
12
+ };
13
+ /** A row returned by {@link MutationTx.query}: column name → cell, plus each materialized
14
+ * relationship name → its nested row(s) — an array (a plural relationship), or a single row /
15
+ * `null` (a `.one()` relationship). Recursive (nested rows share the shape). Presented
16
+ * identically to a `view.data` row of the same query (203-MUTATOR-READS-DESIGN.md §5.2). */
17
+ export type QueryResultRow = {
18
+ [key: string]: WireValue | QueryResultRow | QueryResultRow[];
19
+ };
5
20
  /** The write handle a client mutator runs against (the client `MutationTx`, §4.2):
6
21
  * reads see the current base + this transaction's own staged writes (§4.1).
7
22
  *
@@ -18,16 +33,30 @@ export interface MutationTx {
18
33
  update(table: string, row: KeyedRow): void;
19
34
  /** Insert, or fully replace when the pk already exists (a FULL row, like `insert`). */
20
35
  upsert(table: string, row: KeyedRow): void;
36
+ /** Insert a FULL row, or do nothing if the pk already exists (the isomorphic form of the classic
37
+ * `if (!tx.row(pk)) tx.insert(row)` upsert-if-absent; renders `ON CONFLICT DO NOTHING` server-side). */
38
+ insertIgnore(table: string, row: KeyedRow): void;
21
39
  /** Delete the row identified by the pk columns. A missing row is a NO-OP. */
22
40
  delete(table: string, pk: KeyedRow): void;
41
+ /** Run a one-shot read query (`where`/`orderBy`/`limit`/join) over the state this
42
+ * mutator is mutating — it sees this transaction's own writes-so-far, the same
43
+ * read-your-writes contract as `get`/`row` (§4.1; 203-MUTATOR-READS-DESIGN.md §5.2).
44
+ * Synchronous; returns the matching rows in the query's order, each with its materialized
45
+ * relationship children nested by name (presented identically to a `view.data` row). Pass
46
+ * a query from the typed builder, e.g. `tx.query(q.issue.where("owner", "=", me))`.
47
+ * Refused inside a FOLDED mutator (a reading mutator is non-absorbing, §9.1). */
48
+ query(query: QueryArg): QueryResultRow[];
23
49
  get(table: string, pk: WireValue[]): WireValue[] | undefined;
24
50
  add(table: string, row: WireValue[]): void;
25
51
  remove(table: string, row: WireValue[]): void;
26
52
  edit(table: string, oldRow: WireValue[], newRow: WireValue[]): void;
27
53
  }
28
- /** A client mutator: optimistic, deterministic, replayable — a pure function of
29
- * `(base, args)` (§5: no clock, no randomness; it is RE-INVOKED on every rebase). */
30
- export type ClientMutator = (tx: MutationTx, args: never) => void;
54
+ /** A client mutator: optimistic, deterministic, replayable — a pure function of `(base, args)` (§5:
55
+ * no clock, no randomness; it is RE-INVOKED on every rebase). Either shape is accepted:
56
+ * - a plain synchronous function `(tx, args) => void` (client-only), OR
57
+ * - a shared GENERATOR `(tx, args, ctx) => MutationGen` (the isomorphic form: the SAME body the API
58
+ * server runs against a live async transaction — MUTATORS-ISOMORPHIC). The driver detects which. */
59
+ export type ClientMutator = ((tx: MutationTx, args: never) => void) | ((tx: IsoTx, args: never, ctx: MutatorCtx) => MutationGen);
31
60
  /** The client registry (§4.2) — one of the two registries; the server's authoritative
32
61
  * twin shares names (and possibly code), never the wire. */
33
62
  export type ClientRegistry = Record<string, ClientMutator>;
@@ -63,6 +92,11 @@ export interface FoldHandle {
63
92
  export interface OptimisticBackendOptions {
64
93
  /** Stable per-client identity for the upstream envelopes (§8.1). */
65
94
  clientID: string;
95
+ /** The acting principal, for a shared (generator) mutator's `ctx.user` — re-read per invoke so a
96
+ * mid-session login is picked up, and stable across a rebase re-invoke (replayable). Plain
97
+ * client-only mutators ignore it. Defaults to the empty string (an app that registers generator
98
+ * mutators must supply this). */
99
+ user?: () => string;
66
100
  /** Buffered-frame ceiling before the §8.5 escape (drop + re-hydrate). */
67
101
  bufferCap?: number;
68
102
  /** Virtual-clock seam for the fold debounce timers (FOLDED-MUTATIONS-DESIGN §9). Defaults to
@@ -115,6 +149,8 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
115
149
  private readonly source;
116
150
  private readonly registry;
117
151
  private readonly clientID;
152
+ /** The acting principal provider for a shared mutator's `ctx.user` (§ shared mutators). */
153
+ private readonly user;
118
154
  private readonly bufferCap;
119
155
  /** Column order + pk indices per table, for the keyed `MutationTx` methods. */
120
156
  private readonly specs;
@@ -147,6 +183,12 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
147
183
  * pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
148
184
  private readonly overlay;
149
185
  private handler;
186
+ /** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
187
+ * local engine's `dispatch` brackets so the Store folds every affected view before notifying any
188
+ * subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
189
+ * local engine, so its commit brackets are this backend's commit brackets. */
190
+ private boundaryHandler;
191
+ private readonly devObservers;
150
192
  private pendingMutations;
151
193
  private nextMid;
152
194
  /** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
@@ -204,6 +246,23 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
204
246
  * non-reentrant (A5), so a local write can never interleave with an open server cycle. */
205
247
  writeLocal(mutations: Mutation[]): void;
206
248
  onEvent(handler: (qid: QueryId, ev: ChangeEvent) => void): void;
249
+ onCommitBoundary(handler: (phase: "begin" | "end") => void): void;
250
+ /** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
251
+ * notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
252
+ * and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
253
+ * logical mutation: a relationship-`count` view and the data view it counts must update together.
254
+ * The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
255
+ * outer pair and the Store flushes every affected view (data AND count) once, together, at the
256
+ * outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
257
+ * half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
258
+ * prediction never wedges the Store in deferred mode. */
259
+ private inOneCommit;
260
+ /** Run one client mutator against the staged `tx`, accepting BOTH forms (§ shared mutators):
261
+ * a plain sync function runs as-is; a shared GENERATOR is driven synchronously — every yielded
262
+ * write applies to the wasm txn now, every `tx.row` read is resolved against the same staged
263
+ * state (read-your-writes), the SAME body the API server drives asynchronously. `ctx.user` is
264
+ * the acting principal (re-read per invoke, stable across a rebase re-invoke). */
265
+ private runMutator;
207
266
  /** Run the named client mutator optimistically: the prediction applies to the live
208
267
  * engine now (affected views update synchronously), `(mid, name, args)` joins the
209
268
  * pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
@@ -240,6 +299,7 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
240
299
  * A pending local mutation no longer moves it — see {@link pending}. */
241
300
  resultType(qid: QueryId): ResultType;
242
301
  onResultType(handler: (qid: QueryId, rt: ResultType) => void): void;
302
+ __attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void;
243
303
  /** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
244
304
  * pending here?" (FOLDED-MUTATIONS-DESIGN §7.2). Orthogonal to {@link resultType}; this is the
245
305
  * same `queryTables ∩ pending.touched` computation that used to be smuggled into `unknown`. */
@@ -258,6 +318,8 @@ export declare class OptimisticBackend<S extends ColsMap> implements Backend {
258
318
  * (remove) — exactly where `:359`/`:468` used to flip ResultType (§7.3). */
259
319
  private refreshPending;
260
320
  private onNormalized;
321
+ private emitServerDelta;
322
+ private localQidsForSource;
261
323
  private onProgress;
262
324
  /** Fold the lmid system query's released ops (lmid-as-data): the one row's
263
325
  * `last_mutation_id` cell is this client's confirmed high-water mid. */
@@ -1 +1 @@
1
- {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
package/dist/backend.js CHANGED
@@ -37,7 +37,7 @@
37
37
  //
38
38
  // `Store`/`ArrayView` are untouched: this implements `Backend`, so
39
39
  // `new Store(schema, new OptimisticBackend(...))` reuses the whole machinery.
40
- import { CLIENT_MUTATIONS_SCHEMA, LMID_QUERY_NAME, localTableNames, normalizedTableSchemas, tableSpec } from "@rindle/client";
40
+ import { CLIENT_MUTATIONS_SCHEMA, driveMutationSync, isGeneratorMutator, isoTx, LMID_QUERY_NAME, localTableNames, normalizedTableSchemas, tableSpec, } from "@rindle/client";
41
41
  import { aggTableSchemas, NormalizedSync, rewriteAggregates } from "@rindle/normalized";
42
42
  import { WasmBackend } from "@rindle/wasm";
43
43
  import { AggOverlay, collectAggDefs } from "./agg-overlay.js";
@@ -62,6 +62,8 @@ export class OptimisticBackend {
62
62
  source;
63
63
  registry;
64
64
  clientID;
65
+ /** The acting principal provider for a shared mutator's `ctx.user` (§ shared mutators). */
66
+ user;
65
67
  bufferCap;
66
68
  /** Column order + pk indices per table, for the keyed `MutationTx` methods. */
67
69
  specs;
@@ -94,6 +96,12 @@ export class OptimisticBackend {
94
96
  * pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
95
97
  overlay = new AggOverlay();
96
98
  handler = () => { };
99
+ /** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
100
+ * local engine's `dispatch` brackets so the Store folds every affected view before notifying any
101
+ * subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
102
+ * local engine, so its commit brackets are this backend's commit brackets. */
103
+ boundaryHandler = () => { };
104
+ devObservers = new Set();
97
105
  pendingMutations = [];
98
106
  nextMid = 1;
99
107
  /** The live fold entries, by fold key `${name}\0${identityJSON}` — at most one per key
@@ -127,6 +135,9 @@ export class OptimisticBackend {
127
135
  constructor(schema, source, registry, opts) {
128
136
  this.local = new WasmBackend(schema);
129
137
  this.local.onEvent((qid, ev) => this.handler(qid, ev));
138
+ // Forward the local engine's commit brackets up to the Store (cross-view-atomic notification):
139
+ // every data delta this backend emits comes from `this.local`, so its commit boundaries are ours.
140
+ this.local.onCommitBoundary((phase) => this.boundaryHandler(phase));
130
141
  this.colCounts = colCountsFromSchema(schema);
131
142
  this.colIndex = colIndexFromSchema(schema);
132
143
  this.sync = new NormalizedSync(pkColsFromSchema(schema), this.colCounts);
@@ -135,6 +146,7 @@ export class OptimisticBackend {
135
146
  this.source = source;
136
147
  this.registry = registry;
137
148
  this.clientID = opts.clientID;
149
+ this.user = opts.user ?? (() => "");
138
150
  this.bufferCap = opts.bufferCap ?? 1024;
139
151
  this.clock = opts.clock ?? REAL_CLOCK;
140
152
  // Validate each server hello against our OWN typed schema → reject a schema skew
@@ -314,7 +326,45 @@ export class OptimisticBackend {
314
326
  onEvent(handler) {
315
327
  this.handler = handler;
316
328
  }
329
+ onCommitBoundary(handler) {
330
+ this.boundaryHandler = handler;
331
+ }
317
332
  // --- the named-mutator entry (§9) ----------------------------------------------
333
+ /** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
334
+ * notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
335
+ * and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
336
+ * logical mutation: a relationship-`count` view and the data view it counts must update together.
337
+ * The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
338
+ * outer pair and the Store flushes every affected view (data AND count) once, together, at the
339
+ * outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
340
+ * half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
341
+ * prediction never wedges the Store in deferred mode. */
342
+ inOneCommit(apply) {
343
+ this.boundaryHandler("begin");
344
+ try {
345
+ return apply();
346
+ }
347
+ finally {
348
+ this.boundaryHandler("end");
349
+ }
350
+ }
351
+ /** Run one client mutator against the staged `tx`, accepting BOTH forms (§ shared mutators):
352
+ * a plain sync function runs as-is; a shared GENERATOR is driven synchronously — every yielded
353
+ * write applies to the wasm txn now, every `tx.row` read is resolved against the same staged
354
+ * state (read-your-writes), the SAME body the API server drives asynchronously. `ctx.user` is
355
+ * the acting principal (re-read per invoke, stable across a rebase re-invoke). */
356
+ runMutator(mutator, tx, args) {
357
+ if (isGeneratorMutator(mutator)) {
358
+ const gen = mutator(isoTx, args, { user: this.user() });
359
+ driveMutationSync(gen, {
360
+ apply: (op) => applyOpToTx(tx, op),
361
+ read: (table, pk) => tx.row(table, pk),
362
+ });
363
+ }
364
+ else {
365
+ mutator(tx, args);
366
+ }
367
+ }
318
368
  /** Run the named client mutator optimistically: the prediction applies to the live
319
369
  * engine now (affected views update synchronously), `(mid, name, args)` joins the
320
370
  * pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
@@ -322,32 +372,36 @@ export class OptimisticBackend {
322
372
  const mutator = this.registry[name];
323
373
  if (!mutator)
324
374
  throw new Error(`unknown client mutator: ${name}`);
325
- // Apply the prediction FIRST. If the mutator throws (client-side validation, a bad read),
326
- // the staged write is discarded (the wasm txn is a clean no-op until commit) and the throw
327
- // propagates with NO mid consumed — a burnt mid is a permanent server-side gap that
328
- // silently refuses every later mutation from this client (#10).
329
- const touched = new Set();
330
- const ops = [];
331
- this.local.writeWith((tx) => {
332
- mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), args);
375
+ // One commit boundary spans the prediction AND the `__agg`-head reconcile below, so their views
376
+ // (data + count) flush together rather than tearing across two engine commits.
377
+ return this.inOneCommit(() => {
378
+ // Apply the prediction FIRST. If the mutator throws (client-side validation, a bad read),
379
+ // the staged write is discarded (the wasm txn is a clean no-op until commit) and the throw
380
+ // propagates with NO mid consumed — a burnt mid is a permanent server-side gap that
381
+ // silently refuses every later mutation from this client (#10).
382
+ const touched = new Set();
383
+ const ops = [];
384
+ this.local.writeWith((tx) => {
385
+ this.runMutator(mutator, trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), args);
386
+ });
387
+ // Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
388
+ // this write does, so wire order == local-apply order for any pair that can observe each other
389
+ // (a read-dependent write reading a folded cell sees the same value optimistically and on the
390
+ // wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
391
+ this.drainOverlapping(touched);
392
+ const mid = this.nextMid++;
393
+ this.pendingMutations.push({ mid, name, args, touched });
394
+ // The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
395
+ // the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
396
+ // delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
397
+ // touched group's head as the absolute `server_base ⊕ delta`.
398
+ for (const op of ops)
399
+ this.overlay.observe(op);
400
+ this.reconcileAggHead();
401
+ this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
402
+ void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
403
+ return mid;
333
404
  });
334
- // Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
335
- // this write does, so wire order == local-apply order for any pair that can observe each other
336
- // (a read-dependent write reading a folded cell sees the same value optimistically and on the
337
- // wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
338
- this.drainOverlapping(touched);
339
- const mid = this.nextMid++;
340
- this.pendingMutations.push({ mid, name, args, touched });
341
- // The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
342
- // the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
343
- // delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
344
- // touched group's head as the absolute `server_base ⊕ delta`.
345
- for (const op of ops)
346
- this.overlay.observe(op);
347
- this.reconcileAggHead();
348
- this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
349
- void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
350
- return mid;
351
405
  }
352
406
  /** Run a FOLDED invoke (FOLDED-MUTATIONS-DESIGN §8): apply the prediction to the live engine now
353
407
  * (like `invoke`), but collapse a run of same-key invokes into ONE pending entry whose `args`
@@ -358,65 +412,69 @@ export class OptimisticBackend {
358
412
  if (!mutator)
359
413
  throw new Error(`unknown client mutator: ${name}`);
360
414
  const foldKey = `${name}\0${stableJson(opts.key)}`;
361
- // Apply the prediction with the read trap armed (§5): a folded mutator that reads state to
362
- // compute its write is non-absorbing and refused. A throw discards the staged write (clean
363
- // no-op) and consumes no mid — exactly `invoke`'s guarantee.
364
- const touched = new Set();
365
- const ops = [];
366
- try {
367
- this.local.writeWith((tx) => {
368
- mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops), true), args);
369
- });
370
- }
371
- catch (e) {
372
- if (e instanceof FoldReadError) {
373
- 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)`);
415
+ // One commit boundary spans the prediction AND the `__agg`-head reconcile (see {@link inOneCommit}),
416
+ // so a folded mutation's list view and count view flush together, never torn across two commits.
417
+ return this.inOneCommit(() => {
418
+ // Apply the prediction with the read trap armed (§5): a folded mutator that reads state to
419
+ // compute its write is non-absorbing and refused. A throw discards the staged write (clean
420
+ // no-op) and consumes no mid — exactly `invoke`'s guarantee.
421
+ const touched = new Set();
422
+ const ops = [];
423
+ try {
424
+ this.local.writeWith((tx) => {
425
+ this.runMutator(mutator, trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops), true), args);
426
+ });
374
427
  }
375
- throw e;
376
- }
377
- for (const op of ops)
378
- this.overlay.observe(op);
379
- this.reconcileAggHead();
380
- const now = this.clock.now();
381
- let f = this.folds.get(foldKey);
382
- if (f) {
383
- // Overwrite the single entry in place — the pending stack does NOT grow (§1 #2). The head
384
- // already carries this new prediction (absorbing, last-wins on the cell); the entry holds
385
- // only the LATEST args, which is what a rebase re-derives from and what the flush ships.
386
- f.entry.args = args;
387
- f.entry.touched = touched;
388
- f.args = args;
389
- this.clock.clearTimeout(f.timer);
390
- }
391
- else {
392
- const entry = { mid: null, name, args, touched };
393
- this.pendingMutations.push(entry);
394
- let resolveMid;
395
- const midPromise = new Promise((res) => (resolveMid = res));
396
- f = {
397
- entry,
398
- args,
399
- timer: undefined,
400
- firstAt: now,
401
- debounceMs: opts.debounceMs ?? DEFAULT_FOLD_DEBOUNCE_MS,
402
- maxWaitMs: opts.maxWaitMs,
403
- deferAcrossWrites: opts.deferAcrossWrites ?? false,
404
- midPromise,
405
- resolveMid,
406
- };
407
- this.folds.set(foldKey, f);
408
- }
409
- // (Re)arm the trailing debounce — unless the maxWaitMs cap is already due (a never-idle drag
410
- // still persists periodically, §3/#4), in which case flush now instead of re-arming.
411
- if (f.maxWaitMs !== undefined && now - f.firstAt >= f.maxWaitMs) {
412
- this.flushFold(foldKey);
413
- }
414
- else {
415
- f.timer = this.clock.setTimeout(() => this.flushFold(foldKey), f.debounceMs);
416
- }
417
- this.refreshPending();
418
- const handle = { flush: () => this.flushFold(foldKey), mid: f.midPromise };
419
- return handle;
428
+ catch (e) {
429
+ if (e instanceof FoldReadError) {
430
+ 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)`);
431
+ }
432
+ throw e;
433
+ }
434
+ for (const op of ops)
435
+ this.overlay.observe(op);
436
+ this.reconcileAggHead();
437
+ const now = this.clock.now();
438
+ let f = this.folds.get(foldKey);
439
+ if (f) {
440
+ // Overwrite the single entry in place — the pending stack does NOT grow (§1 #2). The head
441
+ // already carries this new prediction (absorbing, last-wins on the cell); the entry holds
442
+ // only the LATEST args, which is what a rebase re-derives from and what the flush ships.
443
+ f.entry.args = args;
444
+ f.entry.touched = touched;
445
+ f.args = args;
446
+ this.clock.clearTimeout(f.timer);
447
+ }
448
+ else {
449
+ const entry = { mid: null, name, args, touched };
450
+ this.pendingMutations.push(entry);
451
+ let resolveMid;
452
+ const midPromise = new Promise((res) => (resolveMid = res));
453
+ f = {
454
+ entry,
455
+ args,
456
+ timer: undefined,
457
+ firstAt: now,
458
+ debounceMs: opts.debounceMs ?? DEFAULT_FOLD_DEBOUNCE_MS,
459
+ maxWaitMs: opts.maxWaitMs,
460
+ deferAcrossWrites: opts.deferAcrossWrites ?? false,
461
+ midPromise,
462
+ resolveMid,
463
+ };
464
+ this.folds.set(foldKey, f);
465
+ }
466
+ // (Re)arm the trailing debounce — unless the maxWaitMs cap is already due (a never-idle drag
467
+ // still persists periodically, §3/#4), in which case flush now instead of re-arming.
468
+ if (f.maxWaitMs !== undefined && now - f.firstAt >= f.maxWaitMs) {
469
+ this.flushFold(foldKey);
470
+ }
471
+ else {
472
+ f.timer = this.clock.setTimeout(() => this.flushFold(foldKey), f.debounceMs);
473
+ }
474
+ this.refreshPending();
475
+ const handle = { flush: () => this.flushFold(foldKey), mid: f.midPromise };
476
+ return handle;
477
+ });
420
478
  }
421
479
  /** Flush-on-enqueue (§4.2): for each outstanding fold whose touched tables overlap `tables`,
422
480
  * assign its mid NOW and ship it — in creation (insertion) order, so the wire stays gapless. A
@@ -500,6 +558,12 @@ export class OptimisticBackend {
500
558
  onResultType(handler) {
501
559
  this.resultTypeHandler = handler;
502
560
  }
561
+ __attachDevtoolsServerDeltas(observer) {
562
+ this.devObservers.add(observer);
563
+ return () => {
564
+ this.devObservers.delete(observer);
565
+ };
566
+ }
503
567
  // --- the pending AXIS (§7.2): orthogonal to ResultType -------------------------------
504
568
  /** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
505
569
  * pending here?" (FOLDED-MUTATIONS-DESIGN §7.2). Orthogonal to {@link resultType}; this is the
@@ -573,6 +637,8 @@ export class OptimisticBackend {
573
637
  }
574
638
  // --- the downstream stream (§8.5: buffer, then release coherently) ---------------
575
639
  onNormalized(qid, ev) {
640
+ if (qid !== LMID_QID)
641
+ this.emitServerDelta(qid, ev);
576
642
  if (ev.type === "hello") {
577
643
  // A hello is a (re)subscribe = a NEW epoch. The column map below is mutated eagerly, but
578
644
  // data frames are cv-buffered and drained later (onProgress). So any frame still buffered
@@ -613,6 +679,22 @@ export class OptimisticBackend {
613
679
  if (this.buffer.length > this.bufferCap)
614
680
  this.overflow();
615
681
  }
682
+ emitServerDelta(sourceQid, ev) {
683
+ if (!this.devObservers.size)
684
+ return;
685
+ for (const qid of this.localQidsForSource(sourceQid)) {
686
+ for (const o of this.devObservers)
687
+ o.onServerDelta?.(qid, { format: "normalized", event: ev });
688
+ }
689
+ }
690
+ localQidsForSource(sourceQid) {
691
+ const key = this.sourceToRemote.get(sourceQid);
692
+ const sub = key ? this.remoteSubs.get(key) : undefined;
693
+ if (!sub)
694
+ return [sourceQid];
695
+ const localQids = [...sub.localQids.keys()];
696
+ return localQids.length ? localQids : [sourceQid];
697
+ }
616
698
  onProgress(frame) {
617
699
  // (1) Take every buffered frame at cv ≤ cvMin, in (cv, arrival) order, and fold it:
618
700
  // lmid system-query frames advance `confirmedLmid`; data frames fold through the
@@ -714,7 +796,7 @@ export class OptimisticBackend {
714
796
  const ops = [];
715
797
  try {
716
798
  this.local.writeWith((tx) => {
717
- this.registry[p.name](trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), p.args);
799
+ this.runMutator(this.registry[p.name], trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), p.args);
718
800
  });
719
801
  }
720
802
  catch {
@@ -931,6 +1013,23 @@ function colIndexFromSchema(schema) {
931
1013
  * — a folded mutator that reads state to compute its write is non-absorbing and refused. The keyed
932
1014
  * writers (`update`/`upsert`/`delete`) still read internally to preserve unspecified columns; that
933
1015
  * is column-preservation, not value-derivation, so it stays allowed. */
1016
+ /** Apply one logical {@link MutationOp} (yielded by a shared generator mutator) onto the client's
1017
+ * keyed {@link MutationTx} — the same methods a plain client mutator calls directly. Column
1018
+ * validation, touched-table tracking, and op collection all happen inside those methods. */
1019
+ function applyOpToTx(tx, op) {
1020
+ switch (op.kind) {
1021
+ case "insert":
1022
+ return tx.insert(op.table, op.row);
1023
+ case "upsert":
1024
+ return tx.upsert(op.table, op.row);
1025
+ case "insertIgnore":
1026
+ return tx.insertIgnore(op.table, op.row);
1027
+ case "update":
1028
+ return tx.update(op.table, op.row);
1029
+ case "delete":
1030
+ return tx.delete(op.table, op.pk);
1031
+ }
1032
+ }
934
1033
  function trackingTx(tx, touched, specs, localTables, onOp, trapReads = false) {
935
1034
  const spec = (table) => {
936
1035
  const s = specs[table];
@@ -999,8 +1098,20 @@ function trackingTx(tx, touched, specs, localTables, onOp, trapReads = false) {
999
1098
  const trapped = () => {
1000
1099
  throw new FoldReadError();
1001
1100
  };
1101
+ // One-shot query (203 §5.2): lower the builder to an AST, refuse any local-only table it
1102
+ // reads (M1 — same guard as `rawGet`), and run it over the engine's read-cache fork. The
1103
+ // wasm engine returns the rows already keyed by column name with their materialized
1104
+ // relationship children nested by name (`marshal::caught_node_to_js`), in the query's order —
1105
+ // identical in shape to `view.data` — so this is a pass-through.
1106
+ const runQuery = (q) => {
1107
+ const ast = q.ast();
1108
+ for (const t of collectTables(ast))
1109
+ assertNotLocal(t, "read");
1110
+ return tx.query(ast);
1111
+ };
1002
1112
  return {
1003
1113
  get: trapReads ? trapped : rawGet,
1114
+ query: trapReads ? trapped : runQuery,
1004
1115
  add,
1005
1116
  remove,
1006
1117
  edit,
@@ -1032,6 +1143,11 @@ function trackingTx(tx, touched, specs, localTables, onOp, trapReads = false) {
1032
1143
  else
1033
1144
  add(table, toCells(table, row));
1034
1145
  },
1146
+ insertIgnore: (table, row) => {
1147
+ checkColumns(table, row, true);
1148
+ if (!rawGet(table, pkCells(table, row)))
1149
+ add(table, toCells(table, row));
1150
+ },
1035
1151
  delete: (table, pk) => {
1036
1152
  checkColumns(table, pk, false);
1037
1153
  const current = rawGet(table, pkCells(table, pk));