@rindle/optimistic 0.2.0 → 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/client-id.js CHANGED
@@ -37,6 +37,23 @@ let tabInstance = 0;
37
37
  export function resetTabInstanceForTests() {
38
38
  tabInstance = 0;
39
39
  }
40
+ /** Clear the persisted client identity so the next page load starts a fresh mid/lmid stream.
41
+ * Intended for dev recovery after out-of-band server state loss, not for normal operation. */
42
+ export function resetStableClientID() {
43
+ try {
44
+ globalThis.localStorage?.removeItem(CLIENT_ID_KEY);
45
+ }
46
+ catch {
47
+ // Storage can throw in private modes or test shims; best-effort reset.
48
+ }
49
+ try {
50
+ globalThis.sessionStorage?.removeItem(TAB_ID_KEY);
51
+ }
52
+ catch {
53
+ // Storage can throw in private modes or test shims; best-effort reset.
54
+ }
55
+ tabInstance = 0;
56
+ }
40
57
  /** The connection's stable clientID. A per-origin base (localStorage) keeps one logical identity
41
58
  * across reloads; a per-tab suffix (sessionStorage) gives each tab its OWN mid/lmid stream; a
42
59
  * per-load instance suffix keeps two clients constructed in ONE tab from sharing that stream. Falls
@@ -1 +1 @@
1
- {"version":3,"file":"client-id.js","sourceRoot":"","sources":["../src/client-id.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,sGAAsG;AAEtG;sBACsB;AACtB,MAAM,aAAa,GAAG,kBAAkB,CAAC;AACzC,mGAAmG;AACnG,MAAM,UAAU,GAAG,eAAe,CAAC;AAEnC;kFACkF;AAClF,SAAS,WAAW,CAAC,IAAuC,EAAE,GAAW,EAAE,IAAkB;IAC3F,IAAI,CAAC;QACH,MAAM,OAAO,GAAI,UAA6D,CAAC,IAAI,CAAC,CAAC;QACrF,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;oGAGoG;AACpG,IAAI,WAAW,GAAG,CAAC,CAAC;AAEpB;2CAC2C;AAC3C,MAAM,UAAU,wBAAwB;IACtC,WAAW,GAAG,CAAC,CAAC;AAClB,CAAC;AAED;;;;;;sFAMsF;AACtF,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,WAAW,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,IAAI;QAAE,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,mDAAmD;IAC1F,+FAA+F;IAC/F,MAAM,GAAG,GACP,WAAW,CAAC,gBAAgB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,kGAAkG;IAClG,oFAAoF;IACpF,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC3D,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC;AAC7B,CAAC"}
1
+ {"version":3,"file":"client-id.js","sourceRoot":"","sources":["../src/client-id.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,+FAA+F;AAC/F,sGAAsG;AAEtG;sBACsB;AACtB,MAAM,aAAa,GAAG,kBAAkB,CAAC;AACzC,mGAAmG;AACnG,MAAM,UAAU,GAAG,eAAe,CAAC;AAEnC;kFACkF;AAClF,SAAS,WAAW,CAAC,IAAuC,EAAE,GAAW,EAAE,IAAkB;IAC3F,IAAI,CAAC;QACH,MAAM,OAAO,GAAI,UAA6D,CAAC,IAAI,CAAC,CAAC;QACrF,IAAI,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACtC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAC9B,MAAM,KAAK,GAAG,IAAI,EAAE,CAAC;QACrB,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QAC5B,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;oGAGoG;AACpG,IAAI,WAAW,GAAG,CAAC,CAAC;AAEpB;2CAC2C;AAC3C,MAAM,UAAU,wBAAwB;IACtC,WAAW,GAAG,CAAC,CAAC;AAClB,CAAC;AAED;+FAC+F;AAC/F,MAAM,UAAU,mBAAmB;IACjC,IAAI,CAAC;QACH,UAAU,CAAC,YAAY,EAAE,UAAU,CAAC,aAAa,CAAC,CAAC;IACrD,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;IACD,IAAI,CAAC;QACH,UAAU,CAAC,cAAc,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC;IACpD,CAAC;IAAC,MAAM,CAAC;QACP,uEAAuE;IACzE,CAAC;IACD,WAAW,GAAG,CAAC,CAAC;AAClB,CAAC;AAED;;;;;;sFAMsF;AACtF,MAAM,UAAU,cAAc;IAC5B,MAAM,IAAI,GAAG,WAAW,CAAC,cAAc,EAAE,aAAa,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC;IACnF,IAAI,CAAC,IAAI;QAAE,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC,CAAC,mDAAmD;IAC1F,+FAA+F;IAC/F,MAAM,GAAG,GACP,WAAW,CAAC,gBAAgB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAChF,MAAM,CAAC,UAAU,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAClC,kGAAkG;IAClG,oFAAoF;IACpF,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,MAAM,MAAM,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,IAAI,QAAQ,EAAE,CAAC;IAC3D,OAAO,GAAG,IAAI,IAAI,MAAM,EAAE,CAAC;AAC7B,CAAC"}
package/dist/client.d.ts CHANGED
@@ -43,6 +43,13 @@ export interface RindleClientOptions<S extends ColsMap, R extends ClientRegistry
43
43
  maxBatch?: number;
44
44
  retryDelayMs?: (attempt: number) => number;
45
45
  };
46
+ /** Development-only recovery knobs. Keep off in production: a mutation gap means state loss
47
+ * or two writers sharing a clientID, and should be investigated. */
48
+ dev?: {
49
+ /** On a mutation-gap response, clear the persisted clientID and hard reload the page. This
50
+ * recovers from dev DB wipes while making the reset visible to the developer. */
51
+ resetOnMutationGap?: boolean;
52
+ };
46
53
  }
