@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.319
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 +16 -0
- package/package.json +10 -10
- package/src/LocalPlatformRegistry.res +26 -1
- package/src/LocalPlatformRegistry.res.mjs +25 -5
- package/src/Platform.res +27 -2
- package/src/Platform.res.mjs +24 -4
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
- package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
- package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
- package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
- package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
- package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
- package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
- package/src/adapter/LocalBus.res +32 -5
- package/src/adapter/LocalBus.res.mjs +27 -7
- package/src/adapter/LocalEventLogRead.res +477 -0
- package/src/adapter/LocalEventLogRead.res.mjs +507 -0
- package/src/adapter/LocalEventTap.res +18 -80
- package/src/adapter/LocalEventTap.res.mjs +19 -108
- package/src/adapter/LocalFeed.res +433 -0
- package/src/adapter/LocalFeed.res.mjs +419 -0
- package/src/adapter/LocalLogFeed.res +217 -0
- package/src/adapter/LocalLogFeed.res.mjs +295 -0
- package/src/adapter/LocalPgEventLogRead.res +85 -0
- package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
- package/src/adapter/LocalPlatformObserve.res +456 -0
- package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
- package/src/adapter/LocalTapLines.res +149 -0
- package/src/adapter/LocalTapLines.res.mjs +232 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
- package/src/components/AutomationSlice_Builder.res.mjs +1 -1
- package/src/components/LocalAutomationSchedules.res.mjs +1 -1
- package/src/components/LocalAutomationSweep.res.mjs +1 -1
- package/src/reexport/CommandTopic.res.mjs +5 -2
- package/tests/LocalPlatformRegistryTest.res +17 -0
- package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
- package/tests/LocalPlatformStartTest.res.mjs +1 -1
- package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
- package/tests/adapter/LocalEventLogReadTest.res +539 -0
- package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
- package/tests/adapter/LocalFeedTest.res +314 -0
- package/tests/adapter/LocalFeedTest.res.mjs +329 -0
- package/tests/adapter/LocalPlatformObserveTest.res +165 -0
- package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
- package/tests/adapter/LocalTapLinesTest.res +207 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
- package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
- package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
- package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
- package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
- package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
- package/tests/components/outboundtranslationslice/OutboundTranslationSlicePiiTest.res.mjs +7 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
|
|
4
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
4
5
|
import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
|
|
5
6
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
7
|
import * as Effect from "effect/Effect";
|
|
@@ -13,6 +14,7 @@ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_excep
|
|
|
13
14
|
import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
|
|
14
15
|
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
15
16
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
17
|
+
import * as LocalEventLogRead$ReventlessLocal from "../LocalEventLogRead.res.mjs";
|
|
16
18
|
import * as ProjectionPending$ReventlessLocal from "../ProjectionPending.res.mjs";
|
|
17
19
|
|
|
18
20
|
function ensureSchema(db) {
|
|
@@ -20,6 +22,7 @@ function ensureSchema(db) {
|
|
|
20
22
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE TABLE IF NOT EXISTS dcb_tag (log_name TEXT NOT NULL, position INTEGER NOT NULL, tag_key TEXT NOT NULL, tag_value TEXT NOT NULL)");
|
|
21
23
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_kv ON dcb_tag(log_name, tag_key, tag_value, position)");
|
|
22
24
|
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_tag_by_pos ON dcb_tag(log_name, position)");
|
|
25
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS dcb_event_by_correlation ON dcb_event(log_name, json_extract(meta, '$.correlationId'), position)");
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
function posToInt(pos) {
|
|
@@ -128,8 +131,73 @@ function buildQuerySql(logName, query, after) {
|
|
|
128
131
|
];
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
function readBackSql(name, bound, limit, correlationId) {
|
|
135
|
+
let parts = ["log_name = ?"];
|
|
136
|
+
let params = [name];
|
|
137
|
+
if (bound !== undefined) {
|
|
138
|
+
if (bound.TAG === "Below") {
|
|
139
|
+
parts.push("position < ?");
|
|
140
|
+
params.push(bound._0);
|
|
141
|
+
} else {
|
|
142
|
+
parts.push("position <= ?");
|
|
143
|
+
params.push(bound._0);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (correlationId !== undefined) {
|
|
147
|
+
parts.push("json_extract(meta, '$.correlationId') = ?");
|
|
148
|
+
params.push(correlationId);
|
|
149
|
+
}
|
|
150
|
+
params.push(limit);
|
|
151
|
+
return [
|
|
152
|
+
`SELECT position, event_type, data, meta, recorded_at FROM dcb_event WHERE ` + parts.join(" AND ") + ` ORDER BY position DESC LIMIT ?`,
|
|
153
|
+
params
|
|
154
|
+
];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readBack(db, name) {
|
|
158
|
+
return async (bound, limit, correlationId) => {
|
|
159
|
+
let match = readBackSql(name, bound, limit, correlationId);
|
|
160
|
+
let rows = SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, match[0]), match[1]);
|
|
161
|
+
let positions = rows.map(row => Stdlib_Option.mapOr(row["position"], 0, coercePosition));
|
|
162
|
+
let tagsByPos = {};
|
|
163
|
+
if (positions.length !== 0) {
|
|
164
|
+
let placeholders = positions.map(param => "?").join(",");
|
|
165
|
+
SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, `SELECT position, tag_key, tag_value FROM dcb_tag WHERE log_name = ? AND position IN (` + placeholders + `) ORDER BY position ASC, rowid ASC`), [name].concat(positions.map(prim => prim))).forEach(row => {
|
|
166
|
+
let k = Stdlib_Option.mapOr(row["position"], 0, coercePosition).toString();
|
|
167
|
+
let str = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(row[key], Stdlib_JSON.Decode.string), "");
|
|
168
|
+
let tag_key = str("tag_key");
|
|
169
|
+
let tag_value = str("tag_value");
|
|
170
|
+
let tag = {
|
|
171
|
+
key: tag_key,
|
|
172
|
+
value: tag_value
|
|
173
|
+
};
|
|
174
|
+
let list = tagsByPos[k];
|
|
175
|
+
if (list !== undefined) {
|
|
176
|
+
list.push(tag);
|
|
177
|
+
} else {
|
|
178
|
+
tagsByPos[k] = [tag];
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return rows.map(row => {
|
|
183
|
+
let position = Stdlib_Option.mapOr(row["position"], 0, coercePosition);
|
|
184
|
+
let str = key => Stdlib_Option.getOr(Stdlib_Option.flatMap(row[key], Stdlib_JSON.Decode.string), "");
|
|
185
|
+
return {
|
|
186
|
+
log: name,
|
|
187
|
+
position: position,
|
|
188
|
+
eventType: str("event_type"),
|
|
189
|
+
tags: Stdlib_Option.getOr(tagsByPos[position.toString()], []),
|
|
190
|
+
payload: parseDataPayload(str("data")),
|
|
191
|
+
meta: parseDataPayload(str("meta")),
|
|
192
|
+
recordedAt: str("recorded_at")
|
|
193
|
+
};
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
131
198
|
function makeStorage(db, name, param, param$1, param$2) {
|
|
132
199
|
ensureSchema(db);
|
|
200
|
+
LocalEventLogRead$ReventlessLocal.register(name, "Dcb", readBack(db, name));
|
|
133
201
|
let insertEventStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_event(log_name, position, event_type, data, meta, recorded_at) VALUES(?,?,?,?,?,?)");
|
|
134
202
|
let lastRowidStmt = SqliteDriver$ReventlessLocal.prepare(db, "SELECT last_insert_rowid() AS r");
|
|
135
203
|
let insertTagStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO dcb_tag(log_name, position, tag_key, tag_value) VALUES(?,?,?,?)");
|
|
@@ -263,6 +331,7 @@ function makeStorage(db, name, param, param$1, param$2) {
|
|
|
263
331
|
}
|
|
264
332
|
};
|
|
265
333
|
let appended = [];
|
|
334
|
+
let positionsOf = [];
|
|
266
335
|
try {
|
|
267
336
|
SqliteDriver$ReventlessLocal.transaction(db, () => {
|
|
268
337
|
if (condition !== undefined) {
|
|
@@ -310,6 +379,10 @@ function makeStorage(db, name, param, param$1, param$2) {
|
|
|
310
379
|
tag.key,
|
|
311
380
|
tag.value
|
|
312
381
|
]));
|
|
382
|
+
positionsOf.push([
|
|
383
|
+
event.meta.msgId,
|
|
384
|
+
pos
|
|
385
|
+
]);
|
|
313
386
|
lastPos.contents = pos;
|
|
314
387
|
});
|
|
315
388
|
result.contents = {
|
|
@@ -318,6 +391,7 @@ function makeStorage(db, name, param, param$1, param$2) {
|
|
|
318
391
|
};
|
|
319
392
|
});
|
|
320
393
|
ProjectionPending$ReventlessLocal.trackAppended("Dcb", appended);
|
|
394
|
+
positionsOf.forEach(param => LocalEventLogRead$ReventlessLocal.note(name, param[0], param[1]));
|
|
321
395
|
return result.contents;
|
|
322
396
|
} catch (raw_msg) {
|
|
323
397
|
let msg = Primitive_exceptions.internalToException(raw_msg);
|
|
@@ -380,6 +454,8 @@ export {
|
|
|
380
454
|
buildClauseSql,
|
|
381
455
|
buildQueryWhere,
|
|
382
456
|
buildQuerySql,
|
|
457
|
+
readBackSql,
|
|
458
|
+
readBack,
|
|
383
459
|
makeStorage,
|
|
384
460
|
}
|
|
385
461
|
/* Stdlib_JsExn Not a pure module */
|
|
@@ -37,7 +37,14 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
37
37
|
~opts,
|
|
38
38
|
)
|
|
39
39
|
Bus.registerDcbEventLogRead(storageName, ops.read)
|
|
40
|
-
|
|
40
|
+
LocalPgEventLogRead.registerDcb(~pool, ~name=storageName)
|
|
41
|
+
{
|
|
42
|
+
ReventlessCore.DcbEventLog_Adapter.resources: storage.resources,
|
|
43
|
+
operations: Pulumi.Output.make({
|
|
44
|
+
...ops,
|
|
45
|
+
append: LocalPgEventLogRead.notingDcbAppend(~pool, ~name=storageName, ops.append),
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
41
48
|
| (None, None) =>
|
|
42
49
|
let (storageName, read, storage) = DcbEventLogStorage_InMemory.makeStorage(
|
|
43
50
|
~name,
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Pulumi from "@pulumi/pulumi";
|
|
3
4
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
5
|
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
6
|
+
import * as LocalPgEventLogRead$ReventlessLocal from "../LocalPgEventLogRead.res.mjs";
|
|
5
7
|
import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./DcbEventLogStorage_Sqlite.res.mjs";
|
|
6
8
|
import * as DcbEventLogStorage_InMemory$ReventlessLocal from "./DcbEventLogStorage_InMemory.res.mjs";
|
|
7
9
|
import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
|
|
@@ -16,9 +18,20 @@ function Make(Bus) {
|
|
|
16
18
|
return match$2[2];
|
|
17
19
|
}
|
|
18
20
|
if (match$1 !== undefined) {
|
|
19
|
-
let
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
let pool = Primitive_option.valFromOption(match$1);
|
|
22
|
+
let match$3 = DcbEventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, indexes, partitionTag, undefined, opts, undefined, undefined);
|
|
23
|
+
let ops = match$3[1];
|
|
24
|
+
let storageName = match$3[0];
|
|
25
|
+
Bus.registerDcbEventLogRead(storageName, ops.read);
|
|
26
|
+
LocalPgEventLogRead$ReventlessLocal.registerDcb(pool, storageName);
|
|
27
|
+
return {
|
|
28
|
+
resources: match$3[2].resources,
|
|
29
|
+
operations: Pulumi.output({
|
|
30
|
+
read: ops.read,
|
|
31
|
+
append: LocalPgEventLogRead$ReventlessLocal.notingDcbAppend(pool, storageName, ops.append),
|
|
32
|
+
readStream: ops.readStream
|
|
33
|
+
})
|
|
34
|
+
};
|
|
22
35
|
}
|
|
23
36
|
let match$4 = DcbEventLogStorage_InMemory$ReventlessLocal.makeStorage(name, indexes, partitionTag, opts);
|
|
24
37
|
Bus.registerDcbEventLogRead(match$4[0], match$4[1]);
|
|
@@ -32,4 +45,4 @@ function Make(Bus) {
|
|
|
32
45
|
export {
|
|
33
46
|
Make,
|
|
34
47
|
}
|
|
35
|
-
/*
|
|
48
|
+
/* @pulumi/pulumi Not a pure module */
|
|
@@ -7,6 +7,46 @@
|
|
|
7
7
|
// both backends.
|
|
8
8
|
|
|
9
9
|
let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
10
|
+
// Every event of the log in the order it was appended, across entities, for
|
|
11
|
+
// the read that runs newest first. The insertion number is its position.
|
|
12
|
+
let ordered: array<LocalEventLogRead.stored> = []
|
|
13
|
+
let keepOrdered = (~track: bool, id: string, jsons: array<JSON.t>) =>
|
|
14
|
+
jsons->Array.forEach(json => {
|
|
15
|
+
let position = (ordered->Array.length + 1)->Int.toFloat
|
|
16
|
+
ordered->Array.push(LocalEventLogRead.storedOfFlat(~log=_name, ~position, ~entityId=id, json))
|
|
17
|
+
if track {
|
|
18
|
+
json
|
|
19
|
+
->JSON.Decode.object
|
|
20
|
+
->Option.flatMap(d => d->Dict.get("msgId"))
|
|
21
|
+
->Option.flatMap(JSON.Decode.string)
|
|
22
|
+
->Option.forEach(msgId => LocalEventLogRead.note(~log=_name, ~msgId, ~position))
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
LocalEventLogRead.register(~log=_name, ~kind=Aggregate, async (
|
|
26
|
+
~bound,
|
|
27
|
+
~limit,
|
|
28
|
+
~correlationId,
|
|
29
|
+
) => {
|
|
30
|
+
let out = []
|
|
31
|
+
let i = ref(ordered->Array.length - 1)
|
|
32
|
+
while i.contents >= 0 && out->Array.length < limit {
|
|
33
|
+
let r = ordered->Array.getUnsafe(i.contents)
|
|
34
|
+
let inBound = switch bound {
|
|
35
|
+
| Some(LocalEventLogRead.Below(p)) => r.position < p
|
|
36
|
+
| Some(AtMost(p)) => r.position <= p
|
|
37
|
+
| None => true
|
|
38
|
+
}
|
|
39
|
+
let inFlow =
|
|
40
|
+
correlationId->Option.mapOr(true, id =>
|
|
41
|
+
LocalEventLogRead.metaString(r.meta, "correlationId") == Some(id)
|
|
42
|
+
)
|
|
43
|
+
if inBound && inFlow {
|
|
44
|
+
out->Array.push(r)
|
|
45
|
+
}
|
|
46
|
+
i := i.contents - 1
|
|
47
|
+
}
|
|
48
|
+
out
|
|
49
|
+
})
|
|
10
50
|
let eventsRef =
|
|
11
51
|
Stm.TRef.make(Dict.make())
|
|
12
52
|
->Stm.commit
|
|
@@ -16,8 +56,8 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
16
56
|
// writes are fire-and-forget upserts, so a plain dict suffices (no Stm).
|
|
17
57
|
let snapshots: dict<ReventlessCore.EventLog.snapshot> = Dict.make()
|
|
18
58
|
|
|
19
|
-
let append: ReventlessCore.EventLog.append<string, JSON.t> = (seqNr, id, jsons) =>
|
|
20
|
-
Stm.TRef.modify(eventsRef, events => {
|
|
59
|
+
let append: ReventlessCore.EventLog.append<string, JSON.t> = async (seqNr, id, jsons) => {
|
|
60
|
+
let result = await Stm.TRef.modify(eventsRef, events => {
|
|
21
61
|
let existing = events->Dict.get(id)->Option.getOr([])
|
|
22
62
|
let currentCount = existing->Array.length
|
|
23
63
|
if seqNr != currentCount {
|
|
@@ -29,6 +69,12 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
29
69
|
})
|
|
30
70
|
->Stm.commit
|
|
31
71
|
->Effect.runPromise
|
|
72
|
+
switch result {
|
|
73
|
+
| Ok() => keepOrdered(~track=true, id, jsons)
|
|
74
|
+
| Error(_) => ()
|
|
75
|
+
}
|
|
76
|
+
result
|
|
77
|
+
}
|
|
32
78
|
|
|
33
79
|
// The in-memory array is already resident; expose it as a stream for API uniformity.
|
|
34
80
|
// The real performance gain from streaming comes from the DynamoDB adapter.
|
|
@@ -83,6 +129,7 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
|
|
|
83
129
|
switch result {
|
|
84
130
|
| Ok() =>
|
|
85
131
|
seqNrRef := seqNrRef.contents + 1
|
|
132
|
+
keepOrdered(~track=false, id, [json])
|
|
86
133
|
Effect.succeed()
|
|
87
134
|
| Error(msg) => Effect.fail(msg)
|
|
88
135
|
}
|
|
@@ -2,37 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
|
|
4
4
|
import * as STM from "effect/STM";
|
|
5
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
5
6
|
import * as TRef from "effect/TRef";
|
|
6
7
|
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
8
|
import * as Effect from "effect/Effect";
|
|
8
9
|
import * as Stream$1 from "effect/Stream";
|
|
9
10
|
import * as Pulumi from "@pulumi/pulumi";
|
|
11
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
12
|
+
import * as LocalEventLogRead$ReventlessLocal from "../LocalEventLogRead.res.mjs";
|
|
10
13
|
|
|
11
14
|
function makeMemoryStorage(_name, param) {
|
|
15
|
+
let ordered = [];
|
|
16
|
+
let keepOrdered = (track, id, jsons) => {
|
|
17
|
+
jsons.forEach(json => {
|
|
18
|
+
let position = ordered.length + 1 | 0;
|
|
19
|
+
ordered.push(LocalEventLogRead$ReventlessLocal.storedOfFlat(_name, position, id, json));
|
|
20
|
+
if (track) {
|
|
21
|
+
return Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string), msgId => LocalEventLogRead$ReventlessLocal.note(_name, msgId, position));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
LocalEventLogRead$ReventlessLocal.register(_name, "Aggregate", async (bound, limit, correlationId) => {
|
|
26
|
+
let out = [];
|
|
27
|
+
let i = ordered.length - 1 | 0;
|
|
28
|
+
while (i >= 0 && out.length < limit) {
|
|
29
|
+
let r = ordered[i];
|
|
30
|
+
let inBound = bound !== undefined ? (
|
|
31
|
+
bound.TAG === "Below" ? r.position < bound._0 : r.position <= bound._0
|
|
32
|
+
) : true;
|
|
33
|
+
let inFlow = Stdlib_Option.mapOr(correlationId, true, id => Primitive_object.equal(LocalEventLogRead$ReventlessLocal.metaString(r.meta, "correlationId"), id));
|
|
34
|
+
if (inBound && inFlow) {
|
|
35
|
+
out.push(r);
|
|
36
|
+
}
|
|
37
|
+
i = i - 1 | 0;
|
|
38
|
+
};
|
|
39
|
+
return out;
|
|
40
|
+
});
|
|
12
41
|
let eventsRef = Effect.runSync(STM.commit(TRef.make({})));
|
|
13
42
|
let snapshots = {};
|
|
14
|
-
let append = (seqNr, id, jsons) =>
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
43
|
+
let append = async (seqNr, id, jsons) => {
|
|
44
|
+
let result = await Effect.runPromise(STM.commit(TRef.modify(eventsRef, events => {
|
|
45
|
+
let existing = Stdlib_Option.getOr(events[id], []);
|
|
46
|
+
let currentCount = existing.length;
|
|
47
|
+
if (seqNr !== currentCount) {
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
TAG: "Error",
|
|
51
|
+
_0: "Conflict"
|
|
52
|
+
},
|
|
53
|
+
events
|
|
54
|
+
];
|
|
55
|
+
} else {
|
|
56
|
+
events[id] = existing.concat(jsons);
|
|
57
|
+
return [
|
|
58
|
+
{
|
|
59
|
+
TAG: "Ok",
|
|
60
|
+
_0: undefined
|
|
61
|
+
},
|
|
62
|
+
events
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
})));
|
|
66
|
+
if (result.TAG === "Ok") {
|
|
67
|
+
keepOrdered(true, id, jsons);
|
|
34
68
|
}
|
|
35
|
-
|
|
69
|
+
return result;
|
|
70
|
+
};
|
|
36
71
|
let replayStream = (id, fromSeqOpt) => {
|
|
37
72
|
let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
|
|
38
73
|
return Stream$1.flatMap(Stream$1.fromEffect(Effect.map(STM.commit(TRef.get(eventsRef)), events => Stdlib_Option.getOr(events[id], []).filter((param, i) => i >= fromSeq))), arr => Stream$1.fromIterable(arr));
|
|
@@ -79,6 +114,7 @@ function makeMemoryStorage(_name, param) {
|
|
|
79
114
|
return Effect.fail(result._0);
|
|
80
115
|
}
|
|
81
116
|
seqNrRef.contents = seqNrRef.contents + 1 | 0;
|
|
117
|
+
keepOrdered(false, id, [json]);
|
|
82
118
|
return Effect.succeed();
|
|
83
119
|
}));
|
|
84
120
|
};
|
|
@@ -27,8 +27,78 @@ let ensureSchema = (db: SqliteDriver.t) => {
|
|
|
27
27
|
db->SqliteDriver.exec(
|
|
28
28
|
"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))",
|
|
29
29
|
)
|
|
30
|
+
// Reading one log newest first across its entities walks this index by row id
|
|
31
|
+
// (an index on log_name holds the row id after it).
|
|
32
|
+
db->SqliteDriver.exec("CREATE INDEX IF NOT EXISTS event_log_by_log ON event_log(log_name)")
|
|
33
|
+
// Finding every event of one flow in one log. Created on open, so an existing
|
|
34
|
+
// store gains it; a query must use this exact expression for the index to
|
|
35
|
+
// apply. The stored event is flat, so the correlation id is a top-level key.
|
|
36
|
+
db->SqliteDriver.exec(
|
|
37
|
+
"CREATE INDEX IF NOT EXISTS event_log_by_correlation ON event_log(log_name, json_extract(payload, '$.correlationId'))",
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The SQL of one backwards read, and its parameters. */
|
|
42
|
+
let readBackSql = (
|
|
43
|
+
~name: string,
|
|
44
|
+
~bound: option<LocalEventLogRead.bound>,
|
|
45
|
+
~limit: int,
|
|
46
|
+
~correlationId: option<string>,
|
|
47
|
+
): (string, array<JSON.t>) => {
|
|
48
|
+
let parts = ["log_name = ?"]
|
|
49
|
+
let params = [JSON.Encode.string(name)]
|
|
50
|
+
switch bound {
|
|
51
|
+
| Some(LocalEventLogRead.Below(p)) =>
|
|
52
|
+
parts->Array.push("rowid < ?")
|
|
53
|
+
params->Array.push(JSON.Encode.float(p))
|
|
54
|
+
| Some(AtMost(p)) =>
|
|
55
|
+
parts->Array.push("rowid <= ?")
|
|
56
|
+
params->Array.push(JSON.Encode.float(p))
|
|
57
|
+
| None => ()
|
|
58
|
+
}
|
|
59
|
+
switch correlationId {
|
|
60
|
+
| Some(id) =>
|
|
61
|
+
parts->Array.push("json_extract(payload, '$.correlationId') = ?")
|
|
62
|
+
params->Array.push(JSON.Encode.string(id))
|
|
63
|
+
| None => ()
|
|
64
|
+
}
|
|
65
|
+
params->Array.push(JSON.Encode.int(limit))
|
|
66
|
+
(
|
|
67
|
+
`SELECT rowid AS pos, aggregate_id, payload FROM event_log WHERE ${parts->Array.join(
|
|
68
|
+
" AND ",
|
|
69
|
+
)} ORDER BY rowid DESC LIMIT ?`,
|
|
70
|
+
params,
|
|
71
|
+
)
|
|
30
72
|
}
|
|
31
73
|
|
|
74
|
+
/** Reads one log newest first, across its entities, by row id. */
|
|
75
|
+
let readBack = (~db: SqliteDriver.t, ~name: string): LocalEventLogRead.readBack =>
|
|
76
|
+
async (~bound, ~limit, ~correlationId) => {
|
|
77
|
+
let (sql, params) = readBackSql(~name, ~bound, ~limit, ~correlationId)
|
|
78
|
+
db
|
|
79
|
+
->SqliteDriver.prepare(sql)
|
|
80
|
+
->SqliteDriver.all(params)
|
|
81
|
+
->Array.map(row => {
|
|
82
|
+
let payload = switch row->Dict.get("payload") {
|
|
83
|
+
| Some(JSON.String(s)) =>
|
|
84
|
+
switch JSON.parseOrThrow(s) {
|
|
85
|
+
| json => json
|
|
86
|
+
| exception _ => JSON.Encode.null
|
|
87
|
+
}
|
|
88
|
+
| _ => JSON.Encode.null
|
|
89
|
+
}
|
|
90
|
+
LocalEventLogRead.storedOfFlat(
|
|
91
|
+
~log=name,
|
|
92
|
+
~position=LocalEventLogRead.numberOf(row->Dict.get("pos")),
|
|
93
|
+
~entityId=row
|
|
94
|
+
->Dict.get("aggregate_id")
|
|
95
|
+
->Option.flatMap(JSON.Decode.string)
|
|
96
|
+
->Option.getOr(""),
|
|
97
|
+
payload,
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
32
102
|
// Decodes the JSON column to a JSON.t. Returns JSON.Null on unparseable input
|
|
33
103
|
// (should not happen for data we wrote ourselves).
|
|
34
104
|
let decodePayload = (row: dict<JSON.t>): JSON.t =>
|
|
@@ -57,6 +127,7 @@ let countAll = (db: SqliteDriver.t): int => {
|
|
|
57
127
|
|
|
58
128
|
let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
59
129
|
ensureSchema(db)
|
|
130
|
+
LocalEventLogRead.register(~log=name, ~kind=Aggregate, readBack(~db, ~name))
|
|
60
131
|
|
|
61
132
|
let insertStmt =
|
|
62
133
|
db->SqliteDriver.prepare(
|
|
@@ -130,6 +201,20 @@ let makeStorage = (~db: SqliteDriver.t, ~name: string, ~opts as _) => {
|
|
|
130
201
|
// inserts get contiguous rowids ending at lastRowid.
|
|
131
202
|
if track && lastRowid > 0 {
|
|
132
203
|
let count = jsons->Array.length
|
|
204
|
+
// Where each event sits, for the tap line that publishes it.
|
|
205
|
+
jsons->Array.forEachWithIndex((json, i) =>
|
|
206
|
+
json
|
|
207
|
+
->JSON.Decode.object
|
|
208
|
+
->Option.flatMap(d => d->Dict.get("msgId"))
|
|
209
|
+
->Option.flatMap(JSON.Decode.string)
|
|
210
|
+
->Option.forEach(msgId =>
|
|
211
|
+
LocalEventLogRead.note(
|
|
212
|
+
~log=name,
|
|
213
|
+
~msgId,
|
|
214
|
+
~position=(lastRowid - count + 1 + i)->Int.toFloat,
|
|
215
|
+
)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
133
218
|
ProjectionPending.trackAppended(
|
|
134
219
|
~axis=ProjectionPending.Aggregate,
|
|
135
220
|
jsons
|
|
@@ -10,11 +10,57 @@ import * as Stream$1 from "effect/Stream";
|
|
|
10
10
|
import * as Pulumi from "@pulumi/pulumi";
|
|
11
11
|
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
12
|
import * as SqliteDriver$ReventlessLocal from "../SqliteDriver.res.mjs";
|
|
13
|
+
import * as LocalEventLogRead$ReventlessLocal from "../LocalEventLogRead.res.mjs";
|
|
13
14
|
import * as ProjectionPending$ReventlessLocal from "../ProjectionPending.res.mjs";
|
|
14
15
|
|
|
15
16
|
function ensureSchema(db) {
|
|
16
17
|
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
18
|
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))");
|
|
19
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS event_log_by_log ON event_log(log_name)");
|
|
20
|
+
SqliteDriver$ReventlessLocal.exec(db, "CREATE INDEX IF NOT EXISTS event_log_by_correlation ON event_log(log_name, json_extract(payload, '$.correlationId'))");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readBackSql(name, bound, limit, correlationId) {
|
|
24
|
+
let parts = ["log_name = ?"];
|
|
25
|
+
let params = [name];
|
|
26
|
+
if (bound !== undefined) {
|
|
27
|
+
if (bound.TAG === "Below") {
|
|
28
|
+
parts.push("rowid < ?");
|
|
29
|
+
params.push(bound._0);
|
|
30
|
+
} else {
|
|
31
|
+
parts.push("rowid <= ?");
|
|
32
|
+
params.push(bound._0);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (correlationId !== undefined) {
|
|
36
|
+
parts.push("json_extract(payload, '$.correlationId') = ?");
|
|
37
|
+
params.push(correlationId);
|
|
38
|
+
}
|
|
39
|
+
params.push(limit);
|
|
40
|
+
return [
|
|
41
|
+
`SELECT rowid AS pos, aggregate_id, payload FROM event_log WHERE ` + parts.join(" AND ") + ` ORDER BY rowid DESC LIMIT ?`,
|
|
42
|
+
params
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readBack(db, name) {
|
|
47
|
+
return async (bound, limit, correlationId) => {
|
|
48
|
+
let match = readBackSql(name, bound, limit, correlationId);
|
|
49
|
+
return SqliteDriver$ReventlessLocal.all(SqliteDriver$ReventlessLocal.prepare(db, match[0]), match[1]).map(row => {
|
|
50
|
+
let match = row["payload"];
|
|
51
|
+
let payload;
|
|
52
|
+
if (typeof match === "string") {
|
|
53
|
+
try {
|
|
54
|
+
payload = JSON.parse(match);
|
|
55
|
+
} catch (exn) {
|
|
56
|
+
payload = null;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
payload = null;
|
|
60
|
+
}
|
|
61
|
+
return LocalEventLogRead$ReventlessLocal.storedOfFlat(name, LocalEventLogRead$ReventlessLocal.numberOf(row["pos"]), Stdlib_Option.getOr(Stdlib_Option.flatMap(row["aggregate_id"], Stdlib_JSON.Decode.string), ""), payload);
|
|
62
|
+
});
|
|
63
|
+
};
|
|
18
64
|
}
|
|
19
65
|
|
|
20
66
|
function decodePayload(row) {
|
|
@@ -48,6 +94,7 @@ function countAll(db) {
|
|
|
48
94
|
|
|
49
95
|
function makeStorage(db, name, param) {
|
|
50
96
|
ensureSchema(db);
|
|
97
|
+
LocalEventLogRead$ReventlessLocal.register(name, "Aggregate", readBack(db, name));
|
|
51
98
|
let insertStmt = SqliteDriver$ReventlessLocal.prepare(db, "INSERT INTO event_log(log_name, aggregate_id, seq_nr, payload) VALUES(?,?,?,?)");
|
|
52
99
|
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
100
|
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");
|
|
@@ -99,6 +146,7 @@ function makeStorage(db, name, param) {
|
|
|
99
146
|
});
|
|
100
147
|
if (track && lastRowid > 0) {
|
|
101
148
|
let count = jsons.length;
|
|
149
|
+
jsons.forEach((json, i) => Stdlib_Option.forEach(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d["msgId"]), Stdlib_JSON.Decode.string), msgId => LocalEventLogRead$ReventlessLocal.note(name, msgId, ((lastRowid - count | 0) + 1 | 0) + i | 0)));
|
|
102
150
|
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
151
|
msgId,
|
|
104
152
|
((lastRowid - count | 0) + 1 | 0) + i | 0
|
|
@@ -257,6 +305,8 @@ function Make(Bus) {
|
|
|
257
305
|
|
|
258
306
|
export {
|
|
259
307
|
ensureSchema,
|
|
308
|
+
readBackSql,
|
|
309
|
+
readBack,
|
|
260
310
|
decodePayload,
|
|
261
311
|
countAll,
|
|
262
312
|
makeStorage,
|
|
@@ -21,8 +21,11 @@ module Make = (Bus: LocalBus.T) => {
|
|
|
21
21
|
~pool,
|
|
22
22
|
~name,
|
|
23
23
|
~opts,
|
|
24
|
+
// Where each event sits, for the tap line that publishes it.
|
|
25
|
+
~onAppended=LocalPgEventLogRead.noteAppended(~name),
|
|
24
26
|
)
|
|
25
27
|
Bus.registerEventLogReplay(storageName, ops.replay)
|
|
28
|
+
LocalPgEventLogRead.registerAggregate(~pool, ~name=storageName)
|
|
26
29
|
storage
|
|
27
30
|
| (None, None) =>
|
|
28
31
|
let (storageName, replay, storage) = EventLogStorage_InMemory.makeMemoryStorage(~name, ~opts)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
4
4
|
import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
|
|
5
|
+
import * as LocalPgEventLogRead$ReventlessLocal from "../LocalPgEventLogRead.res.mjs";
|
|
5
6
|
import * as EventLogStorage_Sqlite$ReventlessLocal from "./EventLogStorage_Sqlite.res.mjs";
|
|
6
7
|
import * as EventLogStorage_InMemory$ReventlessLocal from "./EventLogStorage_InMemory.res.mjs";
|
|
7
8
|
import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
|
|
@@ -16,8 +17,11 @@ function Make(Bus) {
|
|
|
16
17
|
return match$2[2];
|
|
17
18
|
}
|
|
18
19
|
if (match$1 !== undefined) {
|
|
19
|
-
let
|
|
20
|
-
|
|
20
|
+
let pool = Primitive_option.valFromOption(match$1);
|
|
21
|
+
let match$3 = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(pool, name, opts, LocalPgEventLogRead$ReventlessLocal.noteAppended(name));
|
|
22
|
+
let storageName = match$3[0];
|
|
23
|
+
Bus.registerEventLogReplay(storageName, match$3[1].replay);
|
|
24
|
+
LocalPgEventLogRead$ReventlessLocal.registerAggregate(pool, storageName);
|
|
21
25
|
return match$3[2];
|
|
22
26
|
}
|
|
23
27
|
let match$4 = EventLogStorage_InMemory$ReventlessLocal.makeMemoryStorage(name, opts);
|
package/src/adapter/LocalBus.res
CHANGED
|
@@ -61,29 +61,56 @@ type queuedEvent = {
|
|
|
61
61
|
let eventTapSeq = ref(0)
|
|
62
62
|
// Continues the timeline's #N across restarts for a persistent store.
|
|
63
63
|
let seedEventTapSeq = (n: int) => eventTapSeq := n
|
|
64
|
-
let emitEventTap = (
|
|
64
|
+
let emitEventTap = (
|
|
65
|
+
~topic: string,
|
|
66
|
+
~service: string,
|
|
67
|
+
~meta: ReventlessCore.Message.meta,
|
|
68
|
+
~payload: JSON.t,
|
|
69
|
+
) => {
|
|
65
70
|
// Counted even when nothing is listening, so the sequence a reader that connects
|
|
66
71
|
// later sees is the event's ordinal rather than the ordinal of its own arrival.
|
|
67
72
|
eventTapSeq := eventTapSeq.contents + 1
|
|
73
|
+
// Taken whether or not anyone reads, so the note does not wait for a reader.
|
|
74
|
+
let stored = LocalEventLogRead.take(meta.msgId)
|
|
68
75
|
let toStdout = LocalEventTap.stdoutEnabled()
|
|
69
76
|
|
|
70
77
|
// The line is built only for somebody: serialising every event for no reader is
|
|
71
78
|
// pure cost on the publish path.
|
|
72
79
|
if toStdout || LocalEventTap.hasReaders() {
|
|
80
|
+
// Where the event is stored — its log and the log's own position — so a
|
|
81
|
+
// reader can join a live line to the read across every log by a key that
|
|
82
|
+
// survives a restart. Absent for an event published without a log.
|
|
83
|
+
let where = switch stored {
|
|
84
|
+
| Some((log, position)) => [
|
|
85
|
+
("log", JSON.Encode.string(log)),
|
|
86
|
+
("position", JSON.Encode.string(position->LocalEventLogRead.positionToString)),
|
|
87
|
+
]
|
|
88
|
+
| None => []
|
|
89
|
+
}
|
|
73
90
|
let line =
|
|
74
|
-
|
|
91
|
+
[
|
|
75
92
|
("event", JSON.Encode.string("domainEvent")),
|
|
76
93
|
("seq", JSON.Encode.int(eventTapSeq.contents)),
|
|
77
94
|
("topic", JSON.Encode.string(topic)),
|
|
78
95
|
("service", JSON.Encode.string(service)),
|
|
79
96
|
("payload", payload),
|
|
80
97
|
("ts", JSON.Encode.string(Date.make()->Date.toISOString)),
|
|
81
|
-
]
|
|
98
|
+
]
|
|
99
|
+
->Array.concat(where)
|
|
100
|
+
->Dict.fromArray
|
|
101
|
+
->JSON.Encode.object
|
|
82
102
|
let tapLine = "@@RVLESS_EVT@@ " ++ line->JSON.stringify
|
|
83
103
|
if toStdout {
|
|
84
104
|
Console.log(tapLine)
|
|
85
105
|
}
|
|
86
|
-
LocalEventTap.
|
|
106
|
+
LocalEventTap.publish({
|
|
107
|
+
line: tapLine,
|
|
108
|
+
facets: {
|
|
109
|
+
plugin: ?(stored->Option.flatMap(((log, _)) => LocalEventLogRead.pluginOf(log))),
|
|
110
|
+
eventType: ReventlessCore.Message.eventNameOfEvent'Json(payload),
|
|
111
|
+
correlationId: meta.correlationId,
|
|
112
|
+
},
|
|
113
|
+
})
|
|
87
114
|
}
|
|
88
115
|
}
|
|
89
116
|
|
|
@@ -338,7 +365,7 @@ module Impl = (C: BusConfig): T => {
|
|
|
338
365
|
if Reventless.Pii.needsPrepare([json]) {
|
|
339
366
|
await Reventless.Pii.prepareOrThrow([json])
|
|
340
367
|
}
|
|
341
|
-
emitEventTap(~topic=topicName, ~service, ~payload=json)
|
|
368
|
+
emitEventTap(~topic=topicName, ~service, ~meta, ~payload=json)
|
|
342
369
|
switch eventHubs.contents->Dict.get(topicName) {
|
|
343
370
|
| None => ()
|
|
344
371
|
| Some(hub) =>
|