@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +202 -0
- package/__mocks__/emptyModule.js +1 -0
- package/lib/bs/.compiler.log +2 -0
- package/lib/bs/ReventlessPostgres.cmi +0 -0
- package/lib/bs/ReventlessPostgres.cmj +0 -0
- package/lib/bs/ReventlessPostgres.cmt +0 -0
- package/lib/bs/ReventlessPostgres.mlmap +9 -0
- package/lib/bs/build.ninja +0 -0
- package/lib/bs/compiler-info.json +8 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres.ast +0 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres.res +352 -0
- package/lib/bs/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
- package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/EventLogStorage_Postgres.ast +0 -0
- package/lib/bs/src/EventLogStorage_Postgres.res +176 -0
- package/lib/bs/src/EventLogStorage_Postgres.res.mjs +207 -0
- package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/PgChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/PgChangeFeed.ast +0 -0
- package/lib/bs/src/PgChangeFeed.res +145 -0
- package/lib/bs/src/PgChangeFeed.res.mjs +90 -0
- package/lib/bs/src/PgDriver-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/PgDriver-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/PgDriver-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/PgDriver-ReventlessPostgres.cmti +0 -0
- package/lib/bs/src/PgDriver.ast +0 -0
- package/lib/bs/src/PgDriver.iast +0 -0
- package/lib/bs/src/PgDriver.res +111 -0
- package/lib/bs/src/PgDriver.res.mjs +85 -0
- package/lib/bs/src/PgDriver.resi +68 -0
- package/lib/bs/src/PgSchema-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/PgSchema-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/PgSchema-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/PgSchema.ast +0 -0
- package/lib/bs/src/PgSchema.res +256 -0
- package/lib/bs/src/PgSchema.res.mjs +46 -0
- package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres.ast +0 -0
- package/lib/bs/src/QueryDbStorage_Postgres.res +244 -0
- package/lib/bs/src/QueryDbStorage_Postgres.res.mjs +225 -0
- package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
- package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
- package/lib/bs/src/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
- package/lib/bs/src/QueryEnginePostgres.ast +0 -0
- package/lib/bs/src/QueryEnginePostgres.res +127 -0
- package/lib/bs/src/QueryEnginePostgres.res.mjs +172 -0
- package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
- package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
- package/lib/bs/tests/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
- package/lib/bs/tests/PostgresIntegrationTest.ast +0 -0
- package/lib/bs/tests/PostgresIntegrationTest.res +236 -0
- package/lib/bs/tests/PostgresIntegrationTest.res.mjs +328 -0
- package/lib/ocaml/.compiler.log +2 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres.ast +0 -0
- package/lib/ocaml/DcbEventLogStorage_Postgres.res +352 -0
- package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/EventLogStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/EventLogStorage_Postgres.ast +0 -0
- package/lib/ocaml/EventLogStorage_Postgres.res +176 -0
- package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/PgChangeFeed-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PgChangeFeed.ast +0 -0
- package/lib/ocaml/PgChangeFeed.res +145 -0
- package/lib/ocaml/PgDriver-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/PgDriver-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/PgDriver-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PgDriver-ReventlessPostgres.cmti +0 -0
- package/lib/ocaml/PgDriver.ast +0 -0
- package/lib/ocaml/PgDriver.iast +0 -0
- package/lib/ocaml/PgDriver.res +111 -0
- package/lib/ocaml/PgDriver.resi +68 -0
- package/lib/ocaml/PgSchema-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/PgSchema-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/PgSchema-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PgSchema.ast +0 -0
- package/lib/ocaml/PgSchema.res +256 -0
- package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/PostgresIntegrationTest-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/PostgresIntegrationTest.ast +0 -0
- package/lib/ocaml/PostgresIntegrationTest.res +236 -0
- package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres.ast +0 -0
- package/lib/ocaml/QueryDbStorage_Postgres.res +244 -0
- package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/QueryEnginePostgres-ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/QueryEnginePostgres.ast +0 -0
- package/lib/ocaml/QueryEnginePostgres.res +127 -0
- package/lib/ocaml/ReventlessPostgres.cmi +0 -0
- package/lib/ocaml/ReventlessPostgres.cmj +0 -0
- package/lib/ocaml/ReventlessPostgres.cmt +0 -0
- package/lib/ocaml/ReventlessPostgres.mlmap +9 -0
- package/package.json +56 -0
- package/rescript.json +36 -0
- package/src/DcbEventLogStorage_Postgres.res +352 -0
- package/src/DcbEventLogStorage_Postgres.res.mjs +385 -0
- package/src/EventLogStorage_Postgres.res +176 -0
- package/src/EventLogStorage_Postgres.res.mjs +207 -0
- package/src/PgChangeFeed.res +145 -0
- package/src/PgChangeFeed.res.mjs +90 -0
- package/src/PgDriver.res +111 -0
- package/src/PgDriver.res.mjs +85 -0
- package/src/PgDriver.resi +68 -0
- package/src/PgSchema.res +256 -0
- package/src/PgSchema.res.mjs +46 -0
- package/src/QueryDbStorage_Postgres.res +244 -0
- package/src/QueryDbStorage_Postgres.res.mjs +225 -0
- package/src/QueryEnginePostgres.res +127 -0
- package/src/QueryEnginePostgres.res.mjs +172 -0
- package/tests/PostgresIntegrationTest.res +236 -0
- 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 */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 */
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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 */
|