@reventlessdev/reventless-local 3.0.0-alpha.116 → 3.0.0-alpha.118

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +6 -5
  3. package/rescript.json +2 -1
  4. package/src/Platform.res +44 -6
  5. package/src/Platform.res.mjs +53 -16
  6. package/src/adapter/Backend.res +23 -0
  7. package/src/adapter/Backend.res.mjs +23 -0
  8. package/src/adapter/BackendState.res +12 -7
  9. package/src/adapter/BackendState.res.mjs +18 -6
  10. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res +3 -22
  11. package/src/adapter/DcbEventLog/DcbEventLogStorage_InMemory.res.mjs +1 -22
  12. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res +13 -10
  13. package/src/adapter/DcbEventLog/DcbEventLogStorage_Sqlite.res.mjs +22 -8
  14. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res +52 -0
  15. package/src/adapter/DcbEventLog/LocalDcbEventLogStorage.res.mjs +35 -0
  16. package/src/adapter/EventLog/EventLogStorage_InMemory.res +3 -15
  17. package/src/adapter/EventLog/EventLogStorage_InMemory.res.mjs +0 -21
  18. package/src/adapter/EventLog/LocalEventLogStorage.res +33 -0
  19. package/src/adapter/EventLog/LocalEventLogStorage.res.mjs +35 -0
  20. package/src/adapter/LocalBus.res +1 -1
  21. package/src/adapter/PgProjectionCatchup.res +146 -0
  22. package/src/adapter/PgProjectionCatchup.res.mjs +114 -0
  23. package/src/adapter/QueryDb/LocalQueryDbStorage.res +43 -0
  24. package/src/adapter/QueryDb/LocalQueryDbStorage.res.mjs +42 -0
  25. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res +3 -24
  26. package/src/adapter/QueryDb/QueryDbStorage_InMemory.res.mjs +1 -28
  27. package/src/adapter/Task/LocalTaskBucket.res +1 -1
  28. package/src/adapter/Task/LocalTaskBucket.res.mjs +1 -1
  29. package/src/components/Aggregate_Builder.res +1 -1
  30. package/src/components/Aggregate_Builder.res.mjs +3 -3
  31. package/src/components/AutomationSlice_Builder.res +1 -1
  32. package/src/components/AutomationSlice_Builder.res.mjs +3 -3
  33. package/src/components/Counter_Builder.res +1 -1
  34. package/src/components/Counter_Builder.res.mjs +2 -2
  35. package/src/components/DcbEventLog_Builder.res +1 -1
  36. package/src/components/DcbEventLog_Builder.res.mjs +2 -2
  37. package/src/components/InboundTranslationSlice_Builder.res +1 -1
  38. package/src/components/InboundTranslationSlice_Builder.res.mjs +2 -2
  39. package/src/components/OutboundTranslationSlice_Builder.res +1 -1
  40. package/src/components/OutboundTranslationSlice_Builder.res.mjs +2 -2
  41. package/src/components/Plugin_Builder.res +1 -1
  42. package/src/components/Plugin_Builder.res.mjs +2 -2
  43. package/src/components/ReadModel_Builder.res +1 -1
  44. package/src/components/ReadModel_Builder.res.mjs +2 -2
  45. package/src/components/StateViewSlice_Builder.res +1 -1
  46. package/src/components/StateViewSlice_Builder.res.mjs +2 -2
  47. package/src/test/Mocks/MockDcbEventLogStorage.res +2 -2
  48. package/src/test/Mocks/MockDcbEventLogStorage.res.mjs +5 -2
  49. package/tests/adapter/BackendParityTest.res +43 -8
  50. package/tests/adapter/BackendParityTest.res.mjs +39 -9
  51. package/tests/adapter/DcbEventLogStorageSqliteTest.res +17 -9
  52. package/tests/adapter/DcbEventLogStorageSqliteTest.res.mjs +19 -10
  53. package/tests/adapter/PgProjectionCatchupTest.res +149 -0
  54. package/tests/adapter/PgProjectionCatchupTest.res.mjs +181 -0
  55. package/tests/adapter/QueryDbGsiTtlTest.res +2 -2
  56. package/tests/adapter/QueryDbGsiTtlTest.res.mjs +3 -2
  57. package/tests/adapter/QueryDbListResolverTest.res +1 -1
  58. package/tests/adapter/QueryDbListResolverTest.res.mjs +2 -2
  59. package/tests/adapter/QueryDbStorageTest.res +10 -10
  60. package/tests/adapter/QueryDbStorageTest.res.mjs +11 -11
  61. package/tests/adapter/QueryEngineTest.res +8 -8
  62. package/tests/adapter/QueryEngineTest.res.mjs +9 -9
  63. package/tests/components/querydb/QueryDbFixtures.res +1 -1
  64. package/tests/components/querydb/QueryDbFixtures.res.mjs +2 -2
  65. package/tests/components/querydb/QueryDbSubIdFixtures.res +1 -1
  66. package/tests/components/querydb/QueryDbSubIdFixtures.res.mjs +2 -2
