@reventlessdev/reventless-local 3.0.0-alpha.117 → 3.0.0-alpha.119
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 +15 -0
- package/package.json +8 -7
- package/rescript.json +2 -1
- package/src/Platform.res +44 -6
- package/src/Platform.res.mjs +53 -16
- package/src/adapter/Backend.res +23 -0
- package/src/adapter/Backend.res.mjs +23 -0
- package/src/adapter/BackendState.res +12 -7
- package/src/adapter/BackendState.res.mjs +18 -6
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
- package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
- package/src/adapter/LocalBus.res +1 -1
- package/src/adapter/PgProjectionCatchup.res +146 -0
- package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
- package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
- package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
- package/src/adapter/Task/LocalTaskBucket.res +1 -1
- package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
- package/src/components/Aggregate_Builder.res +1 -1
- package/src/components/Aggregate_Builder.res.mjs +3 -3
- package/src/components/AutomationSlice_Builder.res +1 -1
- package/src/components/AutomationSlice_Builder.res.mjs +3 -3
- package/src/components/Counter_Builder.res +1 -1
- package/src/components/Counter_Builder.res.mjs +2 -2
- package/src/components/DcbEventLog_Builder.res +1 -1
- package/src/components/DcbEventLog_Builder.res.mjs +2 -2
- package/src/components/InboundTranslationSlice_Builder.res +1 -1
- package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/OutboundTranslationSlice_Builder.res +1 -1
- package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
- package/src/components/Plugin_Builder.res +1 -1
- package/src/components/Plugin_Builder.res.mjs +2 -2
- package/src/components/ReadModel_Builder.res +1 -1
- package/src/components/ReadModel_Builder.res.mjs +2 -2
- package/src/components/StateViewSlice_Builder.res +1 -1
- package/src/components/StateViewSlice_Builder.res.mjs +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
- package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
- package/tests/adapter/BackendParityTest.res +43 -8
- package/tests/adapter/BackendParityTest.res.mjs +39 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
- package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
- package/tests/adapter/PgProjectionCatchupTest.res +149 -0
- package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
- package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
- package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
- package/tests/adapter/QueryDbListResolverTest.res +1 -1
- package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
- package/tests/adapter/QueryDbStorageTest.res +10 -10
- package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
- package/tests/adapter/QueryEngineTest.res +8 -8
- package/tests/adapter/QueryEngineTest.res.mjs +9 -9
- package/tests/components/querydb/QueryDbFixtures.res +1 -1
- package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
- package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
- package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +2 -2
|
@@ -322,21 +322,35 @@ function makeStorage(db, name, param, param$1, param$2) {
|
|
|
322
322
|
} catch (raw_msg) {
|
|
323
323
|
let msg = Primitive_exceptions.internalToException(raw_msg);
|
|
324
324
|
if (msg.RE_EXN_ID === "Failure") {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
325
|
+
let msg$1 = msg._1;
|
|
326
|
+
if (msg$1.includes("conflict")) {
|
|
327
|
+
return {
|
|
328
|
+
TAG: "Error",
|
|
329
|
+
_0: "Conflict"
|
|
330
|
+
};
|
|
331
|
+
} else {
|
|
332
|
+
return {
|
|
333
|
+
TAG: "Error",
|
|
334
|
+
_0: {
|
|
335
|
+
TAG: "StorageFailure",
|
|
336
|
+
_0: msg$1
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
}
|
|
329
340
|
}
|
|
330
|
-
let msg$
|
|
331
|
-
if (msg$
|
|
341
|
+
let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
|
|
342
|
+
if (msg$2.includes("constraint failed")) {
|
|
332
343
|
return {
|
|
333
344
|
TAG: "Error",
|
|
334
|
-
_0: "
|
|
345
|
+
_0: "Conflict"
|
|
335
346
|
};
|
|
336
347
|
} else {
|
|
337
348
|
return {
|
|
338
349
|
TAG: "Error",
|
|
339
|
-
_0:
|
|
350
|
+
_0: {
|
|
351
|
+
TAG: "StorageFailure",
|
|
352
|
+
_0: msg$2 === "" ? "storage error" : msg$2
|
|
353
|
+
}
|
|
340
354
|
};
|
|
341
355
|
}
|
|
342
356
|
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// DCB EventLog storage dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Selects the concrete backend at `make` time by consulting BackendState:
|
|
4
|
+
// - Sqlite → DcbEventLogStorage_Sqlite (true DCB semantics on SQL)
|
|
5
|
+
// - Postgres → ReventlessPostgres.DcbEventLogStorage_Postgres (exact DCB
|
|
6
|
+
// semantics: scoped advisory locks + atomic condition check)
|
|
7
|
+
// - Memory → DcbEventLogStorage_InMemory (pure in-memory)
|
|
8
|
+
//
|
|
9
|
+
// Registers each backend's read with the Bus so the LocalBus DCB event-tap can
|
|
10
|
+
// stream historical events uniformly.
|
|
11
|
+
|
|
12
|
+
module Make = (Bus: LocalBus.T) => {
|
|
13
|
+
let make: ReventlessCore.DcbEventLog_Adapter.storageMaker = (
|
|
14
|
+
~name,
|
|
15
|
+
~indexes,
|
|
16
|
+
~partitionTag,
|
|
17
|
+
~crossPartitionTagKeys as _=?,
|
|
18
|
+
~opts,
|
|
19
|
+
) => {
|
|
20
|
+
switch (BackendState.getSqliteDb(), BackendState.getPostgresPool()) {
|
|
21
|
+
| (Some(db), _) =>
|
|
22
|
+
let (storageName, read, storage) = DcbEventLogStorage_Sqlite.makeStorage(
|
|
23
|
+
~db,
|
|
24
|
+
~name,
|
|
25
|
+
~indexes,
|
|
26
|
+
~partitionTag,
|
|
27
|
+
~opts,
|
|
28
|
+
)
|
|
29
|
+
Bus.registerDcbEventLogRead(storageName, read)
|
|
30
|
+
storage
|
|
31
|
+
| (_, Some(pool)) =>
|
|
32
|
+
let (storageName, ops, storage) = ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
|
|
33
|
+
~pool,
|
|
34
|
+
~name,
|
|
35
|
+
~indexes,
|
|
36
|
+
~partitionTag,
|
|
37
|
+
~opts,
|
|
38
|
+
)
|
|
39
|
+
Bus.registerDcbEventLogRead(storageName, ops.read)
|
|
40
|
+
storage
|
|
41
|
+
| (None, None) =>
|
|
42
|
+
let (storageName, read, storage) = DcbEventLogStorage_InMemory.makeStorage(
|
|
43
|
+
~name,
|
|
44
|
+
~indexes,
|
|
45
|
+
~partitionTag,
|
|
46
|
+
~opts,
|
|
47
|
+
)
|
|
48
|
+
Bus.registerDcbEventLogRead(storageName, read)
|
|
49
|
+
storage
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
|
+
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
5
|
+
import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./DcbEventLogStorage_Sqlite.res.mjs";
|
|
6
|
+
import * as DcbEventLogStorage_InMemory$ReventlessLocal from "./DcbEventLogStorage_InMemory.res.mjs";
|
|
7
|
+
import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
8
|
+
|
|
9
|
+
function Make(Bus) {
|
|
10
|
+
let make = (name, indexes, partitionTag, param, opts) => {
|
|
11
|
+
let match = BackendState$ReventlessLocal.getSqliteDb();
|
|
12
|
+
let match$1 = BackendState$ReventlessLocal.getPostgresPool();
|
|
13
|
+
if (match !== undefined) {
|
|
14
|
+
let match$2 = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(match), name, indexes, partitionTag, opts);
|
|
15
|
+
Bus.registerDcbEventLogRead(match$2[0], match$2[1]);
|
|
16
|
+
return match$2[2];
|
|
17
|
+
}
|
|
18
|
+
if (match$1 !== undefined) {
|
|
19
|
+
let match$3 = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(Primitive_option.valFromOption(match$1), name, indexes, partitionTag, undefined, opts, undefined, undefined);
|
|
20
|
+
Bus.registerDcbEventLogRead(match$3[0], match$3[1].read);
|
|
21
|
+
return match$3[2];
|
|
22
|
+
}
|
|
23
|
+
let match$4 = DcbEventLogStorage_InMemory$ReventlessLocal.makeStorage(name, indexes, partitionTag, opts);
|
|
24
|
+
Bus.registerDcbEventLogRead(match$4[0], match$4[1]);
|
|
25
|
+
return match$4[2];
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
make: make
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
Make,
|
|
34
|
+
}
|
|
35
|
+
/* DcbEventLogStorage_Sqlite-ReventlessLocal Not a pure module */
|
|
@@ -105,22 +105,10 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
105
105
|
// Alias retained for tests that still want the pure-memory implementation.
|
|
106
106
|
let makeStorage = makeMemoryStorage
|
|
107
107
|
|
|
108
|
+
// Pure in-memory storageMaker (no BackendState dispatch, no Bus). Backend
|
|
109
|
+
// selection lives in LocalEventLogStorage.Make — this file holds only the
|
|
110
|
+
// in-memory implementation, as its name says.
|
|
108
111
|
let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
|
|
109
112
|
let (_, _, storage) = makeMemoryStorage(~name, ~opts)
|
|
110
113
|
storage
|
|
111
114
|
}
|
|
112
|
-
|
|
113
|
-
module Make = (Bus: LocalBus.T) => {
|
|
114
|
-
let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
|
|
115
|
-
switch BackendState.getDb() {
|
|
116
|
-
| Some(db) =>
|
|
117
|
-
let (storageName, replay, storage) = EventLogStorage_Sqlite.makeStorage(~db, ~name, ~opts)
|
|
118
|
-
Bus.registerEventLogReplay(storageName, replay)
|
|
119
|
-
storage
|
|
120
|
-
| None =>
|
|
121
|
-
let (storageName, replay, storage) = makeMemoryStorage(~name, ~opts)
|
|
122
|
-
Bus.registerEventLogReplay(storageName, replay)
|
|
123
|
-
storage
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
@@ -7,9 +7,6 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
7
7
|
import * as Effect from "effect/Effect";
|
|
8
8
|
import * as Stream$1 from "effect/Stream";
|
|
9
9
|
import * as Pulumi from "@pulumi/pulumi";
|
|
10
|
-
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
11
|
-
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
12
|
-
import * as EventLogStorage_Sqlite$ReventlessLocal from "./EventLogStorage_Sqlite.res.mjs";
|
|
13
10
|
|
|
14
11
|
function makeMemoryStorage(_name, param) {
|
|
15
12
|
let eventsRef = Effect.runSync(STM.commit(TRef.make({})));
|
|
@@ -106,29 +103,11 @@ function make(name, opts) {
|
|
|
106
103
|
return makeMemoryStorage(name, opts)[2];
|
|
107
104
|
}
|
|
108
105
|
|
|
109
|
-
function Make(Bus) {
|
|
110
|
-
let make = (name, opts) => {
|
|
111
|
-
let db = BackendState$ReventlessLocal.getDb();
|
|
112
|
-
if (db !== undefined) {
|
|
113
|
-
let match = EventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), name, opts);
|
|
114
|
-
Bus.registerEventLogReplay(match[0], match[1]);
|
|
115
|
-
return match[2];
|
|
116
|
-
}
|
|
117
|
-
let match$1 = makeMemoryStorage(name, opts);
|
|
118
|
-
Bus.registerEventLogReplay(match$1[0], match$1[1]);
|
|
119
|
-
return match$1[2];
|
|
120
|
-
};
|
|
121
|
-
return {
|
|
122
|
-
make: make
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
|
|
126
106
|
let makeStorage = makeMemoryStorage;
|
|
127
107
|
|
|
128
108
|
export {
|
|
129
109
|
makeMemoryStorage,
|
|
130
110
|
makeStorage,
|
|
131
111
|
make,
|
|
132
|
-
Make,
|
|
133
112
|
}
|
|
134
113
|
/* Stream Not a pure module */
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// EventLog storage dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Selects the concrete backend at `make` time by consulting BackendState, so the
|
|
4
|
+
// core EventLog builder wires a single storageMaker module regardless of backend:
|
|
5
|
+
// - Sqlite → EventLogStorage_Sqlite (on-disk / :memory:)
|
|
6
|
+
// - Postgres → ReventlessPostgres.EventLogStorage_Postgres (durable, async)
|
|
7
|
+
// - Memory → EventLogStorage_InMemory (pure in-memory)
|
|
8
|
+
//
|
|
9
|
+
// The `Make(Bus)` functor also registers each backend's replay with the Bus so
|
|
10
|
+
// the LocalBus event-tap can stream historical events uniformly.
|
|
11
|
+
|
|
12
|
+
module Make = (Bus: LocalBus.T) => {
|
|
13
|
+
let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
|
|
14
|
+
switch (BackendState.getSqliteDb(), BackendState.getPostgresPool()) {
|
|
15
|
+
| (Some(db), _) =>
|
|
16
|
+
let (storageName, replay, storage) = EventLogStorage_Sqlite.makeStorage(~db, ~name, ~opts)
|
|
17
|
+
Bus.registerEventLogReplay(storageName, replay)
|
|
18
|
+
storage
|
|
19
|
+
| (_, Some(pool)) =>
|
|
20
|
+
let (storageName, ops, storage) = ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
|
|
21
|
+
~pool,
|
|
22
|
+
~name,
|
|
23
|
+
~opts,
|
|
24
|
+
)
|
|
25
|
+
Bus.registerEventLogReplay(storageName, ops.replay)
|
|
26
|
+
storage
|
|
27
|
+
| (None, None) =>
|
|
28
|
+
let (storageName, replay, storage) = EventLogStorage_InMemory.makeMemoryStorage(~name, ~opts)
|
|
29
|
+
Bus.registerEventLogReplay(storageName, replay)
|
|
30
|
+
storage
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
|
+
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
5
|
+
import * as EventLogStorage_Sqlite$ReventlessLocal from "./EventLogStorage_Sqlite.res.mjs";
|
|
6
|
+
import * as EventLogStorage_InMemory$ReventlessLocal from "./EventLogStorage_InMemory.res.mjs";
|
|
7
|
+
import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
|
|
8
|
+
|
|
9
|
+
function Make(Bus) {
|
|
10
|
+
let make = (name, opts) => {
|
|
11
|
+
let match = BackendState$ReventlessLocal.getSqliteDb();
|
|
12
|
+
let match$1 = BackendState$ReventlessLocal.getPostgresPool();
|
|
13
|
+
if (match !== undefined) {
|
|
14
|
+
let match$2 = EventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(match), name, opts);
|
|
15
|
+
Bus.registerEventLogReplay(match$2[0], match$2[1]);
|
|
16
|
+
return match$2[2];
|
|
17
|
+
}
|
|
18
|
+
if (match$1 !== undefined) {
|
|
19
|
+
let match$3 = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(Primitive_option.valFromOption(match$1), name, opts, undefined);
|
|
20
|
+
Bus.registerEventLogReplay(match$3[0], match$3[1].replay);
|
|
21
|
+
return match$3[2];
|
|
22
|
+
}
|
|
23
|
+
let match$4 = EventLogStorage_InMemory$ReventlessLocal.makeMemoryStorage(name, opts);
|
|
24
|
+
Bus.registerEventLogReplay(match$4[0], match$4[1]);
|
|
25
|
+
return match$4[2];
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
make: make
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
Make,
|
|
34
|
+
}
|
|
35
|
+
/* EventLogStorage_Sqlite-ReventlessLocal Not a pure module */
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -145,7 +145,7 @@ module type T = {
|
|
|
145
145
|
let registerCommandHandler: (string, (JSON.t, unit) => promise<unit>) => unit
|
|
146
146
|
|
|
147
147
|
// QueryDb registry: read model name → storage ops and scan function
|
|
148
|
-
// Populated by
|
|
148
|
+
// Populated by LocalQueryDbStorage.Make(Bus) during component construction.
|
|
149
149
|
let registerQueryDb: (string, ReventlessCore.QueryDb_Adapter.operations) => unit
|
|
150
150
|
let getQueryDb: string => option<ReventlessCore.QueryDb_Adapter.operations>
|
|
151
151
|
let registerQueryDbScan: (string, unit => array<JSON.t>) => unit
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Startup projection catch-up for the Postgres backend.
|
|
2
|
+
//
|
|
3
|
+
// Under Backend.Postgres the event logs (classic + DCB) persist durably, but read
|
|
4
|
+
// models run on the in-memory live-query arm (LocalQueryDbStorage — the sync
|
|
5
|
+
// LocalBus registrations can't consume async pg). So on every startup the
|
|
6
|
+
// in-memory read models begin EMPTY and must be rebuilt by replaying the durable
|
|
7
|
+
// event log through the projection handlers — read models are derived data, the
|
|
8
|
+
// log is the source of truth.
|
|
9
|
+
//
|
|
10
|
+
// Unlike the SQLite ProjectionCheckpoint, there is no persisted checkpoint here:
|
|
11
|
+
// in-memory read models always start at position 0, so the full history
|
|
12
|
+
// `(0, head]` is replayed once. The `head` bound is captured BEFORE the plugins
|
|
13
|
+
// build (and before this session's Connect dispatches / commands append), so this
|
|
14
|
+
// session's own events — which are live-delivered — are never redelivered.
|
|
15
|
+
//
|
|
16
|
+
// The {id, meta, event} envelopes are reconstructed by ProjectionCheckpoint's
|
|
17
|
+
// builders so they match live topic delivery byte-for-byte.
|
|
18
|
+
|
|
19
|
+
module Pg = ReventlessPostgres.PgDriver
|
|
20
|
+
|
|
21
|
+
// Head positions captured before the session appends anything.
|
|
22
|
+
type bounds = {aggBound: int, dcbBound: int}
|
|
23
|
+
|
|
24
|
+
let intCol = (row: dict<JSON.t>, key: string): int =>
|
|
25
|
+
switch row->Dict.get(key) {
|
|
26
|
+
| Some(JSON.Number(n)) => Float.toInt(n)
|
|
27
|
+
| Some(JSON.String(s)) => s->Int.fromString->Option.getOr(0)
|
|
28
|
+
| _ => 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Snapshot the current head of each axis. Dispatched before plugin build so the
|
|
32
|
+
// bound excludes this session's live-delivered appends.
|
|
33
|
+
let captureBounds = async (pool: Pg.pool): bounds => {
|
|
34
|
+
let agg = switch await pool->Pg.queryOne(
|
|
35
|
+
"SELECT COALESCE(MAX(global_seq), 0)::bigint AS m FROM event_log",
|
|
36
|
+
[],
|
|
37
|
+
) {
|
|
38
|
+
| Some(row) => intCol(row, "m")
|
|
39
|
+
| None => 0
|
|
40
|
+
}
|
|
41
|
+
let dcb = switch await pool->Pg.queryOne(
|
|
42
|
+
"SELECT COALESCE(MAX(position), 0)::bigint AS m FROM dcb_event",
|
|
43
|
+
[],
|
|
44
|
+
) {
|
|
45
|
+
| Some(row) => intCol(row, "m")
|
|
46
|
+
| None => 0
|
|
47
|
+
}
|
|
48
|
+
{aggBound: agg, dcbBound: dcb}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// First tag value of a DCB event's text[] `tags` column ('key=value' → value),
|
|
52
|
+
// reproducing publish-time entityId derivation (the first tag).
|
|
53
|
+
let firstTagValue = (row: dict<JSON.t>): option<string> =>
|
|
54
|
+
switch row->Dict.get("tags") {
|
|
55
|
+
| Some(JSON.Array(arr)) =>
|
|
56
|
+
switch arr->Array.get(0) {
|
|
57
|
+
| Some(JSON.String(kv)) =>
|
|
58
|
+
switch kv->String.indexOf("=") {
|
|
59
|
+
| -1 => Some(kv)
|
|
60
|
+
| i => Some(kv->String.slice(~start=i + 1, ~end=kv->String.length))
|
|
61
|
+
}
|
|
62
|
+
| _ => None
|
|
63
|
+
}
|
|
64
|
+
| _ => None
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Deliver `envelope` to every handler in registration order (handlers dispatch by
|
|
68
|
+
// meta.service and no-op on events none of their mappings consume, so delivering
|
|
69
|
+
// both axes to every projection is safe — same at-least-once contract the
|
|
70
|
+
// deployed adapters impose).
|
|
71
|
+
let deliver = async (
|
|
72
|
+
handlers: array<(string, ProjectionCheckpoint.catchupHandler)>,
|
|
73
|
+
envelope: JSON.t,
|
|
74
|
+
) =>
|
|
75
|
+
for i in 0 to handlers->Array.length - 1 {
|
|
76
|
+
let (_name, handler) = handlers->Array.getUnsafe(i)
|
|
77
|
+
switch await handler(envelope, ()) {
|
|
78
|
+
| () => ()
|
|
79
|
+
| exception _ => ()
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let runCatchup = async (
|
|
84
|
+
~pool: Pg.pool,
|
|
85
|
+
~bounds: bounds,
|
|
86
|
+
~handlers: array<(string, ProjectionCheckpoint.catchupHandler)>,
|
|
87
|
+
) => {
|
|
88
|
+
// --- Aggregate axis: flat stored events, global_seq order ---
|
|
89
|
+
let aggRows = await pool->Pg.query(
|
|
90
|
+
"SELECT payload FROM event_log WHERE global_seq <= $1::bigint ORDER BY global_seq ASC",
|
|
91
|
+
[JSON.Encode.int(bounds.aggBound)],
|
|
92
|
+
)
|
|
93
|
+
for i in 0 to aggRows->Array.length - 1 {
|
|
94
|
+
let row = aggRows->Array.getUnsafe(i)
|
|
95
|
+
switch row->Dict.get("payload") {
|
|
96
|
+
| Some(payload) =>
|
|
97
|
+
switch ProjectionCheckpoint.catchupEnvelope(payload) {
|
|
98
|
+
| Some(envelope) => await deliver(handlers, envelope)
|
|
99
|
+
| None => ()
|
|
100
|
+
}
|
|
101
|
+
| None => ()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// --- DCB axis: reconstruct the published envelope, position order ---
|
|
106
|
+
let dcbRows = await pool->Pg.query(
|
|
107
|
+
"SELECT log_name, event_type, data, meta, tags FROM dcb_event WHERE position <= $1::bigint ORDER BY position ASC",
|
|
108
|
+
[JSON.Encode.int(bounds.dcbBound)],
|
|
109
|
+
)
|
|
110
|
+
for i in 0 to dcbRows->Array.length - 1 {
|
|
111
|
+
let row = dcbRows->Array.getUnsafe(i)
|
|
112
|
+
switch (
|
|
113
|
+
row->Dict.get("log_name"),
|
|
114
|
+
row->Dict.get("event_type"),
|
|
115
|
+
row->Dict.get("data"),
|
|
116
|
+
row->Dict.get("meta"),
|
|
117
|
+
) {
|
|
118
|
+
| (Some(JSON.String(logName)), Some(JSON.String(eventType)), Some(data), Some(meta)) =>
|
|
119
|
+
switch ProjectionCheckpoint.dcbCatchupEnvelope(
|
|
120
|
+
~logName,
|
|
121
|
+
~eventType,
|
|
122
|
+
~dataText=JSON.stringify(data),
|
|
123
|
+
~metaText=JSON.stringify(meta),
|
|
124
|
+
~firstTagValue=firstTagValue(row),
|
|
125
|
+
) {
|
|
126
|
+
| Some(envelope) => await deliver(handlers, envelope)
|
|
127
|
+
| None => ()
|
|
128
|
+
}
|
|
129
|
+
| _ => ()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Platform entry point: waits one macrotask so the just-built plugins' Output.apply
|
|
135
|
+
// chains register their collector handlers (mirrors ProjectionCheckpoint.startupCatchup),
|
|
136
|
+
// then replays the captured history.
|
|
137
|
+
let startupCatchup = async (
|
|
138
|
+
~pool: Pg.pool,
|
|
139
|
+
~bounds: bounds,
|
|
140
|
+
~handlers: unit => array<(string, ProjectionCheckpoint.catchupHandler)>,
|
|
141
|
+
) => {
|
|
142
|
+
await Promise.make((resolve, _) => {
|
|
143
|
+
let _ = setTimeout(() => resolve(), 0)
|
|
144
|
+
})
|
|
145
|
+
await runCatchup(~pool, ~bounds, ~handlers=handlers())
|
|
146
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
|
|
6
|
+
import * as ProjectionCheckpoint$ReventlessLocal from "./ProjectionCheckpoint.res.mjs";
|
|
7
|
+
|
|
8
|
+
function intCol(row, key) {
|
|
9
|
+
let match = row[key];
|
|
10
|
+
if (match === undefined) {
|
|
11
|
+
return 0;
|
|
12
|
+
}
|
|
13
|
+
switch (typeof match) {
|
|
14
|
+
case "string" :
|
|
15
|
+
return Stdlib_Option.getOr(Stdlib_Int.fromString(match, undefined), 0);
|
|
16
|
+
case "number" :
|
|
17
|
+
return match | 0;
|
|
18
|
+
default:
|
|
19
|
+
return 0;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function captureBounds(pool) {
|
|
24
|
+
let row = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT COALESCE(MAX(global_seq), 0)::bigint AS m FROM event_log", []);
|
|
25
|
+
let agg = row !== undefined ? intCol(row, "m") : 0;
|
|
26
|
+
let row$1 = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT COALESCE(MAX(position), 0)::bigint AS m FROM dcb_event", []);
|
|
27
|
+
let dcb = row$1 !== undefined ? intCol(row$1, "m") : 0;
|
|
28
|
+
return {
|
|
29
|
+
aggBound: agg,
|
|
30
|
+
dcbBound: dcb
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function firstTagValue(row) {
|
|
35
|
+
let match = row["tags"];
|
|
36
|
+
if (match === undefined) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(match)) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let match$1 = match[0];
|
|
43
|
+
if (match$1 === undefined) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (typeof match$1 !== "string") {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
let i = match$1.indexOf("=");
|
|
50
|
+
if (i !== -1) {
|
|
51
|
+
return match$1.slice(i + 1 | 0, match$1.length);
|
|
52
|
+
} else {
|
|
53
|
+
return match$1;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function deliver(handlers, envelope) {
|
|
58
|
+
for (let i = 0, i_finish = handlers.length; i < i_finish; ++i) {
|
|
59
|
+
let match = handlers[i];
|
|
60
|
+
try {
|
|
61
|
+
await match[1](envelope, undefined);
|
|
62
|
+
} catch (exn) {
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runCatchup(pool, bounds, handlers) {
|
|
69
|
+
let aggRows = await PgDriver$ReventlessPostgres.query(pool, "SELECT payload FROM event_log WHERE global_seq <= $1::bigint ORDER BY global_seq ASC", [bounds.aggBound]);
|
|
70
|
+
for (let i = 0, i_finish = aggRows.length; i < i_finish; ++i) {
|
|
71
|
+
let row = aggRows[i];
|
|
72
|
+
let payload = row["payload"];
|
|
73
|
+
if (payload !== undefined) {
|
|
74
|
+
let envelope = ProjectionCheckpoint$ReventlessLocal.catchupEnvelope(payload);
|
|
75
|
+
if (envelope !== undefined) {
|
|
76
|
+
await deliver(handlers, envelope);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
let dcbRows = await PgDriver$ReventlessPostgres.query(pool, "SELECT log_name, event_type, data, meta, tags FROM dcb_event WHERE position <= $1::bigint ORDER BY position ASC", [bounds.dcbBound]);
|
|
81
|
+
for (let i$1 = 0, i_finish$1 = dcbRows.length; i$1 < i_finish$1; ++i$1) {
|
|
82
|
+
let row$1 = dcbRows[i$1];
|
|
83
|
+
let match = row$1["log_name"];
|
|
84
|
+
let match$1 = row$1["event_type"];
|
|
85
|
+
let match$2 = row$1["data"];
|
|
86
|
+
let match$3 = row$1["meta"];
|
|
87
|
+
if (typeof match === "string" && typeof match$1 === "string" && match$2 !== undefined && match$3 !== undefined) {
|
|
88
|
+
let envelope$1 = ProjectionCheckpoint$ReventlessLocal.dcbCatchupEnvelope(match, match$1, JSON.stringify(match$2), JSON.stringify(match$3), firstTagValue(row$1));
|
|
89
|
+
if (envelope$1 !== undefined) {
|
|
90
|
+
await deliver(handlers, envelope$1);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function startupCatchup(pool, bounds, handlers) {
|
|
97
|
+
await new Promise((resolve, param) => {
|
|
98
|
+
setTimeout(() => resolve(), 0);
|
|
99
|
+
});
|
|
100
|
+
return await runCatchup(pool, bounds, handlers());
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let Pg;
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
Pg,
|
|
107
|
+
intCol,
|
|
108
|
+
captureBounds,
|
|
109
|
+
firstTagValue,
|
|
110
|
+
deliver,
|
|
111
|
+
runCatchup,
|
|
112
|
+
startupCatchup,
|
|
113
|
+
}
|
|
114
|
+
/* PgDriver-ReventlessPostgres Not a pure module */
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// QueryDb storage dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Selects the concrete backend at `make` time by consulting BackendState:
|
|
4
|
+
// - Sqlite → QueryDbStorage_Sqlite (JSON tables + json_extract indexes)
|
|
5
|
+
// - Memory → QueryDbStorage_InMemory (pure in-memory, Bus-registered)
|
|
6
|
+
//
|
|
7
|
+
// Postgres note: the LocalBus scan/stream/index registrations are SYNCHRONOUS
|
|
8
|
+
// (`unit => array<JSON.t>`), which async pg cannot satisfy without a broader bus
|
|
9
|
+
// refactor. So under Backend.Postgres the read-model live-query path uses the
|
|
10
|
+
// in-memory arm (getDb() is None for Postgres → Memory), rebuilt by replay from
|
|
11
|
+
// the durable pg event log; the deploy-usable ReventlessPostgres.QueryDbStorage_Postgres
|
|
12
|
+
// serves non-local compute layers. See docs/plans/postgres-storage-adapter.md §E.
|
|
13
|
+
|
|
14
|
+
module Make = (Bus: LocalBus.T) => {
|
|
15
|
+
module Mem = QueryDbStorage_InMemory.Make(Bus)
|
|
16
|
+
|
|
17
|
+
type api = unit
|
|
18
|
+
type role = unit
|
|
19
|
+
|
|
20
|
+
let sqliteBusCallbacks: QueryDbStorage_Sqlite.busCallbacks = {
|
|
21
|
+
publishStateChange: Bus.publishStateChange,
|
|
22
|
+
registerQueryDb: Bus.registerQueryDb,
|
|
23
|
+
registerQueryDbScan: Bus.registerQueryDbScan,
|
|
24
|
+
registerQueryDbStream: Bus.registerQueryDbStream,
|
|
25
|
+
registerQueryDbIndexLookup: Bus.registerQueryDbIndexLookup,
|
|
26
|
+
registerQueryDbListPage: Bus.registerQueryDbListPage,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let make: ReventlessCore.QueryDb_Adapter.storageMaker<unit, unit> = (
|
|
30
|
+
~name,
|
|
31
|
+
~indexes,
|
|
32
|
+
~subIdField=?,
|
|
33
|
+
~ttl=?,
|
|
34
|
+
~api,
|
|
35
|
+
~apiRole,
|
|
36
|
+
~opts,
|
|
37
|
+
) =>
|
|
38
|
+
switch BackendState.getSqliteDb() {
|
|
39
|
+
| Some(db) =>
|
|
40
|
+
QueryDbStorage_Sqlite.makeStorage(~db, ~bus=sqliteBusCallbacks, ~name, ~indexes, ~subIdField)
|
|
41
|
+
| None => Mem.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
|
+
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
5
|
+
import * as QueryDbStorage_Sqlite$ReventlessLocal from "./QueryDbStorage_Sqlite.res.mjs";
|
|
6
|
+
import * as QueryDbStorage_InMemory$ReventlessLocal from "./QueryDbStorage_InMemory.res.mjs";
|
|
7
|
+
|
|
8
|
+
function Make(Bus) {
|
|
9
|
+
let Mem = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
|
|
10
|
+
let sqliteBusCallbacks_publishStateChange = Bus.publishStateChange;
|
|
11
|
+
let sqliteBusCallbacks_registerQueryDb = Bus.registerQueryDb;
|
|
12
|
+
let sqliteBusCallbacks_registerQueryDbScan = Bus.registerQueryDbScan;
|
|
13
|
+
let sqliteBusCallbacks_registerQueryDbStream = Bus.registerQueryDbStream;
|
|
14
|
+
let sqliteBusCallbacks_registerQueryDbIndexLookup = Bus.registerQueryDbIndexLookup;
|
|
15
|
+
let sqliteBusCallbacks_registerQueryDbListPage = Bus.registerQueryDbListPage;
|
|
16
|
+
let sqliteBusCallbacks = {
|
|
17
|
+
publishStateChange: sqliteBusCallbacks_publishStateChange,
|
|
18
|
+
registerQueryDb: sqliteBusCallbacks_registerQueryDb,
|
|
19
|
+
registerQueryDbScan: sqliteBusCallbacks_registerQueryDbScan,
|
|
20
|
+
registerQueryDbStream: sqliteBusCallbacks_registerQueryDbStream,
|
|
21
|
+
registerQueryDbIndexLookup: sqliteBusCallbacks_registerQueryDbIndexLookup,
|
|
22
|
+
registerQueryDbListPage: sqliteBusCallbacks_registerQueryDbListPage
|
|
23
|
+
};
|
|
24
|
+
let make = (name, indexes, subIdField, ttl, api, apiRole, opts) => {
|
|
25
|
+
let db = BackendState$ReventlessLocal.getSqliteDb();
|
|
26
|
+
if (db !== undefined) {
|
|
27
|
+
return QueryDbStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), sqliteBusCallbacks, name, indexes, subIdField);
|
|
28
|
+
} else {
|
|
29
|
+
return Mem.make(name, indexes, subIdField, ttl, api, apiRole, opts);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
Mem: Mem,
|
|
34
|
+
sqliteBusCallbacks: sqliteBusCallbacks,
|
|
35
|
+
make: make
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
Make,
|
|
41
|
+
}
|
|
42
|
+
/* QueryDbStorage_Sqlite-ReventlessLocal Not a pure module */
|
|
@@ -64,16 +64,9 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
64
64
|
type api = unit
|
|
65
65
|
type role = unit
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
registerQueryDbScan: Bus.registerQueryDbScan,
|
|
71
|
-
registerQueryDbStream: Bus.registerQueryDbStream,
|
|
72
|
-
registerQueryDbIndexLookup: Bus.registerQueryDbIndexLookup,
|
|
73
|
-
registerQueryDbListPage: Bus.registerQueryDbListPage,
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
let makeMemory: QueryDb_Adapter.storageMaker<unit, unit> = (
|
|
67
|
+
// Pure in-memory storageMaker (registers with Bus for live reads). Backend
|
|
68
|
+
// selection lives in LocalQueryDbStorage.Make.
|
|
69
|
+
let make: QueryDb_Adapter.storageMaker<unit, unit> = (
|
|
77
70
|
~name,
|
|
78
71
|
~indexes as _,
|
|
79
72
|
~subIdField=?,
|
|
@@ -343,18 +336,4 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
343
336
|
}
|
|
344
337
|
}
|
|
345
338
|
|
|
346
|
-
let make: QueryDb_Adapter.storageMaker<unit, unit> = (
|
|
347
|
-
~name,
|
|
348
|
-
~indexes,
|
|
349
|
-
~subIdField=?,
|
|
350
|
-
~ttl=?,
|
|
351
|
-
~api,
|
|
352
|
-
~apiRole,
|
|
353
|
-
~opts,
|
|
354
|
-
) =>
|
|
355
|
-
switch BackendState.getDb() {
|
|
356
|
-
| Some(db) =>
|
|
357
|
-
QueryDbStorage_Sqlite.makeStorage(~db, ~bus=sqliteBusCallbacks, ~name, ~indexes, ~subIdField)
|
|
358
|
-
| None => makeMemory(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
|
|
359
|
-
}
|
|
360
339
|
}
|