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

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +8 -7
  3. package/rescript.json +2 -1
  4. package/src/Platform.res +44 -6
  5. package/src/Platform.res.mjs +53 -16
  6. package/src/adapter/Backend.res +23 -0
  7. package/src/adapter/Backend.res.mjs +23 -0
  8. package/src/adapter/BackendState.res +12 -7
  9. package/src/adapter/BackendState.res.mjs +18 -6
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
  12. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
  13. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
  14. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
  15. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
  16. package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
  17. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
  18. package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
  19. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
  20. package/src/adapter/LocalBus.res +1 -1
  21. package/src/adapter/PgProjectionCatchup.res +146 -0
  22. package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
  23. package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
  24. package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
  27. package/src/adapter/Task/LocalTaskBucket.res +1 -1
  28. package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
  29. package/src/components/Aggregate_Builder.res +1 -1
  30. package/src/components/Aggregate_Builder.res.mjs +3 -3
  31. package/src/components/AutomationSlice_Builder.res +1 -1
  32. package/src/components/AutomationSlice_Builder.res.mjs +3 -3
  33. package/src/components/Counter_Builder.res +1 -1
  34. package/src/components/Counter_Builder.res.mjs +2 -2
  35. package/src/components/DcbEventLog_Builder.res +1 -1
  36. package/src/components/DcbEventLog_Builder.res.mjs +2 -2
  37. package/src/components/InboundTranslationSlice_Builder.res +1 -1
  38. package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
  39. package/src/components/OutboundTranslationSlice_Builder.res +1 -1
  40. package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
  41. package/src/components/Plugin_Builder.res +1 -1
  42. package/src/components/Plugin_Builder.res.mjs +2 -2
  43. package/src/components/ReadModel_Builder.res +1 -1
  44. package/src/components/ReadModel_Builder.res.mjs +2 -2
  45. package/src/components/StateViewSlice_Builder.res +1 -1
  46. package/src/components/StateViewSlice_Builder.res.mjs +2 -2
  47. package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
  48. package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
  49. package/tests/adapter/BackendParityTest.res +43 -8
  50. package/tests/adapter/BackendParityTest.res.mjs +39 -9
  51. package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
  52. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
  53. package/tests/adapter/PgProjectionCatchupTest.res +149 -0
  54. package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
  55. package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
  56. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
  57. package/tests/adapter/QueryDbListResolverTest.res +1 -1
  58. package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
  59. package/tests/adapter/QueryDbStorageTest.res +10 -10
  60. package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
  61. package/tests/adapter/QueryEngineTest.res +8 -8
  62. package/tests/adapter/QueryEngineTest.res.mjs +9 -9
  63. package/tests/components/querydb/QueryDbFixtures.res +1 -1
  64. package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
  65. package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
  66. package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +2 -2
@@ -20,11 +20,19 @@ let stored = (eventType, tags, data): ReventlessCore.DcbEventLog_Adapter.rawStor
20
20
  meta: ReventlessCore.Message.generateMeta(~service="test"),
21
21
  }
22
22
 