47
54
  export interface RindleClient<S extends ColsMap, R extends ClientRegistry> {
48
55
  store: Store<S>;
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAMxE,OAAO,KAAK,EAA2C,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGzF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAM5C,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc;IAC9E,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAClB,qFAAqF;IACrF,QAAQ,EAAE,CAAC,CAAC;IACZ,qFAAqF;IACrF,GAAG,EAAE;QACH,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7C,6EAA6E;QAC7E,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QACnE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;KACtB,CAAC;IACF;;;;;;+FAM2F;IAC3F,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,CAAC;IACtD;;;uBAGmB;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAC;CAC3E;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc;IACvE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC9B;gFAC4E;IAC5E,MAAM,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;IAC1D;kFAC8E;IAC9E,UAAU,IAAI,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAsB,kBAAkB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc,EAClF,IAAI,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC9B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CA2E7B;AAED,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC;AAC3C,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAMxE,OAAO,KAAK,EAA2C,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGzF,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAyB,KAAK,QAAQ,EAAE,MAAM,YAAY,CAAC;AAClE,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAM5C,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAEjD,MAAM,WAAW,mBAAmB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc;IAC9E,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC;IAClB,qFAAqF;IACrF,QAAQ,EAAE,CAAC,CAAC;IACZ,qFAAqF;IACrF,GAAG,EAAE;QACH,GAAG,EAAE,MAAM,CAAC;QACZ,MAAM,CAAC,EAAE;YAAE,KAAK,CAAC,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAC7C,6EAA6E;QAC7E,OAAO,CAAC,EAAE,WAAW,GAAG,CAAC,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;QACnE,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;KACtB,CAAC;IACF;;;;;;+FAM2F;IAC3F,MAAM,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,SAAS,EAAE,SAAS,CAAA;KAAE,CAAC;IACtD;;;uBAGmB;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,uFAAuF;IACvF,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,KAAK,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;KAAE,CAAC;IAC1E;yEACqE;IACrE,GAAG,CAAC,EAAE;QACJ;0FACkF;QAClF,kBAAkB,CAAC,EAAE,OAAO,CAAC;KAC9B,CAAC;CACH;AAED,MAAM,WAAW,YAAY,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc;IACvE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC9B;gFAC4E;IAC5E,MAAM,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;IAC1D;kFAC8E;IAC9E,UAAU,IAAI,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAsB,kBAAkB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc,EAClF,IAAI,EAAE,mBAAmB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC9B,OAAO,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAmF7B;AAED,YAAY,EAAE,cAAc,EAAE,UAAU,EAAE,CAAC;AAC3C,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
package/dist/client.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // (rejection reasons surface via `onRejected`).
11
11
  import { RemoteOptimisticSource, WsTransport, createQueuedMutationSender, } from "@rindle/remote";
12
12
  import { initWasm } from "@rindle/wasm";
13
- import { stableClientID } from "./client-id.js";
13
+ import { resetStableClientID, stableClientID } from "./client-id.js";
14
14
  import { createOptimisticStore } from "./index.js";
15
15
  /** Must mirror `DEFAULT_RINDLE_API_ROUTES` in `@rindle/api-server` (the app-wire contract;
16
16
  * duplicated so the browser bundle doesn't import the server package). */
@@ -28,9 +28,10 @@ export async function createRindleClient(opts) {
28
28
  headers: { "content-type": "application/json", ...extra },
29
29
  body: JSON.stringify(payload),
30
30
  });
31
+ const text = await res.text();
31
32
  if (!res.ok)
32
- throw new Error(`rindle api ${path} failed: ${res.status} ${await res.text()}`);
33
- return res.json();
33
+ throw new RindleApiHttpError(path, res.status, text);
34
+ return text ? JSON.parse(text) : undefined;
34
35
  };
35
36
  // Reads-leg connection: a fixed transport (tests/in-process) or a replaceable connection built
36
37
  // from `wsUrl` (eager when present, lazy when omitted). A routed lease's `wsEndpoint` migrates it.
@@ -49,8 +50,16 @@ export async function createRindleClient(opts) {
49
50
  retryDelayMs: opts.queue?.retryDelayMs,
50
51
  onRejected: opts.onRejected,
51
52
  send: async (envelopes) => {
52
- const outcomes = (await post(routes.mutate, { envelopes }));
53
- return outcomes.map((o) => ({ accepted: o.accepted, reason: o.reason }));
53
+ try {
54
+ const outcomes = (await post(routes.mutate, { envelopes }));
55
+ return outcomes.map((o) => ({ accepted: o.accepted, reason: o.reason }));
56
+ }
57
+ catch (err) {
58
+ if (opts.dev?.resetOnMutationGap && reloadAfterMutationGap(err)) {
59
+ return new Promise(() => { });
60
+ }
61
+ throw err;
62
+ }
54
63
  },
55
64
  }),
56
65
  });
@@ -78,4 +87,36 @@ export async function createRindleClient(opts) {
78
87
  },
79
88
  };
80
89
  }
