@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
|
@@ -4,8 +4,10 @@ import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
|
4
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
5
|
import * as Stream from "effect/Stream";
|
|
6
6
|
import * as Pulumi from "@pulumi/pulumi";
|
|
7
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
7
8
|
import * as LocalBus$ReventlessLocal from "../LocalBus.res.mjs";
|
|
8
9
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
10
|
+
import * as QueryDbListQuery$ReventlessLocal from "./QueryDbListQuery.res.mjs";
|
|
9
11
|
|
|
10
12
|
function tableName(name) {
|
|
11
13
|
return "qdb_" + name.replaceAll("-", "_");
|
|
@@ -146,12 +148,27 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
146
148
|
let deleteBySubKeyStmt = SqliteDriver$ReventlessLocal.prepare(db, `DELETE FROM ` + table + ` WHERE partition_key = ? AND sub_key = ?`);
|
|
147
149
|
let scanAllStmt = SqliteDriver$ReventlessLocal.prepare(db, `SELECT partition_key, sub_key, item FROM ` + table + ` WHERE ` + notExpiredClause + ` ORDER BY partition_key, sub_key`);
|
|
148
150
|
let rowsFor = id => SqliteDriver$ReventlessLocal.all(selectByPartitionStmt, [id]).map(decodeItem);
|
|
149
|
-
let
|
|
151
|
+
let rowToItem = row => {
|
|
150
152
|
let item = decodeItem(row);
|
|
151
153
|
let match = row["partition_key"];
|
|
152
154
|
let partition = typeof match === "string" ? match : "";
|
|
153
155
|
return withId(item, partition);
|
|
154
|
-
}
|
|
156
|
+
};
|
|
157
|
+
let allRows = () => SqliteDriver$ReventlessLocal.all(scanAllStmt, []).map(rowToItem);
|
|
158
|
+
let indexLookupStmts = {};
|
|
159
|
+
let lookupByField = (field, value) => {
|
|
160
|
+
let s = indexLookupStmts[field];
|
|
161
|
+
let stmt;
|
|
162
|
+
if (s !== undefined) {
|
|
163
|
+
stmt = Primitive_option.valFromOption(s);
|
|
164
|
+
} else {
|
|
165
|
+
let escapedField = field.replaceAll("'", "''");
|
|
166
|
+
let s$1 = SqliteDriver$ReventlessLocal.prepare(db, `SELECT partition_key, sub_key, item FROM ` + table + ` WHERE json_extract(item, '$.` + escapedField + `') = ? AND ` + notExpiredClause);
|
|
167
|
+
indexLookupStmts[field] = s$1;
|
|
168
|
+
stmt = s$1;
|
|
169
|
+
}
|
|
170
|
+
return SqliteDriver$ReventlessLocal.all(stmt, [value]).map(rowToItem);
|
|
171
|
+
};
|
|
155
172
|
let saveOne = (id, state, ttl) => {
|
|
156
173
|
let subKey = computeSubKey(state, subIdField);
|
|
157
174
|
let expiresAt = ttl !== undefined ? ttl : null;
|
|
@@ -209,10 +226,25 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
209
226
|
_0: undefined
|
|
210
227
|
};
|
|
211
228
|
};
|
|
212
|
-
let count = async (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
229
|
+
let count = async (id, fieldName, inc) => {
|
|
230
|
+
let existing = Stdlib_Option.flatMap(rowsFor(id)[0], Stdlib_JSON.Decode.object);
|
|
231
|
+
let current = Stdlib_Option.mapOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(existing, o => o[fieldName]), Stdlib_JSON.Decode.float), 0, prim => prim | 0);
|
|
232
|
+
let next = current + inc | 0;
|
|
233
|
+
let obj = {};
|
|
234
|
+
if (existing !== undefined) {
|
|
235
|
+
Object.entries(existing).forEach(param => {
|
|
236
|
+
obj[param[0]] = param[1];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
obj["id"] = id;
|
|
240
|
+
obj[fieldName] = next;
|
|
241
|
+
saveOne(id, obj, undefined);
|
|
242
|
+
publishUpdated(id, obj);
|
|
243
|
+
return {
|
|
244
|
+
TAG: "Ok",
|
|
245
|
+
_0: next
|
|
246
|
+
};
|
|
247
|
+
};
|
|
216
248
|
let deleteOne = (id, subIdOpt) => {
|
|
217
249
|
if (subIdOpt !== undefined) {
|
|
218
250
|
return SqliteDriver$ReventlessLocal.run(deleteBySubKeyStmt, [
|
|
@@ -272,6 +304,83 @@ function makeStorage(db, bus, name, indexes, subIdField) {
|
|
|
272
304
|
bus.registerQueryDb(name, ops);
|
|
273
305
|
bus.registerQueryDbScan(name, () => allRows());
|
|
274
306
|
bus.registerQueryDbStream(name, () => Stream.fromIterable(allRows()));
|
|
307
|
+
bus.registerQueryDbIndexLookup(name, lookupByField);
|
|
308
|
+
let listStmtCache = {};
|
|
309
|
+
let preparedList = sql => {
|
|
310
|
+
let s = listStmtCache[sql];
|
|
311
|
+
if (s !== undefined) {
|
|
312
|
+
return Primitive_option.valFromOption(s);
|
|
313
|
+
}
|
|
314
|
+
let s$1 = SqliteDriver$ReventlessLocal.prepare(db, sql);
|
|
315
|
+
listStmtCache[sql] = s$1;
|
|
316
|
+
return s$1;
|
|
317
|
+
};
|
|
318
|
+
let jsonText = field => `CAST(json_extract(item, '$.` + field.replaceAll("'", "''") + `') AS TEXT)`;
|
|
319
|
+
let idExpr = "COALESCE(json_extract(item, '$.id'), partition_key)";
|
|
320
|
+
let listPage = (argsDict, capability, param) => {
|
|
321
|
+
let filterDict = Stdlib_Option.getOr(Stdlib_Option.flatMap(argsDict["filter"], Stdlib_JSON.Decode.object), {});
|
|
322
|
+
let strNonEmpty = k => Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict[k], Stdlib_JSON.Decode.string), false, s => s.length > 0);
|
|
323
|
+
let hasIds = Stdlib_Option.mapOr(Stdlib_Option.flatMap(filterDict["ids"], Stdlib_JSON.Decode.array), false, a => a.length !== 0);
|
|
324
|
+
let last = Stdlib_Option.flatMap(argsDict["last"], Stdlib_JSON.Decode.float);
|
|
325
|
+
let before = Stdlib_Option.flatMap(argsDict["before"], Stdlib_JSON.Decode.string);
|
|
326
|
+
if (strNonEmpty("search") || strNonEmpty("searchPrefix") || hasIds || Stdlib_Option.isSome(last) || Stdlib_Option.isSome(before)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
let whereParts = [notExpiredClause];
|
|
330
|
+
let params = [];
|
|
331
|
+
let valString = v => {
|
|
332
|
+
let s = Stdlib_JSON.Decode.string(v);
|
|
333
|
+
if (s !== undefined) {
|
|
334
|
+
return s;
|
|
335
|
+
} else {
|
|
336
|
+
return Stdlib_Option.map(Stdlib_JSON.Decode.float(v), f => f.toString());
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
capability.filterFields.forEach(f => {
|
|
340
|
+
let v = filterDict[f.name + "Eq"];
|
|
341
|
+
if (v !== undefined && v !== null) {
|
|
342
|
+
whereParts.push(jsonText(f.name) + ` = ?`);
|
|
343
|
+
params.push(Stdlib_Option.getOr(valString(v), ""));
|
|
344
|
+
}
|
|
345
|
+
if (!f.range) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let v$1 = filterDict[f.name + "From"];
|
|
349
|
+
if (v$1 !== undefined && v$1 !== null) {
|
|
350
|
+
whereParts.push(jsonText(f.name) + ` >= ?`);
|
|
351
|
+
params.push(Stdlib_Option.getOr(valString(v$1), ""));
|
|
352
|
+
}
|
|
353
|
+
let v$2 = filterDict[f.name + "To"];
|
|
354
|
+
if (v$2 !== undefined && v$2 !== null) {
|
|
355
|
+
whereParts.push(jsonText(f.name) + ` <= ?`);
|
|
356
|
+
params.push(Stdlib_Option.getOr(valString(v$2), ""));
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
let orderByDict = Stdlib_Option.flatMap(argsDict["orderBy"], Stdlib_JSON.Decode.object);
|
|
361
|
+
let orderByField = Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["field"]), Stdlib_JSON.Decode.string);
|
|
362
|
+
let isDesc = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(orderByDict, ob => ob["direction"]), Stdlib_JSON.Decode.string), "ASC") === "DESC";
|
|
363
|
+
let cursorExpr = orderByField !== undefined ? jsonText(orderByField) : idExpr;
|
|
364
|
+
let c = Stdlib_Option.flatMap(argsDict["after"], Stdlib_JSON.Decode.string);
|
|
365
|
+
if (c !== undefined) {
|
|
366
|
+
whereParts.push(cursorExpr + ` ` + (
|
|
367
|
+
isDesc ? "<" : ">"
|
|
368
|
+
) + ` ?`);
|
|
369
|
+
params.push(QueryDbListQuery$ReventlessLocal.decodeCursor(c));
|
|
370
|
+
}
|
|
371
|
+
let orderClause = orderByField !== undefined ? jsonText(orderByField) + ` ` + (
|
|
372
|
+
isDesc ? "DESC" : "ASC"
|
|
373
|
+
) + `, ` + idExpr + ` ASC` : idExpr + ` ASC`;
|
|
374
|
+
let pageSize = Stdlib_Option.getOr(Stdlib_Option.map(Stdlib_Option.flatMap(argsDict["first"], Stdlib_JSON.Decode.float), prim => prim | 0), QueryDbListQuery$ReventlessLocal.defaultListPageSize);
|
|
375
|
+
let sql = `SELECT partition_key, item FROM ` + table + ` WHERE ` + whereParts.join(" AND ") + ` ORDER BY ` + orderClause + ` LIMIT ?`;
|
|
376
|
+
let rows = SqliteDriver$ReventlessLocal.all(preparedList(sql), params.concat([pageSize + 1 | 0])).map(rowToItem);
|
|
377
|
+
let hasMore = rows.length > pageSize;
|
|
378
|
+
let pageItems = rows.slice(0, pageSize);
|
|
379
|
+
let cursorField = Stdlib_Option.getOr(orderByField, "id");
|
|
380
|
+
let cursorValueOf = item => Stdlib_Option.getOr(QueryDbListQuery$ReventlessLocal.getFieldString(item, cursorField), QueryDbListQuery$ReventlessLocal.getId(item));
|
|
381
|
+
return QueryDbListQuery$ReventlessLocal.buildConnection(pageItems, hasMore, false, cursorValueOf);
|
|
382
|
+
};
|
|
383
|
+
bus.registerQueryDbListPage(name, listPage);
|
|
275
384
|
return {
|
|
276
385
|
resources: [],
|
|
277
386
|
dataSourceName: Pulumi.output(""),
|
|
@@ -285,11 +394,15 @@ function Make(Bus) {
|
|
|
285
394
|
let busCallbacks_registerQueryDb = Bus.registerQueryDb;
|
|
286
395
|
let busCallbacks_registerQueryDbScan = Bus.registerQueryDbScan;
|
|
287
396
|
let busCallbacks_registerQueryDbStream = Bus.registerQueryDbStream;
|
|
397
|
+
let busCallbacks_registerQueryDbIndexLookup = Bus.registerQueryDbIndexLookup;
|
|
398
|
+
let busCallbacks_registerQueryDbListPage = Bus.registerQueryDbListPage;
|
|
288
399
|
let busCallbacks = {
|
|
289
400
|
publishStateChange: busCallbacks_publishStateChange,
|
|
290
401
|
registerQueryDb: busCallbacks_registerQueryDb,
|
|
291
402
|
registerQueryDbScan: busCallbacks_registerQueryDbScan,
|
|
292
|
-
registerQueryDbStream: busCallbacks_registerQueryDbStream
|
|
403
|
+
registerQueryDbStream: busCallbacks_registerQueryDbStream,
|
|
404
|
+
registerQueryDbIndexLookup: busCallbacks_registerQueryDbIndexLookup,
|
|
405
|
+
registerQueryDbListPage: busCallbacks_registerQueryDbListPage
|
|
293
406
|
};
|
|
294
407
|
let make = (name, indexes, subIdField, param, param$1, param$2, param$3) => makeStorage(DbProvider.db, busCallbacks, name, indexes, subIdField);
|
|
295
408
|
return {
|
|
@@ -27,7 +27,17 @@ let openDb = (~path) => {
|
|
|
27
27
|
if path !== ":memory:" {
|
|
28
28
|
mkdirSync(dirname(path), {recursive: true})
|
|
29
29
|
}
|
|
30
|
-
openDatabaseSync(path)
|
|
30
|
+
let db = openDatabaseSync(path)
|
|
31
|
+
// Connection pragmas. WAL lets readers proceed concurrently with a writer and
|
|
32
|
+
// batches fsyncs into the write-ahead log (a no-op for `:memory:`, which stays
|
|
33
|
+
// on its MEMORY journal); `synchronous=NORMAL` is the safe WAL companion —
|
|
34
|
+
// durable across app crashes, only a power-loss window remains — and cuts the
|
|
35
|
+
// per-commit fsync cost that dominated replay-heavy dev sessions;
|
|
36
|
+
// `busy_timeout` makes a momentarily-locked db wait rather than throw SQLITE_BUSY.
|
|
37
|
+
_exec(db, "PRAGMA journal_mode=WAL")
|
|
38
|
+
_exec(db, "PRAGMA synchronous=NORMAL")
|
|
39
|
+
_exec(db, "PRAGMA busy_timeout=5000")
|
|
40
|
+
db
|
|
31
41
|
}
|
|
32
42
|
let close = db => _close(db)
|
|
33
43
|
let exec = (db, sql) => _exec(db, sql)
|
|
@@ -46,7 +56,12 @@ let transaction = (db, fn) => {
|
|
|
46
56
|
result
|
|
47
57
|
} catch {
|
|
48
58
|
| exn =>
|
|
49
|
-
|
|
59
|
+
// Roll back, but never let a failing ROLLBACK replace the original error —
|
|
60
|
+
// the original is the diagnostic one; a rollback failure (e.g. no active
|
|
61
|
+
// transaction) would otherwise mask it.
|
|
62
|
+
try exec(db, "ROLLBACK") catch {
|
|
63
|
+
| _ => ()
|
|
64
|
+
}
|
|
50
65
|
throw(exn)
|
|
51
66
|
}
|
|
52
67
|
}
|
|
@@ -19,7 +19,11 @@ function openDb(path) {
|
|
|
19
19
|
recursive: true
|
|
20
20
|
});
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
let db = new Nodesqlite.DatabaseSync(path);
|
|
23
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
24
|
+
db.exec("PRAGMA synchronous=NORMAL");
|
|
25
|
+
db.exec("PRAGMA busy_timeout=5000");
|
|
26
|
+
return db;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
function close(db) {
|
|
@@ -57,7 +61,11 @@ function transaction(db, fn) {
|
|
|
57
61
|
db.exec("COMMIT");
|
|
58
62
|
return result;
|
|
59
63
|
} catch (exn) {
|
|
60
|
-
|
|
64
|
+
try {
|
|
65
|
+
db.exec("ROLLBACK");
|
|
66
|
+
} catch (exn$1) {
|
|
67
|
+
|
|
68
|
+
}
|
|
61
69
|
throw exn;
|
|
62
70
|
}
|
|
63
71
|
}
|
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
module Make = (Bus: LocalBus.T) => {
|
|
4
4
|
module EventTopicPublisher = LocalEventTopicPublisher.Make(Bus)
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// Use the backend-aware storage functor (as Platform does), not the plain
|
|
7
|
+
// module: the functor consults BackendState so a standalone DCB log honours
|
|
8
|
+
// REVENTLESS_LOCAL_BACKEND=sqlite, and it registers the read with the Bus so
|
|
9
|
+
// `Bus.getDcbEventLogRead` can see it. The plain module did neither — the log
|
|
10
|
+
// silently stayed in memory under SQLite and was invisible to bus readers.
|
|
11
|
+
module Storage = DcbEventLogStorage_InMemory.Make(Bus)
|
|
12
|
+
module Inner = ReventlessCore.DcbEventLog_Builder.Make(Storage, EventTopicPublisher)
|
|
7
13
|
type component = ReventlessInfra.DcbEventLog.component
|
|
8
14
|
let make: (
|
|
9
15
|
~name: string,
|
|
@@ -7,11 +7,11 @@ import * as DcbEventLogStorage_InMemory$ReventlessLocal from "../adapter/DcbEven
|
|
|
7
7
|
|
|
8
8
|
function Make(Bus) {
|
|
9
9
|
let EventTopicPublisher = LocalEventTopicPublisher$ReventlessLocal.Make(Bus);
|
|
10
|
-
let
|
|
11
|
-
|
|
12
|
-
})(EventTopicPublisher);
|
|
10
|
+
let Storage = DcbEventLogStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
11
|
+
let Inner = DcbEventLog_Builder$ReventlessCore.Make(Storage)(EventTopicPublisher);
|
|
13
12
|
return {
|
|
14
13
|
EventTopicPublisher: EventTopicPublisher,
|
|
14
|
+
Storage: Storage,
|
|
15
15
|
Inner: Inner,
|
|
16
16
|
make: Inner.make,
|
|
17
17
|
operations: Component$ReventlessInfra.operations
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Make = (Bus: LocalBus.T) => {
|
|
4
4
|
module RuntimeEnvironment = LocalRuntimeEnvironment
|
|
5
|
-
|
|
5
|
+
// MakeProjection (not Make): also registers the resolved handler for the
|
|
6
|
+
// SQLite backend's startup projection catch-up (ProjectionCheckpoint).
|
|
7
|
+
module EventCollectorChannel = LocalEventCollectorChannel.MakeProjection(Bus)
|
|
6
8
|
module EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder.Make(
|
|
7
9
|
Bus,
|
|
8
10
|
EventCollectorChannel,
|
|
@@ -9,8 +9,11 @@ import * as LocalEventCollectorChannel$ReventlessLocal from "../adapter/EventCol
|
|
|
9
9
|
import * as LocalEventCollectorRuntime_Builder$ReventlessLocal from "../adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs";
|
|
10
10
|
|
|
11
11
|
function Make(Bus) {
|
|
12
|
-
let EventCollectorChannel = LocalEventCollectorChannel$ReventlessLocal.
|
|
13
|
-
let EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder$ReventlessLocal.Make(Bus)(
|
|
12
|
+
let EventCollectorChannel = LocalEventCollectorChannel$ReventlessLocal.MakeProjection(Bus);
|
|
13
|
+
let EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder$ReventlessLocal.Make(Bus)({
|
|
14
|
+
make: EventCollectorChannel.make,
|
|
15
|
+
connect: EventCollectorChannel.connect
|
|
16
|
+
});
|
|
14
17
|
let QueryDbStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
15
18
|
let QueryDbResolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
|
|
16
19
|
let Make$1 = Spec => (Mappings => ReadModel_Builder$ReventlessCore.Make(Spec)(Mappings)({
|
|
@@ -23,7 +26,10 @@ function Make(Bus) {
|
|
|
23
26
|
make: QueryDbStorage.make
|
|
24
27
|
})({
|
|
25
28
|
make: QueryDbResolvers.make
|
|
26
|
-
})(
|
|
29
|
+
})({
|
|
30
|
+
make: EventCollectorChannel.make,
|
|
31
|
+
connect: EventCollectorChannel.connect
|
|
32
|
+
})(EventCollectorRuntimeBuilder));
|
|
27
33
|
let NoResolvers = QueryDb_Adapter$ReventlessCore.NoResolvers({
|
|
28
34
|
make: QueryDbStorage.make
|
|
29
35
|
});
|
|
@@ -35,7 +41,10 @@ function Make(Bus) {
|
|
|
35
41
|
asEffectHandler: prim => prim
|
|
36
42
|
})({
|
|
37
43
|
make: QueryDbStorage.make
|
|
38
|
-
})(NoResolvers)(
|
|
44
|
+
})(NoResolvers)({
|
|
45
|
+
make: EventCollectorChannel.make,
|
|
46
|
+
connect: EventCollectorChannel.connect
|
|
47
|
+
})(EventCollectorRuntimeBuilder));
|
|
39
48
|
return {
|
|
40
49
|
RuntimeEnvironment: undefined,
|
|
41
50
|
EventCollectorChannel: EventCollectorChannel,
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
|
|
4
4
|
module Make = (Bus: LocalBus.T) => {
|
|
5
5
|
module RuntimeEnvironment = LocalRuntimeEnvironment
|
|
6
|
-
|
|
6
|
+
// MakeProjection (not Make): also registers the resolved handler for the
|
|
7
|
+
// SQLite backend's startup projection catch-up (ProjectionCheckpoint) —
|
|
8
|
+
// StateViewSlices are pure projections fed by DCB events.
|
|
9
|
+
module EventCollectorChannel = LocalEventCollectorChannel.MakeProjection(Bus)
|
|
7
10
|
module EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder.Make(
|
|
8
11
|
Bus,
|
|
9
12
|
EventCollectorChannel,
|
|
@@ -9,8 +9,11 @@ import * as LocalEventCollectorChannel$ReventlessLocal from "../adapter/EventCol
|
|
|
9
9
|
import * as LocalEventCollectorRuntime_Builder$ReventlessLocal from "../adapter/Runtime/LocalEventCollectorRuntime_Builder.res.mjs";
|
|
10
10
|
|
|
11
11
|
function Make(Bus) {
|
|
12
|
-
let EventCollectorChannel = LocalEventCollectorChannel$ReventlessLocal.
|
|
13
|
-
let EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder$ReventlessLocal.Make(Bus)(
|
|
12
|
+
let EventCollectorChannel = LocalEventCollectorChannel$ReventlessLocal.MakeProjection(Bus);
|
|
13
|
+
let EventCollectorRuntimeBuilder = LocalEventCollectorRuntime_Builder$ReventlessLocal.Make(Bus)({
|
|
14
|
+
make: EventCollectorChannel.make,
|
|
15
|
+
connect: EventCollectorChannel.connect
|
|
16
|
+
});
|
|
14
17
|
let QueryDbStorage = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
15
18
|
let QueryDbResolvers = QueryDbResolvers_GraphQL$ReventlessLocal.Make(Bus);
|
|
16
19
|
let api = () => {};
|
|
@@ -29,7 +32,10 @@ function Make(Bus) {
|
|
|
29
32
|
make: QueryDbStorage.make
|
|
30
33
|
})({
|
|
31
34
|
make: QueryDbResolvers.make
|
|
32
|
-
})(
|
|
35
|
+
})({
|
|
36
|
+
make: EventCollectorChannel.make,
|
|
37
|
+
connect: EventCollectorChannel.connect
|
|
38
|
+
})(EventCollectorRuntimeBuilder)(Api);
|
|
33
39
|
let Make$1 = Spec => (Projection => {
|
|
34
40
|
let Inner = CoreMaker.Make(Spec)(Projection);
|
|
35
41
|
return {
|
|
@@ -15,7 +15,7 @@ let make = (~name as _="mock-event-log", ~opts as _: Pulumi.CustomResourceOption
|
|
|
15
15
|
let append: ReventlessCore.EventLog.append<string, JSON.t> = async (_seqNr, id, jsons) => {
|
|
16
16
|
if failNextAppends.contents > 0 {
|
|
17
17
|
failNextAppends := failNextAppends.contents - 1
|
|
18
|
-
Error("mock append failure")
|
|
18
|
+
Error(ReventlessCore.EventLog.StorageFailure("mock append failure"))
|
|
19
19
|
} else {
|
|
20
20
|
let existing = events.contents->Dict.get(id)->Option.getOr([])
|
|
21
21
|
events.contents->Dict.set(id, existing->Array.concat(jsons))
|
|
@@ -27,8 +27,22 @@ let make = (~name as _="mock-event-log", ~opts as _: Pulumi.CustomResourceOption
|
|
|
27
27
|
events.contents->Dict.get(id)->Option.getOr([])
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
let replayStream
|
|
31
|
-
events.contents
|
|
30
|
+
let replayStream = (id, ~fromSeq=0) =>
|
|
31
|
+
events.contents
|
|
32
|
+
->Dict.get(id)
|
|
33
|
+
->Option.getOr([])
|
|
34
|
+
->Array.filterWithIndex((_, i) => i >= fromSeq)
|
|
35
|
+
->Stream.fromIterable
|
|
36
|
+
|
|
37
|
+
let snapshots: dict<ReventlessCore.EventLog.snapshot> = Dict.make()
|
|
38
|
+
|
|
39
|
+
let latestSnapshot: ReventlessCore.EventLog.latestSnapshot<string> = async id =>
|
|
40
|
+
Ok(snapshots->Dict.get(id))
|
|
41
|
+
|
|
42
|
+
let writeSnapshot: ReventlessCore.EventLog.writeSnapshot<string> = async (id, snap) => {
|
|
43
|
+
snapshots->Dict.set(id, snap)
|
|
44
|
+
Ok()
|
|
45
|
+
}
|
|
32
46
|
|
|
33
47
|
let appendStream: ReventlessCore.EventLog.appendStream<string, JSON.t> = (_startingSeqNr, id, stream) =>
|
|
34
48
|
stream->Stream.runForEach(json =>
|
|
@@ -45,11 +59,14 @@ let make = (~name as _="mock-event-log", ~opts as _: Pulumi.CustomResourceOption
|
|
|
45
59
|
replay,
|
|
46
60
|
replayStream,
|
|
47
61
|
appendStream,
|
|
62
|
+
latestSnapshot,
|
|
63
|
+
writeSnapshot,
|
|
48
64
|
}),
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
let reset = () => {
|
|
52
68
|
events := Dict.make()
|
|
69
|
+
snapshots->Dict.forEachWithKey((_, k) => snapshots->Dict.delete(k))
|
|
53
70
|
failNextAppends := 0
|
|
54
71
|
}
|
|
55
72
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
3
4
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
5
|
import * as Effect from "effect/Effect";
|
|
5
6
|
import * as Stream from "effect/Stream";
|
|
@@ -21,7 +22,10 @@ function make($staropt$star, $staropt$star$1) {
|
|
|
21
22
|
failNextAppends.contents = failNextAppends.contents - 1 | 0;
|
|
22
23
|
return {
|
|
23
24
|
TAG: "Error",
|
|
24
|
-
_0:
|
|
25
|
+
_0: {
|
|
26
|
+
TAG: "StorageFailure",
|
|
27
|
+
_0: "mock append failure"
|
|
28
|
+
}
|
|
25
29
|
};
|
|
26
30
|
}
|
|
27
31
|
let existing = Stdlib_Option.getOr(events.contents[id], []);
|
|
@@ -32,7 +36,22 @@ function make($staropt$star, $staropt$star$1) {
|
|
|
32
36
|
};
|
|
33
37
|
};
|
|
34
38
|
let replay = async id => Stdlib_Option.getOr(events.contents[id], []);
|
|
35
|
-
let replayStream =
|
|
39
|
+
let replayStream = (id, fromSeqOpt) => {
|
|
40
|
+
let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
|
|
41
|
+
return Stream.fromIterable(Stdlib_Option.getOr(events.contents[id], []).filter((param, i) => i >= fromSeq));
|
|
42
|
+
};
|
|
43
|
+
let snapshots = {};
|
|
44
|
+
let latestSnapshot = async id => ({
|
|
45
|
+
TAG: "Ok",
|
|
46
|
+
_0: snapshots[id]
|
|
47
|
+
});
|
|
48
|
+
let writeSnapshot = async (id, snap) => {
|
|
49
|
+
snapshots[id] = snap;
|
|
50
|
+
return {
|
|
51
|
+
TAG: "Ok",
|
|
52
|
+
_0: undefined
|
|
53
|
+
};
|
|
54
|
+
};
|
|
36
55
|
let appendStream = (_startingSeqNr, id, stream) => Stream.runForEach(stream, json => Effect.sync(() => {
|
|
37
56
|
let existing = Stdlib_Option.getOr(events.contents[id], []);
|
|
38
57
|
events.contents[id] = existing.concat([json]);
|
|
@@ -42,7 +61,9 @@ function make($staropt$star, $staropt$star$1) {
|
|
|
42
61
|
append: append,
|
|
43
62
|
replay: replay,
|
|
44
63
|
replayStream: replayStream,
|
|
45
|
-
appendStream: appendStream
|
|
64
|
+
appendStream: appendStream,
|
|
65
|
+
latestSnapshot: latestSnapshot,
|
|
66
|
+
writeSnapshot: writeSnapshot
|
|
46
67
|
});
|
|
47
68
|
let storage = {
|
|
48
69
|
resources: storage_resources,
|
|
@@ -50,6 +71,7 @@ function make($staropt$star, $staropt$star$1) {
|
|
|
50
71
|
};
|
|
51
72
|
let reset = () => {
|
|
52
73
|
events.contents = {};
|
|
74
|
+
Stdlib_Dict.forEachWithKey(snapshots, (param, k) => Stdlib_Dict.$$delete(snapshots, k));
|
|
53
75
|
failNextAppends.contents = 0;
|
|
54
76
|
};
|
|
55
77
|
return {
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Regression: subscribeToPluginEvents' Source-C emission decoded the WHOLE bus
|
|
2
|
+
// envelope {id, meta, event} with the bare Plugin event schema — convert mode
|
|
3
|
+
// "succeeded" and then threw a TypeError on every variant's payload access, so
|
|
4
|
+
// the onPluginStatusChange / onUIFragmentChange subscription emissions never
|
|
5
|
+
// fired on the local platform. decodePluginEventEnvelope must decode the
|
|
6
|
+
// payload under "event" and degrade to None on anything malformed.
|
|
7
|
+
|
|
8
|
+
open JestGlobals
|
|
9
|
+
|
|
10
|
+
let _ = TestRunner.setup()
|
|
11
|
+
|
|
12
|
+
let meta = ReventlessCore.Message.generateMeta(~service=ReventlessCore.PluginSpec.name)
|
|
13
|
+
|
|
14
|
+
let envelopeOf = (event: ReventlessCore.PluginSpec.event) =>
|
|
15
|
+
ReventlessCore.Message.composeEventJson'(
|
|
16
|
+
"Catalog",
|
|
17
|
+
meta,
|
|
18
|
+
event->S.reverseConvertToJsonOrThrow(ReventlessCore.PluginSpec.eventSchema),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
describe("Platform.decodePluginEventEnvelope", () => {
|
|
22
|
+
testSync("decodes a VersionConnected event from the published envelope", () => {
|
|
23
|
+
let envelope = envelopeOf(VersionConnected(Plugin_Fixtures.pluginDefinition))
|
|
24
|
+
switch Platform.decodePluginEventEnvelope(envelope) {
|
|
25
|
+
| Some(VersionConnected(def)) => expect(def.name)->toBe(Plugin_Fixtures.pluginDefinition.name)
|
|
26
|
+
| _ => expect("VersionConnected")->toBe("something else")
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
testSync("decodes a payload-light variant (VersionDetected)", () => {
|
|
31
|
+
let envelope = envelopeOf(VersionDetected("2.0.0"))
|
|
32
|
+
expect(Platform.decodePluginEventEnvelope(envelope))->toEqual(
|
|
33
|
+
Some(ReventlessCore.PluginSpec.VersionDetected("2.0.0")),
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
testSync("returns None for a bare event JSON (no envelope) and malformed input", () => {
|
|
38
|
+
// The old bug class: input that is not the {id, meta, event} envelope.
|
|
39
|
+
let bare =
|
|
40
|
+
ReventlessCore.PluginSpec.VersionDetected("2.0.0")->S.reverseConvertToJsonOrThrow(
|
|
41
|
+
ReventlessCore.PluginSpec.eventSchema,
|
|
42
|
+
)
|
|
43
|
+
expect(Platform.decodePluginEventEnvelope(bare))->toEqual(None)
|
|
44
|
+
expect(Platform.decodePluginEventEnvelope(JSON.Encode.string("junk")))->toEqual(None)
|
|
45
|
+
expect(
|
|
46
|
+
Platform.decodePluginEventEnvelope(
|
|
47
|
+
JSON.Encode.object(Dict.fromArray([("event", JSON.Encode.object(Dict.make()))])),
|
|
48
|
+
),
|
|
49
|
+
)->toEqual(None)
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
5
|
+
import * as Platform$ReventlessLocal from "../src/Platform.res.mjs";
|
|
6
|
+
import * as PluginSpec$ReventlessCore from "@reventlessdev/reventless-core/src/plugin/lifecycle/PluginSpec.res.mjs";
|
|
7
|
+
import * as TestRunner$ReventlessLocal from "../src/test/TestRunner.res.mjs";
|
|
8
|
+
import * as Plugin_Fixtures$ReventlessLocal from "./plugin/Plugin_Fixtures.res.mjs";
|
|
9
|
+
|
|
10
|
+
TestRunner$ReventlessLocal.setup();
|
|
11
|
+
|
|
12
|
+
let meta = Message$ReventlessCore.generateMeta(PluginSpec$ReventlessCore.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
|
|
13
|
+
|
|
14
|
+
function envelopeOf(event) {
|
|
15
|
+
return Message$ReventlessCore.composeEventJson$p("Catalog", meta, S.reverseConvertToJsonOrThrow(event, PluginSpec$ReventlessCore.eventSchema));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
globalThis.describe("Platform.decodePluginEventEnvelope", () => {
|
|
19
|
+
globalThis.test("decodes a VersionConnected event from the published envelope", () => {
|
|
20
|
+
let envelope = envelopeOf({
|
|
21
|
+
TAG: "VersionConnected",
|
|
22
|
+
_0: Plugin_Fixtures$ReventlessLocal.pluginDefinition
|
|
23
|
+
});
|
|
24
|
+
let match = Platform$ReventlessLocal.decodePluginEventEnvelope(envelope);
|
|
25
|
+
if (match !== undefined) {
|
|
26
|
+
if (match.TAG === "VersionConnected") {
|
|
27
|
+
globalThis.expect(match._0.name).toBe(Plugin_Fixtures$ReventlessLocal.pluginDefinition.name);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
globalThis.expect("VersionConnected").toBe("something else");
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
globalThis.expect("VersionConnected").toBe("something else");
|
|
34
|
+
});
|
|
35
|
+
globalThis.test("decodes a payload-light variant (VersionDetected)", () => {
|
|
36
|
+
let envelope = envelopeOf({
|
|
37
|
+
TAG: "VersionDetected",
|
|
38
|
+
_0: "2.0.0"
|
|
39
|
+
});
|
|
40
|
+
globalThis.expect(Platform$ReventlessLocal.decodePluginEventEnvelope(envelope)).toEqual({
|
|
41
|
+
TAG: "VersionDetected",
|
|
42
|
+
_0: "2.0.0"
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
globalThis.test("returns None for a bare event JSON (no envelope) and malformed input", () => {
|
|
46
|
+
let bare = S.reverseConvertToJsonOrThrow({
|
|
47
|
+
TAG: "VersionDetected",
|
|
48
|
+
_0: "2.0.0"
|
|
49
|
+
}, PluginSpec$ReventlessCore.eventSchema);
|
|
50
|
+
globalThis.expect(Platform$ReventlessLocal.decodePluginEventEnvelope(bare)).toEqual(undefined);
|
|
51
|
+
globalThis.expect(Platform$ReventlessLocal.decodePluginEventEnvelope("junk")).toEqual(undefined);
|
|
52
|
+
globalThis.expect(Platform$ReventlessLocal.decodePluginEventEnvelope(Object.fromEntries([[
|
|
53
|
+
"event",
|
|
54
|
+
{}
|
|
55
|
+
]]))).toEqual(undefined);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export {
|
|
60
|
+
meta,
|
|
61
|
+
envelopeOf,
|
|
62
|
+
}
|
|
63
|
+
/* Not a pure module */
|
|
@@ -70,6 +70,70 @@ describe("Backend parity (Memory vs Sqlite)", () => {
|
|
|
70
70
|
await runUnderSqlite(scenario)
|
|
71
71
|
})
|
|
72
72
|
|
|
73
|
+
testPromise("QueryDb: count returns a running total and loadStream reflects it under both", async () => {
|
|
74
|
+
let scenario = async () => {
|
|
75
|
+
module TestBus = LocalBus.Make()
|
|
76
|
+
module Storage = QueryDbStorage_InMemory.Make(TestBus)
|
|
77
|
+
let s = Storage.make(~name="parity-count", ~indexes=[], ~api=(), ~apiRole=(), ~opts)
|
|
78
|
+
let ops = await s.operations->TestRunner.resolve
|
|
79
|
+
|
|
80
|
+
// First increment creates the counter item and returns the new total.
|
|
81
|
+
let r1 = await ops.count("prod-1", "orderCount", 3)
|
|
82
|
+
expect(r1)->toEqual(Ok(3))
|
|
83
|
+
// Second increment accumulates (not just echoes the increment).
|
|
84
|
+
let r2 = await ops.count("prod-1", "orderCount", 2)
|
|
85
|
+
expect(r2)->toEqual(Ok(5))
|
|
86
|
+
|
|
87
|
+
// loadStream must see the persisted counter — count is not a side channel.
|
|
88
|
+
let items =
|
|
89
|
+
await ops.loadStream("prod-1")
|
|
90
|
+
->Stream.runCollect
|
|
91
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
92
|
+
->Effect.runPromise
|
|
93
|
+
expect(items->Array.length)->toBe(1)
|
|
94
|
+
let field =
|
|
95
|
+
items
|
|
96
|
+
->Array.getUnsafe(0)
|
|
97
|
+
->JSON.Decode.object
|
|
98
|
+
->Option.flatMap(o => o->Dict.get("orderCount"))
|
|
99
|
+
->Option.flatMap(JSON.Decode.float)
|
|
100
|
+
->Option.mapOr(0, Float.toInt)
|
|
101
|
+
expect(field)->toBe(5)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
await runUnderMemory(scenario)
|
|
105
|
+
await runUnderSqlite(scenario)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
testPromise("QueryDb: an expired-TTL item is filtered from loadStream under both", async () => {
|
|
109
|
+
let scenario = async () => {
|
|
110
|
+
module TestBus = LocalBus.Make()
|
|
111
|
+
module Storage = QueryDbStorage_InMemory.Make(TestBus)
|
|
112
|
+
let s = Storage.make(~name="parity-ttl", ~indexes=[], ~api=(), ~apiRole=(), ~opts)
|
|
113
|
+
let ops = await s.operations->TestRunner.resolve
|
|
114
|
+
|
|
115
|
+
let item = k => JSON.Encode.object(Dict.fromArray([("id", JSON.Encode.string(k))]))
|
|
116
|
+
let readCount = async k =>
|
|
117
|
+
(
|
|
118
|
+
await ops.loadStream(k)
|
|
119
|
+
->Stream.runCollect
|
|
120
|
+
->Effect.catchAll(_ => Effect.succeed([]))
|
|
121
|
+
->Effect.runPromise
|
|
122
|
+
)->Array.length
|
|
123
|
+
|
|
124
|
+
// A live item (no TTL) and one whose absolute expiry (epoch second 1) is
|
|
125
|
+
// long past — the expired one must not surface under either backend.
|
|
126
|
+
let _ = await ops.save("live", item("live"), ReventlessCore.QueryDb.Any, None)
|
|
127
|
+
let _ = await ops.save("dead", item("dead"), ReventlessCore.QueryDb.Any, Some(1))
|
|
128
|
+
|
|
129
|
+
expect(await readCount("live"))->toBe(1)
|
|
130
|
+
expect(await readCount("dead"))->toBe(0)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await runUnderMemory(scenario)
|
|
134
|
+
await runUnderSqlite(scenario)
|
|
135
|
+
})
|
|
136
|
+
|
|
73
137
|
testPromise("EventLog: conflict detection works under both", async () => {
|
|
74
138
|
let scenario = async () => {
|
|
75
139
|
module TestBus = LocalBus.Make()
|