@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
|
@@ -1,13 +1,20 @@
|
|
|
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 Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
7
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
3
8
|
import * as Effect from "effect/Effect";
|
|
4
|
-
import * as Stream from "effect/Stream";
|
|
9
|
+
import * as Stream$1 from "effect/Stream";
|
|
5
10
|
import * as Pulumi from "@pulumi/pulumi";
|
|
6
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
7
12
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
13
|
+
import * as ProjectionPending$ReventlessLocal from "../ProjectionPending.res.mjs";
|
|
8
14
|
|
|
9
15
|
function ensureSchema(db) {
|
|
10
16
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS event_log (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, payload TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id, seq_nr))");
|
|
17
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS snapshot (log_name TEXT NOT NULL, aggregate_id TEXT NOT NULL, seq_nr INTEGER NOT NULL, state TEXT NOT NULL, schema_hash TEXT NOT NULL, PRIMARY KEY (log_name, aggregate_id))");
|
|
11
18
|
}
|
|
12
19
|
|
|
13
20
|
function decodePayload(row) {
|
|
@@ -26,7 +33,7 @@ function decodePayload(row) {
|
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
function countAll(db) {
|
|
29
|
-
|
|
36
|
+
ensureSchema(db);
|
|
30
37
|
let row = SqliteDriver$ReventlessLocal.get(SqliteDriver$ReventlessLocal.prepare(db, "SELECT COUNT(*) AS c FROM event_log"), []);
|
|
31
38
|
if (row === undefined) {
|
|
32
39
|
return 0;
|
|
@@ -40,12 +47,15 @@ function countAll(db) {
|
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
function makeStorage(db, name, param) {
|
|
43
|
-
|
|
50
|
+
ensureSchema(db);
|
|
44
51
|
let insertStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO event_log(log_name, aggregate_id, seq_nr, payload) VALUES(?,?,?,?)");
|
|
45
|
-
let
|
|
46
|
-
let selectByIdStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT payload FROM event_log WHERE log_name=? AND aggregate_id=? ORDER BY seq_nr ASC");
|
|
47
|
-
let
|
|
48
|
-
|
|
52
|
+
let nextSeqStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT COALESCE(MAX(seq_nr), -1) + 1 AS c FROM event_log WHERE log_name=? AND aggregate_id=?");
|
|
53
|
+
let selectByIdStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT payload FROM event_log WHERE log_name=? AND aggregate_id=? AND seq_nr >= ? ORDER BY seq_nr ASC");
|
|
54
|
+
let lastRowidStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT last_insert_rowid() AS r");
|
|
55
|
+
let upsertSnapshotStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT OR REPLACE INTO snapshot(log_name, aggregate_id, seq_nr, state, schema_hash) VALUES(?,?,?,?,?)");
|
|
56
|
+
let selectSnapshotStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT seq_nr, state, schema_hash FROM snapshot WHERE log_name=? AND aggregate_id=?");
|
|
57
|
+
let expectedNextSeq = id => {
|
|
58
|
+
let row = SqliteDriver$ReventlessLocal.get(nextSeqStmt, [
|
|
49
59
|
name,
|
|
50
60
|
id
|
|
51
61
|
]);
|
|
@@ -59,10 +69,10 @@ function makeStorage(db, name, param) {
|
|
|
59
69
|
return 0;
|
|
60
70
|
}
|
|
61
71
|
};
|
|
62
|
-
let
|
|
72
|
+
let appendTracked = async (track, seqNr, id, jsons) => {
|
|
63
73
|
try {
|
|
64
|
-
SqliteDriver$ReventlessLocal.transaction(db, () => {
|
|
65
|
-
let existing =
|
|
74
|
+
let lastRowid = SqliteDriver$ReventlessLocal.transaction(db, () => {
|
|
75
|
+
let existing = expectedNextSeq(id);
|
|
66
76
|
if (seqNr !== existing) {
|
|
67
77
|
throw {
|
|
68
78
|
RE_EXN_ID: "Failure",
|
|
@@ -76,7 +86,24 @@ function makeStorage(db, name, param) {
|
|
|
76
86
|
seqNr + i | 0,
|
|
77
87
|
JSON.stringify(json)
|
|
78
88
|
]));
|
|
89
|
+
let row = SqliteDriver$ReventlessLocal.get(lastRowidStmt, []);
|
|
90
|
+
if (row === undefined) {
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
let match = row["r"];
|
|
94
|
+
if (typeof match === "number") {
|
|
95
|
+
return match | 0;
|
|
96
|
+
} else {
|
|
97
|
+
return 0;
|
|
98
|
+
}
|
|
79
99
|
});
|
|
100
|
+
if (track && lastRowid > 0) {
|
|
101
|
+
let count = jsons.length;
|
|
102
|
+
ProjectionPending$ReventlessLocal.trackAppended("Aggregate", Stdlib_Array.filterMap(jsons.map((json, i) => Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string), msgId => [
|
|
103
|
+
msgId,
|
|
104
|
+
((lastRowid - count | 0) + 1 | 0) + i | 0
|
|
105
|
+
])), x => x));
|
|
106
|
+
}
|
|
80
107
|
return {
|
|
81
108
|
TAG: "Ok",
|
|
82
109
|
_0: undefined
|
|
@@ -84,39 +111,120 @@ function makeStorage(db, name, param) {
|
|
|
84
111
|
} catch (raw_msg) {
|
|
85
112
|
let msg = Primitive_exceptions.internalToException(raw_msg);
|
|
86
113
|
if (msg.RE_EXN_ID === "Failure") {
|
|
114
|
+
let msg$1 = msg._1;
|
|
115
|
+
if (msg$1 === "conflict") {
|
|
116
|
+
return {
|
|
117
|
+
TAG: "Error",
|
|
118
|
+
_0: "Conflict"
|
|
119
|
+
};
|
|
120
|
+
} else {
|
|
121
|
+
return {
|
|
122
|
+
TAG: "Error",
|
|
123
|
+
_0: {
|
|
124
|
+
TAG: "StorageFailure",
|
|
125
|
+
_0: msg$1
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
|
|
131
|
+
if (msg$2.includes("constraint failed")) {
|
|
87
132
|
return {
|
|
88
133
|
TAG: "Error",
|
|
89
|
-
_0:
|
|
134
|
+
_0: "Conflict"
|
|
90
135
|
};
|
|
91
136
|
} else {
|
|
92
137
|
return {
|
|
93
138
|
TAG: "Error",
|
|
94
|
-
_0:
|
|
139
|
+
_0: {
|
|
140
|
+
TAG: "StorageFailure",
|
|
141
|
+
_0: msg$2 === "" ? "storage error" : msg$2
|
|
142
|
+
}
|
|
95
143
|
};
|
|
96
144
|
}
|
|
97
145
|
}
|
|
98
146
|
};
|
|
99
|
-
let
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
contents: startingSeqNr
|
|
108
|
-
};
|
|
109
|
-
return Stream.runForEach(stream, json => {
|
|
110
|
-
let currentSeq = seqNrRef.contents;
|
|
111
|
-
return Effect.flatMap(Effect.promise(() => append(currentSeq, id, [json])), result => {
|
|
112
|
-
if (result.TAG !== "Ok") {
|
|
113
|
-
return Effect.fail(result._0);
|
|
114
|
-
}
|
|
115
|
-
seqNrRef.contents = currentSeq + 1 | 0;
|
|
116
|
-
return Effect.succeed();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
147
|
+
let append = (seqNr, id, jsons) => appendTracked(true, seqNr, id, jsons);
|
|
148
|
+
let replayArray = (id, fromSeqOpt) => {
|
|
149
|
+
let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
|
|
150
|
+
return SqliteDriver$ReventlessLocal.all(selectByIdStmt, [
|
|
151
|
+
name,
|
|
152
|
+
id,
|
|
153
|
+
fromSeq
|
|
154
|
+
]).map(decodePayload);
|
|
119
155
|
};
|
|
156
|
+
let replay = async id => replayArray(id, undefined);
|
|
157
|
+
let replayStream = (id, fromSeq) => Stream$1.fromIterable(replayArray(id, fromSeq));
|
|
158
|
+
let latestSnapshot = async id => {
|
|
159
|
+
try {
|
|
160
|
+
let row = SqliteDriver$ReventlessLocal.get(selectSnapshotStmt, [
|
|
161
|
+
name,
|
|
162
|
+
id
|
|
163
|
+
]);
|
|
164
|
+
if (row === undefined) {
|
|
165
|
+
return {
|
|
166
|
+
TAG: "Ok",
|
|
167
|
+
_0: undefined
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
let match = row["seq_nr"];
|
|
171
|
+
let match$1 = row["state"];
|
|
172
|
+
let match$2 = row["schema_hash"];
|
|
173
|
+
if (typeof match === "number" && typeof match$1 === "string" && typeof match$2 === "string") {
|
|
174
|
+
return {
|
|
175
|
+
TAG: "Ok",
|
|
176
|
+
_0: {
|
|
177
|
+
seqNr: match | 0,
|
|
178
|
+
state: JSON.parse(match$1),
|
|
179
|
+
schemaHash: match$2
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
} else {
|
|
183
|
+
return {
|
|
184
|
+
TAG: "Error",
|
|
185
|
+
_0: "snapshot row has unexpected shape"
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
} catch (raw_exn) {
|
|
189
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
190
|
+
return {
|
|
191
|
+
TAG: "Error",
|
|
192
|
+
_0: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "snapshot read error")
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
let writeSnapshot = async (id, snap) => {
|
|
197
|
+
try {
|
|
198
|
+
SqliteDriver$ReventlessLocal.run(upsertSnapshotStmt, [
|
|
199
|
+
name,
|
|
200
|
+
id,
|
|
201
|
+
snap.seqNr,
|
|
202
|
+
JSON.stringify(snap.state),
|
|
203
|
+
snap.schemaHash
|
|
204
|
+
]);
|
|
205
|
+
return {
|
|
206
|
+
TAG: "Ok",
|
|
207
|
+
_0: undefined
|
|
208
|
+
};
|
|
209
|
+
} catch (raw_exn) {
|
|
210
|
+
let exn = Primitive_exceptions.internalToException(raw_exn);
|
|
211
|
+
return {
|
|
212
|
+
TAG: "Error",
|
|
213
|
+
_0: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "snapshot write error")
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
let appendStream = (startingSeqNr, id, stream) => Effect.flatMap(Stream.runCollect(stream), jsons => Effect.flatMap(Effect.promise(() => appendTracked(false, startingSeqNr, id, jsons)), result => {
|
|
218
|
+
if (result.TAG === "Ok") {
|
|
219
|
+
return Effect.succeed();
|
|
220
|
+
}
|
|
221
|
+
let msg = result._0;
|
|
222
|
+
if (typeof msg !== "object") {
|
|
223
|
+
return Effect.fail("conflict");
|
|
224
|
+
} else {
|
|
225
|
+
return Effect.fail(msg._0);
|
|
226
|
+
}
|
|
227
|
+
}));
|
|
120
228
|
return [
|
|
121
229
|
name,
|
|
122
230
|
replay,
|
|
@@ -126,7 +234,9 @@ function makeStorage(db, name, param) {
|
|
|
126
234
|
append: append,
|
|
127
235
|
replay: replay,
|
|
128
236
|
replayStream: replayStream,
|
|
129
|
-
appendStream: appendStream
|
|
237
|
+
appendStream: appendStream,
|
|
238
|
+
latestSnapshot: latestSnapshot,
|
|
239
|
+
writeSnapshot: writeSnapshot
|
|
130
240
|
})
|
|
131
241
|
}
|
|
132
242
|
];
|
|
@@ -152,4 +262,4 @@ export {
|
|
|
152
262
|
makeStorage,
|
|
153
263
|
Make,
|
|
154
264
|
}
|
|
155
|
-
/*
|
|
265
|
+
/* Stream Not a pure module */
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -154,6 +154,33 @@ module type T = {
|
|
|
154
154
|
// Used by LocalQueryEngine to honour ~limit via Stream.take without loading all items.
|
|
155
155
|
let registerQueryDbStream: (string, unit => Stream.t<JSON.t, string, unit>) => unit
|
|
156
156
|
let getQueryDbStream: string => option<unit => Stream.t<JSON.t, string, unit>>
|
|
157
|
+
// Indexed equality lookup: (field, value) → matching items. The SQLite backend
|
|
158
|
+
// registers a closure that pushes the predicate down to a `json_extract` GSI
|
|
159
|
+
// index instead of materialising + parsing the whole table; the in-memory
|
|
160
|
+
// backend registers a scan+filter. The `{name}By{Index}` resolvers use this
|
|
161
|
+
// when present and fall back to `getQueryDbScan` otherwise.
|
|
162
|
+
let registerQueryDbIndexLookup: (string, (string, string) => array<JSON.t>) => unit
|
|
163
|
+
let getQueryDbIndexLookup: string => option<(string, string) => array<JSON.t>>
|
|
164
|
+
// Connection-list push-down: given the GraphQL args + derived capability +
|
|
165
|
+
// label field, a backend may return a ready Relay connection object computed
|
|
166
|
+
// entirely in storage (SQLite: json_extract predicates + ORDER BY … LIMIT), or
|
|
167
|
+
// `None` when it can't serve that query shape — the resolver then falls back to
|
|
168
|
+
// materialise + `QueryDbListQuery.run`. Only the SQLite backend registers one.
|
|
169
|
+
let registerQueryDbListPage: (
|
|
170
|
+
string,
|
|
171
|
+
(
|
|
172
|
+
~argsDict: dict<JSON.t>,
|
|
173
|
+
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
174
|
+
~labelField: string,
|
|
175
|
+
) => option<JSON.t>,
|
|
176
|
+
) => unit
|
|
177
|
+
let getQueryDbListPage: string => option<
|
|
178
|
+
(
|
|
179
|
+
~argsDict: dict<JSON.t>,
|
|
180
|
+
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
181
|
+
~labelField: string,
|
|
182
|
+
) => option<JSON.t>,
|
|
183
|
+
>
|
|
157
184
|
|
|
158
185
|
// Event log replay registry: aggregate EventLog name → replay function (entityId → events)
|
|
159
186
|
let registerEventLogReplay: (string, string => promise<array<JSON.t>>) => unit
|
|
@@ -196,6 +223,15 @@ module type T = {
|
|
|
196
223
|
let registerEventCollectorHandler: (string, (JSON.t, unit) => promise<unit>) => unit
|
|
197
224
|
let subscribeEventCollectorToTopic: (string, string) => unit
|
|
198
225
|
|
|
226
|
+
// Projection catch-up registry: collector name → its resolved JSON event
|
|
227
|
+
// handler, registered only by projection-type collectors (the ReadModel
|
|
228
|
+
// builders, via LocalEventCollectorChannel.MakeProjection). The SQLite
|
|
229
|
+
// backend's startup catch-up (ProjectionCheckpoint.runCatchup) replays missed
|
|
230
|
+
// stored events into these handlers; automation/translation collectors are
|
|
231
|
+
// deliberately NOT in this registry so catch-up can never re-run side effects.
|
|
232
|
+
let registerProjectionCatchupHandler: (string, (JSON.t, unit) => promise<unit>) => unit
|
|
233
|
+
let projectionCatchupHandlers: unit => array<(string, (JSON.t, unit) => promise<unit>)>
|
|
234
|
+
|
|
199
235
|
let reset: unit => unit
|
|
200
236
|
}
|
|
201
237
|
|
|
@@ -227,6 +263,16 @@ module Impl = (C: BusConfig): T => {
|
|
|
227
263
|
let queryDbRegistry: ref<dict<ReventlessCore.QueryDb_Adapter.operations>> = ref(Dict.make())
|
|
228
264
|
let queryDbScanRegistry: ref<dict<unit => array<JSON.t>>> = ref(Dict.make())
|
|
229
265
|
let queryDbStreamRegistry: ref<dict<unit => Stream.t<JSON.t, string, unit>>> = ref(Dict.make())
|
|
266
|
+
let queryDbIndexLookupRegistry: ref<dict<(string, string) => array<JSON.t>>> = ref(Dict.make())
|
|
267
|
+
let queryDbListPageRegistry: ref<
|
|
268
|
+
dict<
|
|
269
|
+
(
|
|
270
|
+
~argsDict: dict<JSON.t>,
|
|
271
|
+
~capability: ReventlessCore.GraphQL_FragmentGenerator.serverCapability,
|
|
272
|
+
~labelField: string,
|
|
273
|
+
) => option<JSON.t>,
|
|
274
|
+
>,
|
|
275
|
+
> = ref(Dict.make())
|
|
230
276
|
let eventLogReplayRegistry: ref<dict<string => promise<array<JSON.t>>>> = ref(Dict.make())
|
|
231
277
|
let dcbEventLogReadRegistry: ref<
|
|
232
278
|
dict<
|
|
@@ -265,7 +311,19 @@ module Impl = (C: BusConfig): T => {
|
|
|
265
311
|
let drainLoop = Effect.scoped(
|
|
266
312
|
PubSub.subscribe(hub)->Effect.flatMap(queue =>
|
|
267
313
|
Stream.fromQueue(queue)->Stream.runForEach(msg =>
|
|
268
|
-
|
|
314
|
+
// A throwing/rejecting handler must not (a) leave `done_` uncalled —
|
|
315
|
+
// the publisher's countdown would never reach zero and every publish
|
|
316
|
+
// on this topic would hang — nor (b) kill this drain fiber, which
|
|
317
|
+
// would stop consuming while the subscriber stays counted, hanging all
|
|
318
|
+
// future publishes too. So: convert a rejection into a typed error
|
|
319
|
+
// (`tryPromise`), log-and-recover (`catchAll`) so the stream keeps
|
|
320
|
+
// draining, and run `done_` via `ensuring` so the countdown always
|
|
321
|
+
// advances regardless of outcome.
|
|
322
|
+
Effect.tryPromise(~catch=e => e, () => handler(msg.service, msg.meta, msg.json))
|
|
323
|
+
->Effect.catchAll(err =>
|
|
324
|
+
Effect.sync(() => Console.error2("[LocalBus] subscriber handler failed:", err))
|
|
325
|
+
)
|
|
326
|
+
->Effect.ensuring(msg.done_)
|
|
269
327
|
)
|
|
270
328
|
),
|
|
271
329
|
)
|
|
@@ -372,7 +430,11 @@ module Impl = (C: BusConfig): T => {
|
|
|
372
430
|
let pending = queue.contents
|
|
373
431
|
queue.contents = []
|
|
374
432
|
pending->Array.forEach(json => {
|
|
375
|
-
let _ =
|
|
433
|
+
let _ =
|
|
434
|
+
handler(json, ())->Promise.catch(e => {
|
|
435
|
+
Console.error2("[LocalBus] parked command handler failed:", e)
|
|
436
|
+
Promise.resolve()
|
|
437
|
+
})
|
|
376
438
|
})
|
|
377
439
|
| None => ()
|
|
378
440
|
}
|
|
@@ -385,6 +447,12 @@ module Impl = (C: BusConfig): T => {
|
|
|
385
447
|
let registerQueryDbStream = (name, streamFn) =>
|
|
386
448
|
queryDbStreamRegistry.contents->Dict.set(name, streamFn)
|
|
387
449
|
let getQueryDbStream = name => queryDbStreamRegistry.contents->Dict.get(name)
|
|
450
|
+
let registerQueryDbIndexLookup = (name, lookup) =>
|
|
451
|
+
queryDbIndexLookupRegistry.contents->Dict.set(name, lookup)
|
|
452
|
+
let getQueryDbIndexLookup = name => queryDbIndexLookupRegistry.contents->Dict.get(name)
|
|
453
|
+
let registerQueryDbListPage = (name, listPage) =>
|
|
454
|
+
queryDbListPageRegistry.contents->Dict.set(name, listPage)
|
|
455
|
+
let getQueryDbListPage = name => queryDbListPageRegistry.contents->Dict.get(name)
|
|
388
456
|
|
|
389
457
|
let registerEventLogReplay = (name, replay) =>
|
|
390
458
|
eventLogReplayRegistry.contents->Dict.set(name, replay)
|
|
@@ -414,7 +482,11 @@ module Impl = (C: BusConfig): T => {
|
|
|
414
482
|
// and done_ is called immediately. This prevents a deadlock where publishEvent on the
|
|
415
483
|
// EP topic would block until the downstream aggregate command chain completes.
|
|
416
484
|
let makeFireAndForgetHandler = handler => (_, _, json) => {
|
|
417
|
-
let _ =
|
|
485
|
+
let _ =
|
|
486
|
+
handler(json, ())->Promise.catch(e => {
|
|
487
|
+
Console.error2("[LocalBus] fire-and-forget handler failed:", e)
|
|
488
|
+
Promise.resolve()
|
|
489
|
+
})
|
|
418
490
|
Promise.resolve()
|
|
419
491
|
}
|
|
420
492
|
|
|
@@ -440,6 +512,13 @@ module Impl = (C: BusConfig): T => {
|
|
|
440
512
|
}
|
|
441
513
|
}
|
|
442
514
|
|
|
515
|
+
let projectionCatchupRegistry: ref<dict<(JSON.t, unit) => promise<unit>>> = ref(Dict.make())
|
|
516
|
+
|
|
517
|
+
let registerProjectionCatchupHandler = (name, handler) =>
|
|
518
|
+
projectionCatchupRegistry.contents->Dict.set(name, handler)
|
|
519
|
+
|
|
520
|
+
let projectionCatchupHandlers = () => projectionCatchupRegistry.contents->Dict.toArray
|
|
521
|
+
|
|
443
522
|
let reset = () => {
|
|
444
523
|
// Shut down all hubs — PubSub.shutdown interrupts Stream.fromQueue consumers,
|
|
445
524
|
// causing Stream.runForEach to complete and Effect.scoped to close the subscription.
|
|
@@ -457,11 +536,14 @@ module Impl = (C: BusConfig): T => {
|
|
|
457
536
|
queryDbRegistry := Dict.make()
|
|
458
537
|
queryDbScanRegistry := Dict.make()
|
|
459
538
|
queryDbStreamRegistry := Dict.make()
|
|
539
|
+
queryDbIndexLookupRegistry := Dict.make()
|
|
540
|
+
queryDbListPageRegistry := Dict.make()
|
|
460
541
|
eventLogReplayRegistry := Dict.make()
|
|
461
542
|
dcbEventLogReadRegistry := Dict.make()
|
|
462
543
|
stateChangeListeners := Dict.make()
|
|
463
544
|
eventCollectorHandlers := Dict.make()
|
|
464
545
|
eventCollectorPendingTopics := Dict.make()
|
|
546
|
+
projectionCatchupRegistry := Dict.make()
|
|
465
547
|
}
|
|
466
548
|
}
|
|
467
549
|
|