90
+ class RindleApiHttpError extends Error {
91
+ path;
92
+ status;
93
+ body;
94
+ constructor(path, status, body) {
95
+ super(`rindle api ${path} failed: ${status}${body ? ` ${body}` : ""}`);
96
+ this.name = "RindleApiHttpError";
97
+ this.path = path;
98
+ this.status = status;
99
+ this.body = body;
100
+ }
101
+ }
102
+ const MUTATION_GAP_RE = /\bmutation(?: id)? gap\b/i;
103
+ function reloadAfterMutationGap(err) {
104
+ const text = err instanceof RindleApiHttpError
105
+ ? `${err.status} ${err.body} ${err.message}`
106
+ : String(err?.message ?? err);
107
+ if (!MUTATION_GAP_RE.test(text))
108
+ return false;
109
+ resetStableClientID();
110
+ console.error("[rindle] mutation gap detected; cleared the dev client id and reloading so this tab starts a fresh mutation stream.", err);
111
+ const loc = globalThis.location;
112
+ if (typeof loc?.reload !== "function")
113
+ return false;
114
+ try {
115
+ loc.reload();
116
+ return true;
117
+ }
118
+ catch {
119
+ return false;
120
+ }
121
+ }
81
122
  //# sourceMappingURL=client.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,EAAE;AACF,iGAAiG;AACjG,6DAA6D;AAC7D,8DAA8D;AAC9D,EAAE;AACF,oGAAoG;AACpG,yFAAyF;AACzF,sFAAsF;AACtF,gDAAgD;AAGhD,OAAO,EACL,sBAAsB,EACtB,WAAW,EACX,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAIxC,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAiB,MAAM,YAAY,CAAC;AAGlE;2EAC2E;AAC3E,MAAM,cAAc,GAAG,EAAE,KAAK,EAAE,mBAAmB,EAAE,MAAM,EAAE,oBAAoB,EAAW,CAAC;AAsD7F,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,IAA+B;IAE/B,MAAM,QAAQ,EAAE,CAAC;IAEjB,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,IAAI,cAAc,EAAE,CAAC;IACnD,MAAM,MAAM,GAAG,EAAE,GAAG,cAAc,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;IACzD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,CAAC,GAAG,IAA8B,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAC5F,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE7C,MAAM,IAAI,GAAG,KAAK,EAAE,IAAY,EAAE,OAAgB,EAAoB,EAAE;QACtE,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC;QAC3G,MAAM,GAAG,GAAG,MAAM,SAAS,CAAC,GAAG,IAAI,GAAG,IAAI,EAAE,EAAE;YAC5C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,GAAG,KAAK,EAAE;YACzD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;SAC9B,CAAC,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,GAAG,CAAC,EAAE;YAAE,MAAM,IAAI,kBAAkB,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QAClE,OAAO,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7C,CAAC,CAAC;IAEF,+FAA+F;IAC/F,mGAAmG;IACnG,MAAM,UAAU,GACd,WAAW,IAAI,IAAI,CAAC,MAAM;QACxB,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;QACtC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,QAAQ,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IAExF,MAAM,MAAM,GAAG,IAAI,sBAAsB,CAAC,UAAU,EAAE,QAAQ,EAAE;QAC9D,gBAAgB,EAAE,KAAK,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE;YACrC,8FAA8F;YAC9F,qFAAqF;YACrF,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAGlG,CAAC;YACF,OAAO,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,UAAU,EAAE,GAAG,CAAC,UAAU,EAAE,CAAC;QACpE,CAAC;QACD,YAAY,EAAE,0BAA0B,CAAC;YACvC,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,QAAQ;YAC9B,YAAY,EAAE,IAAI,CAAC,KAAK,EAAE,YAAY;YACtC,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,EAAE;gBACxB,IAAI,CAAC;oBACH,MAAM,QAAQ,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAkD,CAAC;oBAC7G,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;gBACxF,CAAC;gBAAC,OAAO,GAAG,EAAE,CAAC;oBACb,IAAI,IAAI,CAAC,GAAG,EAAE,kBAAkB,IAAI,sBAAsB,CAAC,GAAG,CAAC,EAAE,CAAC;wBAChE,OAAO,IAAI,OAAO,CAAQ,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBACtC,CAAC;oBACD,MAAM,GAAG,CAAC;gBACZ,CAAC;YACH,CAAC;SACF,CAAC;KACH,CAAC,CAAC;IAEH,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,qBAAqB,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAE3G,8FAA8F;IAC9F,+FAA+F;IAC/F,6FAA6F;IAC7F,8FAA8F;IAC9F,MAAM,UAAU,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC;IAC9C,yFAAyF;IACzF,MAAM,MAAM,GAAG,UAGd,CAAC;IACF,MAAM,CAAC,gBAAgB,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;IAClD,MAAM,CAAC,gBAAgB,EAAE,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;IAEtD,OAAO;QACL,KAAK;QACL,OAAO;QACP,MAAM;QACN,UAAU;QACV,QAAQ;QACR,KAAK,EAAE,GAAG,EAAE;YACV,UAAU,EAAE,CAAC;YACb,MAAM,CAAC,mBAAmB,EAAE,CAAC,UAAU,EAAE,UAAU,CAAC,CAAC;YACrD,MAAM,CAAC,mBAAmB,EAAE,CAAC,cAAc,EAAE,UAAU,CAAC,CAAC;YACzD,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC;AAKD,MAAM,kBAAmB,SAAQ,KAAK;IAC3B,IAAI,CAAS;IACb,MAAM,CAAS;IACf,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,MAAc,EAAE,IAAY;QACpD,KAAK,CAAC,cAAc,IAAI,YAAY,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACvE,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,eAAe,GAAG,2BAA2B,CAAC;AAEpD,SAAS,sBAAsB,CAAC,GAAY;IAC1C,MAAM,IAAI,GACR,GAAG,YAAY,kBAAkB;QAC/B,CAAC,CAAC,GAAG,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,OAAO,EAAE;QAC5C,CAAC,CAAC,MAAM,CAAE,GAAa,EAAE,OAAO,IAAI,GAAG,CAAC,CAAC;IAC7C,IAAI,CAAC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,KAAK,CAAC;IAE9C,mBAAmB,EAAE,CAAC;IACtB,OAAO,CAAC,KAAK,CACX,qHAAqH,EACrH,GAAG,CACJ,CAAC;IACF,MAAM,GAAG,GAAI,UAAgE,CAAC,QAAQ,CAAC;IACvF,IAAI,OAAO,GAAG,EAAE,MAAM,KAAK,UAAU;QAAE,OAAO,KAAK,CAAC;IACpD,IAAI,CAAC;QACH,GAAG,CAAC,MAAM,EAAE,CAAC;QACb,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
