@reventlessdev/reventless-local 3.0.0-alpha.117 → 3.0.0-alpha.118
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/CHANGELOG.md +7 -0
- package/package.json +7 -6
- package/rescript.json +2 -1
- package/src/Platform.res +44 -6
- package/src/Platform.res.mjs +53 -16
- package/src/adapter/Backend.res +23 -0
- package/src/adapter/Backend.res.mjs +23 -0
- package/src/adapter/BackendState.res +12 -7
- package/src/adapter/BackendState.res.mjs +18 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
- package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
- package/src/adapter/LocalBus.res +1 -1
- package/src/adapter/PgProjectionCatchup.res +146 -0
- package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
- package/src/adapter/Task/LocalTaskBucket.res +1 -1
- package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
- package/src/components/Aggregate_Builder.res +1 -1
- package/src/components/Aggregate_Builder.res.mjs +3 -3
- package/src/components/AutomationSlice_Builder.res +1 -1
- package/src/components/AutomationSlice_Builder.res.mjs +3 -3
- package/src/components/Counter_Builder.res +1 -1
- package/src/components/Counter_Builder.res.mjs +2 -2
- package/src/components/DcbEventLog_Builder.res +1 -1
- package/src/components/DcbEventLog_Builder.res.mjs +2 -2
- package/src/components/InboundTranslationSlice_Builder.res +1 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/Plugin_Builder.res +1 -1
- package/src/components/Plugin_Builder.res.mjs +2 -2
- package/src/components/ReadModel_Builder.res +1 -1
- package/src/components/ReadModel_Builder.res.mjs +2 -2
- package/src/components/StateViewSlice_Builder.res +1 -1
- package/src/components/StateViewSlice_Builder.res.mjs +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
- package/tests/adapter/BackendParityTest.res +43 -8
- package/tests/adapter/BackendParityTest.res.mjs +39 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
- package/tests/adapter/PgProjectionCatchupTest.res +149 -0
- package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
- package/tests/adapter/QueryDbListResolverTest.res +1 -1
- package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
- package/tests/adapter/QueryDbStorageTest.res +10 -10
- package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
- package/tests/adapter/QueryEngineTest.res +8 -8
- package/tests/adapter/QueryEngineTest.res.mjs +9 -9
- package/tests/components/querydb/QueryDbFixtures.res +1 -1
- package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
- package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
- 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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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("
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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 =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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);
|