@reventlessdev/reventless-local 3.0.0-alpha.115 → 3.0.0-alpha.117

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 (59) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +91 -17
  4. package/src/Platform.res.mjs +89 -30
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +113 -39
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +87 -23
  9. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
  10. package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
  11. package/src/adapter/EventLog/EventLogStorage_InMemory.res +23 -2
  12. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +20 -3
  13. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +132 -24
  14. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +119 -37
  15. package/src/adapter/LocalBus.res +62 -0
  16. package/src/adapter/LocalBus.res.mjs +120 -0
  17. package/src/adapter/ProjectionCheckpoint.res +350 -0
  18. package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
  19. package/src/adapter/ProjectionPending.res +60 -0
  20. package/src/adapter/ProjectionPending.res.mjs +64 -0
  21. package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
  22. package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
  23. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
  24. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +32 -6
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +30 -14
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +163 -11
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +101 -3
  29. package/src/adapter/SqliteDriver.res +11 -1
  30. package/src/adapter/SqliteDriver.res.mjs +5 -1
  31. package/src/components/ReadModel_Builder.res +3 -1
  32. package/src/components/ReadModel_Builder.res.mjs +13 -4
  33. package/src/components/StateViewSlice_Builder.res +4 -1
  34. package/src/components/StateViewSlice_Builder.res.mjs +9 -3
  35. package/src/test/Mocks/MockEventLogStorage.res +19 -2
  36. package/src/test/Mocks/MockEventLogStorage.res.mjs +21 -2
  37. package/tests/PluginEventDecodeTest.res +51 -0
  38. package/tests/PluginEventDecodeTest.res.mjs +63 -0
  39. package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
  40. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
  41. package/tests/adapter/DcbEventLogStorageTest.res +46 -0
  42. package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
  43. package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
  44. package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
  45. package/tests/adapter/EventLogStorageSqliteTest.res +128 -0
  46. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +189 -0
  47. package/tests/adapter/ProjectionCheckpointTest.res +422 -0
  48. package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
  49. package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
  50. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
  51. package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
  52. package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
  53. package/tests/adapter/QueryDbListResolverTest.res +76 -0
  54. package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
  55. package/tests/components/aggregate/AggregateFixtures.res +4 -0
  56. package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
  57. package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
  58. package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
  59. package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