package/dist/index.d.ts CHANGED
@@ -8,11 +8,11 @@ export type MutateFn<Args> = ((args: Args) => number) & {
8
8
  folded(opts: FoldOptions, args: Args): FoldHandle;
9
9
  };
10
10
  export { OptimisticBackend } from "./backend.ts";
11
- export type { ClientMutator, ClientRegistry, FoldClock, FoldHandle, FoldInspect, FoldOptions, KeyedRow, MutationTx, OptimisticBackendOptions, OptimisticInspect, PendingInspect, ResultType, } from "./backend.ts";
11
+ export type { ClientMutator, ClientRegistry, FoldClock, FoldHandle, FoldInspect, FoldOptions, KeyedRow, MutationTx, OptimisticBackendOptions, OptimisticInspect, PendingInspect, QueryArg, QueryResultRow, ResultType, } from "./backend.ts";
12
12
  export type { MutationEnvelope, OptimisticSource, ProgressFrame } from "@rindle/client";
13
13
  export { createRindleClient } from "./client.ts";
14
14
  export type { RindleClient, RindleClientOptions } from "./client.ts";
15
- export { stableClientID } from "./client-id.ts";
15
+ export { resetStableClientID, stableClientID } from "./client-id.ts";
16
16
  /** A {@link Store} over an {@link OptimisticBackend}, plus the named-mutator entry
17
17
  * (`mutate.createIssue(args)` — the §9 dream shape) and the §6 lifecycle surface. */
