@reventlessdev/reventless-local 3.0.0-alpha.114 → 3.0.0-alpha.116

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 (65) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/package.json +8 -8
  3. package/src/Platform.res +116 -67
  4. package/src/Platform.res.mjs +114 -190
  5. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +102 -17
  6. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +105 -22
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +123 -40
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +96 -24
  9. package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
  10. package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
  11. package/src/adapter/EventLog/EventLogStorage_InMemory.res +24 -3
  12. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +21 -4
  13. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +148 -27
  14. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +144 -34
  15. package/src/adapter/LocalBus.res +85 -3
  16. package/src/adapter/LocalBus.res.mjs +220 -66
  17. package/src/adapter/ProjectionCheckpoint.res +350 -0
  18. package/src/adapter/ProjectionCheckpoint.res.mjs +327 -0
  19. package/src/adapter/ProjectionPending.res +60 -0
  20. package/src/adapter/ProjectionPending.res.mjs +64 -0
  21. package/src/adapter/QueryDb/QueryDbListQuery.res +261 -0
  22. package/src/adapter/QueryDb/QueryDbListQuery.res.mjs +212 -0
  23. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res +54 -286
  24. package/src/adapter/QueryDb/QueryDbResolvers_GraphQL.res.mjs +28 -190
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +128 -10
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +123 -23
  27. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +187 -12
  28. package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +120 -7
  29. package/src/adapter/SqliteDriver.res +17 -2
  30. package/src/adapter/SqliteDriver.res.mjs +10 -2
  31. package/src/components/DcbEventLog_Builder.res +7 -1
  32. package/src/components/DcbEventLog_Builder.res.mjs +3 -3
  33. package/src/components/ReadModel_Builder.res +3 -1
  34. package/src/components/ReadModel_Builder.res.mjs +13 -4
  35. package/src/components/StateViewSlice_Builder.res +4 -1
  36. package/src/components/StateViewSlice_Builder.res.mjs +9 -3
  37. package/src/test/Mocks/MockEventLogStorage.res +20 -3
  38. package/src/test/Mocks/MockEventLogStorage.res.mjs +25 -3
  39. package/tests/PluginEventDecodeTest.res +51 -0
  40. package/tests/PluginEventDecodeTest.res.mjs +63 -0
  41. package/tests/adapter/BackendParityTest.res +64 -0
  42. package/tests/adapter/BackendParityTest.res.mjs +45 -0
  43. package/tests/adapter/DcbEventLogStorageSqliteTest.res +73 -0
  44. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +86 -0
  45. package/tests/adapter/DcbEventLogStorageTest.res +46 -0
  46. package/tests/adapter/DcbEventLogStorageTest.res.mjs +63 -0
  47. package/tests/adapter/EventLogSnapshotParityTest.res +81 -0
  48. package/tests/adapter/EventLogSnapshotParityTest.res.mjs +141 -0
  49. package/tests/adapter/EventLogStorageSqliteTest.res +132 -2
  50. package/tests/adapter/EventLogStorageSqliteTest.res.mjs +195 -1
  51. package/tests/adapter/LocalBusPubSubTest.res +31 -0
  52. package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -0
  53. package/tests/adapter/ProjectionCheckpointTest.res +422 -0
  54. package/tests/adapter/ProjectionCheckpointTest.res.mjs +401 -0
  55. package/tests/adapter/QueryDbGsiTtlTest.res +78 -0
  56. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +77 -0
  57. package/tests/adapter/QueryDbListPushdownParityTest.res +197 -0
  58. package/tests/adapter/QueryDbListPushdownParityTest.res.mjs +292 -0
  59. package/tests/adapter/QueryDbListResolverTest.res +76 -0
  60. package/tests/adapter/QueryDbListResolverTest.res.mjs +84 -0
  61. package/tests/components/aggregate/AggregateFixtures.res +4 -0
  62. package/tests/components/aggregate/AggregateFixtures.res.mjs +1 -0
  63. package/tests/components/eventlog/EventLogAppendStreamTest.res.mjs +4 -4
  64. package/tests/components/eventlog/EventLogStreamTest.res.mjs +8 -8
  65. package/tests/plugin/PluginBehavior_GWT.res.mjs +1 -0
