@reventlessdev/reventless-local 3.0.0-alpha.317 → 3.0.0-alpha.318
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 +8 -0
- package/package.json +10 -10
- package/src/LocalPlatformRegistry.res +19 -0
- package/src/LocalPlatformRegistry.res.mjs +19 -1
- package/src/Platform.res +24 -2
- package/src/Platform.res.mjs +22 -2
- 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 +147 -0
- package/src/adapter/LocalTapLines.res.mjs +229 -0
- package/src/adapter/PlatformGraphQL_Server.res +36 -3
- package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
- package/src/reexport/CommandTopic.res.mjs +5 -2
- 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 +182 -0
- package/tests/adapter/LocalTapLinesTest.res.mjs +242 -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,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) =>
|
|
@@ -11,7 +11,9 @@ import * as Pii$Reventless from "@reventlessdev/reventless-spec/src/components/P
|
|
|
11
11
|
import * as Stdlib_Promise from "@rescript/runtime/lib/es6/Stdlib_Promise.js";
|
|
12
12
|
import * as Deferred from "effect/Deferred";
|
|
13
13
|
import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
|
|
14
|
+
import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
|
|
14
15
|
import * as LocalEventTap$ReventlessLocal from "./LocalEventTap.res.mjs";
|
|
16
|
+
import * as LocalEventLogRead$ReventlessLocal from "./LocalEventLogRead.res.mjs";
|
|
15
17
|
|
|
16
18
|
let eventTapSeq = {
|
|
17
19
|
contents: 0
|
|
@@ -21,12 +23,23 @@ function seedEventTapSeq(n) {
|
|
|
21
23
|
eventTapSeq.contents = n;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
|
-
function emitEventTap(topic, service, payload) {
|
|
26
|
+
function emitEventTap(topic, service, meta, payload) {
|
|
25
27
|
eventTapSeq.contents = eventTapSeq.contents + 1 | 0;
|
|
28
|
+
let stored = LocalEventLogRead$ReventlessLocal.take(meta.msgId);
|
|
26
29
|
let toStdout = LocalEventTap$ReventlessLocal.stdoutEnabled(undefined);
|
|
27
30
|
if (!(toStdout || LocalEventTap$ReventlessLocal.hasReaders())) {
|
|
28
31
|
return;
|
|
29
32
|
}
|
|
33
|
+
let where = stored !== undefined ? [
|
|
34
|
+
[
|
|
35
|
+
"log",
|
|
36
|
+
stored[0]
|
|
37
|
+
],
|
|
38
|
+
[
|
|
39
|
+
"position",
|
|
40
|
+
LocalEventLogRead$ReventlessLocal.positionToString(stored[1])
|
|
41
|
+
]
|
|
42
|
+
] : [];
|
|
30
43
|
let line = Object.fromEntries([
|
|
31
44
|
[
|
|
32
45
|
"event",
|
|
@@ -52,12 +65,19 @@ function emitEventTap(topic, service, payload) {
|
|
|
52
65
|
"ts",
|
|
53
66
|
new Date().toISOString()
|
|
54
67
|
]
|
|
55
|
-
]);
|
|
68
|
+
].concat(where));
|
|
56
69
|
let tapLine = "@@RVLESS_EVT@@ " + JSON.stringify(line);
|
|
57
70
|
if (toStdout) {
|
|
58
71
|
console.log(tapLine);
|
|
59
72
|
}
|
|
60
|
-
LocalEventTap$ReventlessLocal.
|
|
73
|
+
LocalEventTap$ReventlessLocal.publish({
|
|
74
|
+
line: tapLine,
|
|
75
|
+
facets: {
|
|
76
|
+
plugin: Stdlib_Option.flatMap(stored, param => LocalEventLogRead$ReventlessLocal.pluginOf(param[0])),
|
|
77
|
+
eventType: Message$ReventlessCore.eventNameOfEvent$pJson(payload),
|
|
78
|
+
correlationId: meta.correlationId
|
|
79
|
+
}
|
|
80
|
+
});
|
|
61
81
|
}
|
|
62
82
|
|
|
63
83
|
function Impl(C) {
|
|
@@ -141,7 +161,7 @@ function Impl(C) {
|
|
|
141
161
|
if (Pii$Reventless.needsPrepare([json])) {
|
|
142
162
|
await Pii$Reventless.prepareOrThrow([json]);
|
|
143
163
|
}
|
|
144
|
-
emitEventTap(topicName, service, json);
|
|
164
|
+
emitEventTap(topicName, service, meta, json);
|
|
145
165
|
let hub = eventHubs.contents[topicName];
|
|
146
166
|
if (hub === undefined) {
|
|
147
167
|
return;
|
|
@@ -417,7 +437,7 @@ function Make($star) {
|
|
|
417
437
|
if (Pii$Reventless.needsPrepare([json])) {
|
|
418
438
|
await Pii$Reventless.prepareOrThrow([json]);
|
|
419
439
|
}
|
|
420
|
-
emitEventTap(topicName, service, json);
|
|
440
|
+
emitEventTap(topicName, service, meta, json);
|
|
421
441
|
let hub = eventHubs.contents[topicName];
|
|
422
442
|
if (hub === undefined) {
|
|
423
443
|
return;
|
|
@@ -693,7 +713,7 @@ function MakeSilent($star) {
|
|
|
693
713
|
if (Pii$Reventless.needsPrepare([json])) {
|
|
694
714
|
await Pii$Reventless.prepareOrThrow([json]);
|
|
695
715
|
}
|
|
696
|
-
emitEventTap(topicName, service, json);
|
|
716
|
+
emitEventTap(topicName, service, meta, json);
|
|
697
717
|
let hub = eventHubs.contents[topicName];
|
|
698
718
|
if (hub === undefined) {
|
|
699
719
|
return;
|
|
@@ -970,7 +990,7 @@ function MakeBounded(C) {
|
|
|
970
990
|
if (Pii$Reventless.needsPrepare([json])) {
|
|
971
991
|
await Pii$Reventless.prepareOrThrow([json]);
|
|
972
992
|
}
|
|
973
|
-
emitEventTap(topicName, service, json);
|
|
993
|
+
emitEventTap(topicName, service, meta, json);
|
|
974
994
|
let hub = eventHubs.contents[topicName];
|
|
975
995
|
if (hub === undefined) {
|
|
976
996
|
return;
|