@reventlessdev/reventless-postgres 3.0.0-alpha.1

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 (128) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +202 -0
  3. package/__mocks__/emptyModule.js +1 -0
  4. package/lib/bs/.compiler.log +2 -0
  5. package/lib/bs/ReventlessPostgres.cmi +0 -0
  6. package/lib/bs/ReventlessPostgres.cmj +0 -0
  7. package/lib/bs/ReventlessPostgres.cmt +0 -0
  8. package/lib/bs/ReventlessPostgres.mlmap +9 -0
  9. package/lib/bs/build.ninja +0 -0
  10. package/lib/bs/compiler-info.json +8 -0
  11. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  12. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  13. package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  14. package/lib/bs/src/DcbEventLogStorage_Postgres.ast +0 -0
  15. package/lib/bs/src/DcbEventLogStorage_Postgres.res +352 -0
  16. package/lib/bs/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
  17. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  18. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  19. package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  20. package/lib/bs/src/EventLogStorage_Postgres.ast +0 -0
  21. package/lib/bs/src/EventLogStorage_Postgres.res +176 -0
  22. package/lib/bs/src/EventLogStorage_Postgres.res.mjs +207 -0
  23. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  24. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmj +0 -0
  25. package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  26. package/lib/bs/src/PgChangeFeed.ast +0 -0
  27. package/lib/bs/src/PgChangeFeed.res +145 -0
  28. package/lib/bs/src/PgChangeFeed.res.mjs +90 -0
  29. package/lib/bs/src/PgDriver-ReventlessPostgres.cmi +0 -0
  30. package/lib/bs/src/PgDriver-ReventlessPostgres.cmj +0 -0
  31. package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
  32. package/lib/bs/src/PgDriver-ReventlessPostgres.cmti +0 -0
  33. package/lib/bs/src/PgDriver.ast +0 -0
  34. package/lib/bs/src/PgDriver.iast +0 -0
  35. package/lib/bs/src/PgDriver.res +111 -0
  36. package/lib/bs/src/PgDriver.res.mjs +85 -0
  37. package/lib/bs/src/PgDriver.resi +68 -0
  38. package/lib/bs/src/PgSchema-ReventlessPostgres.cmi +0 -0
  39. package/lib/bs/src/PgSchema-ReventlessPostgres.cmj +0 -0
  40. package/lib/bs/src/PgSchema-ReventlessPostgres.cmt +0 -0
  41. package/lib/bs/src/PgSchema.ast +0 -0
  42. package/lib/bs/src/PgSchema.res +256 -0
  43. package/lib/bs/src/PgSchema.res.mjs +46 -0
  44. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  45. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
  46. package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  47. package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
  48. package/lib/bs/src/QueryDbStorage_Postgres.res +244 -0
  49. package/lib/bs/src/QueryDbStorage_Postgres.res.mjs +225 -0
  50. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  51. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
  52. package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  53. package/lib/bs/src/QueryEnginePostgres.ast +0 -0
  54. package/lib/bs/src/QueryEnginePostgres.res +127 -0
  55. package/lib/bs/src/QueryEnginePostgres.res.mjs +172 -0
  56. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  57. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
  58. package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  59. package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
  60. package/lib/bs/tests/PostgresIntegrationTest.res +236 -0
  61. package/lib/bs/tests/PostgresIntegrationTest.res.mjs +328 -0
  62. package/lib/ocaml/.compiler.log +2 -0
  63. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  64. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  65. package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  66. package/lib/ocaml/DcbEventLogStorage_Postgres.ast +0 -0
  67. package/lib/ocaml/DcbEventLogStorage_Postgres.res +352 -0
  68. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
  69. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
  70. package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
  71. package/lib/ocaml/EventLogStorage_Postgres.ast +0 -0
  72. package/lib/ocaml/EventLogStorage_Postgres.res +176 -0
  73. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
  74. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmj +0 -0
  75. package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
  76. package/lib/ocaml/PgChangeFeed.ast +0 -0
  77. package/lib/ocaml/PgChangeFeed.res +145 -0
  78. package/lib/ocaml/PgDriver-ReventlessPostgres.cmi +0 -0
  79. package/lib/ocaml/PgDriver-ReventlessPostgres.cmj +0 -0
  80. package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
  81. package/lib/ocaml/PgDriver-ReventlessPostgres.cmti +0 -0
  82. package/lib/ocaml/PgDriver.ast +0 -0
  83. package/lib/ocaml/PgDriver.iast +0 -0
  84. package/lib/ocaml/PgDriver.res +111 -0
  85. package/lib/ocaml/PgDriver.resi +68 -0
  86. package/lib/ocaml/PgSchema-ReventlessPostgres.cmi +0 -0
  87. package/lib/ocaml/PgSchema-ReventlessPostgres.cmj +0 -0
  88. package/lib/ocaml/PgSchema-ReventlessPostgres.cmt +0 -0
  89. package/lib/ocaml/PgSchema.ast +0 -0
  90. package/lib/ocaml/PgSchema.res +256 -0
  91. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
  92. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
  93. package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
  94. package/lib/ocaml/PostgresIntegrationTest.ast +0 -0
  95. package/lib/ocaml/PostgresIntegrationTest.res +236 -0
  96. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
  97. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
  98. package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
  99. package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
  100. package/lib/ocaml/QueryDbStorage_Postgres.res +244 -0
  101. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
  102. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
  103. package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
  104. package/lib/ocaml/QueryEnginePostgres.ast +0 -0
  105. package/lib/ocaml/QueryEnginePostgres.res +127 -0
  106. package/lib/ocaml/ReventlessPostgres.cmi +0 -0
  107. package/lib/ocaml/ReventlessPostgres.cmj +0 -0
  108. package/lib/ocaml/ReventlessPostgres.cmt +0 -0
  109. package/lib/ocaml/ReventlessPostgres.mlmap +9 -0
  110. package/package.json +56 -0
  111. package/rescript.json +36 -0
  112. package/src/DcbEventLogStorage_Postgres.res +352 -0
  113. package/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
  114. package/src/EventLogStorage_Postgres.res +176 -0
  115. package/src/EventLogStorage_Postgres.res.mjs +207 -0
  116. package/src/PgChangeFeed.res +145 -0
  117. package/src/PgChangeFeed.res.mjs +90 -0
  118. package/src/PgDriver.res +111 -0
  119. package/src/PgDriver.res.mjs +85 -0
  120. package/src/PgDriver.resi +68 -0
  121. package/src/PgSchema.res +256 -0
  122. package/src/PgSchema.res.mjs +46 -0
  123. package/src/QueryDbStorage_Postgres.res +244 -0
  124. package/src/QueryDbStorage_Postgres.res.mjs +225 -0
  125. package/src/QueryEnginePostgres.res +127 -0
  126. package/src/QueryEnginePostgres.res.mjs +172 -0
  127. package/tests/PostgresIntegrationTest.res +236 -0
  128. package/tests/PostgresIntegrationTest.res.mjs +328 -0