@@ -0,0 +1,401 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Stream from "effect/Stream";
7
+ import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
8
+ import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
9
+ import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
10
+ import * as ProjectionPending$ReventlessLocal from "../../src/adapter/ProjectionPending.res.mjs";
11
+ import * as ProjectionCheckpoint$ReventlessLocal from "../../src/adapter/ProjectionCheckpoint.res.mjs";
12
+ import * as EventLogStorage_Sqlite$ReventlessLocal from "../../src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs";
13
+ import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "../../src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs";
14
+
15
+ TestRunner$ReventlessLocal.setup();
16
+
17
+ let opts = {};
18
+
19
+ function makeFreshDb() {
20
+ return SqliteDriver$ReventlessLocal.openDb(":memory:");
21
+ }
22
+
23
+ function flatEvent(id, seq, eventType, dataOpt, msgId, serviceOpt) {
24
+ let data = dataOpt !== undefined ? dataOpt : [];
25
+ let service = serviceOpt !== undefined ? serviceOpt : "Products";
26
+ return Object.fromEntries([
27
+ [
28
+ "id",
29
+ id
30
+ ],
31
+ [
32
+ "position",
33
+ seq.toString().padStart(9, "0")
34
+ ],
35
+ [
36
+ "event",
37
+ eventType
38
+ ],
39
+ [
40
+ "data",
41
+ Object.fromEntries(data)
42
+ ],
43
+ [
44
+ "recordedAt",
45
+ "2026-07-03T00:00:00.000Z"
46
+ ],
47
+ [
48
+ "service",
49
+ service
50
+ ],
51
+ [
52
+ "time",
53
+ "2026-07-03T00:00:00.000Z"
54
+ ],
55
+ [
56
+ "msgId",
57
+ msgId
58
+ ],
59
+ [
60
+ "correlationId",
61
+ msgId
62
+ ]
63
+ ]);
64
+ }
65
+
66
+ function msgIdOfEnvelope(json) {
67
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["meta"]), Stdlib_JSON.Decode.object), m => m["msgId"]), Stdlib_JSON.Decode.string), "<none>");
68
+ }
69
+
70
+ function makeHandler() {
71
+ let received = [];
72
+ let handler = (json, _ctx) => {
73
+ received.push(json);
74
+ return Promise.resolve();
75
+ };
76
+ return [
77
+ received,
78
+ handler
79
+ ];
80
+ }
81
+
82
+ function dcbEvent(eventType, tags, dataOpt, msgId) {
83
+ let data = dataOpt !== undefined ? dataOpt : [];
84
+ return {
85
+ eventType: eventType,
86
+ data: Object.fromEntries(data),
87
+ tags: tags,
88
+ meta: {
89
+ service: "OrderingDcbEventLog",
90
+ time: "2026-07-03T00:00:00.000Z",
91
+ msgId: msgId,
92
+ correlationId: msgId
93
+ }
94
+ };
95
+ }
96
+
97
+ async function makeDcbStorage(db, name) {
98
+ let match = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(db, name, [], {
99
+ TAG: "Simple",
100
+ _0: {
101
+ key: "k"
102
+ }
103
+ }, opts);
104
+ return await TestRunner$ReventlessLocal.resolve(match[2].operations);
105
+ }
106
+
107
+ function idOfEnvelope(json) {
108
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["id"]), Stdlib_JSON.Decode.string), "<none>");
109
+ }
110
+
111
+ globalThis.describe("ProjectionCheckpoint", () => {
112
+ globalThis.describe("pending low-watermark", () => {
113
+ globalThis.test("out-of-order publish completion cannot advance past a pending earlier append", async () => {
114
+ ProjectionPending$ReventlessLocal.reset();
115
+ ProjectionPending$ReventlessLocal.enableTracking();
116
+ let TestBus = LocalBus$ReventlessLocal.Make({});
117
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
118
+ let DbProvider = {
119
+ db: db
120
+ };
121
+ let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
122
+ let s = Storage.make("agg", opts);
123
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
124
+ await ops.append(0, "id-1", [
125
+ flatEvent("id-1", 0, "Created", undefined, "a1", undefined),
126
+ flatEvent("id-1", 1, "Renamed", undefined, "a2", undefined)
127
+ ]);
128
+ await ops.append(0, "id-2", [flatEvent("id-2", 0, "Created", undefined, "b1", undefined)]);
129
+ ProjectionCheckpoint$ReventlessLocal.setPosition(db, "RM", 0);
130
+ ProjectionCheckpoint$ReventlessLocal.completePublished(db, ["b1"]);
131
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM")).toBe(0);
132
+ ProjectionCheckpoint$ReventlessLocal.completePublished(db, [
133
+ "a1",
134
+ "a2"
135
+ ]);
136
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM")).toBe(3);
137
+ return ProjectionPending$ReventlessLocal.reset();
138
+ });
139
+ globalThis.test("appendStream batches never enter the pending set", async () => {
140
+ ProjectionPending$ReventlessLocal.reset();
141
+ ProjectionPending$ReventlessLocal.enableTracking();
142
+ let TestBus = LocalBus$ReventlessLocal.Make({});
143
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
144
+ let DbProvider = {
145
+ db: db
146
+ };
147
+ let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
148
+ let s = Storage.make("agg", opts);
149
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
150
+ await Effect.runPromise(ops.appendStream(0, "id-bulk", Stream.fromIterable([
151
+ flatEvent("id-bulk", 0, "Created", undefined, "s1", undefined),
152
+ flatEvent("id-bulk", 1, "Renamed", undefined, "s2", undefined)
153
+ ])));
154
+ globalThis.expect(ProjectionPending$ReventlessLocal.minPending("Aggregate")).toEqual(undefined);
155
+ ProjectionCheckpoint$ReventlessLocal.setPosition(db, "RM", 0);
156
+ ProjectionCheckpoint$ReventlessLocal.completePublished(db, []);
157
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM")).toBe(2);
158
+ return ProjectionPending$ReventlessLocal.reset();
159
+ });
160
+ });
161
+ globalThis.describe("catchupEnvelope", () => {
162
+ globalThis.test("rebuilds the live {id, meta, event} envelope from a stored row", () => {
163
+ let flat = flatEvent("p-1", 4, "Created", [[
164
+ "name",
165
+ "Widget"
166
+ ]], "m-1", undefined);
167
+ let envelope = ProjectionCheckpoint$ReventlessLocal.catchupEnvelope(flat);
168
+ if (envelope !== undefined) {
169
+ let dict = Stdlib_Option.getOrThrow(Stdlib_JSON.Decode.object(envelope), undefined);
170
+ globalThis.expect(dict["id"]).toEqual("p-1");
171
+ globalThis.expect(msgIdOfEnvelope(envelope)).toBe("m-1");
172
+ globalThis.expect(dict["event"]).toEqual(Object.fromEntries([
173
+ [
174
+ "TAG",
175
+ "Created"
176
+ ],
177
+ [
178
+ "name",
179
+ "Widget"
180
+ ]
181
+ ]));
182
+ return;
183
+ }
184
+ globalThis.expect("envelope").toBe("None");
185
+ });
186
+ globalThis.test("payload-less variants come back as a bare JSON string", () => {
187
+ let flat = flatEvent("p-1", 0, "Deleted", undefined, "m-2", undefined);
188
+ let event = Stdlib_Option.flatMap(Stdlib_Option.flatMap(ProjectionCheckpoint$ReventlessLocal.catchupEnvelope(flat), Stdlib_JSON.Decode.object), d => d["event"]);
189
+ globalThis.expect(event).toEqual("Deleted");
190
+ });
191
+ globalThis.test("malformed stored rows yield None instead of throwing", () => {
192
+ let flat = Object.fromEntries([
193
+ [
194
+ "id",
195
+ "p-1"
196
+ ],
197
+ [
198
+ "event",
199
+ "Created"
200
+ ]
201
+ ]);
202
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.catchupEnvelope(flat)).toEqual(undefined);
203
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.catchupEnvelope("junk")).toEqual(undefined);
204
+ });
205
+ });
206
+ globalThis.describe("runCatchup", () => {
207
+ globalThis.test("delivers the missed range in rowid order, exactly once", async () => {
208
+ ProjectionPending$ReventlessLocal.reset();
209
+ let TestBus = LocalBus$ReventlessLocal.Make({});
210
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
211
+ let DbProvider = {
212
+ db: db
213
+ };
214
+ let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
215
+ let s = Storage.make("agg", opts);
216
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
217
+ await ops.append(0, "id-1", [flatEvent("id-1", 0, "Created", undefined, "m1", undefined)]);
218
+ await ops.append(0, "id-2", [flatEvent("id-2", 0, "Created", undefined, "m2", undefined)]);
219
+ await ops.append(1, "id-1", [flatEvent("id-1", 1, "Renamed", undefined, "m3", undefined)]);
220
+ let match = makeHandler();
221
+ let handlerA = match[1];
222
+ let receivedA = match[0];
223
+ let match$1 = makeHandler();
224
+ let handlerB = match$1[1];
225
+ let receivedB = match$1[0];
226
+ let upperBound = ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate");
227
+ globalThis.expect(upperBound).toBe(3);
228
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, upperBound, 0, [
229
+ [
230
+ "RM-A",
231
+ handlerA
232
+ ],
233
+ [
234
+ "RM-B",
235
+ handlerB
236
+ ]
237
+ ]);
238
+ globalThis.expect(receivedA.map(msgIdOfEnvelope)).toEqual([
239
+ "m1",
240
+ "m2",
241
+ "m3"
242
+ ]);
243
+ globalThis.expect(receivedB.map(msgIdOfEnvelope)).toEqual([
244
+ "m1",
245
+ "m2",
246
+ "m3"
247
+ ]);
248
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM-A")).toBe(3);
249
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM-B")).toBe(3);
250
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"), 0, [
251
+ [
252
+ "RM-A",
253
+ handlerA
254
+ ],
255
+ [
256
+ "RM-B",
257
+ handlerB
258
+ ]
259
+ ]);
260
+ globalThis.expect(receivedA.length).toBe(3);
261
+ globalThis.expect(receivedB.length).toBe(3);
262
+ });
263
+ globalThis.test("a fresh database still stamps checkpoint rows at 0", async () => {
264
+ ProjectionPending$ReventlessLocal.reset();
265
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
266
+ let match = makeHandler();
267
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, 0, 0, [[
268
+ "RM-Fresh",
269
+ match[1]
270
+ ]]);
271
+ let stamped = SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, "SELECT COUNT(*) AS c FROM projection_checkpoint WHERE read_model = ?"), ["RM-Fresh"]);
272
+ globalThis.expect(Stdlib_Option.flatMap(stamped, r => r["c"])).toEqual(1.0);
273
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM-Fresh")).toBe(0);
274
+ });
275
+ globalThis.test("a read model without a checkpoint row is seeded from history", async () => {
276
+ ProjectionPending$ReventlessLocal.reset();
277
+ let TestBus = LocalBus$ReventlessLocal.Make({});
278
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
279
+ let DbProvider = {
280
+ db: db
281
+ };
282
+ let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
283
+ let s = Storage.make("agg", opts);
284
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
285
+ await ops.append(0, "id-1", [flatEvent("id-1", 0, "Created", undefined, "m1", undefined)]);
286
+ let match = makeHandler();
287
+ let handlerA = match[1];
288
+ let receivedA = match[0];
289
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"), 0, [[
290
+ "RM-A",
291
+ handlerA
292
+ ]]);
293
+ globalThis.expect(receivedA.length).toBe(1);
294
+ let match$1 = makeHandler();
295
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Aggregate"), 0, [
296
+ [
297
+ "RM-A",
298
+ handlerA
299
+ ],
300
+ [
301
+ "RM-New",
302
+ match$1[1]
303
+ ]
304
+ ]);
305
+ globalThis.expect(receivedA.length).toBe(1);
306
+ globalThis.expect(match$1[0].map(msgIdOfEnvelope)).toEqual(["m1"]);
307
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM-New")).toBe(1);
308
+ });
309
+ });
310
+ globalThis.describe("DCB axis", () => {
311
+ globalThis.test("catch-up delivers missed DCB events with the tag-derived entityId, exactly once", async () => {
312
+ ProjectionPending$ReventlessLocal.reset();
313
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
314
+ let dcbOps = await makeDcbStorage(db, "OrderingDcbEventLog");
315
+ await dcbOps.append([dcbEvent("OrderPlaced", [
316
+ {
317
+ key: "orderId",
318
+ value: "o-1"
319
+ },
320
+ {
321
+ key: "productId",
322
+ value: "p-1"
323
+ }
324
+ ], [[
325
+ "qty",
326
+ 2
327
+ ]], "d1")], undefined);
328
+ await dcbOps.append([dcbEvent("MaintenanceRan", [], undefined, "d2")], undefined);
329
+ let match = makeHandler();
330
+ let handler = match[1];
331
+ let received = match[0];
332
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, 0, ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb"), [[
333
+ "SV",
334
+ handler
335
+ ]]);
336
+ globalThis.expect(received.map(msgIdOfEnvelope)).toEqual([
337
+ "d1",
338
+ "d2"
339
+ ]);
340
+ globalThis.expect(received.map(idOfEnvelope)).toEqual([
341
+ "o-1",
342
+ "OrderingDcbEventLog"
343
+ ]);
344
+ globalThis.expect(Stdlib_Option.flatMap(Stdlib_Option.flatMap(received[0], Stdlib_JSON.Decode.object), d => d["event"])).toEqual(Object.fromEntries([
345
+ [
346
+ "TAG",
347
+ "OrderPlaced"
348
+ ],
349
+ [
350
+ "qty",
351
+ 2
352
+ ]
353
+ ]));
354
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "dcb:SV")).toBe(2);
355
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "SV")).toBe(0);
356
+ await ProjectionCheckpoint$ReventlessLocal.runCatchup(db, 0, ProjectionCheckpoint$ReventlessLocal.maxPosition(db, "Dcb"), [[
357
+ "SV",
358
+ handler
359
+ ]]);
360
+ globalThis.expect(received.length).toBe(2);
361
+ });
362
+ globalThis.test("the two axes' watermarks are independent", async () => {
363
+ ProjectionPending$ReventlessLocal.reset();
364
+ ProjectionPending$ReventlessLocal.enableTracking();
365
+ let TestBus = LocalBus$ReventlessLocal.Make({});
366
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
367
+ let DbProvider = {
368
+ db: db
369
+ };
370
+ let AggStorage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
371
+ let aggS = AggStorage.make("agg", opts);
372
+ let aggOps = await TestRunner$ReventlessLocal.resolve(aggS.operations);
373
+ let dcbOps = await makeDcbStorage(db, "dcb-log");
374
+ await aggOps.append(0, "id-1", [flatEvent("id-1", 0, "Created", undefined, "agg1", undefined)]);
375
+ await dcbOps.append([dcbEvent("OrderPlaced", [{
376
+ key: "orderId",
377
+ value: "o-1"
378
+ }], undefined, "dcb1")], undefined);
379
+ ProjectionCheckpoint$ReventlessLocal.setPosition(db, "RM", 0);
380
+ ProjectionCheckpoint$ReventlessLocal.setPosition(db, "dcb:RM", 0);
381
+ ProjectionCheckpoint$ReventlessLocal.completePublished(db, ["dcb1"]);
382
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "dcb:RM")).toBe(1);
383
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM")).toBe(0);
384
+ ProjectionCheckpoint$ReventlessLocal.completePublished(db, ["agg1"]);
385
+ globalThis.expect(ProjectionCheckpoint$ReventlessLocal.getPosition(db, "RM")).toBe(1);
386
+ return ProjectionPending$ReventlessLocal.reset();
387
+ });
388
+ });
389
+ });
390
+
391
+ export {
392
+ opts,
393
+ makeFreshDb,
394
+ flatEvent,
395
+ msgIdOfEnvelope,
396
+ makeHandler,
397
+ dcbEvent,
398
+ makeDcbStorage,
399
+ idOfEnvelope,
400
+ }
401
+ /* Not a pure module */
@@ -147,6 +147,84 @@ describe("QueryDbStorage_Sqlite — GSI", () => {
147
147
  })