18
18
  export declare function createOptimisticStore<S extends ColsMap, R extends ClientRegistry>(schema: Schema<S>, source: OptimisticSource, registry: R, opts: OptimisticBackendOptions): {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAC;AAEtB;;mGAEmG;AACnG,MAAM,MAAM,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,GAAG;IACtD,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC;CACnD,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EACV,aAAa,EACb,cAAc,EACd,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,QAAQ,EACR,UAAU,EACV,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD;sFACsF;AACtF,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc,EAC/E,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,CAAC,EACX,IAAI,EAAE,wBAAwB,GAC7B;IACD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;CAC3D,CAcA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AACvC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAExE,OAAO,EACL,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAC;AAEtB;;mGAEmG;AACnG,MAAM,MAAM,QAAQ,CAAC,IAAI,IAAI,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,GAAG;IACtD,MAAM,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,GAAG,UAAU,CAAC;CACnD,CAAC;AAEF,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AACjD,YAAY,EACV,aAAa,EACb,cAAc,EACd,SAAS,EACT,UAAU,EACV,WAAW,EACX,WAAW,EACX,QAAQ,EACR,UAAU,EACV,wBAAwB,EACxB,iBAAiB,EACjB,cAAc,EACd,QAAQ,EACR,cAAc,EACd,UAAU,GACX,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGxF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,YAAY,EAAE,YAAY,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AACrE,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErE;sFACsF;AACtF,wBAAgB,qBAAqB,CAAC,CAAC,SAAS,OAAO,EAAE,CAAC,SAAS,cAAc,EAC/E,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,EACjB,MAAM,EAAE,gBAAgB,EACxB,QAAQ,EAAE,CAAC,EACX,IAAI,EAAE,wBAAwB,GAC7B;IACD,KAAK,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAChB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAC9B,MAAM,EAAE;SAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;KAAE,CAAC;CAC3D,CAcA"}
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { OptimisticBackend, } from "./backend.js";
7
7
  export { OptimisticBackend } from "./backend.js";
8
8
  // The one-call client for the daemon/serverless topology (API server + daemon ws).
9
9
  export { createRindleClient } from "./client.js";
10
- export { stableClientID } from "./client-id.js";
10
+ export { resetStableClientID, stableClientID } from "./client-id.js";
11
11
  /** A {@link Store} over an {@link OptimisticBackend}, plus the named-mutator entry
12
12
  * (`mutate.createIssue(args)` — the §9 dream shape) and the §6 lifecycle surface. */
13
13
  export function createOptimisticStore(schema, source, registry, opts) {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,gFAAgF;AAChF,gFAAgF;AAChF,qFAAqF;AAErF,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EACL,iBAAiB,GAKlB,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAiBjD,mFAAmF;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAEhD;sFACsF;AACtF,MAAM,UAAU,qBAAqB,CACnC,MAAiB,EACjB,MAAwB,EACxB,QAAW,EACX,IAA8B;IAM9B,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,KAAK,CACtB,EAAE,EACF;QACE,GAAG,EAAE,CAAC,EAAE,EAAE,IAAY,EAAE,EAAE;YACxB,MAAM,EAAE,GAAG,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAsB,CAAC;YAChF,EAAE,CAAC,MAAM,GAAG,CAAC,QAAqB,EAAE,IAAa,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;YACjG,OAAO,EAAE,CAAC;QACZ,CAAC;KACF,CACmD,CAAC;IACvD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAC1E,gFAAgF;AAChF,gFAAgF;AAChF,qFAAqF;AAErF,OAAO,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAGvC,OAAO,EACL,iBAAiB,GAKlB,MAAM,cAAc,CAAC;AAStB,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAmBjD,mFAAmF;AACnF,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAErE;sFACsF;AACtF,MAAM,UAAU,qBAAqB,CACnC,MAAiB,EACjB,MAAwB,EACxB,QAAW,EACX,IAA8B;IAM9B,MAAM,OAAO,GAAG,IAAI,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;IACtE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACzC,MAAM,MAAM,GAAG,IAAI,KAAK,CACtB,EAAE,EACF;QACE,GAAG,EAAE,CAAC,EAAE,EAAE,IAAY,EAAE,EAAE;YACxB,MAAM,EAAE,GAAG,CAAC,CAAC,IAAa,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,CAAsB,CAAC;YAChF,EAAE,CAAC,MAAM,GAAG,CAAC,QAAqB,EAAE,IAAa,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;YACjG,OAAO,EAAE,CAAC;QACZ,CAAC;KACF,CACmD,CAAC;IACvD,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rindle/optimistic",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "license": "Apache-2.0",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,10 +25,10 @@
25
25
  "README.md"
26
26
  ],
27
27
  "dependencies": {
28
- "@rindle/client": "0.2.0",
29
- "@rindle/normalized": "0.2.0",
30
- "@rindle/remote": "0.2.0",
31
- "@rindle/wasm": "0.2.0"
28
+ "@rindle/client": "0.3.1",
29
+ "@rindle/remote": "0.3.1",
30
+ "@rindle/wasm": "0.3.1",
31
+ "@rindle/normalized": "0.3.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^22.10.0",
package/src/backend.ts CHANGED
@@ -42,6 +42,7 @@ import { CLIENT_MUTATIONS_SCHEMA, LMID_QUERY_NAME, localTableNames, normalizedTa
42
42
  import type {
43
43
  Ast,
44
44
  Backend,
45
+ BackendDevObserver,
45
46
  ChangeEvent,
46
47
  ColsMap,
47
48
  Mutation,
@@ -65,6 +66,18 @@ import { AggOverlay, type ChildOp, collectAggDefs } from "./agg-overlay.ts";
65
66
  * against the schema at runtime, so a typo throws immediately with the valid names. */
66
67
  export type KeyedRow = Record<string, WireValue>;
67
68
 
69
+ /** What `MutationTx.query` accepts: a query handle whose `.ast()` lowers to the wire
70
+ * {@link Ast} — exactly what the typed query builder (`store.query.<table>…` or
71
+ * `newQueryBuilder(schema).<table>…`) produces (203-MUTATOR-READS-DESIGN.md §9.1). Typed
72
+ * structurally so the `MutationTx` surface need not carry the builder's heavy generics. */
73
+ export type QueryArg = { ast(): Ast };
74
+
75
+ /** A row returned by {@link MutationTx.query}: column name → cell, plus each materialized
76
+ * relationship name → its nested row(s) — an array (a plural relationship), or a single row /
77
+ * `null` (a `.one()` relationship). Recursive (nested rows share the shape). Presented
78
+ * identically to a `view.data` row of the same query (203-MUTATOR-READS-DESIGN.md §5.2). */
79
+ export type QueryResultRow = { [key: string]: WireValue | QueryResultRow | QueryResultRow[] };
80
+
68
81
  /** The write handle a client mutator runs against (the client `MutationTx`, §4.2):
69
82
  * reads see the current base + this transaction's own staged writes (§4.1).
70
83
  *
@@ -84,6 +97,14 @@ export interface MutationTx {
84
97
  upsert(table: string, row: KeyedRow): void;
85
98
  /** Delete the row identified by the pk columns. A missing row is a NO-OP. */
86
99
  delete(table: string, pk: KeyedRow): void;
100
+ /** Run a one-shot read query (`where`/`orderBy`/`limit`/join) over the state this
101
+ * mutator is mutating — it sees this transaction's own writes-so-far, the same
102
+ * read-your-writes contract as `get`/`row` (§4.1; 203-MUTATOR-READS-DESIGN.md §5.2).
103
+ * Synchronous; returns the matching rows in the query's order, each with its materialized
104
+ * relationship children nested by name (presented identically to a `view.data` row). Pass
105
+ * a query from the typed builder, e.g. `tx.query(q.issue.where("owner", "=", me))`.
106
+ * Refused inside a FOLDED mutator (a reading mutator is non-absorbing, §9.1). */
107
+ query(query: QueryArg): QueryResultRow[];
87
108
  // --- positional (the wire shape) ---
88
109
  get(table: string, pk: WireValue[]): WireValue[] | undefined;
89
110
  add(table: string, row: WireValue[]): void;
@@ -287,6 +308,12 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
287
308
  * pending delta `displayed = server_base ⊕ local_pending_delta` is applied from. */
288
309
  private readonly overlay = new AggOverlay();
289
310
  private handler: (qid: QueryId, ev: ChangeEvent) => void = () => {};
311
+ /** The Store's commit-boundary handler ({@link Backend.onCommitBoundary}), forwarded from the
312
+ * local engine's `dispatch` brackets so the Store folds every affected view before notifying any
313
+ * subscriber (cross-view-atomic notification). All this backend's data deltas originate from the
314
+ * local engine, so its commit brackets are this backend's commit brackets. */
315
+ private boundaryHandler: (phase: "begin" | "end") => void = () => {};
316
+ private readonly devObservers = new Set<BackendDevObserver>();
290
317
 
291
318
  private pendingMutations: PendingMutation[] = [];
292
319
  private nextMid = 1;
@@ -328,6 +355,9 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
328
355
  ) {
329
356
  this.local = new WasmBackend(schema);
330
357
  this.local.onEvent((qid, ev) => this.handler(qid, ev));
358
+ // Forward the local engine's commit brackets up to the Store (cross-view-atomic notification):
359
+ // every data delta this backend emits comes from `this.local`, so its commit boundaries are ours.
360
+ this.local.onCommitBoundary((phase) => this.boundaryHandler(phase));
331
361
  this.colCounts = colCountsFromSchema(schema);
332
362
  this.colIndex = colIndexFromSchema(schema);
333
363
  this.sync = new NormalizedSync(pkColsFromSchema(schema), this.colCounts);
@@ -518,39 +548,65 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
518
548
  this.handler = handler;
519
549
  }
520
550
 
551
+ onCommitBoundary(handler: (phase: "begin" | "end") => void): void {
552
+ this.boundaryHandler = handler;
553
+ }
554
+
521
555
  // --- the named-mutator entry (§9) ----------------------------------------------
522
556
 
557
+ /** Bracket a multi-step optimistic apply as ONE notification commit (cross-view-atomic
558
+ * notification — {@link Backend.onCommitBoundary}). `invoke`/`invokeFolded` apply the prediction
559
+ * and then reconcile the `__agg` head in TWO `this.local` commits, but they are two halves of one
560
+ * logical mutation: a relationship-`count` view and the data view it counts must update together.
561
+ * The Store's `commitDepth` is a counter, so each inner commit's own `begin`/`end` nests under this
562
+ * outer pair and the Store flushes every affected view (data AND count) once, together, at the
563
+ * outer `end` — a subscriber re-reading a sibling view then sees post-commit data, never a torn
564
+ * half. Balanced on throw (the prediction mutator may reject) via the `finally`, so a thrown
565
+ * prediction never wedges the Store in deferred mode. */
566
+ private inOneCommit<T>(apply: () => T): T {
567
+ this.boundaryHandler("begin");
568
+ try {
569
+ return apply();
570
+ } finally {
571
+ this.boundaryHandler("end");
572
+ }
573
+ }
574
+
523
575
  /** Run the named client mutator optimistically: the prediction applies to the live
524
576
  * engine now (affected views update synchronously), `(mid, name, args)` joins the
525
577
  * pending stack, and the envelope ships upstream. Returns the assigned `mid`. */
526
578
  invoke(name: string, args: unknown): number {
527
579
  const mutator = this.registry[name];
528
580
  if (!mutator) throw new Error(`unknown client mutator: ${name}`);
529
- // Apply the prediction FIRST. If the mutator throws (client-side validation, a bad read),
530
- // the staged write is discarded (the wasm txn is a clean no-op until commit) and the throw
531
- // propagates with NO mid consumed — a burnt mid is a permanent server-side gap that
532
- // silently refuses every later mutation from this client (#10).
533
- const touched = new Set<string>();
534
- const ops: ChildOp[] = [];
535
- this.local.writeWith((tx) => {
536
- mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), args as never);
581
+ // One commit boundary spans the prediction AND the `__agg`-head reconcile below, so their views
582
+ // (data + count) flush together rather than tearing across two engine commits.
583
+ return this.inOneCommit(() => {
584
+ // Apply the prediction FIRST. If the mutator throws (client-side validation, a bad read),
585
+ // the staged write is discarded (the wasm txn is a clean no-op until commit) and the throw
586
+ // propagates with NO mid consumed — a burnt mid is a permanent server-side gap that
587
+ // silently refuses every later mutation from this client (#10).
588
+ const touched = new Set<string>();
589
+ const ops: ChildOp[] = [];
590
+ this.local.writeWith((tx) => {
591
+ mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops)), args as never);
592
+ });
593
+ // Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
594
+ // this write does, so wire order == local-apply order for any pair that can observe each other
595
+ // (a read-dependent write reading a folded cell sees the same value optimistically and on the
596
+ // wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
597
+ this.drainOverlapping(touched);
598
+ const mid = this.nextMid++;
599
+ this.pendingMutations.push({ mid, name, args, touched });
600
+ // The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
601
+ // the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
602
+ // delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
603
+ // touched group's head as the absolute `server_base ⊕ delta`.
604
+ for (const op of ops) this.overlay.observe(op);
605
+ this.reconcileAggHead();
606
+ this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
607
+ void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
608
+ return mid;
537
609
  });
538
- // Flush-on-enqueue (§4.2): a fold whose tables overlap this write must take its mid NOW, BEFORE
539
- // this write does, so wire order == local-apply order for any pair that can observe each other
540
- // (a read-dependent write reading a folded cell sees the same value optimistically and on the
541
- // wire — no snap). Drained folds ship with smaller mids; this write's mid is dealt after.
542
- this.drainOverlapping(touched);
543
- const mid = this.nextMid++;
544
- this.pendingMutations.push({ mid, name, args, touched });
545
- // The prediction stuck — fold its child ops into the optimistic agg delta and push it onto
546
- // the `__agg` head rows (§4). No reset here (this is the §1.3 trivial case, no rewind): the
547
- // delta accumulates on top of the prior pending set, and `reconcileAggHead` recomputes each
548
- // touched group's head as the absolute `server_base ⊕ delta`.
549
- for (const op of ops) this.overlay.observe(op);
550
- this.reconcileAggHead();
551
- this.refreshPending(); // §7.2: this write now touches its queries' pending axis (NOT ResultType).
552
- void this.source.pushMutation({ clientID: this.clientID, mid, name, args });
553
- return mid;
554
610
  }
