@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.
- package/CHANGELOG.md +30 -0
- package/package.json +8 -8
- package/src/Platform.res +116 -67
- package/src/Platform.res.mjs +114 -190
- 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 +123 -40
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +96 -24
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res +33 -0
- package/src/adapter/EventCollector/LocalEventCollectorChannel.res.mjs +44 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +24 -3
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +21 -4
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +148 -27
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +144 -34
- package/src/adapter/LocalBus.res +85 -3
- package/src/adapter/LocalBus.res.mjs +220 -66
- 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 +128 -10
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +123 -23
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res +187 -12
- package/src/adapter/QueryDb/QueryDbStorage_Sqlite.res.mjs +120 -7
- package/src/adapter/SqliteDriver.res +17 -2
- package/src/adapter/SqliteDriver.res.mjs +10 -2
- package/src/components/DcbEventLog_Builder.res +7 -1
- package/src/components/DcbEventLog_Builder.res.mjs +3 -3
- 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 +20 -3
- package/src/test/Mocks/MockEventLogStorage.res.mjs +25 -3
- package/tests/PluginEventDecodeTest.res +51 -0
- package/tests/PluginEventDecodeTest.res.mjs +63 -0
- package/tests/adapter/BackendParityTest.res +64 -0
- package/tests/adapter/BackendParityTest.res.mjs +45 -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 +132 -2
- package/tests/adapter/EventLogStorageSqliteTest.res.mjs +195 -1
- package/tests/adapter/LocalBusPubSubTest.res +31 -0
- package/tests/adapter/LocalBusPubSubTest.res.mjs +28 -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
|
@@ -51,14 +51,62 @@ describe("EventLogStorage_Sqlite", () => {
|
|
|
51
51
|
let e = JSON.Encode.object(Dict.fromArray([("t", JSON.Encode.string("X"))]))
|
|
52
52
|
let _ = await ops.append(0, "id-c", [e])
|
|
53
53
|
|
|
54
|
-
// Caller re-uses seqNr=0 — conflict
|
|
54
|
+
// Caller re-uses seqNr=0 — a genuine conflict must be the typed Conflict
|
|
55
|
+
// sentinel (not a StorageFailure), so the core retry loop treats it as OCC.
|
|
55
56
|
let result = await ops.append(0, "id-c", [e])
|
|
56
57
|
switch result {
|
|
57
|
-
| Error(
|
|
58
|
+
| Error(ReventlessCore.EventLog.Conflict) => expect(true)->toBe(true)
|
|
59
|
+
| Error(StorageFailure(msg)) => expect("expected Conflict, got StorageFailure")->toBe(msg)
|
|
58
60
|
| Ok() => expect("expected conflict")->toBe("actual Ok")
|
|
59
61
|
}
|
|
60
62
|
})
|
|
61
63
|
|
|
64
|
+
testPromise("multi-event append advances the expected seq via MAX(seq_nr)+1", async () => {
|
|
65
|
+
module TestBus = LocalBus.Make()
|
|
66
|
+
module DbProvider = {
|
|
67
|
+
let db = makeFreshDb()
|
|
68
|
+
}
|
|
69
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
70
|
+
let s = Storage.make(~name="agg", ~opts)
|
|
71
|
+
let ops = await s.operations->TestRunner.resolve
|
|
72
|
+
|
|
73
|
+
let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
|
|
74
|
+
// Batch of three at seq 0 → occupies seq 0,1,2.
|
|
75
|
+
let r1 = await ops.append(0, "id-m", [ev(0), ev(1), ev(2)])
|
|
76
|
+
expect(r1)->toEqual(Ok())
|
|
77
|
+
// A stale seq (2) is a conflict; the correct next seq is 3.
|
|
78
|
+
let stale = await ops.append(2, "id-m", [ev(9)])
|
|
79
|
+
switch stale {
|
|
80
|
+
| Error(ReventlessCore.EventLog.Conflict) => expect(true)->toBe(true)
|
|
81
|
+
| _ => expect("expected Conflict")->toBe("other")
|
|
82
|
+
}
|
|
83
|
+
let r2 = await ops.append(3, "id-m", [ev(3)])
|
|
84
|
+
expect(r2)->toEqual(Ok())
|
|
85
|
+
let replayed = await ops.replay("id-m")
|
|
86
|
+
expect(replayed->Array.length)->toBe(4)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
testPromise("appendStream persists the whole batch in order from the starting seq", async () => {
|
|
90
|
+
module TestBus = LocalBus.Make()
|
|
91
|
+
module DbProvider = {
|
|
92
|
+
let db = makeFreshDb()
|
|
93
|
+
}
|
|
94
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
95
|
+
let s = Storage.make(~name="agg", ~opts)
|
|
96
|
+
let ops = await s.operations->TestRunner.resolve
|
|
97
|
+
|
|
98
|
+
let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
|
|
99
|
+
// Seed one event, then stream three more starting at seq 1.
|
|
100
|
+
let _ = await ops.append(0, "id-s", [ev(0)])
|
|
101
|
+
let _ =
|
|
102
|
+
await ops.appendStream(1, "id-s", Stream.fromIterable([ev(1), ev(2), ev(3)]))
|
|
103
|
+
->Effect.runPromise
|
|
104
|
+
let replayed = await ops.replay("id-s")
|
|
105
|
+
expect(replayed->Array.length)->toBe(4)
|
|
106
|
+
expect(replayed->Array.getUnsafe(0))->toEqual(ev(0))
|
|
107
|
+
expect(replayed->Array.getUnsafe(3))->toEqual(ev(3))
|
|
108
|
+
})
|
|
109
|
+
|
|
62
110
|
testPromise("replay returns empty array for unknown id", async () => {
|
|
63
111
|
module TestBus = LocalBus.Make()
|
|
64
112
|
module DbProvider = {
|
|
@@ -73,6 +121,88 @@ describe("EventLogStorage_Sqlite", () => {
|
|
|
73
121
|
expect(replayed->Array.length)->toBe(0)
|
|
74
122
|
})
|
|
75
123
|
|
|
124
|
+
testPromise("replayStream ~fromSeq reads only the delta", async () => {
|
|
125
|
+
module TestBus = LocalBus.Make()
|
|
126
|
+
module DbProvider = {
|
|
127
|
+
let db = makeFreshDb()
|
|
128
|
+
}
|
|
129
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
130
|
+
let s = Storage.make(~name="agg", ~opts)
|
|
131
|
+
let ops = await s.operations->TestRunner.resolve
|
|
132
|
+
|
|
133
|
+
let ev = i => JSON.Encode.object(Dict.fromArray([("i", JSON.Encode.int(i))]))
|
|
134
|
+
let _ = await ops.append(0, "id-d", [ev(0), ev(1), ev(2), ev(3)])
|
|
135
|
+
let delta = await ops.replayStream("id-d", ~fromSeq=2)->Stream.runCollect->Effect.runPromise
|
|
136
|
+
expect(delta)->toEqual([ev(2), ev(3)])
|
|
137
|
+
// Omitted fromSeq still replays from 0 (unchanged default).
|
|
138
|
+
let full = await ops.replayStream("id-d")->Stream.runCollect->Effect.runPromise
|
|
139
|
+
expect(full->Array.length)->toBe(4)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
testPromise("snapshot round-trip with keep-one overwrite", async () => {
|
|
143
|
+
module TestBus = LocalBus.Make()
|
|
144
|
+
module DbProvider = {
|
|
145
|
+
let db = makeFreshDb()
|
|
146
|
+
}
|
|
147
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
148
|
+
let s = Storage.make(~name="agg", ~opts)
|
|
149
|
+
let ops = await s.operations->TestRunner.resolve
|
|
150
|
+
|
|
151
|
+
let none = await ops.latestSnapshot("id-snap")
|
|
152
|
+
expect(none)->toEqual(Ok(None))
|
|
153
|
+
|
|
154
|
+
let state = JSON.Encode.object(Dict.fromArray([("names", JSON.Encode.int(2))]))
|
|
155
|
+
let w1 = await ops.writeSnapshot(
|
|
156
|
+
"id-snap",
|
|
157
|
+
{ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"},
|
|
158
|
+
)
|
|
159
|
+
expect(w1)->toEqual(Ok())
|
|
160
|
+
let r1 = await ops.latestSnapshot("id-snap")
|
|
161
|
+
expect(r1)->toEqual(Ok(Some({ReventlessCore.EventLog.seqNr: 2, state, schemaHash: "h1"})))
|
|
162
|
+
|
|
163
|
+
// Keep-one: a later write replaces the row, it does not accumulate.
|
|
164
|
+
let state5 = JSON.Encode.object(Dict.fromArray([("names", JSON.Encode.int(5))]))
|
|
165
|
+
let _ = await ops.writeSnapshot(
|
|
166
|
+
"id-snap",
|
|
167
|
+
{ReventlessCore.EventLog.seqNr: 5, state: state5, schemaHash: "h2"},
|
|
168
|
+
)
|
|
169
|
+
let r2 = await ops.latestSnapshot("id-snap")
|
|
170
|
+
expect(r2)->toEqual(
|
|
171
|
+
Ok(Some({ReventlessCore.EventLog.seqNr: 5, state: state5, schemaHash: "h2"})),
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
testPromise("snapshots persist across a reopen of the database file", async () => {
|
|
176
|
+
let path = `/tmp/reventless-test-snapshot-${Float.toString(Date.now())}.db`
|
|
177
|
+
let state = JSON.Encode.object(Dict.fromArray([("v", JSON.Encode.int(7))]))
|
|
178
|
+
|
|
179
|
+
{
|
|
180
|
+
module TestBus = LocalBus.Make()
|
|
181
|
+
module DbProvider = {
|
|
182
|
+
let db = SqliteDriver.openDb(~path)
|
|
183
|
+
}
|
|
184
|
+
module Storage = EventLogStorage_Sqlite.Make(TestBus, DbProvider)
|
|
185
|
+
let s = Storage.make(~name="persist", ~opts)
|
|
186
|
+
let ops = await s.operations->TestRunner.resolve
|
|
187
|
+
let _ = await ops.writeSnapshot(
|
|
188
|
+
"id-sp",
|
|
189
|
+
{ReventlessCore.EventLog.seqNr: 3, state, schemaHash: "h"},
|
|
190
|
+
)
|
|
191
|
+
DbProvider.db->SqliteDriver.close
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module TestBus2 = LocalBus.Make()
|
|
195
|
+
module DbProvider2 = {
|
|
196
|
+
let db = SqliteDriver.openDb(~path)
|
|
197
|
+
}
|
|
198
|
+
module Storage2 = EventLogStorage_Sqlite.Make(TestBus2, DbProvider2)
|
|
199
|
+
let s2 = Storage2.make(~name="persist", ~opts)
|
|
200
|
+
let ops2 = await s2.operations->TestRunner.resolve
|
|
201
|
+
let r = await ops2.latestSnapshot("id-sp")
|
|
202
|
+
expect(r)->toEqual(Ok(Some({ReventlessCore.EventLog.seqNr: 3, state, schemaHash: "h"})))
|
|
203
|
+
DbProvider2.db->SqliteDriver.close
|
|
204
|
+
})
|
|
205
|
+
|
|
76
206
|
testPromise("events persist across a reopen of the database file", async () => {
|
|
77
207
|
let path = `/tmp/reventless-test-eventlog-${Float.toString(Date.now())}.db`
|
|
78
208
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
|
+
import * as Effect from "effect/Effect";
|
|
5
|
+
import * as Stream$1 from "effect/Stream";
|
|
3
6
|
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
4
7
|
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
5
8
|
import * as SqliteDriver$ReventlessLocal from "../../src/adapter/SqliteDriver.res.mjs";
|
|
@@ -65,7 +68,77 @@ globalThis.describe("EventLogStorage_Sqlite", () => {
|
|
|
65
68
|
globalThis.expect("expected conflict").toBe("actual Ok");
|
|
66
69
|
return;
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
let msg = result._0;
|
|
72
|
+
if (typeof msg !== "object") {
|
|
73
|
+
globalThis.expect(true).toBe(true);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
globalThis.expect("expected Conflict, got StorageFailure").toBe(msg._0);
|
|
77
|
+
});
|
|
78
|
+
globalThis.test("multi-event append advances the expected seq via MAX(seq_nr)+1", async () => {
|
|
79
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
80
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
81
|
+
let DbProvider = {
|
|
82
|
+
db: db
|
|
83
|
+
};
|
|
84
|
+
let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
|
|
85
|
+
let s = Storage.make("agg", opts);
|
|
86
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
87
|
+
let ev = i => Object.fromEntries([[
|
|
88
|
+
"i",
|
|
89
|
+
i
|
|
90
|
+
]]);
|
|
91
|
+
let r1 = await ops.append(0, "id-m", [
|
|
92
|
+
ev(0),
|
|
93
|
+
ev(1),
|
|
94
|
+
ev(2)
|
|
95
|
+
]);
|
|
96
|
+
globalThis.expect(r1).toEqual({
|
|
97
|
+
TAG: "Ok",
|
|
98
|
+
_0: undefined
|
|
99
|
+
});
|
|
100
|
+
let stale = await ops.append(2, "id-m", [ev(9)]);
|
|
101
|
+
if (stale.TAG === "Ok") {
|
|
102
|
+
globalThis.expect("expected Conflict").toBe("other");
|
|
103
|
+
} else {
|
|
104
|
+
let tmp = stale._0;
|
|
105
|
+
if (typeof tmp !== "object") {
|
|
106
|
+
globalThis.expect(true).toBe(true);
|
|
107
|
+
} else {
|
|
108
|
+
globalThis.expect("expected Conflict").toBe("other");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
let r2 = await ops.append(3, "id-m", [ev(3)]);
|
|
112
|
+
globalThis.expect(r2).toEqual({
|
|
113
|
+
TAG: "Ok",
|
|
114
|
+
_0: undefined
|
|
115
|
+
});
|
|
116
|
+
let replayed = await ops.replay("id-m");
|
|
117
|
+
globalThis.expect(replayed.length).toBe(4);
|
|
118
|
+
});
|
|
119
|
+
globalThis.test("appendStream persists the whole batch in order from the starting seq", async () => {
|
|
120
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
121
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
122
|
+
let DbProvider = {
|
|
123
|
+
db: db
|
|
124
|
+
};
|
|
125
|
+
let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
|
|
126
|
+
let s = Storage.make("agg", opts);
|
|
127
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
128
|
+
let ev = i => Object.fromEntries([[
|
|
129
|
+
"i",
|
|
130
|
+
i
|
|
131
|
+
]]);
|
|
132
|
+
await ops.append(0, "id-s", [ev(0)]);
|
|
133
|
+
await Effect.runPromise(ops.appendStream(1, "id-s", Stream$1.fromIterable([
|
|
134
|
+
ev(1),
|
|
135
|
+
ev(2),
|
|
136
|
+
ev(3)
|
|
137
|
+
])));
|
|
138
|
+
let replayed = await ops.replay("id-s");
|
|
139
|
+
globalThis.expect(replayed.length).toBe(4);
|
|
140
|
+
globalThis.expect(replayed[0]).toEqual(ev(0));
|
|
141
|
+
globalThis.expect(replayed[3]).toEqual(ev(3));
|
|
69
142
|
});
|
|
70
143
|
globalThis.test("replay returns empty array for unknown id", async () => {
|
|
71
144
|
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
@@ -79,6 +152,127 @@ globalThis.describe("EventLogStorage_Sqlite", () => {
|
|
|
79
152
|
let replayed = await ops.replay("not-there");
|
|
80
153
|
globalThis.expect(replayed.length).toBe(0);
|
|
81
154
|
});
|
|
155
|
+
globalThis.test("replayStream ~fromSeq reads only the delta", async () => {
|
|
156
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
157
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
158
|
+
let DbProvider = {
|
|
159
|
+
db: db
|
|
160
|
+
};
|
|
161
|
+
let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
|
|
162
|
+
let s = Storage.make("agg", opts);
|
|
163
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
164
|
+
let ev = i => Object.fromEntries([[
|
|
165
|
+
"i",
|
|
166
|
+
i
|
|
167
|
+
]]);
|
|
168
|
+
await ops.append(0, "id-d", [
|
|
169
|
+
ev(0),
|
|
170
|
+
ev(1),
|
|
171
|
+
ev(2),
|
|
172
|
+
ev(3)
|
|
173
|
+
]);
|
|
174
|
+
let delta = await Effect.runPromise(Stream.runCollect(ops.replayStream("id-d", 2)));
|
|
175
|
+
globalThis.expect(delta).toEqual([
|
|
176
|
+
ev(2),
|
|
177
|
+
ev(3)
|
|
178
|
+
]);
|
|
179
|
+
let full = await Effect.runPromise(Stream.runCollect(ops.replayStream("id-d", undefined)));
|
|
180
|
+
globalThis.expect(full.length).toBe(4);
|
|
181
|
+
});
|
|
182
|
+
globalThis.test("snapshot round-trip with keep-one overwrite", async () => {
|
|
183
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
184
|
+
let db = SqliteDriver$ReventlessLocal.openDb(":memory:");
|
|
185
|
+
let DbProvider = {
|
|
186
|
+
db: db
|
|
187
|
+
};
|
|
188
|
+
let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
|
|
189
|
+
let s = Storage.make("agg", opts);
|
|
190
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
191
|
+
let none = await ops.latestSnapshot("id-snap");
|
|
192
|
+
globalThis.expect(none).toEqual({
|
|
193
|
+
TAG: "Ok",
|
|
194
|
+
_0: undefined
|
|
195
|
+
});
|
|
196
|
+
let state = Object.fromEntries([[
|
|
197
|
+
"names",
|
|
198
|
+
2
|
|
199
|
+
]]);
|
|
200
|
+
let w1 = await ops.writeSnapshot("id-snap", {
|
|
201
|
+
seqNr: 2,
|
|
202
|
+
state: state,
|
|
203
|
+
schemaHash: "h1"
|
|
204
|
+
});
|
|
205
|
+
globalThis.expect(w1).toEqual({
|
|
206
|
+
TAG: "Ok",
|
|
207
|
+
_0: undefined
|
|
208
|
+
});
|
|
209
|
+
let r1 = await ops.latestSnapshot("id-snap");
|
|
210
|
+
globalThis.expect(r1).toEqual({
|
|
211
|
+
TAG: "Ok",
|
|
212
|
+
_0: {
|
|
213
|
+
seqNr: 2,
|
|
214
|
+
state: state,
|
|
215
|
+
schemaHash: "h1"
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
let state5 = Object.fromEntries([[
|
|
219
|
+
"names",
|
|
220
|
+
5
|
|
221
|
+
]]);
|
|
222
|
+
await ops.writeSnapshot("id-snap", {
|
|
223
|
+
seqNr: 5,
|
|
224
|
+
state: state5,
|
|
225
|
+
schemaHash: "h2"
|
|
226
|
+
});
|
|
227
|
+
let r2 = await ops.latestSnapshot("id-snap");
|
|
228
|
+
globalThis.expect(r2).toEqual({
|
|
229
|
+
TAG: "Ok",
|
|
230
|
+
_0: {
|
|
231
|
+
seqNr: 5,
|
|
232
|
+
state: state5,
|
|
233
|
+
schemaHash: "h2"
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
globalThis.test("snapshots persist across a reopen of the database file", async () => {
|
|
238
|
+
let path = `/tmp/reventless-test-snapshot-` + Date.now().toString() + `.db`;
|
|
239
|
+
let state = Object.fromEntries([[
|
|
240
|
+
"v",
|
|
241
|
+
7
|
|
242
|
+
]]);
|
|
243
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
244
|
+
let db = SqliteDriver$ReventlessLocal.openDb(path);
|
|
245
|
+
let DbProvider = {
|
|
246
|
+
db: db
|
|
247
|
+
};
|
|
248
|
+
let Storage = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus)(DbProvider);
|
|
249
|
+
let s = Storage.make("persist", opts);
|
|
250
|
+
let ops = await TestRunner$ReventlessLocal.resolve(s.operations);
|
|
251
|
+
await ops.writeSnapshot("id-sp", {
|
|
252
|
+
seqNr: 3,
|
|
253
|
+
state: state,
|
|
254
|
+
schemaHash: "h"
|
|
255
|
+
});
|
|
256
|
+
SqliteDriver$ReventlessLocal.close(db);
|
|
257
|
+
let TestBus2 = LocalBus$ReventlessLocal.Make({});
|
|
258
|
+
let db$1 = SqliteDriver$ReventlessLocal.openDb(path);
|
|
259
|
+
let DbProvider2 = {
|
|
260
|
+
db: db$1
|
|
261
|
+
};
|
|
262
|
+
let Storage2 = EventLogStorage_Sqlite$ReventlessLocal.Make(TestBus2)(DbProvider2);
|
|
263
|
+
let s2 = Storage2.make("persist", opts);
|
|
264
|
+
let ops2 = await TestRunner$ReventlessLocal.resolve(s2.operations);
|
|
265
|
+
let r = await ops2.latestSnapshot("id-sp");
|
|
266
|
+
globalThis.expect(r).toEqual({
|
|
267
|
+
TAG: "Ok",
|
|
268
|
+
_0: {
|
|
269
|
+
seqNr: 3,
|
|
270
|
+
state: state,
|
|
271
|
+
schemaHash: "h"
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
return SqliteDriver$ReventlessLocal.close(db$1);
|
|
275
|
+
});
|
|
82
276
|
globalThis.test("events persist across a reopen of the database file", async () => {
|
|
83
277
|
let path = `/tmp/reventless-test-eventlog-` + Date.now().toString() + `.db`;
|
|
84
278
|
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
@@ -78,4 +78,35 @@ describe("LocalBus PubSub (Phase F)", () => {
|
|
|
78
78
|
expect(countB.contents)->toBe(0)
|
|
79
79
|
})
|
|
80
80
|
})
|
|
81
|
+
|
|
82
|
+
// A5: a throwing/rejecting subscriber must not (a) hang publishEvent (its
|
|
83
|
+
// done_ countdown never reaching zero) nor (b) kill the drain fiber so the
|
|
84
|
+
// topic stops working. If either regressed, these tests would time out.
|
|
85
|
+
describe("failing subscriber", () => {
|
|
86
|
+
testPromise("a throwing subscriber does not hang publish; healthy siblings still receive it", async () => {
|
|
87
|
+
module TestBus = LocalBus.Make()
|
|
88
|
+
let healthy = ref(0)
|
|
89
|
+
TestBus.subscribeToEvents("T", async (_, _, _) => JsError.throwWithMessage("boom"))
|
|
90
|
+
TestBus.subscribeToEvents("T", async (_, _, _) => {
|
|
91
|
+
healthy := healthy.contents + 1
|
|
92
|
+
})
|
|
93
|
+
// Must resolve — not hang — even though one subscriber threw.
|
|
94
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
95
|
+
expect(healthy.contents)->toBe(1)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
testPromise("the topic keeps working after a subscriber throws (drain fiber survives)", async () => {
|
|
99
|
+
module TestBus = LocalBus.Make()
|
|
100
|
+
let healthy = ref(0)
|
|
101
|
+
TestBus.subscribeToEvents("T", async (_, _, _) => JsError.throwWithMessage("boom"))
|
|
102
|
+
TestBus.subscribeToEvents("T", async (_, _, _) => {
|
|
103
|
+
healthy := healthy.contents + 1
|
|
104
|
+
})
|
|
105
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
106
|
+
// Second publish on the same topic must also complete and be delivered —
|
|
107
|
+
// proving the dead-fiber cascade is gone.
|
|
108
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, JSON.Null)
|
|
109
|
+
expect(healthy.contents)->toBe(2)
|
|
110
|
+
})
|
|
111
|
+
})
|
|
81
112
|
})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
|
|
3
4
|
import * as LocalBus$ReventlessLocal from "../../src/adapter/LocalBus.res.mjs";
|
|
4
5
|
import * as TestRunner$ReventlessLocal from "../../src/test/TestRunner.res.mjs";
|
|
5
6
|
|
|
@@ -73,6 +74,33 @@ globalThis.describe("LocalBus PubSub (Phase F)", () => {
|
|
|
73
74
|
globalThis.expect(countB.contents).toBe(0);
|
|
74
75
|
});
|
|
75
76
|
});
|
|
77
|
+
globalThis.describe("failing subscriber", () => {
|
|
78
|
+
globalThis.test("a throwing subscriber does not hang publish; healthy siblings still receive it", async () => {
|
|
79
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
80
|
+
let healthy = {
|
|
81
|
+
contents: 0
|
|
82
|
+
};
|
|
83
|
+
TestBus.subscribeToEvents("T", async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("boom"));
|
|
84
|
+
TestBus.subscribeToEvents("T", async (param, param$1, param$2) => {
|
|
85
|
+
healthy.contents = healthy.contents + 1 | 0;
|
|
86
|
+
});
|
|
87
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, null);
|
|
88
|
+
globalThis.expect(healthy.contents).toBe(1);
|
|
89
|
+
});
|
|
90
|
+
globalThis.test("the topic keeps working after a subscriber throws (drain fiber survives)", async () => {
|
|
91
|
+
let TestBus = LocalBus$ReventlessLocal.Make({});
|
|
92
|
+
let healthy = {
|
|
93
|
+
contents: 0
|
|
94
|
+
};
|
|
95
|
+
TestBus.subscribeToEvents("T", async (param, param$1, param$2) => Stdlib_JsError.throwWithMessage("boom"));
|
|
96
|
+
TestBus.subscribeToEvents("T", async (param, param$1, param$2) => {
|
|
97
|
+
healthy.contents = healthy.contents + 1 | 0;
|
|
98
|
+
});
|
|
99
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, null);
|
|
100
|
+
await TestBus.publishEvent("T", "svc", defaultMeta, null);
|
|
101
|
+
globalThis.expect(healthy.contents).toBe(2);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
76
104
|
});
|
|
77
105
|
|
|
78
106
|
export {
|