148
148
  })
149
149
 
150
+ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
151
+ let byOwner: Reventless.ReadModel.indexConfig = {
152
+ index: "ByOwner",
153
+ type_: "S",
154
+ idField: "ownerId",
155
+ projectionType: ALL,
156
+ }
157
+ let item = (owner: string) => {
158
+ let o = Dict.make()
159
+ o->Dict.set("ownerId", JSON.Encode.string(owner))
160
+ JSON.Encode.object(o)
161
+ }
162
+
163
+ testPromise("sqlite lookup returns only rows whose indexed field matches", async () => {
164
+ module TestBus = LocalBus.Make()
165
+ module DbProvider = {
166
+ let db = openFreshDb()
167
+ }
168
+ module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
169
+ let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~opts)
170
+ let ops = await s.operations->TestRunner.resolve
171
+ let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
172
+ let _ = await ops.save("k2", item("o2"), ReventlessCore.QueryDb.Any, None)
173
+ let _ = await ops.save("k3", item("o1"), ReventlessCore.QueryDb.Any, None)
174
+
175
+ let lookup = TestBus.getQueryDbIndexLookup("orders")->Option.getOrThrow
176
+ expect(lookup("ownerId", "o1")->Array.length)->toBe(2)
177
+ expect(lookup("ownerId", "o2")->Array.length)->toBe(1)
178
+ expect(lookup("ownerId", "absent")->Array.length)->toBe(0)
179
+ })
180
+
181
+ testPromise("sqlite lookup excludes expired rows", async () => {
182
+ module TestBus = LocalBus.Make()
183
+ module DbProvider = {
184
+ let db = openFreshDb()
185
+ }
186
+ module Storage = QueryDbStorage_Sqlite.Make(TestBus, DbProvider)
187
+ let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~opts)
188
+ let ops = await s.operations->TestRunner.resolve
189
+ let aMinuteAgo = Float.toInt(Date.now() /. 1000.0) - 60
190
+ let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
191
+ let _ = await ops.save("k2", item("o1"), ReventlessCore.QueryDb.Any, Some(aMinuteAgo))
192
+ let lookup = TestBus.getQueryDbIndexLookup("orders")->Option.getOrThrow
193
+ expect(lookup("ownerId", "o1")->Array.length)->toBe(1)
194
+ })
195
+
196
+ testPromise("in-memory lookup matches the sqlite result (backend parity)", async () => {
197
+ module TestBus = LocalBus.Make()
198
+ module Storage = QueryDbStorage_InMemory.Make(TestBus)
199
+ let s = Storage.makeMemory(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~opts)
200
+ let ops = await s.operations->TestRunner.resolve
201
+ let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
202
+ let _ = await ops.save("k2", item("o2"), ReventlessCore.QueryDb.Any, None)
203
+ let _ = await ops.save("k3", item("o1"), ReventlessCore.QueryDb.Any, None)
204
+ let lookup = TestBus.getQueryDbIndexLookup("orders")->Option.getOrThrow
205
+ expect(lookup("ownerId", "o1")->Array.length)->toBe(2)
206
+ expect(lookup("ownerId", "o2")->Array.length)->toBe(1)
207
+ expect(lookup("ownerId", "absent")->Array.length)->toBe(0)
208
+ })
209
+ })
210
+
211
+ describe("QueryDbStorage_InMemory — lazy scan snapshot (B4 dirty-flag)", () => {
212
+ testPromise("interleaved save/scan reflects each write without stale reads", async () => {
213
+ module TestBus = LocalBus.Make()
214
+ module Storage = QueryDbStorage_InMemory.Make(TestBus)
215
+ let s = Storage.make(~name="lazy", ~indexes=[], ~api=(), ~apiRole=(), ~opts)
216
+ let ops = await s.operations->TestRunner.resolve
217
+ let scanFn = TestBus.getQueryDbScan("lazy")->Option.getOrThrow
218
+ expect(scanFn()->Array.length)->toBe(0)
219
+ let _ = await ops.save("a", JSON.Encode.string("va"), ReventlessCore.QueryDb.Any, None)
220
+ expect(scanFn()->Array.length)->toBe(1)
221
+ let _ = await ops.save("b", JSON.Encode.string("vb"), ReventlessCore.QueryDb.Any, None)
222
+ expect(scanFn()->Array.length)->toBe(2)
223
+ let _ = await ops.delete("a", None)
224
+ expect(scanFn()->Array.length)->toBe(1)
225
+ })
226
+ })
227
+
150
228
  describe("QueryDbStorage_Sqlite — TTL", () => {
151
229
  testPromise("save with TTL in the future returns the item", async () => {
152
230
  module TestBus = LocalBus.Make()
@@ -7,6 +7,7 @@ import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
7
7
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
8
8
  import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
9
9
  import * as QueryDbStorage_Sqlite$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs";
10
+ import * as QueryDbStorage_InMemory$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
10
11
 
11
12
  TestRunner$ReventlessLocal.setup();
12
13
 
@@ -120,6 +121,82 @@ globalThis.describe("QueryDbStorage_Sqlite — GSI", () => {
120
121
  });
121
122
  });
122
123
 
124
+ globalThis.describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
125
+ let byOwner = {
126
+ index: "ByOwner",
127
+ type_: "S",
128
+ idField: "ownerId",
129
+ projectionType: "ALL"
130
+ };
131
+ let item = owner => {
132
+ let o = {};
133
+ o["ownerId"] = owner;
134
+ return o;
135
+ };
136
+ globalThis.test("sqlite lookup returns only rows whose indexed field matches", async () => {
137
+ let TestBus = LocalBus$ReventlessLocal.Make({});
138
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
139
+ let DbProvider = {
140
+ db: db
141
+ };
142
+ let Storage = QueryDbStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
143
+ let s = Storage.make("orders", [byOwner], undefined, undefined, undefined, undefined, opts);
144
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
145
+ await ops.save("k1", item("o1"), "Any", undefined);
146
+ await ops.save("k2", item("o2"), "Any", undefined);
147
+ await ops.save("k3", item("o1"), "Any", undefined);
148
+ let lookup = Stdlib_Option.getOrThrow(TestBus.getQueryDbIndexLookup("orders"), undefined);
149
+ globalThis.expect(lookup("ownerId", "o1").length).toBe(2);
150
+ globalThis.expect(lookup("ownerId", "o2").length).toBe(1);
151
+ globalThis.expect(lookup("ownerId", "absent").length).toBe(0);
152
+ });
153
+ globalThis.test("sqlite lookup excludes expired rows", async () => {
154
+ let TestBus = LocalBus$ReventlessLocal.Make({});
155
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
156
+ let DbProvider = {
157
+ db: db
158
+ };
159
+ let Storage = QueryDbStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
160
+ let s = Storage.make("orders", [byOwner], undefined, undefined, undefined, undefined, opts);
161
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
162
+ let aMinuteAgo = (Date.now() / 1000.0 | 0) - 60 | 0;
163
+ await ops.save("k1", item("o1"), "Any", undefined);
164
+ await ops.save("k2", item("o1"), "Any", aMinuteAgo);
165
+ let lookup = Stdlib_Option.getOrThrow(TestBus.getQueryDbIndexLookup("orders"), undefined);
166
+ globalThis.expect(lookup("ownerId", "o1").length).toBe(1);
167
+ });
168
+ globalThis.test("in-memory lookup matches the sqlite result (backend parity)", async () => {
169
+ let TestBus = LocalBus$ReventlessLocal.Make({});
170
+ let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
171
+ let s = Storage.makeMemory("orders", [byOwner], undefined, undefined, undefined, undefined, opts);
172
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
173
+ await ops.save("k1", item("o1"), "Any", undefined);
174
+ await ops.save("k2", item("o2"), "Any", undefined);
175
+ await ops.save("k3", item("o1"), "Any", undefined);
176
+ let lookup = Stdlib_Option.getOrThrow(TestBus.getQueryDbIndexLookup("orders"), undefined);
177
+ globalThis.expect(lookup("ownerId", "o1").length).toBe(2);
178
+ globalThis.expect(lookup("ownerId", "o2").length).toBe(1);
179
+ globalThis.expect(lookup("ownerId", "absent").length).toBe(0);
180
+ });
181
+ });
182
+
183
+ globalThis.describe("QueryDbStorage_InMemory — lazy scan snapshot (B4 dirty-flag)", () => {
184
+ globalThis.test("interleaved save/scan reflects each write without stale reads", async () => {
185
+ let TestBus = LocalBus$ReventlessLocal.Make({});
186
+ let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
187
+ let s = Storage.make("lazy", [], undefined, undefined, undefined, undefined, opts);
188
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
189
+ let scanFn = Stdlib_Option.getOrThrow(TestBus.getQueryDbScan("lazy"), undefined);
190
+ globalThis.expect(scanFn().length).toBe(0);
191
+ await ops.save("a", "va", "Any", undefined);
192
+ globalThis.expect(scanFn().length).toBe(1);
193
+ await ops.save("b", "vb", "Any", undefined);
194
+ globalThis.expect(scanFn().length).toBe(2);
195
+ await ops.delete("a", undefined);
196
+ globalThis.expect(scanFn().length).toBe(1);
197
+ });
198
+ });
199
+
123
200
  globalThis.describe("QueryDbStorage_Sqlite — TTL", () => {
124
201
  globalThis.test("save with TTL in the future returns the item", async () => {
125
202
  let TestBus = LocalBus$ReventlessLocal.Make({});