@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.
- package/CHANGELOG.md +27 -0
- package/package.json +8 -8
- package/src/Platform.res +91 -17
- package/src/Platform.res.mjs +89 -30
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +113 -39
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +87 -23
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +23 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +20 -3
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +132 -24
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +119 -37
- package/src/adapter/LocalBus.res +62 -0
- package/src/adapter/LocalBus.res.mjs +120 -0
- package/src/adapter/ProjectionCheckpoint.res +350 -0
- package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
- package/src/adapter/ProjectionPending.res +60 -0
- package/src/adapter/ProjectionPending.res.mjs +64 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
- package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
- package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +32 -6
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +30 -14
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +163 -11
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +101 -3
- package/src/adapter/SqliteDriver.res +11 -1
- package/src/adapter/SqliteDriver.res.mjs +5 -1
- package/src/components/ReadModel_Builder.res +3 -1
- package/src/components/ReadModel_Builder.res.mjs +13 -4
- package/src/components/StateViewSlice_Builder.res +4 -1
- package/src/components/StateViewSlice_Builder.res.mjs +9 -3
- package/src/test/Mocks/MockEventLogStorage.res +19 -2
- package/src/test/Mocks/MockEventLogStorage.res.mjs +21 -2
- package/tests/PluginEventDecodeTest.res +51 -0
- package/tests/PluginEventDecodeTest.res.mjs +63 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
- package/tests/adapter/DcbEventLogStorageTest.res +46 -0
- package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
- package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
- package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
- package/tests/adapter/EventLogStorageSqliteTest.res +128 -0
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +189 -0
- package/tests/adapter/ProjectionCheckpointTest.res +422 -0
- package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
- package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
- package/tests/adapter/QueryDbListResolverTest.res +76 -0
- package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
- package/tests/components/aggregate/AggregateFixtures.res +4 -0
- package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
- package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
- package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
- 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({});
|