@@ -0,0 +1,207 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stream from "@reventlessdev/rescript-effect/src/Stream.res.mjs";
4
+ import * as Stdlib_Int from "@rescript/runtime/lib/es6/Stdlib_Int.js";
5
+ import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
7
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Stream$1 from "effect/Stream";
10
+ import * as Pulumi from "@pulumi/pulumi";
11
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
12
+ import * as PgDriver$ReventlessPostgres from "./PgDriver.res.mjs";
13
+
14
+ function noTracking(param) {
15
+
16
+ }
17
+
18
+ function decodePayload(row) {
19
+ return Stdlib_Option.getOr(row["payload"], null);
20
+ }
21
+
22
+ function coerceInt(v) {
23
+ switch (typeof v) {
24
+ case "string" :
25
+ return Stdlib_Option.getOr(Stdlib_Int.fromString(v, undefined), 0);
26
+ case "number" :
27
+ return v | 0;
28
+ default:
29
+ return 0;
30
+ }
31
+ }
32
+
33
+ async function countAll(pool) {
34
+ let row = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT COUNT(*)::bigint AS c FROM event_log", []);
35
+ if (row !== undefined) {
36
+ return Stdlib_Option.getOr(Stdlib_Option.map(row["c"], coerceInt), 0);
37
+ } else {
38
+ return 0;
39
+ }
40
+ }
41
+
42
+ function isUniqueViolation(exn) {
43
+ let msg = Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "");
44
+ if (msg.includes("duplicate key") || msg.includes("unique")) {
45
+ return true;
46
+ } else {
47
+ return msg.includes("23505");
48
+ }
49
+ }
50
+
51
+ function makeStorage(pool, name, param, onAppendedOpt) {
52
+ let onAppended = onAppendedOpt !== undefined ? onAppendedOpt : noTracking;
53
+ let appendTracked = async (track, seqNr, id, jsons) => {
54
+ if (jsons.length === 0) {
55
+ return {
56
+ TAG: "Ok",
57
+ _0: undefined
58
+ };
59
+ }
60
+ try {
61
+ let rows = await PgDriver$ReventlessPostgres.query(pool, "INSERT INTO event_log(log_name, aggregate_id, seq_nr, payload, msg_id)\n SELECT $1, $2, $3::bigint + (ord - 1), value, value->>'msgId'\n FROM jsonb_array_elements($4::jsonb) WITH ORDINALITY AS t(value, ord)\n WHERE $3::bigint = (SELECT COALESCE(MAX(seq_nr), -1) + 1\n FROM event_log\n WHERE log_name = $1 AND aggregate_id = $2)\n RETURNING global_seq, msg_id", [
62
+ name,
63
+ id,
64
+ seqNr,
65
+ JSON.stringify(jsons)
66
+ ]);
67
+ if (rows.length === 0) {
68
+ return {
69
+ TAG: "Error",
70
+ _0: "Conflict"
71
+ };
72
+ } else {
73
+ if (track) {
74
+ onAppended(Stdlib_Array.filterMap(rows, row => {
75
+ let match = row["msg_id"];
76
+ let match$1 = row["global_seq"];
77
+ if (typeof match === "string" && match$1 !== undefined) {
78
+ return [
79
+ match,
80
+ coerceInt(match$1)
81
+ ];
82
+ }
83
+ }));
84
+ }
85
+ return {
86
+ TAG: "Ok",
87
+ _0: undefined
88
+ };
89
+ }
90
+ } catch (raw_exn) {
91
+ let exn = Primitive_exceptions.internalToException(raw_exn);
92
+ if (isUniqueViolation(exn)) {
93
+ return {
94
+ TAG: "Error",
95
+ _0: "Conflict"
96
+ };
97
+ } else {
98
+ return {
99
+ TAG: "Error",
100
+ _0: {
101
+ TAG: "StorageFailure",
102
+ _0: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "storage error")
103
+ }
104
+ };
105
+ }
106
+ }
107
+ };
108
+ let append = (seqNr, id, jsons) => appendTracked(true, seqNr, id, jsons);
109
+ let replayArray = async (id, fromSeqOpt) => {
110
+ let fromSeq = fromSeqOpt !== undefined ? fromSeqOpt : 0;
111
+ return (await PgDriver$ReventlessPostgres.query(pool, "SELECT payload FROM event_log\n WHERE log_name = $1 AND aggregate_id = $2 AND seq_nr >= $3::bigint\n ORDER BY seq_nr ASC", [
112
+ name,
113
+ id,
114
+ fromSeq
115
+ ])).map(decodePayload);
116
+ };
117
+ let replay = id => replayArray(id, undefined);
118
+ let replayStream = (id, fromSeq) => Stream$1.flatMap(Stream$1.fromEffect(Effect.promise(() => replayArray(id, fromSeq))), arr => Stream$1.fromIterable(arr));
119
+ let latestSnapshot = async id => {
120
+ let row = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT seq_nr, state, schema_hash FROM snapshot WHERE log_name = $1 AND aggregate_id = $2", [
121
+ name,
122
+ id
123
+ ]);
124
+ if (row === undefined) {
125
+ return {
126
+ TAG: "Ok",
127
+ _0: undefined
128
+ };
129
+ }
130
+ let match = row["seq_nr"];
131
+ let match$1 = row["state"];
132
+ let match$2 = row["schema_hash"];
133
+ if (match !== undefined && match$1 !== undefined && typeof match$2 === "string") {
134
+ return {
135
+ TAG: "Ok",
136
+ _0: {
137
+ seqNr: coerceInt(match),
138
+ state: match$1,
139
+ schemaHash: match$2
140
+ }
141
+ };
142
+ } else {
143
+ return {
144
+ TAG: "Error",
145
+ _0: "snapshot row has unexpected shape"
146
+ };
147
+ }
148
+ };
149
+ let writeSnapshot = async (id, snap) => {
150
+ try {
151
+ await PgDriver$ReventlessPostgres.query(pool, "INSERT INTO snapshot(log_name, aggregate_id, seq_nr, state, schema_hash)\n VALUES ($1, $2, $3::bigint, $4::jsonb, $5)\n ON CONFLICT (log_name, aggregate_id)\n DO UPDATE SET seq_nr = EXCLUDED.seq_nr, state = EXCLUDED.state, schema_hash = EXCLUDED.schema_hash", [
152
+ name,
153
+ id,
154
+ snap.seqNr,
155
+ JSON.stringify(snap.state),
156
+ snap.schemaHash
157
+ ]);
158
+ return {
159
+ TAG: "Ok",
160
+ _0: undefined
161
+ };
162
+ } catch (raw_exn) {
163
+ let exn = Primitive_exceptions.internalToException(raw_exn);
164
+ return {
165
+ TAG: "Error",
166
+ _0: Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_JsExn.fromException(exn), Stdlib_JsExn.message), "snapshot write error")
167
+ };
168
+ }
169
+ };
170
+ let appendStream = (startingSeqNr, id, stream) => Effect.flatMap(Stream.runCollect(stream), jsons => Effect.flatMap(Effect.promise(() => appendTracked(false, startingSeqNr, id, jsons)), result => {
171
+ if (result.TAG === "Ok") {
172
+ return Effect.succeed();
173
+ }
174
+ let msg = result._0;
175
+ if (typeof msg !== "object") {
176
+ return Effect.fail("conflict");
177
+ } else {
178
+ return Effect.fail(msg._0);
179
+ }
180
+ }));
181
+ let ops = {
182
+ append: append,
183
+ replay: replay,
184
+ replayStream: replayStream,
185
+ appendStream: appendStream,
186
+ latestSnapshot: latestSnapshot,
187
+ writeSnapshot: writeSnapshot
188
+ };
189
+ return [
190
+ name,
191
+ ops,
192
+ {
193
+ resources: [],
194
+ operations: Pulumi.output(ops)
195
+ }
196
+ ];
197
+ }
198
+
199
+ export {
200
+ noTracking,
201
+ decodePayload,
202
+ coerceInt,
203
+ countAll,
204
+ isUniqueViolation,
205
+ makeStorage,
206
+ }
207
+ /* Stream Not a pure module */
@@ -0,0 +1,145 @@
1
+ // DCB change feed — checkpointed polling with LISTEN/NOTIFY wakeup.
2
+ //
3
+ // This is the one component with no in-repo template (DynamoDB Streams has no
4
+ // drop-in equivalent). It is NOT logical decoding: events are already durably
5
+ // queryable in-table, so the feed is a fenced read over dcb_event plus a
6
+ // `dcb_subscription` checkpoint. `dcb_append` issues `pg_notify('dcb_<log>')`,
7
+ // which turns polling into near-real-time wakeup; a low-frequency fallback tick
8
+ // covers missed notifications (payload overflow, listener reconnect).
9
+ //
10
+ // The feed-consumer API below is a DOCUMENTED PUBLIC SURFACE of the package —
11
+ // external relays and platform packages bridging the feed onto a bus/topic are
12
+ // first-class consumers. Cursor semantics: the opaque `<xid8>:<position>` string
13
+ // (see DcbEventLogStorage_Postgres), which decodes to the (transaction_id,
14
+ // position) pair. Reads apply the xmin barrier, so a returned cursor is stable
15
+ // forever — an event committed late (lower position, later commit) is never
16
+ // skipped by a checkpointing reader.
17
+
18
+ open ReventlessCore
19
+ open Reventless
20
+
21
+ module Dcb = DcbEventLogStorage_Postgres
22
+
23
+ // One page of the feed. `events` are in (transaction_id, position) order;
24
+ // `cursor` is the checkpoint to persist and to pass as the next `after` (None
25
+ // when the page was empty — nothing new).
26
+ type batch = {
27
+ events: array<DcbEventLog_Adapter.rawSequencedEvent>,
28
+ cursor: option<DcbTag.sequencePosition>,
29
+ }
30
+
31
+ // Read up to `limit` events after `after` (the xmin-fenced feed query — no tag
32
+ // filter, whole log). Returns the events and the cursor of the last one.
33
+ let readBatch = async (
34
+ pool: PgDriver.pool,
35
+ ~logName: string,
36
+ ~after: option<DcbTag.sequencePosition>=?,
37
+ ~limit: int=500,
38
+ ): batch => {
39
+ let b = Dcb.mkBuilder()
40
+ let where = Dcb.buildReadWhere(
41
+ b,
42
+ ~name=logName,
43
+ ~query=[],
44
+ ~after=after->Option.flatMap(Dcb.decodeCursor),
45
+ ~applyFence=true,
46
+ )
47
+ let sql = `SELECT ${Dcb.selectColumns} FROM dcb_event WHERE ${where} ORDER BY transaction_id ASC, position ASC LIMIT ${Int.toString(limit)}`
48
+ let rows = await pool->PgDriver.query(sql, b.params)
49
+ let events = rows->Array.map(Dcb.rowToEvent)
50
+ let cursor = switch events->Array.get(events->Array.length - 1) {
51
+ | Some(last) => Some(last.position)
52
+ | None => None
53
+ }
54
+ {events, cursor}
55
+ }
56
+
57
+ // --- Checkpoint contract (dcb_subscription) ---
58
+
59
+ // The persisted checkpoint for `subscriber`, encoded as a cursor, or None if the
60
+ // subscriber has never committed (→ replay from the beginning).
61
+ let loadCheckpoint = async (
62
+ pool: PgDriver.pool,
63
+ ~subscriber: string,
64
+ ): option<DcbTag.sequencePosition> =>
65
+ switch await pool->PgDriver.queryOne(
66
+ "SELECT lpad(last_tx::text, 20, '0') || ':' || lpad(last_position::text, 20, '0') AS cursor
67
+ FROM dcb_subscription WHERE subscriber = $1 AND last_position > 0",
68
+ [JSON.Encode.string(subscriber)],
69
+ ) {
70
+ | Some(row) =>
71
+ switch row->Dict.get("cursor") {
72
+ | Some(JSON.String(s)) => Some(s)
73
+ | _ => None
74
+ }
75
+ | None => None
76
+ }
77
+
78
+ // Persist the checkpoint. Idempotent upsert; decodes the cursor to its two
79
+ // columns so a later `loadCheckpoint` round-trips exactly.
80
+ let saveCheckpoint = async (
81
+ pool: PgDriver.pool,
82
+ ~subscriber: string,
83
+ ~cursor: DcbTag.sequencePosition,
84
+ ): unit =>
85
+ switch Dcb.decodeCursor(cursor) {
86
+ | Some((tx, pos)) =>
87
+ let _ = await pool->PgDriver.query(
88
+ "INSERT INTO dcb_subscription(subscriber, last_tx, last_position)
89
+ VALUES ($1, $2::xid8, $3::bigint)
90
+ ON CONFLICT (subscriber)
91
+ DO UPDATE SET last_tx = EXCLUDED.last_tx, last_position = EXCLUDED.last_position",
92
+ [JSON.Encode.string(subscriber), JSON.Encode.string(tx), JSON.Encode.string(pos)],
93
+ )
94
+ | None => ()
95
+ }
96
+
97
+ // --- Wakeup ---
98
+
99
+ // LISTEN on `dcb_<logName>`; `onWake` fires on every notification. Returns the
100
+ // dedicated client (caller owns it — release via PgDriver.unlisten). Pair with a
101
+ // low-frequency fallback tick in the consumer loop so a missed NOTIFY only adds
102
+ // latency, never a permanent stall.
103
+ let listen = (pool: PgDriver.pool, ~logName: string, ~onWake: unit => unit): promise<PgDriver.client> =>
104
+ pool->PgDriver.listen(~channel="dcb_" ++ logName, ~onNotify=_ => onWake())
105
+
106
+ let unlisten = (client: PgDriver.client, ~logName: string): promise<unit> =>
107
+ client->PgDriver.unlisten(~channel="dcb_" ++ logName)
108
+
109
+ // --- Reference consumer driver ---
110
+
111
+ // Drain the feed for `subscriber` from its checkpoint: repeatedly readBatch →
112
+ // `handle` the events → saveCheckpoint, until a short page. `handle` must be
113
+ // idempotent (a crash between handle and checkpoint replays the last batch).
114
+ // Returns the number of events processed. This is the in-repo reference consumer;
115
+ // a bus/topic relay would call the same primitives with its own `handle`.
116
+ let drain = async (
117
+ pool: PgDriver.pool,
118
+ ~logName: string,
119
+ ~subscriber: string,
120
+ ~limit: int=500,
121
+ ~handle: array<DcbEventLog_Adapter.rawSequencedEvent> => promise<unit>,
122
+ ): int => {
123
+ let processed = ref(0)
124
+ let cursor = ref(await loadCheckpoint(pool, ~subscriber))
125
+ let continue = ref(true)
126
+ while continue.contents {
127
+ let {events, cursor: newCursor} = await readBatch(pool, ~logName, ~after=?cursor.contents, ~limit)
128
+ if events->Array.length == 0 {
129
+ continue := false
130
+ } else {
131
+ await handle(events)
132
+ switch newCursor {
133
+ | Some(c) =>
134
+ await saveCheckpoint(pool, ~subscriber, ~cursor=c)
135
+ cursor := Some(c)
136
+ | None => ()
137
+ }
138
+ processed := processed.contents + events->Array.length
139
+ if events->Array.length < limit {
140
+ continue := false
141
+ }
142
+ }
143
+ }
144
+ processed.contents
145
+ }
@@ -0,0 +1,90 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as PgDriver$ReventlessPostgres from "./PgDriver.res.mjs";
5
+ import * as DcbEventLogStorage_Postgres$ReventlessPostgres from "./DcbEventLogStorage_Postgres.res.mjs";
6
+
7
+ async function readBatch(pool, logName, after, limitOpt) {
8
+ let limit = limitOpt !== undefined ? limitOpt : 500;
9
+ let b = DcbEventLogStorage_Postgres$ReventlessPostgres.mkBuilder();
10
+ let where = DcbEventLogStorage_Postgres$ReventlessPostgres.buildReadWhere(b, logName, [], Stdlib_Option.flatMap(after, DcbEventLogStorage_Postgres$ReventlessPostgres.decodeCursor), true);
11
+ let sql = `SELECT ` + DcbEventLogStorage_Postgres$ReventlessPostgres.selectColumns + ` FROM dcb_event WHERE ` + where + ` ORDER BY transaction_id ASC, position ASC LIMIT ` + limit.toString();
12
+ let rows = await PgDriver$ReventlessPostgres.query(pool, sql, b.params);
13
+ let events = rows.map(DcbEventLogStorage_Postgres$ReventlessPostgres.rowToEvent);
14
+ let last = events[events.length - 1 | 0];
15
+ let cursor = last !== undefined ? last.position : undefined;
16
+ return {
17
+ events: events,
18
+ cursor: cursor
19
+ };
20
+ }
21
+
22
+ async function loadCheckpoint(pool, subscriber) {
23
+ let row = await PgDriver$ReventlessPostgres.queryOne(pool, "SELECT lpad(last_tx::text, 20, '0') || ':' || lpad(last_position::text, 20, '0') AS cursor\n FROM dcb_subscription WHERE subscriber = $1 AND last_position > 0", [subscriber]);
24
+ if (row === undefined) {
25
+ return;
26
+ }
27
+ let match = row["cursor"];
28
+ if (typeof match === "string") {
29
+ return match;
30
+ }
31
+ }
32
+
33
+ async function saveCheckpoint(pool, subscriber, cursor) {
34
+ let match = DcbEventLogStorage_Postgres$ReventlessPostgres.decodeCursor(cursor);
35
+ if (match !== undefined) {
36
+ await PgDriver$ReventlessPostgres.query(pool, "INSERT INTO dcb_subscription(subscriber, last_tx, last_position)\n VALUES ($1, $2::xid8, $3::bigint)\n ON CONFLICT (subscriber)\n DO UPDATE SET last_tx = EXCLUDED.last_tx, last_position = EXCLUDED.last_position", [
37
+ subscriber,
38
+ match[0],
39
+ match[1]
40
+ ]);
41
+ return;
42
+ }
43
+ }
44
+
45
+ function listen(pool, logName, onWake) {
46
+ return PgDriver$ReventlessPostgres.listen(pool, "dcb_" + logName, param => onWake());
47
+ }
48
+
49
+ function unlisten(client, logName) {
50
+ return PgDriver$ReventlessPostgres.unlisten(client, "dcb_" + logName);
51
+ }
52
+
53
+ async function drain(pool, logName, subscriber, limitOpt, handle) {
54
+ let limit = limitOpt !== undefined ? limitOpt : 500;
55
+ let processed = 0;
56
+ let cursor = await loadCheckpoint(pool, subscriber);
57
+ let $$continue = true;
58
+ while ($$continue) {
59
+ let match = await readBatch(pool, logName, cursor, limit);
60
+ let events = match.events;
61
+ if (events.length === 0) {
62
+ $$continue = false;
63
+ } else {
64
+ let newCursor = match.cursor;
65
+ await handle(events);
66
+ if (newCursor !== undefined) {
67
+ await saveCheckpoint(pool, subscriber, newCursor);
68
+ cursor = newCursor;
69
+ }
70
+ processed = processed + events.length | 0;
71
+ if (events.length < limit) {
72
+ $$continue = false;
73
+ }
74
+ }
75
+ };
76
+ return processed;
77
+ }
78
+
79
+ let Dcb;
80
+
81
+ export {
82
+ Dcb,
83
+ readBatch,
84
+ loadCheckpoint,
85
+ saveCheckpoint,
86
+ listen,
87
+ unlisten,
88
+ drain,
89
+ }
90
+ /* PgDriver-ReventlessPostgres Not a pure module */
@@ -0,0 +1,111 @@
1
+ // node-postgres (`pg`) binding. See PgDriver.resi for the contract.
2
+
3
+ type pool
4
+ type client
5
+
6
+ // TLS options passed straight to node's tls. RDS Postgres (PG15+) defaults to
7
+ // `rds.force_ssl=1`, so a managed-Postgres pool must enable SSL. Verifying the
8
+ // RDS CA needs the bundle wired in (`ca`); `rejectUnauthorized: false` encrypts
9
+ // without CA verification — acceptable for v1, tighten later.
10
+ type sslConfig = {rejectUnauthorized: bool}
11
+
12
+ // pg calls the password provider (with no args) each time it opens a physical
13
+ // connection, so a rotated secret is picked up on the next new connection
14
+ // without rebuilding the pool.
15
+ type passwordProvider = unit => promise<string>
16
+
17
+ type poolConfig = {
18
+ // Either supply a full `connectionString`, or the discrete fields below (used
19
+ // by the AWS adapter, where the password comes from a Secrets Manager
20
+ // provider and cannot be baked into a static string).
21
+ connectionString?: string,
22
+ host?: string,
23
+ port?: int,
24
+ database?: string,
25
+ user?: string,
26
+ password?: passwordProvider,
27
+ ssl?: sslConfig,
28
+ max?: int,
29
+ }
30
+
31
+ // pg is CJS (`module.exports = { Pool, Client, ... }`); Node's ESM interop
32
+ // exposes `Pool` as a named import.
33
+ @module("pg") @new external makePool: poolConfig => pool = "Pool"
34
+
35
+ @send external _end: pool => promise<unit> = "end"
36
+ let endPool = pool => _end(pool)
37
+
38
+ // pg's `query` resolves to a result object; `.rows` is the payload we want.
39
+ type queryResult = {rows: array<dict<JSON.t>>}
40
+
41
+ @send external _query: (pool, string, array<JSON.t>) => promise<queryResult> = "query"
42
+ @send external _queryText: (pool, string) => promise<queryResult> = "query"
43
+ @send external _connect: pool => promise<client> = "connect"
44
+ @send external _release: client => unit = "release"
45
+ @send external _clientQuery: (client, string, array<JSON.t>) => promise<queryResult> = "query"
46
+ @send external _clientQueryText: (client, string) => promise<queryResult> = "query"
47
+
48
+ let query = async (pool, sql, params) => (await _query(pool, sql, params)).rows
49
+
50
+ let queryOne = async (pool, sql, params) =>
51
+ (await _query(pool, sql, params)).rows->Array.get(0)
52
+
53
+ let exec = async (pool, sql) => {
54
+ let _ = await _queryText(pool, sql)
55
+ }
56
+
57
+ let queryOn = async (client, sql, params) => (await _clientQuery(client, sql, params)).rows
58
+ let execOn = async (client, sql) => {
59
+ let _ = await _clientQueryText(client, sql)
60
+ }
61
+
62
+ let transaction = async (pool, fn) => {
63
+ let client = await _connect(pool)
64
+ try {
65
+ let _ = await _clientQueryText(client, "BEGIN")
66
+ let result = await fn(client)
67
+ let _ = await _clientQueryText(client, "COMMIT")
68
+ _release(client)
69
+ result
70
+ } catch {
71
+ | exn =>
72
+ // Roll back and release, but never let a failing ROLLBACK mask the original
73
+ // error (the original is the diagnostic one). Always return the connection.
74
+ try {
75
+ let _ = await _clientQueryText(client, "ROLLBACK")
76
+ } catch {
77
+ | _ => ()
78
+ }
79
+ _release(client)
80
+ throw(exn)
81
+ }
82
+ }
83
+
84
+ // LISTEN/NOTIFY on a dedicated connection. `notification` events carry
85
+ // `{channel, payload}`; we forward the payload string.
86
+ type notification = {payload: Nullable.t<string>}
87
+ @send
88
+ external _onNotification: (client, @as("notification") _, notification => unit) => unit = "on"
89
+
90
+ let listen = async (pool, ~channel, ~onNotify) => {
91
+ let client = await _connect(pool)
92
+ client->_onNotification(n =>
93
+ switch n.payload->Nullable.toOption {
94
+ | Some(p) => onNotify(p)
95
+ | None => ()
96
+ }
97
+ )
98
+ // Channel identifiers can't be parameterised; the caller supplies a
99
+ // framework-controlled name (`dcb_<log>`), never user input.
100
+ let _ = await _clientQueryText(client, `LISTEN "${channel}"`)
101
+ client
102
+ }
103
+
104
+ let unlisten = async (client, ~channel) => {
105
+ try {
106
+ let _ = await _clientQueryText(client, `UNLISTEN "${channel}"`)
107
+ } catch {
108
+ | _ => ()
109
+ }
110
+ _release(client)
111
+ }
@@ -0,0 +1,85 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Pg from "pg";
4
+
5
+ function endPool(pool) {
6
+ return pool.end();
7
+ }
8
+
9
+ async function query(pool, sql, params) {
10
+ return (await pool.query(sql, params)).rows;
11
+ }
12
+
13
+ async function queryOne(pool, sql, params) {
14
+ return (await pool.query(sql, params)).rows[0];
15
+ }
16
+
17
+ async function exec(pool, sql) {
18
+ await pool.query(sql);
19
+ }
20
+
21
+ async function queryOn(client, sql, params) {
22
+ return (await client.query(sql, params)).rows;
23
+ }
24
+
25
+ async function execOn(client, sql) {
26
+ await client.query(sql);
27
+ }
28
+
29
+ async function transaction(pool, fn) {
30
+ let client = await pool.connect();
31
+ try {
32
+ await client.query("BEGIN");
33
+ let result = await fn(client);
34
+ await client.query("COMMIT");
35
+ client.release();
36
+ return result;
37
+ } catch (exn) {
38
+ try {
39
+ await client.query("ROLLBACK");
40
+ } catch (exn$1) {
41
+
42
+ }
43
+ client.release();
44
+ throw exn;
45
+ }
46
+ }
47
+
48
+ async function listen(pool, channel, onNotify) {
49
+ let client = await pool.connect();
50
+ client.on("notification", n => {
51
+ let p = n.payload;
52
+ if (!(p == null)) {
53
+ return onNotify(p);
54
+ }
55
+ });
56
+ await client.query(`LISTEN "` + channel + `"`);
57
+ return client;
58
+ }
59
+
60
+ async function unlisten(client, channel) {
61
+ try {
62
+ await client.query(`UNLISTEN "` + channel + `"`);
63
+ } catch (exn) {
64
+
65
+ }
66
+ client.release();
67
+ }
68
+
69
+ function makePool(prim) {
70
+ return new Pg.Pool(prim);
71
+ }
72
+
73
+ export {
74
+ makePool,
75
+ endPool,
76
+ query,
77
+ queryOne,
78
+ exec,
79
+ transaction,
80
+ queryOn,
81
+ execOn,
82
+ listen,
83
+ unlisten,
84
+ }
85
+ /* pg Not a pure module */