@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +10 -10
  3. package/src/LocalPlatformRegistry.res +26 -1
  4. package/src/LocalPlatformRegistry.res.mjs +25 -5
  5. package/src/Platform.res +27 -2
  6. package/src/Platform.res.mjs +24 -4
  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 +149 -0
  34. package/src/adapter/LocalTapLines.res.mjs +232 -0
  35. package/src/adapter/PlatformGraphQL_Server.res +36 -3
  36. package/src/adapter/PlatformGraphQL_Server.res.mjs +15 -1
  37. package/src/adapter/ProjectionCheckpoint.res.mjs +3 -3
  38. package/src/components/AutomationSlice_Builder.res.mjs +1 -1
  39. package/src/components/LocalAutomationSchedules.res.mjs +1 -1
  40. package/src/components/LocalAutomationSweep.res.mjs +1 -1
  41. package/src/reexport/CommandTopic.res.mjs +5 -2
  42. package/tests/LocalPlatformRegistryTest.res +17 -0
  43. package/tests/LocalPlatformRegistryTest.res.mjs +14 -2
  44. package/tests/LocalPlatformStartTest.res.mjs +1 -1
  45. package/tests/adapter/GraphQLWsFixtures.mjs +46 -0
  46. package/tests/adapter/LocalEventLogReadTest.res +539 -0
  47. package/tests/adapter/LocalEventLogReadTest.res.mjs +466 -0
  48. package/tests/adapter/LocalFeedTest.res +314 -0
  49. package/tests/adapter/LocalFeedTest.res.mjs +329 -0
  50. package/tests/adapter/LocalPlatformObserveTest.res +165 -0
  51. package/tests/adapter/LocalPlatformObserveTest.res.mjs +183 -0
  52. package/tests/adapter/LocalTapLinesTest.res +207 -0
  53. package/tests/adapter/LocalTapLinesTest.res.mjs +274 -0
  54. package/tests/adapter/PlatformSubscriptionsTest.res +123 -0
  55. package/tests/adapter/PlatformSubscriptionsTest.res.mjs +132 -0
  56. package/tests/components/automationslice/AutomationSliceCallbackTest.res +80 -0
  57. package/tests/components/automationslice/AutomationSliceCallbackTest.res.mjs +81 -0
  58. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res +17 -0
  59. package/tests/components/automationslice/AutomationSliceSelfDeadlockFixtures.res.mjs +24 -0
  60. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res +30 -0
  61. package/tests/components/automationslice/AutomationSliceSelfDeadlockTest.res.mjs +28 -0
  62. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res +58 -0
  63. package/tests/components/outboundtranslationslice/OutboundTranslationSliceCallbackTest.res.mjs +71 -19
  64. 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
- storage
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 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) =>