@@ -1,6 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
5
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
6
  import * as Effect from "effect/Effect";
5
7
  import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
6
8
  import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
@@ -65,6 +67,49 @@ globalThis.describe("Backend parity (Memory vs Sqlite)", () => {
65
67
  await runUnderMemory(scenario);
66
68
  return await runUnderSqlite(scenario);
67
69
  });
70
+ globalThis.test("QueryDb: count returns a running total and loadStream reflects it under both", async () => {
71
+ let scenario = async () => {
72
+ let TestBus = LocalBus$ReventlessLocal.Make({});
73
+ let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
74
+ let s = Storage.make("parity-count", [], undefined, undefined, undefined, undefined, opts);
75
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
76
+ let r1 = await ops.count("prod-1", "orderCount", 3);
77
+ globalThis.expect(r1).toEqual({
78
+ TAG: "Ok",
79
+ _0: 3
80
+ });
81
+ let r2 = await ops.count("prod-1", "orderCount", 2);
82
+ globalThis.expect(r2).toEqual({
83
+ TAG: "Ok",
84
+ _0: 5
85
+ });
86
+ let items = await Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream("prod-1")), param => Effect.succeed([])));
87
+ globalThis.expect(items.length).toBe(1);
88
+ let field = Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(items[0]), o => o["orderCount"]), Stdlib_JSON.Decode.float), 0, prim => prim | 0);
89
+ globalThis.expect(field).toBe(5);
90
+ };
91
+ await runUnderMemory(scenario);
92
+ return await runUnderSqlite(scenario);
93
+ });
94
+ globalThis.test("QueryDb: an expired-TTL item is filtered from loadStream under both", async () => {
95
+ let scenario = async () => {
96
+ let TestBus = LocalBus$ReventlessLocal.Make({});
97
+ let Storage = QueryDbStorage_InMemory$ReventlessLocal.Make(TestBus);
98
+ let s = Storage.make("parity-ttl", [], undefined, undefined, undefined, undefined, opts);
99
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
100
+ let item = k => Object.fromEntries([[
101
+ "id",
102
+ k
103
+ ]]);
104
+ let readCount = async k => (await Effect.runPromise(Effect.catchAll(Stream.runCollect(ops.loadStream(k)), param => Effect.succeed([])))).length;
105
+ await ops.save("live", item("live"), "Any", undefined);
106
+ await ops.save("dead", item("dead"), "Any", 1);
107
+ globalThis.expect(await readCount("live")).toBe(1);
108
+ globalThis.expect(await readCount("dead")).toBe(0);
109
+ };
110
+ await runUnderMemory(scenario);
111
+ return await runUnderSqlite(scenario);
112
+ });
68
113
  globalThis.test("EventLog: conflict detection works under both", async () => {
69
114
  let scenario = async () => {
70
115
  let TestBus = LocalBus$ReventlessLocal.Make({});
@@ -83,6 +83,79 @@ describe("DcbEventLogStorage_Sqlite", () => {
83
83
  })
84
84
  })
85
85
 
86
+ testPromise("batched tag hydration preserves per-event tags and order", async () => {
87
+ await runUnderSqlite(async () => {
88
+ module TestBus = LocalBus.Make()
89
+ module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
90
+ let s = Storage.make(
91
+ ~name="dcb-multitag",
92
+ ~indexes=[],
93
+ ~partitionTag=Reventless.DcbTag.Simple({key: "k"}),
94
+ ~opts,
95
+ )
96
+ let ops = await s.operations->TestRunner.resolve
97
+
98
+ // e1 carries three tags (declaration order matters); e2 carries one; e3 none.
99
+ let e1 = stored(
100
+ "Multi",
101
+ [
102
+ {key: "a", value: "1"},
103
+ {key: "b", value: "2"},
104
+ {key: "c", value: "3"},
105
+ ],
106
+ JSON.Encode.string("first"),
107
+ )
108
+ let e2 = stored("Single", [{key: "only", value: "z"}], JSON.Encode.string("second"))
109
+ let e3 = stored("None", [], JSON.Encode.string("third"))
110
+ let _ = await ops.append([e1])
111
+ let _ = await ops.append([e2])
112
+ let _ = await ops.append([e3])
113
+
114
+ let read = await ops.read(~query=[])
115
+ expect(read.events->Array.length)->toBe(3)
116
+ let first = read.events->Array.getUnsafe(0)
117
+ // Order and grouping survive the single batched IN query.
118
+ expect(first.tags->Array.map(t => t.key))->toEqual(["a", "b", "c"])
119
+ expect(first.tags->Array.map(t => t.value))->toEqual(["1", "2", "3"])
120
+ let second = read.events->Array.getUnsafe(1)
121
+ expect(second.tags->Array.length)->toBe(1)
122
+ expect((second.tags->Array.getUnsafe(0)).key)->toBe("only")
123
+ let third = read.events->Array.getUnsafe(2)
124
+ expect(third.tags->Array.length)->toBe(0)
125
+ })
126
+ })
127
+
128
+ testPromise("append with a non-conflicting condition succeeds", async () => {
129
+ await runUnderSqlite(async () => {
130
+ module TestBus = LocalBus.Make()
131
+ module Storage = DcbEventLogStorage_InMemory.Make(TestBus)
132
+ let s = Storage.make(
133
+ ~name="dcb-noconflict",
134
+ ~indexes=[],
135
+ ~partitionTag=Reventless.DcbTag.Simple({key: "k"}),
136
+ ~opts,
137
+ )
138
+ let ops = await s.operations->TestRunner.resolve
139
+
140
+ let _ = await ops.append([stored("Created", [{key: "id", value: "x"}], JSON.Encode.string("a"))])
141
+ // Condition queries a tag value that does not exist → anyMatch is false →
142
+ // the append is allowed.
143
+ let cond: Reventless.DcbTag.appendCondition = {
144
+ query: [{tags: [{key: "id", value: "other"}]}],
145
+ }
146
+ let result = await ops.append(
147
+ [stored("Created", [{key: "id", value: "other"}], JSON.Encode.string("b"))],
148
+ ~condition=cond,
149
+ )
150
+ switch result {
151
+ | Ok(_) => expect(true)->toBe(true)
152
+ | Error(m) => expect(m)->toBe("expected Ok")
153
+ }
154
+ let read = await ops.read(~query=[])
155
+ expect(read.events->Array.length)->toBe(2)
156
+ })
157
+ })
158
+
86
159
  testPromise("append with conflicting condition returns Error", async () => {
87
160
  await runUnderSqlite(async () => {
88
161
  module TestBus = LocalBus.Make()
@@ -88,6 +88,92 @@ globalThis.describe("DcbEventLogStorage_Sqlite", () => {
88
88
  }], undefined);
89
89
  globalThis.expect(read.events.length).toBe(2);
90
90
  }));
91
+ globalThis.test("batched tag hydration preserves per-event tags and order", async () => await runUnderSqlite(async () => {
92
+ let TestBus = LocalBus$ReventlessLocal.Make({});
93
+ let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
94
+ let s = Storage.make("dcb-multitag", [], {
95
+ TAG: "Simple",
96
+ _0: {
97
+ key: "k"
98
+ }
99
+ }, undefined, opts);
100
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
101
+ let e1 = stored("Multi", [
102
+ {
103
+ key: "a",
104
+ value: "1"
105
+ },
106
+ {
107
+ key: "b",
108
+ value: "2"
109
+ },
110
+ {
111
+ key: "c",
112
+ value: "3"
113
+ }
114
+ ], "first");
115
+ let e2 = stored("Single", [{
116
+ key: "only",
117
+ value: "z"
118
+ }], "second");
119
+ let e3 = stored("None", [], "third");
120
+ await ops.append([e1], undefined);
121
+ await ops.append([e2], undefined);
122
+ await ops.append([e3], undefined);
123
+ let read = await ops.read([], undefined);
124
+ globalThis.expect(read.events.length).toBe(3);
125
+ let first = read.events[0];
126
+ globalThis.expect(first.tags.map(t => t.key)).toEqual([
127
+ "a",
128
+ "b",
129
+ "c"
130
+ ]);
131
+ globalThis.expect(first.tags.map(t => t.value)).toEqual([
132
+ "1",
133
+ "2",
134
+ "3"
135
+ ]);
136
+ let second = read.events[1];
137
+ globalThis.expect(second.tags.length).toBe(1);
138
+ globalThis.expect(second.tags[0].key).toBe("only");
139
+ let third = read.events[2];
140
+ globalThis.expect(third.tags.length).toBe(0);
141
+ }));
142
+ globalThis.test("append with a non-conflicting condition succeeds", async () => await runUnderSqlite(async () => {
143
+ let TestBus = LocalBus$ReventlessLocal.Make({});
144
+ let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
145
+ let s = Storage.make("dcb-noconflict", [], {
146
+ TAG: "Simple",
147
+ _0: {
148
+ key: "k"
149
+ }
150
+ }, undefined, opts);
151
+ let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
152
+ await ops.append([stored("Created", [{
153
+ key: "id",
154
+ value: "x"
155
+ }], "a")], undefined);
156
+ let cond_query = [{
157
+ tags: [{
158
+ key: "id",
159
+ value: "other"
160
+ }]
161
+ }];
162
+ let cond = {
163
+ query: cond_query
164
+ };
165
+ let result = await ops.append([stored("Created", [{
166
+ key: "id",
167
+ value: "other"
168
+ }], "b")], cond);
169
+ if (result.TAG === "Ok") {
170
+ globalThis.expect(true).toBe(true);
171
+ } else {
172
+ globalThis.expect(result._0).toBe("expected Ok");
173
+ }
174
+ let read = await ops.read([], undefined);
175
+ globalThis.expect(read.events.length).toBe(2);
176
+ }));
91
177
  globalThis.test("append with conflicting condition returns Error", async () => await runUnderSqlite(async () => {
92
178
  let TestBus = LocalBus$ReventlessLocal.Make({});
93
179
  let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(TestBus);
@@ -96,6 +96,52 @@ describe("DcbEventLogStorage_InMemory", () => {
96
96
  expect(firstEvent.tags->Array.length)->toBe(1)
97
97
  })
98
98
 
99
+ testPromise("multi-tag clause matches only events carrying ALL tags", async () => {
100
+ let storage = makeStorage()
101
+ let ops = await storage.operations->TestRunner.resolve
102
+ let _ = await ops.append([
103
+ makeEvent(
104
+ ~eventType="Evt",
105
+ ~data=JSON.Null,
106
+ ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}, {key: "region", value: "eu"}],
107
+ ),
108
+ makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "tenant", value: "acme"}]),
109
+ makeEvent(~eventType="Evt", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "region", value: "eu"}]),
110
+ ])
111
+ // Intersection of the two tags' posting lists → only the first event.
112
+ let result = await ops.read(
113
+ ~query=[
114
+ {
115
+ Reventless.DcbTag.tags: [
116
+ {Reventless.DcbTag.key: "tenant", value: "acme"},
117
+ {key: "region", value: "eu"},
118
+ ],
119
+ },
120
+ ],
121
+ )
122
+ expect(result.events->Array.length)->toBe(1)
123
+ })
124
+
125
+ testPromise("OR of clauses unions matches across clauses in position order", async () => {
126
+ let storage = makeStorage()
127
+ let ops = await storage.operations->TestRunner.resolve
128
+ let _ = await ops.append([
129
+ makeEvent(~eventType="A", ~data=JSON.Null),
130
+ makeEvent(~eventType="B", ~data=JSON.Null, ~tags=[{Reventless.DcbTag.key: "x", value: "1"}]),
131
+ makeEvent(~eventType="C", ~data=JSON.Null),
132
+ ])
133
+ // Clause 1 (type A) ∪ clause 2 (tag x=1) → events at positions 1 and 2.
134
+ let result = await ops.read(
135
+ ~query=[
136
+ {Reventless.DcbTag.eventTypes: ["A"]},
137
+ {Reventless.DcbTag.tags: [{Reventless.DcbTag.key: "x", value: "1"}]},
138
+ ],
139
+ )
140
+ expect(result.events->Array.length)->toBe(2)
141
+ expect((result.events->Array.getUnsafe(0)).eventType)->toBe("A")
142
+ expect((result.events->Array.getUnsafe(1)).eventType)->toBe("B")
143
+ })
144
+
99
145
  testPromise("after parameter skips events at or before that position", async () => {
100
146
  let storage = makeStorage()
101
147
  let ops = await storage.operations->TestRunner.resolve
@@ -102,6 +102,69 @@ globalThis.describe("DcbEventLogStorage_InMemory", () => {
102
102
  let firstEvent = result.events[0];
103
103
  globalThis.expect(firstEvent.tags.length).toBe(1);
104
104
  });
105
+ globalThis.test("multi-tag clause matches only events carrying ALL tags", async () => {
106
+ let storage = makeStorage();
107
+ let ops = await TestRunner$ReventlessLocal.resolve(storage.operations);
108
+ await ops.append([
109
+ makeEvent("Evt", null, [
110
+ {
111
+ key: "tenant",
112
+ value: "acme"
113
+ },
114
+ {
115
+ key: "region",
116
+ value: "eu"
117
+ }
118
+ ]),
119
+ makeEvent("Evt", null, [{
120
+ key: "tenant",
121
+ value: "acme"
122
+ }]),
123
+ makeEvent("Evt", null, [{
124
+ key: "region",
125
+ value: "eu"
126
+ }])
127
+ ], undefined);
128
+ let result = await ops.read([{
129
+ tags: [
130
+ {
131
+ key: "tenant",
132
+ value: "acme"
133
+ },
134
+ {
135
+ key: "region",
136
+ value: "eu"
137
+ }
138
+ ]
139
+ }], undefined);
140
+ globalThis.expect(result.events.length).toBe(1);
141
+ });
142
+ globalThis.test("OR of clauses unions matches across clauses in position order", async () => {
143
+ let storage = makeStorage();
144
+ let ops = await TestRunner$ReventlessLocal.resolve(storage.operations);
145
+ await ops.append([
146
+ makeEvent("A", null, undefined),
147
+ makeEvent("B", null, [{
148
+ key: "x",
149
+ value: "1"
150
+ }]),
151
+ makeEvent("C", null, undefined)
152
+ ], undefined);
153
+ let result = await ops.read([
154
+ {
155
+ eventTypes: ["A"]
156
+ },
157
+ {
158
+ tags: [{
159
+ key: "x",
160
+ value: "1"
161
+ }]
162
+ }
163
+ ], undefined);
164
+ globalThis.expect(result.events.length).toBe(2);
165
+ globalThis.expect(result.events[0].eventType).toBe("A");
166
+ globalThis.expect(result.events[1].eventType).toBe("B");
167
+ });
105
168
  globalThis.test("after parameter skips events at or before that position", async () => {
106
169
  let storage = makeStorage();
107
170
  let ops = await TestRunner$ReventlessLocal.resolve(storage.operations);
@@ -0,0 +1,81 @@
1
+ // Backend-parity tests for the EventLog snapshot ops
2
+ // (docs/plans/aggregate-snapshotting.md): the in-memory and SQLite backends
3
+ // must behave identically for latestSnapshot / writeSnapshot (keep-one) and
4
+ // the replayStream ~fromSeq delta read, so GWT/local tests of
5
+ // snapshot-enabled aggregates give the same results under either backend.
6
+
7
+ open JestGlobals
8
+
9
+ let _ = TestRunner.setup()
10
+ let opts: Pulumi.CustomResourceOptions.t = {}
11
+
12
+ let makeMemoryOps = () => {
13
+ let s = EventLogStorage_InMemory.make(~name="parity", ~opts)
14
+ s.operations->TestRunner.resolve
15
+ }
16
+
17
+ let makeSqliteOps = () => {
18
+ module TestBus = LocalBus.Make()
19
+ module DbProvider = {
20
+ let db = SqliteDriver.openDb(~path=":memory:")
21
+ }
22
+ module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
23
+ let s = Storage.make(~name="parity", ~opts)
24
+ s.operations->TestRunner.resolve
25
+ }
26
+
27
+ let backends = [("memory", makeMemoryOps), ("sqlite", makeSqliteOps)]
28
+
29
+ describe("EventLog snapshot backend parity", () => {
30
+ backends->Array.forEach(((label, makeOps)) => {
31
+ testPromise(`${label}: latestSnapshot is None before any write`, async () => {
32
+ let ops = await makeOps()
33
+ expect(await ops.latestSnapshot("id-1"))->toEqual(Ok(None))
34
+ })
35
+
36
+ testPromise(`${label}: snapshot round-trips and keep-one overwrites`, async () => {
37
+ let ops = await makeOps()
38
+ let state = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(1))]))
39
+ expect(
40
+ await ops.writeSnapshot(
41
+ "id-1",
42
+ {ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"},
43
+ ),
44
+ )->toEqual(Ok())
45
+ expect(await ops.latestSnapshot("id-1"))->toEqual(
46
+ Ok(Some({ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"})),
47
+ )
48
+ let state2 = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(9))]))
49
+ let _ = await ops.writeSnapshot(
50
+ "id-1",
51
+ {ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"},
52
+ )
53
+ expect(await ops.latestSnapshot("id-1"))->toEqual(
54
+ Ok(Some({ReventlessCore.EventLog.seqNr: 9, state: state2, schemaHash: "h2"})),
55
+ )
56
+ })
57
+
58
+ testPromise(`${label}: snapshots are per-aggregate-id`, async () => {
59
+ let ops = await makeOps()
60
+ let state = JSON.Encode.object(Dict.fromArray([("n", JSON.Encode.int(1))]))
61
+ let _ = await ops.writeSnapshot(
62
+ "id-a",
63
+ {ReventlessCore.EventLog.seqNr: 1, state, schemaHash: "h"},
64
+ )
65
+ expect(await ops.latestSnapshot("id-b"))->toEqual(Ok(None))
66
+ })
67
+
68
+ testPromise(`${label}: replayStream ~fromSeq equals the tail of a full replay`, async () => {
69
+ let ops = await makeOps()
70
+ let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
71
+ let _ = await ops.append(0, "id-1", [ev(0), ev(1), ev(2), ev(3), ev(4)])
72
+ let delta = await ops.replayStream("id-1", ~fromSeq=3)->Stream.runCollect->Effect.runPromise
73
+ expect(delta)->toEqual([ev(3), ev(4)])
74
+ // fromSeq at the head yields nothing; omitted fromSeq yields everything.
75
+ let empty = await ops.replayStream("id-1", ~fromSeq=5)->Stream.runCollect->Effect.runPromise
76
+ expect(empty)->toEqual([])
77
+ let full = await ops.replayStream("id-1")->Stream.runCollect->Effect.runPromise
78
+ expect(full->Array.length)->toBe(5)
79
+ })
80
+ })
81
+ })
@@ -0,0 +1,141 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Effect from "effect/Effect";
5
+ import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
6
+ import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
7
+ import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
8
+ import * as EventLogStorage_Sqlite$ReventlessLocal from "../../src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs";
9
+ import * as EventLogStorage_InMemory$ReventlessLocal from "../../src/adapter/EventLog/EventLogStorage_InMemory.res.mjs";
10
+
11
+ TestRunner$ReventlessLocal.setup();
12
+
13
+ let opts = {};
14
+
15
+ function makeMemoryOps() {
16
+ let s = EventLogStorage_InMemory$ReventlessLocal.make("parity", opts);
17
+ return TestRunner$ReventlessLocal.resolve(s.operations);
18
+ }
19
+
20
+ function makeSqliteOps() {
21
+ let TestBus = LocalBus$ReventlessLocal.Make({});
22
+ let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
23
+ let DbProvider = {
24
+ db: db
25
+ };
26
+ let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
27
+ let s = Storage.make("parity", opts);
28
+ return TestRunner$ReventlessLocal.resolve(s.operations);
29
+ }
30
+
31
+ let backends = [
32
+ [
33
+ "memory",
34
+ makeMemoryOps
35
+ ],
36
+ [
37
+ "sqlite",
38
+ makeSqliteOps
39
+ ]
40
+ ];
41
+
42
+ globalThis.describe("EventLog snapshot backend parity", () => {
43
+ backends.forEach(param => {
44
+ let makeOps = param[1];
45
+ let label = param[0];
46
+ globalThis.test(label + `: latestSnapshot is None before any write`, async () => {
47
+ let ops = await makeOps();
48
+ globalThis.expect(await ops.latestSnapshot("id-1")).toEqual({
49
+ TAG: "Ok",
50
+ _0: undefined
51
+ });
52
+ });
53
+ globalThis.test(label + `: snapshot round-trips and keep-one overwrites`, async () => {
54
+ let ops = await makeOps();
55
+ let state = Object.fromEntries([[
56
+ "n",
57
+ 1
58
+ ]]);
59
+ globalThis.expect(await ops.writeSnapshot("id-1", {
60
+ seqNr: 2,
61
+ state: state,
62
+ schemaHash: "h1"
63
+ })).toEqual({
64
+ TAG: "Ok",
65
+ _0: undefined
66
+ });
67
+ globalThis.expect(await ops.latestSnapshot("id-1")).toEqual({
68
+ TAG: "Ok",
69
+ _0: {
70
+ seqNr: 2,
71
+ state: state,
72
+ schemaHash: "h1"
73
+ }
74
+ });
75
+ let state2 = Object.fromEntries([[
76
+ "n",
77
+ 9
78
+ ]]);
79
+ await ops.writeSnapshot("id-1", {
80
+ seqNr: 9,
81
+ state: state2,
82
+ schemaHash: "h2"
83
+ });
84
+ globalThis.expect(await ops.latestSnapshot("id-1")).toEqual({
85
+ TAG: "Ok",
86
+ _0: {
87
+ seqNr: 9,
88
+ state: state2,
89
+ schemaHash: "h2"
90
+ }
91
+ });
92
+ });
93
+ globalThis.test(label + `: snapshots are per-aggregate-id`, async () => {
94
+ let ops = await makeOps();
95
+ let state = Object.fromEntries([[
96
+ "n",
97
+ 1
98
+ ]]);
99
+ await ops.writeSnapshot("id-a", {
100
+ seqNr: 1,
101
+ state: state,
102
+ schemaHash: "h"
103
+ });
104
+ globalThis.expect(await ops.latestSnapshot("id-b")).toEqual({
105
+ TAG: "Ok",
106
+ _0: undefined
107
+ });
108
+ });
109
+ globalThis.test(label + `: replayStream ~fromSeq equals the tail of a full replay`, async () => {
110
+ let ops = await makeOps();
111
+ let ev = i => Object.fromEntries([[
112
+ "i",
113
+ i
114
+ ]]);
115
+ await ops.append(0, "id-1", [
116
+ ev(0),
117
+ ev(1),
118
+ ev(2),
119
+ ev(3),
120
+ ev(4)
121
+ ]);
122
+ let delta = await Effect.runPromise(Stream.runCollect(ops.replayStream("id-1", 3)));
123
+ globalThis.expect(delta).toEqual([
124
+ ev(3),
125
+ ev(4)
126
+ ]);
127
+ let empty = await Effect.runPromise(Stream.runCollect(ops.replayStream("id-1", 5)));
128
+ globalThis.expect(empty).toEqual([]);
129
+ let full = await Effect.runPromise(Stream.runCollect(ops.replayStream("id-1", undefined)));
130
+ globalThis.expect(full.length).toBe(5);
131
+ });
132
+ });
133
+ });
134
+
135
+ export {
136
+ opts,
137
+ makeMemoryOps,
138
+ makeSqliteOps,
139
+ backends,
140
+ }
141
+ /* Not a pure module */