okengine 0.17.2 → 0.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (209) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +14 -10
  3. package/manifest.v1.schema.json +61 -2
  4. package/package.json +19 -5
  5. package/site/content/docs/ai/index.mdx +1 -1
  6. package/site/content/docs/ai/mcp.mdx +10 -1
  7. package/site/content/docs/ai/skills.mdx +9 -9
  8. package/site/content/docs/elements/ai.mdx +1 -1
  9. package/site/content/docs/elements/clock.mdx +1 -1
  10. package/site/content/docs/elements/flow.mdx +25 -1
  11. package/site/content/docs/elements/gate.mdx +3 -2
  12. package/site/content/docs/elements/store.mdx +289 -341
  13. package/site/content/docs/elements/vault.mdx +5 -5
  14. package/site/content/docs/get-started/basic-usage.mdx +3 -10
  15. package/site/content/docs/get-started/index.mdx +1 -1
  16. package/site/content/docs/get-started/installation.mdx +2 -3
  17. package/site/content/docs/get-started/introduction.mdx +58 -121
  18. package/site/content/docs/get-started/meta.json +9 -1
  19. package/site/content/docs/get-started/project-structure.mdx +4 -11
  20. package/site/content/docs/get-started/testing.mdx +328 -0
  21. package/site/content/docs/get-started/why.mdx +93 -71
  22. package/site/content/docs/index.mdx +44 -11
  23. package/site/content/docs/meta.json +8 -5
  24. package/site/content/docs/plugins/apple.mdx +151 -0
  25. package/site/content/docs/plugins/discord.mdx +139 -0
  26. package/site/content/docs/plugins/facebook.mdx +134 -0
  27. package/site/content/docs/plugins/figma.mdx +138 -0
  28. package/site/content/docs/plugins/github.mdx +138 -0
  29. package/site/content/docs/plugins/google.mdx +153 -0
  30. package/site/content/docs/plugins/index.mdx +47 -1
  31. package/site/content/docs/plugins/meta.json +10 -0
  32. package/site/content/docs/plugins/microsoft.mdx +151 -0
  33. package/site/content/docs/plugins/oauth.mdx +188 -0
  34. package/site/content/docs/plugins/x.mdx +125 -0
  35. package/site/content/docs/providers/index.mdx +2 -0
  36. package/site/content/docs/recipes/index.mdx +2 -0
  37. package/site/content/docs/reference/cli.md +3 -2
  38. package/site/content/docs/reference/client.mdx +58 -1
  39. package/site/content/docs/reference/configuration.mdx +2 -4
  40. package/site/content/docs/reference/fx.mdx +3 -1
  41. package/site/content/docs/reference/index.mdx +0 -5
  42. package/site/content/docs/reference/meta.json +2 -2
  43. package/site/content/docs/reference/okid.mdx +137 -0
  44. package/src/auth/bindings.ts +1 -1
  45. package/src/auth/config.ts +9 -0
  46. package/src/auth/identity-sql.ts +314 -0
  47. package/src/auth/identity.ts +140 -2
  48. package/src/auth/index.ts +17 -1
  49. package/src/auth/method-context.ts +3 -0
  50. package/src/auth/oauth-as/cimd.ts +132 -0
  51. package/src/auth/oauth-as/crypto.test.ts +101 -0
  52. package/src/auth/oauth-as/crypto.ts +393 -0
  53. package/src/auth/oauth-as/errors.ts +68 -0
  54. package/src/auth/oauth-as/http.test.ts +419 -0
  55. package/src/auth/oauth-as/http.ts +842 -0
  56. package/src/auth/oauth-as/stores.ts +61 -0
  57. package/src/auth/oauth-as/tables.ts +142 -0
  58. package/src/auth/tables.ts +0 -11
  59. package/src/bench/README.md +83 -0
  60. package/src/bench/REPORT.md +176 -0
  61. package/src/bench/g01-rls-stamp.bench.ts +194 -0
  62. package/src/bench/g02-clock-per-tenant.bench.ts +158 -0
  63. package/src/bench/g03-signal-once.bench.ts +157 -0
  64. package/src/bench/g03-signal-reconnect.bench.ts +254 -0
  65. package/src/bench/g03-signal-sse-memory.bench.ts +191 -0
  66. package/src/bench/g04-auth-vault-hotpath.bench.ts +170 -0
  67. package/src/bench/g05-sustained-full.bench.ts +265 -0
  68. package/src/bench/g06-mixed-load.bench.ts +260 -0
  69. package/src/bench/g07-vault-crypto.bench.ts +100 -0
  70. package/src/bench/g07-vault-rotate-under-read.bench.ts +285 -0
  71. package/src/bench/g08-conn-oversubscribe.bench.ts +194 -0
  72. package/src/bench/g08-store-kv-durable.bench.ts +133 -0
  73. package/src/bench/g08-store-sql.bench.ts +178 -0
  74. package/src/bench/g09-journal-sustained.bench.ts +203 -0
  75. package/src/bench/g10-observability-contention.bench.ts +246 -0
  76. package/src/bench/g11-cold-start-cycle.bench.ts +164 -0
  77. package/src/bench/g13-elements.bench.ts +427 -0
  78. package/src/bench/g14-graceful-shutdown.bench.ts +244 -0
  79. package/src/bench/g15-postgres-degradation.bench.ts +264 -0
  80. package/src/bench/g16-live-query-fanout.bench.ts +206 -0
  81. package/src/bench/lib/event-loop-lag.ts +26 -0
  82. package/src/bench/lib/infra.ts +60 -0
  83. package/src/bench/lib/report.ts +52 -0
  84. package/src/bench/lib/rss-sampler.ts +61 -0
  85. package/src/bench/lib/signal-pg.ts +88 -0
  86. package/src/bench/load-app.ts +337 -0
  87. package/src/bench/load-child.ts +108 -0
  88. package/src/bench/smoke.bench.ts +43 -0
  89. package/src/cli/competitor-mention-removal.test.ts +28 -0
  90. package/src/cli/doctor-fd.ts +117 -0
  91. package/src/cli/doctor.test.ts +192 -0
  92. package/src/cli/doctor.ts +129 -1
  93. package/src/client/create.ts +95 -1
  94. package/src/client/index.ts +9 -2
  95. package/src/client/transport.ts +11 -4
  96. package/src/client/use-live-query.ts +154 -0
  97. package/src/client-react/index.ts +15 -1
  98. package/src/client-react/live-resource.ts +246 -0
  99. package/src/client-react/use-live-query.test.ts +475 -0
  100. package/src/client-react/use-live-query.ts +530 -0
  101. package/src/compiler/extract.test.ts +518 -0
  102. package/src/compiler/extract.ts +386 -19
  103. package/src/console/server/invoke-user-flow.ts +2 -1
  104. package/src/console/ui-next/dist/assets/{access-page-DnWbnGzq.js → access-page-De7Lc2JC.js} +1 -1
  105. package/src/console/ui-next/dist/assets/{flows-page-BiZ4-6yQ.js → flows-page-RGy7VEA_.js} +1 -1
  106. package/src/console/ui-next/dist/assets/{index-C8NRK2R-.js → index-_rgpdVzo.js} +3 -3
  107. package/src/console/ui-next/dist/assets/{observability-page-CrB6vd1T.js → observability-page-Ds6pcnh-.js} +1 -1
  108. package/src/console/ui-next/dist/assets/{store-page-CS5-aETQ.js → store-page-02xOiqIK.js} +3 -3
  109. package/src/console/ui-next/dist/assets/{units-page-CjtdlW8l.js → units-page-4rHOePuE.js} +1 -1
  110. package/src/console/ui-next/dist/assets/{vault-page-C6Xxm9SA.js → vault-page-DISPgxLM.js} +1 -1
  111. package/src/console/ui-next/dist/index.html +1 -1
  112. package/src/console/ui-next/src/features/store/lib/fields-from-table.ts +36 -2
  113. package/src/drivers/cdc-outbox.ts +389 -0
  114. package/src/drivers/memory.ts +20 -0
  115. package/src/drivers/oauth-apple.ts +156 -0
  116. package/src/drivers/oauth-discord.ts +79 -0
  117. package/src/drivers/oauth-facebook.ts +80 -0
  118. package/src/drivers/oauth-figma.ts +116 -0
  119. package/src/drivers/oauth-github.ts +92 -0
  120. package/src/drivers/oauth-google.ts +142 -0
  121. package/src/drivers/oauth-microsoft.ts +174 -0
  122. package/src/drivers/oauth-oidc.ts +293 -0
  123. package/src/drivers/oauth-shared.ts +326 -0
  124. package/src/drivers/oauth-types.ts +159 -0
  125. package/src/drivers/oauth-x.ts +77 -0
  126. package/src/drivers/oauth2-common.ts +95 -0
  127. package/src/drivers/oauth2-token.ts +61 -0
  128. package/src/drivers/pg-rls-row-passes.ts +251 -0
  129. package/src/drivers/pg-rls.ts +2 -0
  130. package/src/drivers/postgres.ts +45 -2
  131. package/src/drivers/signal-postgres.ts +2 -1
  132. package/src/elements/channel/runtime.ts +29 -2
  133. package/src/elements/channel.test.ts +52 -0
  134. package/src/elements/gate/boot.ts +29 -2
  135. package/src/elements/store/emit-drizzle.ts +147 -14
  136. package/src/elements/store/field-ddl.test.ts +118 -0
  137. package/src/elements/store/field-types.test.ts +455 -0
  138. package/src/elements/store/list-query.golden.json +777 -0
  139. package/src/elements/store/list-query.parity.test.ts +396 -0
  140. package/src/elements/store/list-query.ts +792 -0
  141. package/src/elements/store/live-default.test.ts +136 -0
  142. package/src/elements/store/live-http.test.ts +160 -0
  143. package/src/elements/store/live-isolation.test.ts +291 -0
  144. package/src/elements/store/live-query-runtime.test.ts +323 -0
  145. package/src/elements/store/live-query-runtime.ts +403 -0
  146. package/src/elements/store/live-query-server.test.ts +377 -0
  147. package/src/elements/store/live-query-server.ts +102 -0
  148. package/src/elements/store/live-query.ts +97 -0
  149. package/src/elements/store/resource.ts +189 -680
  150. package/src/elements/store/rls-row-passes-policies.parity.test.ts +665 -0
  151. package/src/elements/store/schema-decl.ts +539 -41
  152. package/src/elements/store/sql-rls-stamp.test.ts +27 -0
  153. package/src/elements/store/sql-session.ts +297 -35
  154. package/src/elements/store/table.ts +102 -21
  155. package/src/elements/store.test.ts +3 -1
  156. package/src/elements/store.ts +12 -1
  157. package/src/elements/vault/chaos-child.ts +74 -1
  158. package/src/elements/vault/chaos.test.ts +4 -2
  159. package/src/elements/vault/storage.ts +4 -2
  160. package/src/index.ts +5 -2
  161. package/src/kernel/app-auth.ts +1 -0
  162. package/src/kernel/app.ts +116 -2
  163. package/src/kernel/auth-sharing.test.ts +196 -0
  164. package/src/kernel/boot.test.ts +3 -3
  165. package/src/kernel/errors.ts +8 -0
  166. package/src/kernel/fx.test.ts +1 -0
  167. package/src/kernel/fx.ts +14 -2
  168. package/src/kernel/horizontal-child.ts +2 -1
  169. package/src/kernel/http-resource.ts +33 -7
  170. package/src/kernel/identity-host-persist.test.ts +119 -0
  171. package/src/kernel/instance-id.ts +4 -2
  172. package/src/kernel/journal.ts +2 -1
  173. package/src/kernel/mcp-tool.test.ts +95 -0
  174. package/src/kernel/on.ts +9 -0
  175. package/src/kernel/realtime-bind.ts +326 -0
  176. package/src/kernel/resource-live.ts +117 -0
  177. package/src/kernel/triggers.ts +86 -4
  178. package/src/manifest/diff.ts +37 -0
  179. package/src/manifest/types.ts +64 -2
  180. package/src/okid.bench.test.ts +64 -0
  181. package/src/okid.test.ts +338 -0
  182. package/src/okid.ts +245 -0
  183. package/src/plugins/anonymous.ts +19 -1
  184. package/src/plugins/auth/shared.ts +15 -0
  185. package/src/plugins/index.ts +2 -0
  186. package/src/plugins/magic-link.ts +10 -8
  187. package/src/plugins/mcp-oauth.ts +208 -0
  188. package/src/plugins/oauth/flow-store.ts +117 -0
  189. package/src/plugins/oauth/link.ts +69 -0
  190. package/src/plugins/oauth/shared.ts +108 -0
  191. package/src/plugins/oauth/token-vault.ts +100 -0
  192. package/src/plugins/oauth.security.test.ts +535 -0
  193. package/src/plugins/oauth.ts +532 -0
  194. package/src/plugins/otp.ts +48 -6
  195. package/src/plugins/passkey.ts +20 -1
  196. package/src/plugins/two-factor.ts +11 -0
  197. package/src/plugins/username.ts +40 -7
  198. package/src/release/build-lib.ts +7 -1
  199. package/src/release/measure.ts +1 -0
  200. package/src/release/official-plugins.ts +4 -1
  201. package/src/runs/collect.ts +2 -1
  202. package/src/runs/drivers/files.ts +2 -1
  203. package/src/test/create-test-app.ts +114 -5
  204. package/src/test/export-bundle.test.ts +33 -0
  205. package/src/test/live-signals.test.ts +83 -0
  206. package/src/test/tenant-isolation.test.ts +175 -0
  207. package/src/testing.ts +26 -0
  208. package/src/upgrade/codemods.ts +1 -1
  209. package/site/content/docs/reference/migrating-environments.mdx +0 -158