@@ -322,21 +322,35 @@ function makeStorage(db, name, param, param$1, param$2) {
322
322
  } catch (raw_msg) {
323
323
  let msg = Primitive_exceptions.internalToException(raw_msg);
324
324
  if (msg.RE_EXN_ID === "Failure") {
325
- return {
326
- TAG: "Error",
327
- _0: msg._1
328
- };
325
+ let msg$1 = msg._1;
326
+ if (msg$1.includes("conflict")) {
327
+ return {
328
+ TAG: "Error",
329
+ _0: "Conflict"
330
+ };
331
+ } else {
332
+ return {
333
+ TAG: "Error",
334
+ _0: {
335
+ TAG: "StorageFailure",
336
+ _0: msg$1
337
+ }
338
+ };
339
+ }
329
340
  }
330
- let msg$1 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
331
- if (msg$1.includes("constraint failed")) {
341
+ let msg$2 = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(msg), Stdlib_JsExn.message), "");
342
+ if (msg$2.includes("constraint failed")) {
332
343
  return {
333
344
  TAG: "Error",
334
- _0: "conflict"
345
+ _0: "Conflict"
335
346
  };
336
347
  } else {
337
348
  return {
338
349
  TAG: "Error",
339
- _0: msg$1 === "" ? "storage error" : msg$1
350
+ _0: {
351
+ TAG: "StorageFailure",
352
+ _0: msg$2 === "" ? "storage error" : msg$2
353
+ }
340
354
  };
341
355
  }
342
356
  }
@@ -0,0 +1,52 @@
1
+ // DCB EventLog storage dispatcher.
2
+ //
3
+ // Selects the concrete backend at `make` time by consulting BackendState:
4
+ // - Sqlite → DcbEventLogStorage_Sqlite (true DCB semantics on SQL)
5
+ // - Postgres → ReventlessPostgres.DcbEventLogStorage_Postgres (exact DCB
6
+ // semantics: scoped advisory locks + atomic condition check)
7
+ // - Memory → DcbEventLogStorage_InMemory (pure in-memory)
8
+ //
9
+ // Registers each backend's read with the Bus so the LocalBus DCB event-tap can
10
+ // stream historical events uniformly.
11
+
12
+ module Make = (Bus: LocalBus.T) => {
13
+ let make: ReventlessCore.DcbEventLog_Adapter.storageMaker = (
14
+ ~name,
15
+ ~indexes,
16
+ ~partitionTag,
17
+ ~crossPartitionTagKeys as _=?,
18
+ ~opts,
19
+ ) => {
20
+ switch (BackendState.getSqliteDb(), BackendState.getPostgresPool()) {
21
+ | (Some(db), _) =>
22
+ let (storageName, read, storage) = DcbEventLogStorage_Sqlite.makeStorage(
23
+ ~db,
24
+ ~name,
25
+ ~indexes,
26
+ ~partitionTag,
27
+ ~opts,
28
+ )
29
+ Bus.registerDcbEventLogRead(storageName, read)
30
+ storage
31
+ | (_, Some(pool)) =>
32
+ let (storageName, ops, storage) = ReventlessPostgres.DcbEventLogStorage_Postgres.makeStorage(
33
+ ~pool,
34
+ ~name,
35
+ ~indexes,
36
+ ~partitionTag,
37
+ ~opts,
38
+ )
39
+ Bus.registerDcbEventLogRead(storageName, ops.read)
40
+ storage
41
+ | (None, None) =>
42
+ let (storageName, read, storage) = DcbEventLogStorage_InMemory.makeStorage(
43
+ ~name,
44
+ ~indexes,
45
+ ~partitionTag,
46
+ ~opts,
47
+ )
48
+ Bus.registerDcbEventLogRead(storageName, read)
49
+ storage
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,35 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
+ import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
5
+ import * as DcbEventLogStorage_Sqlite$ReventlessLocal from "./DcbEventLogStorage_Sqlite.res.mjs";
6
+ import * as DcbEventLogStorage_InMemory$ReventlessLocal from "./DcbEventLogStorage_InMemory.res.mjs";
7
+ import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/DcbEventLogStorage_Postgres.res.mjs";
8
+
9
+ function Make(Bus) {
10
+ let make = (name, indexes, partitionTag, param, opts) => {
11
+ let match = BackendState$ReventlessLocal.getSqliteDb();
12
+ let match$1 = BackendState$ReventlessLocal.getPostgresPool();
13
+ if (match !== undefined) {
14
+ let match$2 = DcbEventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(match), name, indexes, partitionTag, opts);
15
+ Bus.registerDcbEventLogRead(match$2[0], match$2[1]);
16
+ return match$2[2];
17
+ }
18
+ 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];
22
+ }
23
+ let match$4 = DcbEventLogStorage_InMemory$ReventlessLocal.makeStorage(name, indexes, partitionTag, opts);
24
+ Bus.registerDcbEventLogRead(match$4[0], match$4[1]);
25
+ return match$4[2];
26
+ };
27
+ return {
28
+ make: make
29
+ };
30
+ }
31
+
32
+ export {
33
+ Make,
34
+ }
35
+ /* DcbEventLogStorage_Sqlite-ReventlessLocal Not a pure module */
@@ -105,22 +105,10 @@ let makeMemoryStorage = (~name as _name, ~opts as _) => {
105
105
  // Alias retained for tests that still want the pure-memory implementation.
106
106
  let makeStorage = makeMemoryStorage
107
107
 
108
+ // Pure in-memory storageMaker (no BackendState dispatch, no Bus). Backend
109
+ // selection lives in LocalEventLogStorage.Make — this file holds only the
110
+ // in-memory implementation, as its name says.
108
111
  let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
109
112
  let (_, _, storage) = makeMemoryStorage(~name, ~opts)
110
113
  storage
111
114
  }
