@reventlessdev/reventless-local 3.0.0-alpha.316 → 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.
Files changed (57) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +11 -11
  3. package/src/LocalPlatformRegistry.res +19 -0
  4. package/src/LocalPlatformRegistry.res.mjs +19 -1
  5. package/src/Platform.res +24 -2
  6. package/src/Platform.res.mjs +22 -2
  7. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +38 -0
  8. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +29 -0
  9. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +87 -0
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +76 -0
  11. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +8 -1
  12. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +17 -4
  13. package/src/adapter/EventLog/EventLogStorage_InMemory.res +49 -2
  14. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +57 -21
  15. package/src/adapter/EventLog/EventLogStorage_Sqlite.res +85 -0
  16. package/src/adapter/EventLog/EventLogStorage_Sqlite.res.mjs +50 -0
  17. package/src/adapter/EventLog/LocalEventLogStorage.res +3 -0
  18. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +6 -2
  19. package/src/adapter/LocalBus.res +32 -5
  20. package/src/adapter/LocalBus.res.mjs +27 -7
  21. package/src/adapter/LocalEventLogRead.res +477 -0
  22. package/src/adapter/LocalEventLogRead.res.mjs +507 -0
  23. package/src/adapter/LocalEventTap.res +18 -80
  24. package/src/adapter/LocalEventTap.res.mjs +19 -108
  25. package/src/adapter/LocalFeed.res +433 -0
  26. package/src/adapter/LocalFeed.res.mjs +419 -0
  27. package/src/adapter/LocalLogFeed.res +217 -0
  28. package/src/adapter/LocalLogFeed.res.mjs +295 -0
  29. package/src/adapter/LocalPgEventLogRead.res +85 -0
  30. package/src/adapter/LocalPgEventLogRead.res.mjs +91 -0
  31. package/src/adapter/LocalPlatformObserve.res +456 -0
  32. package/src/adapter/LocalPlatformObserve.res.mjs +505 -0
  33. package/src/adapter/LocalTapLines.res +147 -0
  34. package/src/adapter/LocalTapLines.res.mjs +229 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/reexport/CommandTopic.res.mjs +5 -2
  38. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  39. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  40. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  41. package/tests/adapter/LocalFeedTest.res +314 -0
  42. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  43. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  44. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  45. package/tests/adapter/LocalTapLinesTest.res +182 -0
  46. package/tests/adapter/LocalTapLinesTest.res.mjs +242 -0
  47. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  48. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  49. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  50. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  51. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  52. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  53. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  54. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  55. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  56. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  57. 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 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];
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
- /* BackendState-ReventlessLocal Not a pure module */
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) => Effect.runPromise(STM.commit(TRef.modify(eventsRef, events => {
15
- let existing = Stdlib_Option.getOr(events[id], []);
16
- let currentCount = existing.length;
17
- if (seqNr !== currentCount) {
18
- return [
19
- {
20
- TAG: "Error",
21
- _0: "Conflict"
22
- },
23
- events
24
- ];
25
- } else {
26
- events[id] = existing.concat(jsons);
27
- return [
28
- {
29
- TAG: "Ok",
30
- _0: undefined
31
- },
32
- events
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 match$3 = EventLogStorage_Postgres$ReventlessPostgres.makeStorage(Primitive_option.valFromOption(match$1), name, opts, undefined);
20
- Bus.registerEventLogReplay(match$3[0], match$3[1].replay);
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);
@@ -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 = (~topic: string, ~service: string, ~payload: JSON.t) => {
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
- Dict.fromArray([
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
- ])->JSON.Encode.object
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.broadcast(tapLine)
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.broadcast(tapLine);
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;