@@ -0,0 +1,323 @@
1
+ /**
2
+ * LiveQuery runtime classification + bounded fan-out pool.
3
+ *
4
+ * Probes are scripted BY SQL SHAPE (EXISTS vs policy replay vs window
5
+ * predicate), matching how classifyForSubscriber actually queries. The RLS
6
+ * truthfulness of those probes is proven separately by the parity gate.
7
+ */
8
+
9
+ import { describe, expect, test } from "bun:test";
10
+ import {
11
+ classifyForSubscriber,
12
+ identityBucket,
13
+ LiveQueryRuntime,
14
+ type CdcEventInput,
15
+ type LiveProbeExec,
16
+ type LiveProbeRunner,
17
+ type LiveSubscription,
18
+ } from "./live-query-runtime.ts";
19
+ import type { RlsIdentity } from "../../drivers/pg-rls.ts";
20
+
21
+ const ALICE: RlsIdentity = { gate: "member", userId: "alice", scopes: ["member"] };
22
+
23
+ /** Script probe answers by SQL inspection (row_passes / EXISTS / custom). */
24
+ function shapeProbes(answerFor: (sql: string) => boolean): LiveProbeRunner {
25
+ return {
26
+ async runStamped(_identity, fn) {
27
+ const exec: LiveProbeExec = {
28
+ async query(sql) {
29
+ return answerFor(sql) ? [{ ok: true }] : [];
30
+ },
31
+ };
32
+ return fn(exec);
33
+ },
34
+ };
35
+ }
36
+
37
+ function sub(overrides: Partial<LiveSubscription> = {}): LiveSubscription {
38
+ return {
39
+ id: "s1",
40
+ ref: "sql:app",
41
+ table: "tasks",
42
+ pkColumn: "id",
43
+ identity: ALICE,
44
+ deliver() {},
45
+ ...overrides,
46
+ };
47
+ }
48
+
49
+ const isReplay = (sql: string): boolean => sql.includes("row_passes_policies");
50
+ const isWindow = (sql: string): boolean => sql.includes("before_row");
51
+
52
+ describe("classification algorithm (stateless)", () => {
53
+ test("insert visible → upsert", async () => {
54
+ const after = { id: "a", status: "open" };
55
+ const event: CdcEventInput = { tableName: "tasks", op: "insert", before: null, after };
56
+ const out = await classifyForSubscriber(
57
+ shapeProbes((sql) => !isReplay(sql)),
58
+ sub(),
59
+ event,
60
+ );
61
+ expect(out).toEqual({ kind: "upsert", row: after });
62
+ });
63
+
64
+ test("update still visible → upsert", async () => {
65
+ const event: CdcEventInput = {
66
+ tableName: "tasks",
67
+ op: "update",
68
+ before: { id: "a", status: "todo" },
69
+ after: { id: "a", status: "open" },
70
+ };
71
+ const out = await classifyForSubscriber(
72
+ shapeProbes((sql) => !isReplay(sql)),
73
+ sub(),
74
+ event,
75
+ );
76
+ expect(out).toEqual({ kind: "upsert", row: { id: "a", status: "open" } });
77
+ });
78
+
79
+ test("delete of previously-visible row → delete", async () => {
80
+ const event: CdcEventInput = {
81
+ tableName: "tasks",
82
+ op: "delete",
83
+ before: { id: "a", status: "open" },
84
+ after: null,
85
+ };
86
+ // Replay on before image says it WAS visible → delete.
87
+ const out = await classifyForSubscriber(shapeProbes(isReplay), sub(), event);
88
+ expect(out).toEqual({ kind: "delete", id: "a" });
89
+ });
90
+
91
+ test("delete of never-visible row → ignore", async () => {
92
+ const event: CdcEventInput = {
93
+ tableName: "tasks",
94
+ op: "delete",
95
+ before: { id: "z" },
96
+ after: null,
97
+ };
98
+ const out = await classifyForSubscriber(
99
+ shapeProbes(() => false),
100
+ sub(),
101
+ event,
102
+ );
103
+ expect(out).toBeNull();
104
+ });
105
+
106
+ test("insert of invisible row → ignore (no state, no delete)", async () => {
107
+ const event: CdcEventInput = {
108
+ tableName: "tasks",
109
+ op: "insert",
110
+ before: null,
111
+ after: { id: "x", status: "closed" },
112
+ };
113
+ const out = await classifyForSubscriber(
114
+ shapeProbes(() => false),
115
+ sub(),
116
+ event,
117
+ );
118
+ expect(out).toBeNull();
119
+ });
120
+
121
+ test("query-window exit → revoked reason query", async () => {
122
+ const event: CdcEventInput = {
123
+ tableName: "tasks",
124
+ op: "update",
125
+ before: { id: "a", status: "open" },
126
+ after: { id: "a", status: "closed" },
127
+ };
128
+ // EXISTS(after)=false; before replay=true; before-window predicate=
129
+ // matched (uses before_row table); after replay=allowed ⇒ query exit.
130
+ const out = await classifyForSubscriber(
131
+ shapeProbes((sql) => isReplay(sql) || isWindow(sql)),
132
+ sub({ whereSql: "status = 'open'" }),
133
+ event,
134
+ );
135
+ expect(out).toEqual({ kind: "revoked", id: "a", reason: "query" });
136
+ });
137
+
138
+ test("RLS revocation → revoked reason rls", async () => {
139
+ const event: CdcEventInput = {
140
+ tableName: "tasks",
141
+ op: "update",
142
+ before: { id: "a", owner: "alice" },
143
+ after: { id: "a", owner: "mallory" },
144
+ };
145
+ // Sequence: EXISTS(after)=false → replay(BEFORE)=true → replay(AFTER)=false.
146
+ // Keyed on call ordinal since replay SQL text is identical (params differ).
147
+ let replayCalls = 0;
148
+ const out = await classifyForSubscriber(
149
+ {
150
+ async runStamped(_identity, fn) {
151
+ return fn({
152
+ async query(sql) {
153
+ if (sql.includes("row_passes_policies")) {
154
+ replayCalls += 1;
155
+ return [{ ok: replayCalls === 1 }];
156
+ }
157
+ return []; // EXISTS after-image: invisible
158
+ },
159
+ });
160
+ },
161
+ },
162
+ sub(),
163
+ event,
164
+ );
165
+ expect(replayCalls).toBe(2);
166
+ expect(out).toEqual({ kind: "revoked", id: "a", reason: "rls" });
167
+ });
168
+
169
+ test("never visible in any world → ignore entirely", async () => {
170
+ const event: CdcEventInput = {
171
+ tableName: "tasks",
172
+ op: "update",
173
+ before: { id: "z", owner: "mallory" },
174
+ after: { id: "z", owner: "mallory2" },
175
+ };
176
+ const out = await classifyForSubscriber(
177
+ shapeProbes(() => false),
178
+ sub(),
179
+ event,
180
+ );
181
+ expect(out).toBeNull();
182
+ });
183
+ });
184
+
185
+ describe("bounded fan-out pool (architecture contract)", () => {
186
+ function deferred<T>(): { promise: Promise<T>; resolve: (v: T) => void } {
187
+ let resolve!: (v: T) => void;
188
+ const promise = new Promise<T>((r) => {
189
+ resolve = r;
190
+ });
191
+ return { promise, resolve };
192
+ }
193
+
194
+ test("concurrency cap never exceeded; queue drains in waves", async () => {
195
+ let inflight = 0;
196
+ let peak = 0;
197
+ const gate = deferred<void>();
198
+ const runtime = new LiveQueryRuntime(
199
+ {
200
+ async runStamped(_identity, fn) {
201
+ inflight += 1;
202
+ peak = Math.max(peak, inflight);
203
+ await gate.promise;
204
+ try {
205
+ return fn({
206
+ async query() {
207
+ return [{ ok: true }];
208
+ },
209
+ });
210
+ } finally {
211
+ inflight -= 1;
212
+ }
213
+ },
214
+ },
215
+ { concurrency: 4 },
216
+ );
217
+ runtime.subscribe(sub({ id: "all" }));
218
+ for (let i = 0; i < 12; i += 1) {
219
+ runtime.onCdc({ tableName: "tasks", op: "insert", before: null, after: { id: `r${i}` } });
220
+ }
221
+ await new Promise((r) => setTimeout(r, 10));
222
+ expect(runtime.queueDepth).toBeGreaterThan(0);
223
+ expect(peak).toBeLessThanOrEqual(4);
224
+ gate.resolve();
225
+ await new Promise((r) => setTimeout(r, 20));
226
+ expect(runtime.queueDepth).toBe(0);
227
+ expect(runtime.metrics.checkFailures).toBe(0);
228
+ });
229
+
230
+ test("shed on overflow increments eventsShed — never unbounded growth", async () => {
231
+ const gate = deferred<void>();
232
+ const runtime = new LiveQueryRuntime(
233
+ {
234
+ async runStamped(_i, fn) {
235
+ await gate.promise;
236
+ return fn({
237
+ async query() {
238
+ return [];
239
+ },
240
+ });
241
+ },
242
+ },
243
+ { concurrency: 1, maxQueue: 2 },
244
+ );
245
+ runtime.subscribe(sub({ id: "one" }));
246
+ for (let i = 0; i < 10; i += 1) {
247
+ runtime.onCdc({ tableName: "tasks", op: "insert", before: null, after: { id: `r${i}` } });
248
+ }
249
+ expect(runtime.metrics.eventsShed).toBeGreaterThan(0);
250
+ gate.resolve();
251
+ });
252
+
253
+ test("probe failures count as checkFailures and skip delivery", async () => {
254
+ const delivered: unknown[] = [];
255
+ const runtime = new LiveQueryRuntime({
256
+ async runStamped() {
257
+ throw new Error("stamp broken");
258
+ },
259
+ });
260
+ runtime.subscribe(
261
+ sub({
262
+ id: "broke",
263
+ deliver(e) {
264
+ delivered.push(e);
265
+ },
266
+ }),
267
+ );
268
+ runtime.onCdc({ tableName: "tasks", op: "update", before: { id: "a" }, after: { id: "a" } });
269
+ await new Promise((r) => setTimeout(r, 20));
270
+ expect(delivered).toHaveLength(0);
271
+ expect(runtime.metrics.checkFailures).toBe(1);
272
+ });
273
+
274
+ test("unsubscriber removes target — later CDC events ignored", async () => {
275
+ const delivered: unknown[] = [];
276
+ const runtime = new LiveQueryRuntime(shapeProbes(() => true) as LiveProbeRunner & object);
277
+ const off = runtime.subscribe(
278
+ sub({
279
+ id: "t1",
280
+ deliver(e) {
281
+ delivered.push(e);
282
+ },
283
+ }),
284
+ );
285
+ off();
286
+ expect(runtime.size).toBe(0);
287
+ runtime.onCdc({ tableName: "tasks", op: "insert", before: null, after: { id: "a" } });
288
+ await new Promise((r) => setTimeout(r, 10));
289
+ expect(delivered).toHaveLength(0);
290
+ expect(runtime.metrics.eventsIn).toBe(1);
291
+ });
292
+
293
+ test("mutationId echoes onto upsert events", async () => {
294
+ const delivered: Extract<
295
+ Awaited<ReturnType<typeof classifyForSubscriber>>,
296
+ { kind?: string }
297
+ >[] = [];
298
+ const runtime = new LiveQueryRuntime(shapeProbes((sql) => !isReplay(sql)));
299
+ runtime.subscribe(
300
+ sub({
301
+ id: "mut",
302
+ deliver(e) {
303
+ delivered.push(e);
304
+ },
305
+ }),
306
+ );
307
+ runtime.onCdc({
308
+ tableName: "tasks",
309
+ op: "update",
310
+ before: { id: "a" },
311
+ after: { id: "a", done: true },
312
+ mutationId: "m-123",
313
+ });
314
+ await new Promise((r) => setTimeout(r, 20));
315
+ expect(delivered[0]).toMatchObject({ kind: "upsert", mutationId: "m-123" });
316
+ });
317
+
318
+ test("identity buckets group identical stamps (v2 dedupe prep)", () => {
319
+ expect(identityBucket(ALICE)).toBe(identityBucket({ ...ALICE }));
320
+ expect(identityBucket(ALICE)).not.toBe(identityBucket({ ...ALICE, userId: "bob" }));
321
+ expect(identityBucket({ ...ALICE, tenantId: "acme" })).not.toBe(identityBucket(ALICE));
322
+ });
323
+ });