112
-
113
- module Make = (Bus: LocalBus.T) => {
114
- let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
115
- switch BackendState.getDb() {
116
- | Some(db) =>
117
- let (storageName, replay, storage) = EventLogStorage_Sqlite.makeStorage(~db, ~name, ~opts)
118
- Bus.registerEventLogReplay(storageName, replay)
119
- storage
120
- | None =>
121
- let (storageName, replay, storage) = makeMemoryStorage(~name, ~opts)
122
- Bus.registerEventLogReplay(storageName, replay)
123
- storage
124
- }
125
- }
126
- }
@@ -7,9 +7,6 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
7
  import * as Effect from "effect/Effect";
8
8
  import * as Stream$1 from "effect/Stream";
9
9
  import * as Pulumi from "@pulumi/pulumi";
10
- import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
11
- import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
12
- import * as EventLogStorage_Sqlite$ReventlessLocal from "./EventLogStorage_Sqlite.res.mjs";
13
10
 
14
11
  function makeMemoryStorage(_name, param) {
15
12
  let eventsRef = Effect.runSync(STM.commit(TRef.make({})));
@@ -106,29 +103,11 @@ function make(name, opts) {
106
103
  return makeMemoryStorage(name, opts)[2];
107
104
  }
108
105
 
109
- function Make(Bus) {
110
- let make = (name, opts) => {
111
- let db = BackendState$ReventlessLocal.getDb();
112
- if (db !== undefined) {
113
- let match = EventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), name, opts);
114
- Bus.registerEventLogReplay(match[0], match[1]);
115
- return match[2];
116
- }
117
- let match$1 = makeMemoryStorage(name, opts);
118
- Bus.registerEventLogReplay(match$1[0], match$1[1]);
119
- return match$1[2];
120
- };
121
- return {
122
- make: make
123
- };
124
- }
125
-
126
106
  let makeStorage = makeMemoryStorage;
127
107
 
128
108
  export {
129
109
  makeMemoryStorage,
130
110
  makeStorage,
131
111
  make,
132
- Make,
133
112
  }
134
113
  /* Stream Not a pure module */