23
+ // Render a typed appendError for assertion diagnostics: an "expected Ok" guard
24
+ // surfaces the real error message on failure instead of a meaningless literal.
25
+ let appendErrMsg = e =>
26
+ switch e {
27
+ | ReventlessInfra.DcbEventLog.Conflict => "conflict"
28
+ | StorageFailure(m) => m
29
+ }
30
+
23
31
  describe("DcbEventLogStorage_Sqlite", () => {
24
32
  testPromise("append + read round-trips events with tags", async () => {
25
33
  await runUnderSqlite(async () => {
26
34
  module TestBus = LocalBus.Make()
27
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
35
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
28
36
  let s = Storage.make(
29
37
  ~name="dcb-rt",
30
38
  ~indexes=[],
@@ -42,7 +50,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
42
50
  let result = await ops.append([e1])
43
51
  switch result {
44
52
  | Ok(_) => expect(true)->toBe(true)
45
- | Error(m) => expect(m)->toBe("Ok")
53
+ | Error(e) => expect(appendErrMsg(e))->toBe("Ok")
46
54
  }
47
55
 
48
56
  let read = await ops.read(~query=[])
@@ -58,7 +66,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
58
66
  testPromise("read filters by tag", async () => {
59
67
  await runUnderSqlite(async () => {
60
68
  module TestBus = LocalBus.Make()
61
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
69
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
62
70
  let s = Storage.make(
63
71
  ~name="dcb-tagfilter",
64
72
  ~indexes=[],
@@ -86,7 +94,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
86
94
  testPromise("batched tag hydration preserves per-event tags and order", async () => {
87
95
  await runUnderSqlite(async () => {
88
96
  module TestBus = LocalBus.Make()
89
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
97
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
90
98
  let s = Storage.make(
91
99
  ~name="dcb-multitag",
92
100
  ~indexes=[],
@@ -128,7 +136,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
128
136
  testPromise("append with a non-conflicting condition succeeds", async () => {
129
137
  await runUnderSqlite(async () => {
130
138
  module TestBus = LocalBus.Make()
131
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
139
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
132
140
  let s = Storage.make(
133
141
  ~name="dcb-noconflict",
134
142
  ~indexes=[],
@@ -149,7 +157,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
149
157
  )
150
158
  switch result {
151
159
  | Ok(_) => expect(true)->toBe(true)
152
- | Error(m) => expect(m)->toBe("expected Ok")
160
+ | Error(e) => expect(appendErrMsg(e))->toBe("Ok")
153
161
  }
154
162
  let read = await ops.read(~query=[])
155
163
  expect(read.events->Array.length)->toBe(2)
@@ -159,7 +167,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
159
167
  testPromise("append with conflicting condition returns Error", async () => {
160
168
  await runUnderSqlite(async () => {
161
169
  module TestBus = LocalBus.Make()
162
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
170
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
163
171
  let s = Storage.make(
164
172
  ~name="dcb-conflict",
165
173
  ~indexes=[],
@@ -193,7 +201,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
193
201
  let db = SqliteDriver.openDb(~path)
194
202
  BackendState.setSqlite(~db, ~path)
195
203
  module TestBus = LocalBus.Make()
196
- module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
204
+ module Storage = LocalDcbEventLogStorage.Make(TestBus)
197
205
  let s = Storage.make(
198
206
  ~name="persist",
199
207
  ~indexes=[],
@@ -214,7 +222,7 @@ describe("DcbEventLogStorage_Sqlite", () => {
214
222
  let db2 = SqliteDriver.openDb(~path)
215
223
  BackendState.setSqlite(~db=db2, ~path)
216
224
  module TestBus2 = LocalBus.Make()
217
- module Storage2 = DcbEventLogStorage_InMemory.Make(TestBus2)
225
+ module Storage2 = LocalDcbEventLogStorage.Make(TestBus2)
218
226
  let s2 = Storage2.make(
219
227
  ~name="persist",
220
228
  ~indexes=[],
@@ -5,7 +5,7 @@ import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
5
5
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
6
6
  import * as BackendState$ReventlessLocal from "../../src/adapter/BackendState.res.mjs";
7
7
  import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
8
- import * as DcbEventLogStorage_InMemory$ReventlessLocal from "../../src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs";
8
+ import * as LocalDcbEventLogStorage$ReventlessLocal from "../../src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs";
9
9
 
10
10
  TestRunner$ReventlessLocal.setup();
11
11
 
@@ -28,10 +28,18 @@ function stored(eventType, tags, data) {
28
28
  };
29
29
  }
30
30
 
31
+ function appendErrMsg(e) {
32
+ if (typeof e !== "object") {
33
+ return "conflict";
34
+ } else {
35
+ return e._0;
36
+ }
37
+ }
38
+
31
39
  globalThis.describe("DcbEventLogStorage_Sqlite", () => {
32
40
  globalThis.test("append + read round-trips events with tags", async () => await runUnderSqlite(async () => {
33
41
  let TestBus = LocalBus$ReventlessLocal.Make({});
34
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
42
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
35
43
  let s = Storage.make("dcb-rt", [], {
36
44
  TAG: "Simple",
37
45
  _0: {
@@ -50,7 +58,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
50
58
  if (result.TAG === "Ok") {
51
59
  globalThis.expect(true).toBe(true);
52
60
  } else {
53
- globalThis.expect(result._0).toBe("Ok");
61
+ globalThis.expect(appendErrMsg(result._0)).toBe("Ok");
54
62
  }
55
63
  let read = await ops.read([], undefined);
56
64
  globalThis.expect(read.events.length).toBe(1);
@@ -62,7 +70,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
62
70
  }));
63
71
  globalThis.test("read filters by tag", async () => await runUnderSqlite(async () => {
64
72
  let TestBus = LocalBus$ReventlessLocal.Make({});
65
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
73
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
66
74
  let s = Storage.make("dcb-tagfilter", [], {
67
75
  TAG: "Simple",
68
76
  _0: {
@@ -90,7 +98,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
90
98
  }));
91
99
  globalThis.test("batched tag hydration preserves per-event tags and order", async () => await runUnderSqlite(async () => {
92
100
  let TestBus = LocalBus$ReventlessLocal.Make({});
93
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
101
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
94
102
  let s = Storage.make("dcb-multitag", [], {
95
103
  TAG: "Simple",
96
104
  _0: {
@@ -141,7 +149,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
141
149
  }));
142
150
  globalThis.test("append with a non-conflicting condition succeeds", async () => await runUnderSqlite(async () => {
143
151
  let TestBus = LocalBus$ReventlessLocal.Make({});
144
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
152
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
145
153
  let s = Storage.make("dcb-noconflict", [], {
146
154
  TAG: "Simple",
147
155
  _0: {
@@ -169,14 +177,14 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
169
177
  if (result.TAG === "Ok") {
170
178
  globalThis.expect(true).toBe(true);
171
179
  } else {
172
- globalThis.expect(result._0).toBe("expected Ok");
180
+ globalThis.expect(appendErrMsg(result._0)).toBe("Ok");
173
181
  }
174
182
  let read = await ops.read([], undefined);
175
183
  globalThis.expect(read.events.length).toBe(2);
176
184
  }));
177
185
  globalThis.test("append with conflicting condition returns Error", async () => await runUnderSqlite(async () => {
178
186
  let TestBus = LocalBus$ReventlessLocal.Make({});
179
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
187
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
180
188
  let s = Storage.make("dcb-conflict", [], {
181
189
  TAG: "Simple",
182
190
  _0: {
@@ -210,7 +218,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
210
218
  let db = SqliteDriver$ReventlessLocal.openDb(path);
211
219
  BackendState$ReventlessLocal.setSqlite(db, path);
212
220
  let TestBus = LocalBus$ReventlessLocal.Make({});
213
- let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
221
+ let Storage = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus);
214
222
  let s = Storage.make("persist", [], {
215
223
  TAG: "Simple",
216
224
  _0: {
@@ -228,7 +236,7 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
228
236
  let db2 = SqliteDriver$ReventlessLocal.openDb(path);
229
237
  BackendState$ReventlessLocal.setSqlite(db2, path);
230
238
  let TestBus2 = LocalBus$ReventlessLocal.Make({});
231
- let Storage2 = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus2);
239
+ let Storage2 = LocalDcbEventLogStorage$ReventlessLocal.Make(TestBus2);
232
240
  let s2 = Storage2.make("persist", [], {
233
241
  TAG: "Simple",
234
242
  _0: {
@@ -247,5 +255,6 @@ export {
247
255
  opts,
248
256
  runUnderSqlite,
249
257
  stored,
258
+ appendErrMsg,
250
259
  }
251
260
  /* Not a pure module */
@@ -0,0 +1,149 @@
1
+ // Postgres startup projection catch-up (#1): under Backend.Postgres the read
2
+ // models are in-memory, so on restart they must be rebuilt by replaying the
3
+ // durable pg event log through the projection handlers.
4
+ //
5
+ // Skipped unless PG_URL is set. Verifies:
6
+ // - every persisted classic + DCB event is redelivered as a well-formed
7
+ // {id, meta, event} envelope, in order;
8
+ // - the pre-session head bound excludes events appended after capture (this
9
+ // session's live-delivered events are never redelivered).
10
+
11
+ open JestGlobals
12
+
13
+ @val external processEnv: dict<string> = "process.env"
14
+ let opts: Pulumi.CustomResourceOptions.t = {}
15
+
16
+ // Flat stored aggregate event — the exact shape EventLog_Operations.encodeEvent'
17
+ // persists (envelope fields + meta flattened to top level).
18
+ let flatEvent = (~id, ~seq, ~eventType, ~data=[], ~msgId, ~service="Products") =>
19
+ JSON.Encode.object(
20
+ Dict.fromArray([
21
+ ("id", JSON.Encode.string(id)),
22
+ ("position", JSON.Encode.string(seq->Int.toString->String.padStart(9, "0"))),
23
+ ("event", JSON.Encode.string(eventType)),
24
+ ("data", JSON.Encode.object(Dict.fromArray(data))),
25
+ ("recordedAt", JSON.Encode.string("2026-07-03T00:00:00.000Z")),
26
+ ("service", JSON.Encode.string(service)),
27
+ ("time", JSON.Encode.string("2026-07-03T00:00:00.000Z")),
28
+ ("msgId", JSON.Encode.string(msgId)),
29
+ ("correlationId", JSON.Encode.string(msgId)),
30
+ ]),
31
+ )
32
+
33
+ let dcbEvent = (~eventType, ~tags, ~data=[], ~msgId): ReventlessCore.DcbEventLog_Adapter.rawStoredEvent => {
34
+ eventType,
35
+ data: JSON.Encode.object(Dict.fromArray(data)),
36
+ tags,
37
+ meta: {
38
+ service: "OrderingDcbEventLog",
39
+ time: "2026-07-03T00:00:00.000Z",
40
+ msgId,
41
+ correlationId: msgId,
42
+ },
43
+ }
44
+
45
+ let idOf = (json: JSON.t) =>
46
+ json
47
+ ->JSON.Decode.object
48
+ ->Option.flatMap(d => d->Dict.get("id"))
49
+ ->Option.flatMap(JSON.Decode.string)
50
+ ->Option.getOr("<none>")
51
+
52
+ let recorder = () => {
53
+ let received: array<JSON.t> = []
54
+ let handler = (json: JSON.t, _ctx: unit) => {
55
+ received->Array.push(json)
56
+ Promise.resolve()
57
+ }
58
+ (received, handler)
59
+ }
60
+
61
+ switch processEnv->Dict.get("PG_URL") {
62
+ | None => testSync("Postgres projection catch-up (skipped — set PG_URL)", () => expect(true)->toBe(true))
63
+ | Some(url) =>
64
+ let pool = ReventlessPostgres.PgDriver.makePool({connectionString: url})
65
+
66
+ beforeAllAsync(async () => await ReventlessPostgres.PgSchema.ensureSchema(pool))
67
+ afterAll(() => ignore(pool->ReventlessPostgres.PgDriver.endPool))
68
+
69
+ // No beforeEachAsync binding exists; reset at the top of each test for isolation.
70
+ let reset = async () => await ReventlessPostgres.PgSchema.truncateAll(pool)
71
+
72
+ let classicOps = () => {
73
+ let (_, ops, _) = ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
74
+ ~pool,
75
+ ~name="catchup-agg",
76
+ ~opts,
77
+ )
78
+ ops
79
+ }
80
+ let dcbOps = () => {
81
+ let (_, ops, _) = ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
82
+ ~pool,
83
+ ~name="catchup-dcb",
84
+ ~indexes=[],
85
+ ~partitionTag=Reventless.DcbTag.Simple({key: "k"}),
86
+ ~opts,
87
+ )
88
+ ops
89
+ }
90
+
91
+ describe("PgProjectionCatchup", () => {
92
+ testPromise("replays every persisted classic + DCB event as an envelope", async () => {
93
+ await reset()
94
+ let agg = classicOps()
95
+ let _ = await agg.append(0, "p1", [flatEvent(~id="p1", ~seq=0, ~eventType="Added", ~msgId="a0")])
96
+ let _ = await agg.append(1, "p1", [flatEvent(~id="p1", ~seq=1, ~eventType="Renamed", ~msgId="a1")])
97
+
98
+ let dcb = dcbOps()
99
+ let _ = await dcb.append([dcbEvent(~eventType="Placed", ~tags=[{key: "orderId", value: "o1"}], ~msgId="d0")])
100
+ let _ = await dcb.append([dcbEvent(~eventType="Paid", ~tags=[{key: "orderId", value: "o2"}], ~msgId="d1")])
101
+
102
+ let bounds = await PgProjectionCatchup.captureBounds(pool)
103
+ let (received, handler) = recorder()
104
+ await PgProjectionCatchup.runCatchup(~pool, ~bounds, ~handlers=[("rm", handler)])
105
+
106
+ // 2 classic + 2 dcb = 4 well-formed envelopes (None envelopes aren't delivered).
107
+ expect(received->Array.length)->toBe(4)
108
+ // Classic events keep their aggregate id, in order.
109
+ expect(idOf(received->Array.getUnsafe(0)))->toBe("p1")
110
+ expect(idOf(received->Array.getUnsafe(1)))->toBe("p1")
111
+ // DCB events derive id from the first tag value.
112
+ expect(idOf(received->Array.getUnsafe(2)))->toBe("o1")
113
+ expect(idOf(received->Array.getUnsafe(3)))->toBe("o2")
114
+ })
115
+
116
+ testPromise("the pre-session bound excludes events appended after capture", async () => {
117
+ await reset()
118
+ let agg = classicOps()
119
+ let _ = await agg.append(0, "p1", [flatEvent(~id="p1", ~seq=0, ~eventType="Added", ~msgId="a0")])
120
+ let _ = await agg.append(1, "p1", [flatEvent(~id="p1", ~seq=1, ~eventType="Renamed", ~msgId="a1")])
121
+
122
+ // Snapshot the head BEFORE the "this-session" append below.
123
+ let bounds = await PgProjectionCatchup.captureBounds(pool)
124
+
125
+ // This event is > the captured bound — it stands in for a live-delivered
126
+ // this-session append and must NOT be redelivered by catch-up.
127
+ let _ = await agg.append(2, "p1", [flatEvent(~id="p1", ~seq=2, ~eventType="Archived", ~msgId="a2")])
128
+
129
+ let (received, handler) = recorder()
130
+ await PgProjectionCatchup.runCatchup(~pool, ~bounds, ~handlers=[("rm", handler)])
131
+
132
+ expect(received->Array.length)->toBe(2)
133
+ })
134
+
135
+ testPromise("delivers each event to every handler", async () => {
136
+ await reset()
137
+ let agg = classicOps()
138
+ let _ = await agg.append(0, "p1", [flatEvent(~id="p1", ~seq=0, ~eventType="Added", ~msgId="a0")])
139
+
140
+ let bounds = await PgProjectionCatchup.captureBounds(pool)
141
+ let (r1, h1) = recorder()
142
+ let (r2, h2) = recorder()
143
+ await PgProjectionCatchup.runCatchup(~pool, ~bounds, ~handlers=[("rm1", h1), ("rm2", h2)])
144
+
145
+ expect(r1->Array.length)->toBe(1)
146
+ expect(r2->Array.length)->toBe(1)
147
+ })
148
+ })
149
+ }
@@ -0,0 +1,181 @@
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 PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
6
+ import * as PgSchema$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgSchema.res.mjs";
7
+ import * as PgProjectionCatchup$ReventlessLocal from "../../src/adapter/PgProjectionCatchup.res.mjs";
8
+ import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
9
+ import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
10
+
11
+ let opts = {};
12
+
13
+ function flatEvent(id, seq, eventType, dataOpt, msgId, serviceOpt) {
14
+ let data = dataOpt !== undefined ? dataOpt : [];
15
+ let service = serviceOpt !== undefined ? serviceOpt : "Products";
16
+ return Object.fromEntries([
17
+ [
18
+ "id",
19
+ id
20
+ ],
21
+ [
22
+ "position",
23
+ seq.toString().padStart(9, "0")
24
+ ],
25
+ [
26
+ "event",
27
+ eventType
28
+ ],
29
+ [
30
+ "data",
31
+ Object.fromEntries(data)
32
+ ],
33
+ [
34
+ "recordedAt",
35
+ "2026-07-03T00:00:00.000Z"
36
+ ],
37
+ [
38
+ "service",
39
+ service
40
+ ],
41
+ [
42
+ "time",
43
+ "2026-07-03T00:00:00.000Z"
44
+ ],
45
+ [
46
+ "msgId",
47
+ msgId
48
+ ],
49
+ [
50
+ "correlationId",
51
+ msgId
52
+ ]
53
+ ]);
54
+ }
55
+
56
+ function dcbEvent(eventType, tags, dataOpt, msgId) {
57
+ let data = dataOpt !== undefined ? dataOpt : [];
58
+ return {
59
+ eventType: eventType,
60
+ data: Object.fromEntries(data),
61
+ tags: tags,
62
+ meta: {
63
+ service: "OrderingDcbEventLog",
64
+ time: "2026-07-03T00:00:00.000Z",
65
+ msgId: msgId,
66
+ correlationId: msgId
67
+ }
68
+ };
69
+ }
70
+
71
+ function idOf(json) {
72
+ return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["id"]), Stdlib_JSON.Decode.string), "<none>");
73
+ }
74
+
75
+ function recorder() {
76
+ let received = [];
77
+ let handler = (json, _ctx) => {
78
+ received.push(json);
79
+ return Promise.resolve();
80
+ };
81
+ return [
82
+ received,
83
+ handler
84
+ ];
85
+ }
86
+
87
+ let url = process.env["PG_URL"];
88
+
89
+ if (url !== undefined) {
90
+ let pool = PgDriver$ReventlessPostgres.makePool({
91
+ connectionString: url
92
+ });
93
+ globalThis.beforeAll(async () => await PgSchema$ReventlessPostgres.ensureSchema(pool));
94
+ globalThis.afterAll(() => {
95
+ PgDriver$ReventlessPostgres.endPool(pool);
96
+ });
97
+ let reset = async () => await PgSchema$ReventlessPostgres.truncateAll(pool);
98
+ let classicOps = () => EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "catchup-agg", opts, undefined)[1];
99
+ let dcbOps = () => DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, "catchup-dcb", [], {
100
+ TAG: "Simple",
101
+ _0: {
102
+ key: "k"
103
+ }
104
+ }, undefined, opts, undefined, undefined)[1];
105
+ globalThis.describe("PgProjectionCatchup", () => {
106
+ globalThis.test("replays every persisted classic + DCB event as an envelope", async () => {
107
+ await reset();
108
+ let agg = classicOps();
109
+ await agg.append(0, "p1", [flatEvent("p1", 0, "Added", undefined, "a0", undefined)]);
110
+ await agg.append(1, "p1", [flatEvent("p1", 1, "Renamed", undefined, "a1", undefined)]);
111
+ let dcb = dcbOps();
112
+ await dcb.append([dcbEvent("Placed", [{
113
+ key: "orderId",
114
+ value: "o1"
115
+ }], undefined, "d0")], undefined);
116
+ await dcb.append([dcbEvent("Paid", [{
117
+ key: "orderId",
118
+ value: "o2"
119
+ }], undefined, "d1")], undefined);
120
+ let bounds = await PgProjectionCatchup$ReventlessLocal.captureBounds(pool);
121
+ let match = recorder();
122
+ let received = match[0];
123
+ await PgProjectionCatchup$ReventlessLocal.runCatchup(pool, bounds, [[
124
+ "rm",
125
+ match[1]
126
+ ]]);
127
+ globalThis.expect(received.length).toBe(4);
128
+ globalThis.expect(idOf(received[0])).toBe("p1");
129
+ globalThis.expect(idOf(received[1])).toBe("p1");
130
+ globalThis.expect(idOf(received[2])).toBe("o1");
131
+ globalThis.expect(idOf(received[3])).toBe("o2");
132
+ });
133
+ globalThis.test("the pre-session bound excludes events appended after capture", async () => {
134
+ await reset();
135
+ let agg = classicOps();
136
+ await agg.append(0, "p1", [flatEvent("p1", 0, "Added", undefined, "a0", undefined)]);
137
+ await agg.append(1, "p1", [flatEvent("p1", 1, "Renamed", undefined, "a1", undefined)]);
138
+ let bounds = await PgProjectionCatchup$ReventlessLocal.captureBounds(pool);
139
+ await agg.append(2, "p1", [flatEvent("p1", 2, "Archived", undefined, "a2", undefined)]);
140
+ let match = recorder();
141
+ await PgProjectionCatchup$ReventlessLocal.runCatchup(pool, bounds, [[
142
+ "rm",
143
+ match[1]
144
+ ]]);
145
+ globalThis.expect(match[0].length).toBe(2);
146
+ });
147
+ globalThis.test("delivers each event to every handler", async () => {
148
+ await reset();
149
+ let agg = classicOps();
150
+ await agg.append(0, "p1", [flatEvent("p1", 0, "Added", undefined, "a0", undefined)]);
151
+ let bounds = await PgProjectionCatchup$ReventlessLocal.captureBounds(pool);
152
+ let match = recorder();
153
+ let match$1 = recorder();
154
+ await PgProjectionCatchup$ReventlessLocal.runCatchup(pool, bounds, [
155
+ [
156
+ "rm1",
157
+ match[1]
158
+ ],
159
+ [
160
+ "rm2",
161
+ match$1[1]
162
+ ]
163
+ ]);
164
+ globalThis.expect(match[0].length).toBe(1);
165
+ globalThis.expect(match$1[0].length).toBe(1);
166
+ });
167
+ });
168
+ } else {
169
+ globalThis.test("Postgres projection catch-up (skipped — set PG_URL)", () => {
170
+ globalThis.expect(true).toBe(true);
171
+ });
172
+ }
173
+
174
+ export {
175
+ opts,
176
+ flatEvent,
177
+ dcbEvent,
178
+ idOf,
179
+ recorder,
180
+ }
181
+ /* url Not a pure module */
@@ -196,7 +196,7 @@ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
196
196
  testPromise("in-memory lookup matches the sqlite result (backend parity)", async () => {
197
197
  module TestBus = LocalBus.Make()
198
198
  module Storage = QueryDbStorage_InMemory.Make(TestBus)
199
- let s = Storage.makeMemory(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~opts)
199
+ let s = Storage.make(~name="orders", ~indexes=[byOwner], ~api=(), ~apiRole=(), ~opts)
200
200
  let ops = await s.operations->TestRunner.resolve
201
201
  let _ = await ops.save("k1", item("o1"), ReventlessCore.QueryDb.Any, None)
202
202
  let _ = await ops.save("k2", item("o2"), ReventlessCore.QueryDb.Any, None)
@@ -211,7 +211,7 @@ describe("QueryDb — indexed equality lookup (B4 push-down)", () => {
211
211
  describe("QueryDbStorage_InMemory — lazy scan snapshot (B4 dirty-flag)", () => {
212
212
  testPromise("interleaved save/scan reflects each write without stale reads", async () => {
213
213
  module TestBus = LocalBus.Make()
214
- module Storage = QueryDbStorage_InMemory.Make(TestBus)
214
+ module Storage = LocalQueryDbStorage.Make(TestBus)
215
215
  let s = Storage.make(~name="lazy", ~indexes=[], ~api=(), ~apiRole=(), ~opts)
216
216
  let ops = await s.operations->TestRunner.resolve
217
217
  let scanFn = TestBus.getQueryDbScan("lazy")->Option.getOrThrow
@@ -6,6 +6,7 @@ import * as Effect from "effect/Effect";
6
6
  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
+ import * as LocalQueryDbStorage$ReventlessLocal from "../../src/adapter/QueryDb/LocalQueryDbStorage.res.mjs";
9
10
  import * as QueryDbStorage_Sqlite$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs";
10
11
  import * as QueryDbStorage_InMemory$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
11
12
 
@@ -168,7 +169,7 @@ globalThis.describe("QueryDb — indexed equality lookup (B4 push-down)", () =>
168
169
  globalThis.test("in-memory lookup matches the sqlite result (backend parity)", async () => {
169
170
  let TestBus = LocalBus$ReventlessLocal.Make({});
170
171
  let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
171
- let s = Storage.makeMemory("orders", [byOwner], undefined, undefined, undefined, undefined, opts);
172
+ let s = Storage.make("orders", [byOwner], undefined, undefined, undefined, undefined, opts);
172
173
  let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
173
174
  await ops.save("k1", item("o1"), "Any", undefined);
174
175
  await ops.save("k2", item("o2"), "Any", undefined);
@@ -183,7 +184,7 @@ globalThis.describe("QueryDb — indexed equality lookup (B4 push-down)", () =>
183
184
  globalThis.describe("QueryDbStorage_InMemory — lazy scan snapshot (B4 dirty-flag)", () => {
184
185
  globalThis.test("interleaved save/scan reflects each write without stale reads", async () => {
185
186
  let TestBus = LocalBus$ReventlessLocal.Make({});
186
- let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
187
+ let Storage = LocalQueryDbStorage$ReventlessLocal.Make(TestBus);
187
188
  let s = Storage.make("lazy", [], undefined, undefined, undefined, undefined, opts);
188
189
  let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
189
190
  let scanFn = Stdlib_Option.getOrThrow(TestBus.getQueryDbScan("lazy"), undefined);
@@ -99,7 +99,7 @@ let pageInfoString = (response: JSON.t, key: string): option<string> =>
99
99
 
100
100
  let buildFixture = async (~name: string) => {
101
101
  module Bus = LocalBus.Make()
102
- module Storage = QueryDbStorage_InMemory.Make(Bus)
102
+ module Storage = LocalQueryDbStorage.Make(Bus)
103
103
  module Resolvers = QueryDbResolvers_GraphQL.Make(Bus)
104
104
 
105
105
  module Spec = {
@@ -18,8 +18,8 @@ import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.re
18
18
  import * as Plugin_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/component/Plugin_Helpers.res.mjs";
19
19
  import * as QueryDb_Adapter$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Adapter.res.mjs";
20
20
  import * as QueryDb_Builder$ReventlessCore from "@reventlessdev/reventless-core/src/components/QueryDb/QueryDb_Builder.res.mjs";
21
+ import * as LocalQueryDbStorage$ReventlessLocal from "../../src/adapter/QueryDb/LocalQueryDbStorage.res.mjs";
21
22
  import * as DomainGraphQL_Server$ReventlessLocal from "../../src/adapter/DomainGraphQL_Server.res.mjs";
22
- import * as QueryDbStorage_InMemory$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs";
23
23
  import * as QueryDbResolvers_GraphQL$ReventlessLocal from "../../src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs";
24
24
 
25
25
  TestRunner$ReventlessLocal.setup();
@@ -109,7 +109,7 @@ function pageInfoString(response, key) {
109
109
 
110
110
  async function buildFixture(name) {
111
111
  let Bus = LocalBus$ReventlessLocal.Make({});
112
- let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
112
+ let Storage = LocalQueryDbStorage$ReventlessLocal.Make(Bus);
113
113
  let Resolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
114
114
  let moduleUrl = import.meta.url;
115
115
  let config = ReadModel$Reventless.config(undefined, undefined, undefined);