555
611
 
556
612
  /** Run a FOLDED invoke (FOLDED-MUTATIONS-DESIGN §8): apply the prediction to the live engine now
@@ -561,64 +617,68 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
561
617
  const mutator = this.registry[name];
562
618
  if (!mutator) throw new Error(`unknown client mutator: ${name}`);
563
619
  const foldKey = `${name}\0${stableJson(opts.key)}`;
564
- // Apply the prediction with the read trap armed (§5): a folded mutator that reads state to
565
- // compute its write is non-absorbing and refused. A throw discards the staged write (clean
566
- // no-op) and consumes no mid — exactly `invoke`'s guarantee.
567
- const touched = new Set<string>();
568
- const ops: ChildOp[] = [];
569
- try {
570
- this.local.writeWith((tx) => {
571
- mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops), true), args as never);
572
- });
573
- } catch (e) {
574
- if (e instanceof FoldReadError) {
575
- throw new Error(
576
- `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)`,
577
- );
620
+ // One commit boundary spans the prediction AND the `__agg`-head reconcile (see {@link inOneCommit}),
621
+ // so a folded mutation's list view and count view flush together, never torn across two commits.
622
+ return this.inOneCommit(() => {
623
+ // Apply the prediction with the read trap armed (§5): a folded mutator that reads state to
624
+ // compute its write is non-absorbing and refused. A throw discards the staged write (clean
625
+ // no-op) and consumes no mid — exactly `invoke`'s guarantee.
626
+ const touched = new Set<string>();
627
+ const ops: ChildOp[] = [];
628
+ try {
629
+ this.local.writeWith((tx) => {
630
+ mutator(trackingTx(tx, touched, this.specs, this.localTables, this.opCollector(ops), true), args as never);
631
+ });
632
+ } catch (e) {
633
+ if (e instanceof FoldReadError) {
634
+ throw new Error(
635
+ `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)`,
636
+ );
637
+ }
638
+ throw e;
578
639
  }
579
- throw e;
580
- }
581
- for (const op of ops) this.overlay.observe(op);
582
- this.reconcileAggHead();
583
-
584
- const now = this.clock.now();
585
- let f = this.folds.get(foldKey);
586
- if (f) {
587
- // Overwrite the single entry in place the pending stack does NOT grow (§1 #2). The head
588
- // already carries this new prediction (absorbing, last-wins on the cell); the entry holds
589
- // only the LATEST args, which is what a rebase re-derives from and what the flush ships.
590
- f.entry.args = args;
591
- f.entry.touched = touched;
592
- f.args = args;
593
- this.clock.clearTimeout(f.timer);
594
- } else {
595
- const entry: PendingMutation = { mid: null, name, args, touched };
596
- this.pendingMutations.push(entry);
597
- let resolveMid!: (mid: number) => void;
598
- const midPromise = new Promise<number>((res) => (resolveMid = res));
599
- f = {
600
- entry,
601
- args,
602
- timer: undefined,
603
- firstAt: now,
604
- debounceMs: opts.debounceMs ?? DEFAULT_FOLD_DEBOUNCE_MS,
605
- maxWaitMs: opts.maxWaitMs,
606
- deferAcrossWrites: opts.deferAcrossWrites ?? false,
607
- midPromise,
608
- resolveMid,
609
- };
610
- this.folds.set(foldKey, f);
611
- }
612
- // (Re)arm the trailing debounce unless the maxWaitMs cap is already due (a never-idle drag
613
- // still persists periodically, §3/#4), in which case flush now instead of re-arming.
614
- if (f.maxWaitMs !== undefined && now - f.firstAt >= f.maxWaitMs) {
615
- this.flushFold(foldKey);
616
- } else {
617
- f.timer = this.clock.setTimeout(() => this.flushFold(foldKey), f.debounceMs);
618
- }
619
- this.refreshPending();
620
- const handle: FoldHandle = { flush: () => this.flushFold(foldKey), mid: f.midPromise };
621
- return handle;
640
+ for (const op of ops) this.overlay.observe(op);
641
+ this.reconcileAggHead();
642
+
643
+ const now = this.clock.now();
644
+ let f = this.folds.get(foldKey);
645
+ if (f) {
646
+ // Overwrite the single entry in place — the pending stack does NOT grow (§1 #2). The head
647
+ // already carries this new prediction (absorbing, last-wins on the cell); the entry holds
648
+ // only the LATEST args, which is what a rebase re-derives from and what the flush ships.
649
+ f.entry.args = args;
650
+ f.entry.touched = touched;
651
+ f.args = args;
652
+ this.clock.clearTimeout(f.timer);
653
+ } else {
654
+ const entry: PendingMutation = { mid: null, name, args, touched };
655
+ this.pendingMutations.push(entry);
656
+ let resolveMid!: (mid: number) => void;
657
+ const midPromise = new Promise<number>((res) => (resolveMid = res));
658
+ f = {
659
+ entry,
660
+ args,
661
+ timer: undefined,
662
+ firstAt: now,
663
+ debounceMs: opts.debounceMs ?? DEFAULT_FOLD_DEBOUNCE_MS,
664
+ maxWaitMs: opts.maxWaitMs,
665
+ deferAcrossWrites: opts.deferAcrossWrites ?? false,
666
+ midPromise,
667
+ resolveMid,
668
+ };
669
+ this.folds.set(foldKey, f);
670
+ }
671
+ // (Re)arm the trailing debounce — unless the maxWaitMs cap is already due (a never-idle drag
672
+ // still persists periodically, §3/#4), in which case flush now instead of re-arming.
673
+ if (f.maxWaitMs !== undefined && now - f.firstAt >= f.maxWaitMs) {
674
+ this.flushFold(foldKey);
675
+ } else {
676
+ f.timer = this.clock.setTimeout(() => this.flushFold(foldKey), f.debounceMs);
677
+ }
678
+ this.refreshPending();
679
+ const handle: FoldHandle = { flush: () => this.flushFold(foldKey), mid: f.midPromise };
680
+ return handle;
681
+ });
622
682
  }
623
683
 
624
684
  /** Flush-on-enqueue (§4.2): for each outstanding fold whose touched tables overlap `tables`,
@@ -701,6 +761,13 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
701
761
  this.resultTypeHandler = handler;
702
762
  }
703
763
 
764
+ __attachDevtoolsServerDeltas(observer: BackendDevObserver): () => void {
765
+ this.devObservers.add(observer);
766
+ return () => {
767
+ this.devObservers.delete(observer);
768
+ };
769
+ }
770
+
704
771
  // --- the pending AXIS (§7.2): orthogonal to ResultType -------------------------------
705
772
 
706
773
  /** Whether any pending mutation (folded or not) touches this query's tables — "is a prediction
@@ -777,6 +844,7 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
777
844
  // --- the downstream stream (§8.5: buffer, then release coherently) ---------------
778
845
 
779
846
  private onNormalized(qid: QueryId, ev: NormalizedEvent): void {
847
+ if (qid !== LMID_QID) this.emitServerDelta(qid, ev);
780
848
  if (ev.type === "hello") {
781
849
  // A hello is a (re)subscribe = a NEW epoch. The column map below is mutated eagerly, but
782
850
  // data frames are cv-buffered and drained later (onProgress). So any frame still buffered
@@ -813,6 +881,21 @@ export class OptimisticBackend<S extends ColsMap> implements Backend {
813
881
  if (this.buffer.length > this.bufferCap) this.overflow();
814
882
  }
815
883
 
884
+ private emitServerDelta(sourceQid: QueryId, ev: NormalizedEvent): void {
885
+ if (!this.devObservers.size) return;
886
+ for (const qid of this.localQidsForSource(sourceQid)) {
887
+ for (const o of this.devObservers) o.onServerDelta?.(qid, { format: "normalized", event: ev });
888
+ }
889
+ }
890
+
891
+ private localQidsForSource(sourceQid: QueryId): QueryId[] {
892
+ const key = this.sourceToRemote.get(sourceQid);
893
+ const sub = key ? this.remoteSubs.get(key) : undefined;
894
+ if (!sub) return [sourceQid];
895
+ const localQids = [...sub.localQids.keys()];
896
+ return localQids.length ? localQids : [sourceQid];
897
+ }
898
+
816
899
  private onProgress(frame: ProgressFrame): void {
817
900
  // (1) Take every buffered frame at cv ≤ cvMin, in (cv, arrival) order, and fold it:
818
901
  // lmid system-query frames advance `confirmedLmid`; data frames fold through the
@@ -1225,8 +1308,20 @@ function trackingTx(
1225
1308
  throw new FoldReadError();
1226
1309
  };
1227
1310
 
1311
+ // One-shot query (203 §5.2): lower the builder to an AST, refuse any local-only table it
1312
+ // reads (M1 — same guard as `rawGet`), and run it over the engine's read-cache fork. The
1313
+ // wasm engine returns the rows already keyed by column name with their materialized
1314
+ // relationship children nested by name (`marshal::caught_node_to_js`), in the query's order —
1315
+ // identical in shape to `view.data` — so this is a pass-through.
1316
+ const runQuery = (q: QueryArg): QueryResultRow[] => {
1317
+ const ast = q.ast();
1318
+ for (const t of collectTables(ast)) assertNotLocal(t, "read");
1319
+ return tx.query(ast) as QueryResultRow[];
1320
+ };
1321
+
1228
1322
  return {
1229
1323
  get: trapReads ? trapped : rawGet,
1324
+ query: trapReads ? trapped : runQuery,
1230
1325
  add,
1231
1326
  remove,
1232
1327
  edit,
package/src/client-id.ts CHANGED
@@ -39,6 +39,22 @@ export function resetTabInstanceForTests(): void {
39
39
  tabInstance = 0;
40
40
  }
41
41
 
42
+ /** Clear the persisted client identity so the next page load starts a fresh mid/lmid stream.
43
+ * Intended for dev recovery after out-of-band server state loss, not for normal operation. */
44
+ export function resetStableClientID(): void {
45
+ try {
46
+ globalThis.localStorage?.removeItem(CLIENT_ID_KEY);
47
+ } catch {
48
+ // Storage can throw in private modes or test shims; best-effort reset.
49
+ }
50
+ try {
51
+ globalThis.sessionStorage?.removeItem(TAB_ID_KEY);
52
+ } catch {
53
+ // Storage can throw in private modes or test shims; best-effort reset.
54
+ }
55
+ tabInstance = 0;
56
+ }
57
+
42
58
  /** The connection's stable clientID. A per-origin base (localStorage) keeps one logical identity
43
59
  * across reloads; a per-tab suffix (sessionStorage) gives each tab its OWN mid/lmid stream; a
44
60
  * per-load instance suffix keeps two clients constructed in ONE tab from sharing that stream. Falls