@@ -0,0 +1,33 @@
1
+ // EventLog storage dispatcher.
2
+ //
3
+ // Selects the concrete backend at `make` time by consulting BackendState, so the
4
+ // core EventLog builder wires a single storageMaker module regardless of backend:
5
+ // - Sqlite → EventLogStorage_Sqlite (on-disk / :memory:)
6
+ // - Postgres → ReventlessPostgres.EventLogStorage_Postgres (durable, async)
7
+ // - Memory → EventLogStorage_InMemory (pure in-memory)
8
+ //
9
+ // The `Make(Bus)` functor also registers each backend's replay with the Bus so
10
+ // the LocalBus event-tap can stream historical events uniformly.
11
+
12
+ module Make = (Bus: LocalBus.T) => {
13
+ let make: ReventlessCore.EventLog_Adapter.storageMaker = (~name, ~opts) => {
14
+ switch (BackendState.getSqliteDb(), BackendState.getPostgresPool()) {
15
+ | (Some(db), _) =>
16
+ let (storageName, replay, storage) = EventLogStorage_Sqlite.makeStorage(~db, ~name, ~opts)
17
+ Bus.registerEventLogReplay(storageName, replay)
18
+ storage
19
+ | (_, Some(pool)) =>
20
+ let (storageName, ops, storage) = ReventlessPostgres.EventLogStorage_Postgres.makeStorage(
21
+ ~pool,
22
+ ~name,
23
+ ~opts,
24
+ )
25
+ Bus.registerEventLogReplay(storageName, ops.replay)
26
+ storage
27
+ | (None, None) =>
28
+ let (storageName, replay, storage) = EventLogStorage_InMemory.makeMemoryStorage(~name, ~opts)
29
+ Bus.registerEventLogReplay(storageName, replay)
30
+ storage
31
+ }
32
+ }
33
+ }
@@ -0,0 +1,35 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
+ import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
5
+ import * as EventLogStorage_Sqlite$ReventlessLocal from "./EventLogStorage_Sqlite.res.mjs";
6
+ import * as EventLogStorage_InMemory$ReventlessLocal from "./EventLogStorage_InMemory.res.mjs";
7
+ import * as EventLogStorage_Postgres$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/EventLogStorage_Postgres.res.mjs";
8
+
9
+ function Make(Bus) {
10
+ let make = (name, opts) => {
11
+ let match = BackendState$ReventlessLocal.getSqliteDb();
12
+ let match$1 = BackendState$ReventlessLocal.getPostgresPool();
13
+ if (match !== undefined) {
14
+ let match$2 = EventLogStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(match), name, opts);
15
+ Bus.registerEventLogReplay(match$2[0], match$2[1]);
16
+ return match$2[2];
17
+ }
18
+ 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);
21
+ return match$3[2];
22
+ }
23
+ let match$4 = EventLogStorage_InMemory$ReventlessLocal.makeMemoryStorage(name, opts);
24
+ Bus.registerEventLogReplay(match$4[0], match$4[1]);
25
+ return match$4[2];
26
+ };
27
+ return {
28
+ make: make
29
+ };
30
+ }
31
+
32
+ export {
33
+ Make,
34
+ }
35
+ /* EventLogStorage_Sqlite-ReventlessLocal Not a pure module */
@@ -145,7 +145,7 @@ module type T = {
145
145
  let registerCommandHandler: (string, (JSON.t, unit) => promise<unit>) => unit
146
146
 
147
147
  // QueryDb registry: read model name → storage ops and scan function
148
- // Populated by QueryDbStorage_InMemory.Make(Bus) during component construction.
148
+ // Populated by LocalQueryDbStorage.Make(Bus) during component construction.
149
149
  let registerQueryDb: (string, ReventlessCore.QueryDb_Adapter.operations) => unit
150
150
  let getQueryDb: string => option<ReventlessCore.QueryDb_Adapter.operations>
151
151
  let registerQueryDbScan: (string, unit => array<JSON.t>) => unit
@@ -0,0 +1,146 @@
1
+ // Startup projection catch-up for the Postgres backend.
2
+ //
3
+ // Under Backend.Postgres the event logs (classic + DCB) persist durably, but read
4
+ // models run on the in-memory live-query arm (LocalQueryDbStorage — the sync
5
+ // LocalBus registrations can't consume async pg). So on every startup the
6
+ // in-memory read models begin EMPTY and must be rebuilt by replaying the durable
7
+ // event log through the projection handlers — read models are derived data, the
8
+ // log is the source of truth.
9
+ //
10
+ // Unlike the SQLite ProjectionCheckpoint, there is no persisted checkpoint here:
11
+ // in-memory read models always start at position 0, so the full history
12
+ // `(0, head]` is replayed once. The `head` bound is captured BEFORE the plugins
13
+ // build (and before this session's Connect dispatches / commands append), so this
14
+ // session's own events — which are live-delivered — are never redelivered.
15
+ //
16
+ // The {id, meta, event} envelopes are reconstructed by ProjectionCheckpoint's
17
+ // builders so they match live topic delivery byte-for-byte.
18
+
19
+ module Pg = ReventlessPostgres.PgDriver
20
+
21
+ // Head positions captured before the session appends anything.
22
+ type bounds = {aggBound: int, dcbBound: int}
23
+
24
+ let intCol = (row: dict<JSON.t>, key: string): int =>
25
+ switch row->Dict.get(key) {
26
+ | Some(JSON.Number(n)) => Float.toInt(n)
27
+ | Some(JSON.String(s)) => s->Int.fromString->Option.getOr(0)
28
+ | _ => 0
29
+ }
30
+
31
+ // Snapshot the current head of each axis. Dispatched before plugin build so the
32
+ // bound excludes this session's live-delivered appends.
33
+ let captureBounds = async (pool: Pg.pool): bounds => {
34
+ let agg = switch await pool->Pg.queryOne(
35
+ "SELECT COALESCE(MAX(global_seq), 0)::bigint AS m FROM event_log",
36
+ [],
37
+ ) {
38
+ | Some(row) => intCol(row, "m")
39
+ | None => 0
40
+ }
41
+ let dcb = switch await pool->Pg.queryOne(
42
+ "SELECT COALESCE(MAX(position), 0)::bigint AS m FROM dcb_event",
43
+ [],
44
+ ) {
45
+ | Some(row) => intCol(row, "m")
46
+ | None => 0
47
+ }
48
+ {aggBound: agg, dcbBound: dcb}
49
+ }
50
+
51
+ // First tag value of a DCB event's text[] `tags` column ('key=value' → value),
52
+ // reproducing publish-time entityId derivation (the first tag).
53
+ let firstTagValue = (row: dict<JSON.t>): option<string> =>
54
+ switch row->Dict.get("tags") {
55
+ | Some(JSON.Array(arr)) =>
56
+ switch arr->Array.get(0) {
57
+ | Some(JSON.String(kv)) =>
58
+ switch kv->String.indexOf("=") {
59
+ | -1 => Some(kv)
60
+ | i => Some(kv->String.slice(~start=i + 1, ~end=kv->String.length))
61
+ }
62
+ | _ => None
63
+ }
64
+ | _ => None
65
+ }
66
+
67
+ // Deliver `envelope` to every handler in registration order (handlers dispatch by
68
+ // meta.service and no-op on events none of their mappings consume, so delivering
69
+ // both axes to every projection is safe — same at-least-once contract the
70
+ // deployed adapters impose).
71
+ let deliver = async (
72
+ handlers: array<(string, ProjectionCheckpoint.catchupHandler)>,
73
+ envelope: JSON.t,
74
+ ) =>
75
+ for i in 0 to handlers->Array.length - 1 {
76
+ let (_name, handler) = handlers->Array.getUnsafe(i)
77
+ switch await handler(envelope, ()) {
78
+ | () => ()
79
+ | exception _ => ()
80
+ }
81
+ }
82
+
83
+ let runCatchup = async (
84
+ ~pool: Pg.pool,
85
+ ~bounds: bounds,
86
+ ~handlers: array<(string, ProjectionCheckpoint.catchupHandler)>,
87
+ ) => {
88
+ // --- Aggregate axis: flat stored events, global_seq order ---
89
+ let aggRows = await pool->Pg.query(
90
+ "SELECT payload FROM event_log WHERE global_seq <= $1::bigint ORDER BY global_seq ASC",
91
+ [JSON.Encode.int(bounds.aggBound)],
92
+ )
93
+ for i in 0 to aggRows->Array.length - 1 {
94
+ let row = aggRows->Array.getUnsafe(i)
95
+ switch row->Dict.get("payload") {
96
+ | Some(payload) =>
97
+ switch ProjectionCheckpoint.catchupEnvelope(payload) {
98
+ | Some(envelope) => await deliver(handlers, envelope)
99
+ | None => ()
100
+ }
101
+ | None => ()
102
+ }
103
+ }
104
+
105
+ // --- DCB axis: reconstruct the published envelope, position order ---
106
+ let dcbRows = await pool->Pg.query(
107
+ "SELECT log_name, event_type, data, meta, tags FROM dcb_event WHERE position <= $1::bigint ORDER BY position ASC",
108
+ [JSON.Encode.int(bounds.dcbBound)],
109
+ )
110
+ for i in 0 to dcbRows->Array.length - 1 {
111
+ let row = dcbRows->Array.getUnsafe(i)
112
+ switch (
113
+ row->Dict.get("log_name"),
114
+ row->Dict.get("event_type"),
115
+ row->Dict.get("data"),
116
+ row->Dict.get("meta"),
117
+ ) {
118
+ | (Some(JSON.String(logName)), Some(JSON.String(eventType)), Some(data), Some(meta)) =>
119
+ switch ProjectionCheckpoint.dcbCatchupEnvelope(
120
+ ~logName,
121
+ ~eventType,
122
+ ~dataText=JSON.stringify(data),
123
+ ~metaText=JSON.stringify(meta),
124
+ ~firstTagValue=firstTagValue(row),
125
+ ) {
126
+ | Some(envelope) => await deliver(handlers, envelope)
127
+ | None => ()
128
+ }
129
+ | _ => ()
130
+ }
131
+ }
132
+ }
133
+
134
+ // Platform entry point: waits one macrotask so the just-built plugins' Output.apply
135
+ // chains register their collector handlers (mirrors ProjectionCheckpoint.startupCatchup),
136
+ // then replays the captured history.
137
+ let startupCatchup = async (
138
+ ~pool: Pg.pool,
139
+ ~bounds: bounds,
140
+ ~handlers: unit => array<(string, ProjectionCheckpoint.catchupHandler)>,
141
+ ) => {
142
+ await Promise.make((resolve, _) => {
143
+ let _ = setTimeout(() => resolve(), 0)
144
+ })
145
+ await runCatchup(~pool, ~bounds, ~handlers=handlers())
146
+ }
@@ -0,0 +1,114 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
4
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as PgDriver$ReventlessPostgres from "@reventlessdev/reventless-postgres/src/PgDriver.res.mjs";
6
+ import * as ProjectionCheckpoint$ReventlessLocal from "./ProjectionCheckpoint.res.mjs";
7
+
8
+ function intCol(row, key) {
9
+ let match = row[key];
10
+ if (match === undefined) {
11
+ return 0;
12
+ }
13
+ switch (typeof match) {
14
+ case "string" :
15
+ return Stdlib_Option.getOr(Stdlib_Int.fromString(match, undefined), 0);
16
+ case "number" :
17
+ return match | 0;
18
+ default:
19
+ return 0;
20
+ }
21
+ }
22
+
23
+ async function captureBounds(pool) {
24
+ let row = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT COALESCE(MAX(global_seq), 0)::bigint AS m FROM event_log", []);
25
+ let agg = row !== undefined ? intCol(row, "m") : 0;
26
+ let row$1 = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT COALESCE(MAX(position), 0)::bigint AS m FROM dcb_event", []);
27
+ let dcb = row$1 !== undefined ? intCol(row$1, "m") : 0;
28
+ return {
29
+ aggBound: agg,
30
+ dcbBound: dcb
31
+ };
32
+ }
33
+
34
+ function firstTagValue(row) {
35
+ let match = row["tags"];
36
+ if (match === undefined) {
37
+ return;
38
+ }
39
+ if (!Array.isArray(match)) {
40
+ return;
41
+ }
42
+ let match$1 = match[0];
43
+ if (match$1 === undefined) {
44
+ return;
45
+ }
46
+ if (typeof match$1 !== "string") {
47
+ return;
48
+ }
49
+ let i = match$1.indexOf("=");
50
+ if (i !== -1) {
51
+ return match$1.slice(i + 1 | 0, match$1.length);
52
+ } else {
53
+ return match$1;
54
+ }
55
+ }
56
+
57
+ async function deliver(handlers, envelope) {
58
+ for (let i = 0, i_finish = handlers.length; i < i_finish; ++i) {
59
+ let match = handlers[i];
60
+ try {
61
+ await match[1](envelope, undefined);
62
+ } catch (exn) {
63
+
64
+ }
65
+ }
66
+ }
67
+
68
+ async function runCatchup(pool, bounds, handlers) {
69
+ let aggRows = await PgDriver$ReventlessPostgres.query(pool, "SELECT payload FROM event_log WHERE global_seq <= $1::bigint ORDER BY global_seq ASC", [bounds.aggBound]);
70
+ for (let i = 0, i_finish = aggRows.length; i < i_finish; ++i) {
71
+ let row = aggRows[i];
72
+ let payload = row["payload"];
73
+ if (payload !== undefined) {
74
+ let envelope = ProjectionCheckpoint$ReventlessLocal.catchupEnvelope(payload);
75
+ if (envelope !== undefined) {
76
+ await deliver(handlers, envelope);
77
+ }
78
+ }
79
+ }
80
+ let dcbRows = await PgDriver$ReventlessPostgres.query(pool, "SELECT log_name, event_type, data, meta, tags FROM dcb_event WHERE position <= $1::bigint ORDER BY position ASC", [bounds.dcbBound]);
81
+ for (let i$1 = 0, i_finish$1 = dcbRows.length; i$1 < i_finish$1; ++i$1) {
82
+ let row$1 = dcbRows[i$1];
83
+ let match = row$1["log_name"];
84
+ let match$1 = row$1["event_type"];
85
+ let match$2 = row$1["data"];
86
+ let match$3 = row$1["meta"];
87
+ if (typeof match === "string" && typeof match$1 === "string" && match$2 !== undefined && match$3 !== undefined) {
88
+ let envelope$1 = ProjectionCheckpoint$ReventlessLocal.dcbCatchupEnvelope(match, match$1, JSON.stringify(match$2), JSON.stringify(match$3), firstTagValue(row$1));
89
+ if (envelope$1 !== undefined) {
90
+ await deliver(handlers, envelope$1);
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ async function startupCatchup(pool, bounds, handlers) {
97
+ await new Promise((resolve, param) => {
98
+ setTimeout(() => resolve(), 0);
99
+ });
100
+ return await runCatchup(pool, bounds, handlers());
101
+ }
102
+
103
+ let Pg;
104
+
105
+ export {
106
+ Pg,
107
+ intCol,
108
+ captureBounds,
109
+ firstTagValue,
110
+ deliver,
111
+ runCatchup,
112
+ startupCatchup,
113
+ }
114
+ /* PgDriver-ReventlessPostgres Not a pure module */
@@ -0,0 +1,43 @@
1
+ // QueryDb storage dispatcher.
2
+ //
3
+ // Selects the concrete backend at `make` time by consulting BackendState:
4
+ // - Sqlite → QueryDbStorage_Sqlite (JSON tables + json_extract indexes)
5
+ // - Memory → QueryDbStorage_InMemory (pure in-memory, Bus-registered)
6
+ //
7
+ // Postgres note: the LocalBus scan/stream/index registrations are SYNCHRONOUS
8
+ // (`unit => array<JSON.t>`), which async pg cannot satisfy without a broader bus
9
+ // refactor. So under Backend.Postgres the read-model live-query path uses the
10
+ // in-memory arm (getDb() is None for Postgres → Memory), rebuilt by replay from
11
+ // the durable pg event log; the deploy-usable ReventlessPostgres.QueryDbStorage_Postgres
12
+ // serves non-local compute layers. See docs/plans/postgres-storage-adapter.md §E.
13
+
14
+ module Make = (Bus: LocalBus.T) => {
15
+ module Mem = QueryDbStorage_InMemory.Make(Bus)
16
+
17
+ type api = unit
18
+ type role = unit
19
+
20
+ let sqliteBusCallbacks: QueryDbStorage_Sqlite.busCallbacks = {
21
+ publishStateChange: Bus.publishStateChange,
22
+ registerQueryDb: Bus.registerQueryDb,
23
+ registerQueryDbScan: Bus.registerQueryDbScan,
24
+ registerQueryDbStream: Bus.registerQueryDbStream,
25
+ registerQueryDbIndexLookup: Bus.registerQueryDbIndexLookup,
26
+ registerQueryDbListPage: Bus.registerQueryDbListPage,
27
+ }
28
+
29
+ let make: ReventlessCore.QueryDb_Adapter.storageMaker<unit, unit> = (
30
+ ~name,
31
+ ~indexes,
32
+ ~subIdField=?,
33
+ ~ttl=?,
34
+ ~api,
35
+ ~apiRole,
36
+ ~opts,
37
+ ) =>
38
+ switch BackendState.getSqliteDb() {
39
+ | Some(db) =>
40
+ QueryDbStorage_Sqlite.makeStorage(~db, ~bus=sqliteBusCallbacks, ~name, ~indexes, ~subIdField)
41
+ | None => Mem.make(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
42
+ }
43
+ }
@@ -0,0 +1,42 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
4
+ import * as BackendState$ReventlessLocal from "../BackendState.res.mjs";
5
+ import * as QueryDbStorage_Sqlite$ReventlessLocal from "./QueryDbStorage_Sqlite.res.mjs";
6
+ import * as QueryDbStorage_InMemory$ReventlessLocal from "./QueryDbStorage_InMemory.res.mjs";
7
+
8
+ function Make(Bus) {
9
+ let Mem = QueryDbStorage_InMemory$ReventlessLocal.Make(Bus);
10
+ let sqliteBusCallbacks_publishStateChange = Bus.publishStateChange;
11
+ let sqliteBusCallbacks_registerQueryDb = Bus.registerQueryDb;
12
+ let sqliteBusCallbacks_registerQueryDbScan = Bus.registerQueryDbScan;
13
+ let sqliteBusCallbacks_registerQueryDbStream = Bus.registerQueryDbStream;
14
+ let sqliteBusCallbacks_registerQueryDbIndexLookup = Bus.registerQueryDbIndexLookup;
15
+ let sqliteBusCallbacks_registerQueryDbListPage = Bus.registerQueryDbListPage;
16
+ let sqliteBusCallbacks = {
17
+ publishStateChange: sqliteBusCallbacks_publishStateChange,
18
+ registerQueryDb: sqliteBusCallbacks_registerQueryDb,
19
+ registerQueryDbScan: sqliteBusCallbacks_registerQueryDbScan,
20
+ registerQueryDbStream: sqliteBusCallbacks_registerQueryDbStream,
21
+ registerQueryDbIndexLookup: sqliteBusCallbacks_registerQueryDbIndexLookup,
22
+ registerQueryDbListPage: sqliteBusCallbacks_registerQueryDbListPage
23
+ };
24
+ let make = (name, indexes, subIdField, ttl, api, apiRole, opts) => {
25
+ let db = BackendState$ReventlessLocal.getSqliteDb();
26
+ if (db !== undefined) {
27
+ return QueryDbStorage_Sqlite$ReventlessLocal.makeStorage(Primitive_option.valFromOption(db), sqliteBusCallbacks, name, indexes, subIdField);
28
+ } else {
29
+ return Mem.make(name, indexes, subIdField, ttl, api, apiRole, opts);
30
+ }
31
+ };
32
+ return {
33
+ Mem: Mem,
34
+ sqliteBusCallbacks: sqliteBusCallbacks,
35
+ make: make
36
+ };
37
+ }
38
+
39
+ export {
40
+ Make,
41
+ }
42
+ /* QueryDbStorage_Sqlite-ReventlessLocal Not a pure module */
@@ -64,16 +64,9 @@ module Make = (Bus: LocalBus.T) => {
64
64
  type api = unit
65
65
  type role = unit
66
66
 
67
- let sqliteBusCallbacks: QueryDbStorage_Sqlite.busCallbacks = {
68
- publishStateChange: Bus.publishStateChange,
69
- registerQueryDb: Bus.registerQueryDb,
70
- registerQueryDbScan: Bus.registerQueryDbScan,
71
- registerQueryDbStream: Bus.registerQueryDbStream,
72
- registerQueryDbIndexLookup: Bus.registerQueryDbIndexLookup,
73
- registerQueryDbListPage: Bus.registerQueryDbListPage,
74
- }
75
-
76
- let makeMemory: QueryDb_Adapter.storageMaker<unit, unit> = (
67
+ // Pure in-memory storageMaker (registers with Bus for live reads). Backend
68
+ // selection lives in LocalQueryDbStorage.Make.
69
+ let make: QueryDb_Adapter.storageMaker<unit, unit> = (
77
70
  ~name,
78
71
  ~indexes as _,
79
72
  ~subIdField=?,
@@ -343,18 +336,4 @@ module Make = (Bus: LocalBus.T) => {
343
336
  }
344
337
  }
345
338
 
346
- let make: QueryDb_Adapter.storageMaker<unit, unit> = (
347
- ~name,
348
- ~indexes,
349
- ~subIdField=?,
350
- ~ttl=?,
351
- ~api,
352
- ~apiRole,
353
- ~opts,
354
- ) =>
355
- switch BackendState.getDb() {
356
- | Some(db) =>
357
- QueryDbStorage_Sqlite.makeStorage(~db, ~bus=sqliteBusCallbacks, ~name, ~indexes, ~subIdField)
358
- | None => makeMemory(~name, ~indexes, ~subIdField?, ~ttl?, ~api, ~apiRole, ~opts)
359
- }
360
339
  }