@voltro/plugin-cdc-out 0.55.0 → 0.57.0
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 +691 -0
- package/dist/index.d.ts +0 -61
- package/dist/index.js +180 -238
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Duration as e, Effect as t, Fiber as n, Schedule as r, Schema as i } from "effect";
|
|
2
2
|
import { definePlugin as a, pluginInstanceName as o } from "@voltro/protocol";
|
|
3
|
-
import {
|
|
4
|
-
import { createHash as y } from "node:crypto";
|
|
3
|
+
import { OccurrenceCounter as s, and as c, changeDigest as ee, composeChangeKey as l, count as u, eq as d, generateId as f, id as p, inSet as m, integer as h, json as te, lt as g, registerRetention as ne, retentionTtlMsFromEnv as re, table as _, text as v, timestamp as y } from "@voltro/database";
|
|
5
4
|
import { withServerTracer as b } from "@voltro/runtime";
|
|
6
5
|
//#region src/errors.ts
|
|
7
6
|
var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {}, S = class extends i.TaggedError()("CdcDeliveryError", {
|
|
@@ -17,34 +16,34 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
17
16
|
}, D = {
|
|
18
17
|
kind: "typeid",
|
|
19
18
|
prefix: "cdcoutclaim"
|
|
20
|
-
}, O = () =>
|
|
21
|
-
id:
|
|
22
|
-
pipe:
|
|
23
|
-
sourceTable:
|
|
24
|
-
op:
|
|
25
|
-
key:
|
|
26
|
-
payload:
|
|
27
|
-
status:
|
|
28
|
-
attempts:
|
|
29
|
-
nextAttemptAt:
|
|
30
|
-
claimedBy:
|
|
31
|
-
claimedAt:
|
|
32
|
-
createdAt:
|
|
33
|
-
deliveredAt:
|
|
34
|
-
lastError:
|
|
35
|
-
}).index(["pipe", "status"]),
|
|
36
|
-
id:
|
|
37
|
-
pipe:
|
|
38
|
-
changeKey:
|
|
39
|
-
claimedBy:
|
|
40
|
-
seenAt:
|
|
41
|
-
}).unique("cdcoutClaimKey", ["pipe", "changeKey"]).index(["seenAt"]),
|
|
42
|
-
id:
|
|
43
|
-
key:
|
|
44
|
-
holder:
|
|
45
|
-
expiresAt:
|
|
46
|
-
renewedAt:
|
|
47
|
-
}).unique("cdcoutLeaseKey", ["key"]),
|
|
19
|
+
}, O = () => f(E, C), ie = () => f(D, T), k = _(C, {
|
|
20
|
+
id: p({ prefix: "cdcout" }),
|
|
21
|
+
pipe: v(),
|
|
22
|
+
sourceTable: v(),
|
|
23
|
+
op: v(),
|
|
24
|
+
key: v(),
|
|
25
|
+
payload: te().nullable(),
|
|
26
|
+
status: v(),
|
|
27
|
+
attempts: h().default(0),
|
|
28
|
+
nextAttemptAt: y(),
|
|
29
|
+
claimedBy: v().nullable(),
|
|
30
|
+
claimedAt: y().nullable(),
|
|
31
|
+
createdAt: y(),
|
|
32
|
+
deliveredAt: y().nullable(),
|
|
33
|
+
lastError: v().nullable()
|
|
34
|
+
}).index(["pipe", "status"]), A = _(T, {
|
|
35
|
+
id: p({ prefix: "cdcoutclaim" }),
|
|
36
|
+
pipe: v(),
|
|
37
|
+
changeKey: v(),
|
|
38
|
+
claimedBy: v(),
|
|
39
|
+
seenAt: y()
|
|
40
|
+
}).unique("cdcoutClaimKey", ["pipe", "changeKey"]).index(["seenAt"]), j = _(w, {
|
|
41
|
+
id: p({ scheme: "numeric" }),
|
|
42
|
+
key: v(),
|
|
43
|
+
holder: v(),
|
|
44
|
+
expiresAt: y(),
|
|
45
|
+
renewedAt: y()
|
|
46
|
+
}).unique("cdcoutLeaseKey", ["key"]), M = (e) => e instanceof Date ? e.getTime() : new Date(String(e)).getTime(), N = class {
|
|
48
47
|
opts;
|
|
49
48
|
held = !1;
|
|
50
49
|
constructor(e) {
|
|
@@ -60,13 +59,13 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
60
59
|
renewedAt: new Date(t)
|
|
61
60
|
};
|
|
62
61
|
try {
|
|
63
|
-
if (await e.updateMany("_voltro_cdcout_leases", n, { where:
|
|
62
|
+
if (await e.updateMany("_voltro_cdcout_leases", n, { where: c(d("key", this.opts.key), d("holder", this.opts.holder)) }) === 1) return this.held = !0, !0;
|
|
64
63
|
let r = await e.insertIgnore(w, {
|
|
65
64
|
key: this.opts.key,
|
|
66
65
|
...n
|
|
67
66
|
}, { conflictColumns: ["key"] });
|
|
68
67
|
if (r.holder === this.opts.holder) return this.held = !0, !0;
|
|
69
|
-
let i = await e.updateMany(w, n, { where:
|
|
68
|
+
let i = await e.updateMany(w, n, { where: c(d("key", this.opts.key), d("holder", String(r.holder)), g("expiresAt", new Date(t))) });
|
|
70
69
|
return this.held = i === 1, this.held;
|
|
71
70
|
} catch {
|
|
72
71
|
return this.held = !1, !1;
|
|
@@ -75,67 +74,10 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
75
74
|
async release(e) {
|
|
76
75
|
this.held = !1;
|
|
77
76
|
try {
|
|
78
|
-
await e.updateMany(w, { expiresAt: /* @__PURE__ */ new Date(0) }, { where:
|
|
77
|
+
await e.updateMany(w, { expiresAt: /* @__PURE__ */ new Date(0) }, { where: c(d("key", this.opts.key), d("holder", this.opts.holder)) });
|
|
79
78
|
} catch {}
|
|
80
79
|
}
|
|
81
|
-
},
|
|
82
|
-
e.pipe,
|
|
83
|
-
e.op,
|
|
84
|
-
e.key,
|
|
85
|
-
N(e.next),
|
|
86
|
-
N(e.prev)
|
|
87
|
-
].join("\0")).digest("hex").slice(0, 32), F = (e, t) => `${e}:${t}`, I = (e) => {
|
|
88
|
-
let t = e.lastIndexOf(":");
|
|
89
|
-
if (t <= 0) return null;
|
|
90
|
-
let n = Number(e.slice(t + 1));
|
|
91
|
-
return !Number.isInteger(n) || n < 0 ? null : {
|
|
92
|
-
digest: e.slice(0, t),
|
|
93
|
-
occurrence: n
|
|
94
|
-
};
|
|
95
|
-
}, L = class {
|
|
96
|
-
#e = /* @__PURE__ */ new Map();
|
|
97
|
-
#t;
|
|
98
|
-
#n;
|
|
99
|
-
constructor(e) {
|
|
100
|
-
this.#t = e.windowMs, this.#n = Math.max(1, e.maxEntries);
|
|
101
|
-
}
|
|
102
|
-
get size() {
|
|
103
|
-
return this.#e.size;
|
|
104
|
-
}
|
|
105
|
-
next(e, t) {
|
|
106
|
-
let n = this.#e.get(e);
|
|
107
|
-
if (n === void 0) return this.#e.set(e, {
|
|
108
|
-
count: 1,
|
|
109
|
-
lastMs: t
|
|
110
|
-
}), this.#r(), 0;
|
|
111
|
-
n.lastMs = t;
|
|
112
|
-
let r = n.count;
|
|
113
|
-
return n.count += 1, this.#e.delete(e), this.#e.set(e, n), r;
|
|
114
|
-
}
|
|
115
|
-
seedFromChangeKeys(e, t) {
|
|
116
|
-
for (let n of e) {
|
|
117
|
-
let e = I(n);
|
|
118
|
-
if (e === null) continue;
|
|
119
|
-
let r = this.#e.get(e.digest), i = Math.max(r?.count ?? 0, e.occurrence + 1);
|
|
120
|
-
this.#e.set(e.digest, {
|
|
121
|
-
count: i,
|
|
122
|
-
lastMs: r?.lastMs ?? t
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
|
-
this.#r();
|
|
126
|
-
}
|
|
127
|
-
prune(e) {
|
|
128
|
-
let t = e - this.#t;
|
|
129
|
-
for (let [e, n] of this.#e) n.lastMs < t && this.#e.delete(e);
|
|
130
|
-
}
|
|
131
|
-
#r() {
|
|
132
|
-
for (; this.#e.size > this.#n;) {
|
|
133
|
-
let e = this.#e.keys().next();
|
|
134
|
-
if (e.done === !0) return;
|
|
135
|
-
this.#e.delete(e.value);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}, R = class {
|
|
80
|
+
}, P = class {
|
|
139
81
|
#e = [];
|
|
140
82
|
#t = 0;
|
|
141
83
|
#n;
|
|
@@ -168,7 +110,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
168
110
|
for (let [e, i] of this.#e.entries()) i.observedAtMs >= t ? (e < this.#t && (r += 1), n.push(i)) : i.enqueued || (this.#i += 1);
|
|
169
111
|
this.#e = n, this.#t = r;
|
|
170
112
|
}
|
|
171
|
-
},
|
|
113
|
+
}, F = (n, r, i) => t.suspend(() => {
|
|
172
114
|
let a = new AbortController();
|
|
173
115
|
return t.try(() => n.deliver(r, { signal: a.signal })).pipe(t.flatMap((e) => t.isEffect(e) ? e : t.tryPromise(() => e))).pipe(t.timeoutFail({
|
|
174
116
|
duration: e.millis(i.timeoutMs),
|
|
@@ -176,21 +118,21 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
176
118
|
sink: n.name,
|
|
177
119
|
reason: `delivery timed out after ${i.timeoutMs}ms`
|
|
178
120
|
})
|
|
179
|
-
}), t.onError(() => t.sync(() => a.abort())), t.mapError((e) => e instanceof S ? e :
|
|
121
|
+
}), t.onError(() => t.sync(() => a.abort())), t.mapError((e) => e instanceof S ? e : I(n.name, e)), t.withSpan("cdcOut.deliver", { attributes: {
|
|
180
122
|
pipe: i.pipe,
|
|
181
123
|
sink: n.name,
|
|
182
124
|
records: r.length
|
|
183
125
|
} }));
|
|
184
|
-
}),
|
|
126
|
+
}), I = (e, t) => {
|
|
185
127
|
let n = typeof t == "object" && t && "error" in t ? t.error : t;
|
|
186
128
|
return new S({
|
|
187
129
|
sink: e,
|
|
188
130
|
reason: n instanceof Error ? n.message : String(n)
|
|
189
131
|
});
|
|
190
|
-
},
|
|
132
|
+
}, L = (e, t) => {
|
|
191
133
|
let n = Math.min(t.baseMs * 2 ** Math.max(0, e - 1), t.maxMs);
|
|
192
134
|
return Math.round(n * (.5 + .5 * (t.random ?? Math.random)()));
|
|
193
|
-
},
|
|
135
|
+
}, ae = class {
|
|
194
136
|
opts;
|
|
195
137
|
now;
|
|
196
138
|
stuckClaimMs;
|
|
@@ -215,9 +157,9 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
215
157
|
status: "pending",
|
|
216
158
|
claimedBy: null,
|
|
217
159
|
claimedAt: null
|
|
218
|
-
}, { where:
|
|
160
|
+
}, { where: c(d("pipe", n.key), d("status", "delivering"), g("claimedAt", new Date(r - this.stuckClaimMs))) }), (await e.query({
|
|
219
161
|
table: "_voltro_cdcout_outbox",
|
|
220
|
-
predicate:
|
|
162
|
+
predicate: c(d("pipe", n.key), d("status", "delivering")),
|
|
221
163
|
order: [],
|
|
222
164
|
take: 1,
|
|
223
165
|
skip: void 0,
|
|
@@ -225,7 +167,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
225
167
|
})).length > 0) return;
|
|
226
168
|
let i = await e.query({
|
|
227
169
|
table: C,
|
|
228
|
-
predicate:
|
|
170
|
+
predicate: c(d("pipe", n.key), d("status", "pending")),
|
|
229
171
|
order: [{
|
|
230
172
|
column: "id",
|
|
231
173
|
direction: "asc"
|
|
@@ -237,7 +179,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
237
179
|
if (i.length === 0) return;
|
|
238
180
|
let a = [];
|
|
239
181
|
for (let e of i) {
|
|
240
|
-
if (
|
|
182
|
+
if (M(e.nextAttemptAt) > r) break;
|
|
241
183
|
a.push(e);
|
|
242
184
|
}
|
|
243
185
|
if (a.length === 0) return;
|
|
@@ -246,10 +188,10 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
246
188
|
status: "delivering",
|
|
247
189
|
claimedBy: this.opts.replicaId,
|
|
248
190
|
claimedAt: new Date(r)
|
|
249
|
-
}, { where:
|
|
250
|
-
let
|
|
191
|
+
}, { where: c(d("pipe", n.key), m("id", o), d("status", "pending")) }) === 0) return;
|
|
192
|
+
let s = await e.query({
|
|
251
193
|
table: C,
|
|
252
|
-
predicate:
|
|
194
|
+
predicate: c(d("pipe", n.key), m("id", o), d("status", "delivering"), d("claimedBy", this.opts.replicaId)),
|
|
253
195
|
order: [{
|
|
254
196
|
column: "id",
|
|
255
197
|
direction: "asc"
|
|
@@ -258,39 +200,39 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
258
200
|
skip: void 0,
|
|
259
201
|
projection: void 0
|
|
260
202
|
});
|
|
261
|
-
if (
|
|
262
|
-
let
|
|
203
|
+
if (s.length === 0) return;
|
|
204
|
+
let ee = s.map((e) => ({
|
|
263
205
|
table: e.sourceTable,
|
|
264
206
|
op: e.op,
|
|
265
207
|
key: e.key,
|
|
266
208
|
data: e.payload,
|
|
267
209
|
deliveryKey: e.id
|
|
268
|
-
})),
|
|
210
|
+
})), l = await t.runPromise(b(t.either(F(n.sink, ee, {
|
|
269
211
|
timeoutMs: this.opts.deliveryTimeoutMs,
|
|
270
212
|
pipe: n.key
|
|
271
|
-
})))),
|
|
272
|
-
if (
|
|
213
|
+
})))), u = this.now();
|
|
214
|
+
if (l._tag === "Right") {
|
|
273
215
|
await e.updateMany(C, {
|
|
274
216
|
status: "delivered",
|
|
275
|
-
deliveredAt: new Date(
|
|
217
|
+
deliveredAt: new Date(u),
|
|
276
218
|
claimedBy: null,
|
|
277
219
|
claimedAt: null,
|
|
278
220
|
lastError: null
|
|
279
|
-
}, { where:
|
|
221
|
+
}, { where: c(d("pipe", n.key), m("id", s.map((e) => e.id)), d("status", "delivering"), d("claimedBy", this.opts.replicaId)) }), n.stats.delivered += s.length, n.stats.lastDeliveryAtMs = u;
|
|
280
222
|
return;
|
|
281
223
|
}
|
|
282
|
-
let
|
|
283
|
-
for (let t of
|
|
224
|
+
let f = l.left.message;
|
|
225
|
+
for (let t of s) {
|
|
284
226
|
let r = t.attempts + 1;
|
|
285
227
|
if (r >= this.opts.maxAttempts) await e.update(C, t.id, {
|
|
286
228
|
status: "dead",
|
|
287
229
|
attempts: r,
|
|
288
230
|
claimedBy: null,
|
|
289
231
|
claimedAt: null,
|
|
290
|
-
lastError:
|
|
232
|
+
lastError: f
|
|
291
233
|
}), n.stats.dead += 1;
|
|
292
234
|
else {
|
|
293
|
-
let n =
|
|
235
|
+
let n = L(r, {
|
|
294
236
|
baseMs: this.opts.backoffBaseMs,
|
|
295
237
|
maxMs: this.opts.backoffMaxMs,
|
|
296
238
|
...this.opts.random === void 0 ? {} : { random: this.opts.random }
|
|
@@ -298,10 +240,10 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
298
240
|
await e.update(C, t.id, {
|
|
299
241
|
status: "pending",
|
|
300
242
|
attempts: r,
|
|
301
|
-
nextAttemptAt: new Date(
|
|
243
|
+
nextAttemptAt: new Date(u + n),
|
|
302
244
|
claimedBy: null,
|
|
303
245
|
claimedAt: null,
|
|
304
|
-
lastError:
|
|
246
|
+
lastError: f
|
|
305
247
|
});
|
|
306
248
|
}
|
|
307
249
|
n.stats.failed += 1;
|
|
@@ -309,13 +251,13 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
309
251
|
this.opts.warn?.("cdc-out delivery failed (durable retry scheduled)", {
|
|
310
252
|
pipe: n.key,
|
|
311
253
|
sink: n.sink.name,
|
|
312
|
-
records:
|
|
313
|
-
attempts:
|
|
254
|
+
records: s.length,
|
|
255
|
+
attempts: s[0] === void 0 ? 1 : s[0].attempts + 1,
|
|
314
256
|
maxAttempts: this.opts.maxAttempts,
|
|
315
|
-
reason:
|
|
257
|
+
reason: f
|
|
316
258
|
});
|
|
317
259
|
}
|
|
318
|
-
},
|
|
260
|
+
}, R = (e = "memory") => {
|
|
319
261
|
let t = [];
|
|
320
262
|
return {
|
|
321
263
|
name: e,
|
|
@@ -329,7 +271,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
329
271
|
return t.flat();
|
|
330
272
|
}
|
|
331
273
|
};
|
|
332
|
-
},
|
|
274
|
+
}, z = (e, t) => {
|
|
333
275
|
let n;
|
|
334
276
|
try {
|
|
335
277
|
n = new URL(e).host;
|
|
@@ -356,14 +298,14 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
356
298
|
});
|
|
357
299
|
}
|
|
358
300
|
};
|
|
359
|
-
},
|
|
301
|
+
}, oe = 100, se = 5, ce = 200, le = 3e4, ue = 250, de = 1e4, B = 15e3, fe = 72, pe = 6e4, me = 1e4, he = 1e3, V = (e) => {
|
|
360
302
|
throw new x({ message: e });
|
|
361
|
-
},
|
|
362
|
-
(e.sinks === void 0 || e.sinks.length === 0) &&
|
|
303
|
+
}, H = (e) => {
|
|
304
|
+
(e.sinks === void 0 || e.sinks.length === 0) && V("cdcOutPlugin: at least one sink config is required");
|
|
363
305
|
let t = /* @__PURE__ */ new Set();
|
|
364
306
|
e.sinks.forEach((e, n) => {
|
|
365
|
-
(typeof e.table != "string" || e.table.length === 0) &&
|
|
366
|
-
}), e.name !== void 0 && !/^[a-zA-Z0-9-]+$/.test(e.name) &&
|
|
307
|
+
(typeof e.table != "string" || e.table.length === 0) && V(`cdcOutPlugin: sinks[${n}].table is required`), e.table.startsWith("_voltro_cdcout") && V(`cdcOutPlugin: sinks[${n}].table '${e.table}' is the plugin's own delivery state — it cannot be mirrored`), t.has(e.table) && V(`cdcOutPlugin: duplicate sink config for table '${e.table}' — one sink config per table per instance; for multiple sinks on one table wire a second cdcOutPlugin({ name: '…' }) instance`), t.add(e.table), (e.sink === void 0 || typeof e.sink.deliver != "function") && V(`cdcOutPlugin: sinks[${n}] (table '${e.table}') needs a sink with a deliver(batch) function`), e.batchSize !== void 0 && e.batchSize <= 0 && V(`cdcOutPlugin: sinks[${n}] (table '${e.table}') batchSize must be > 0`);
|
|
308
|
+
}), e.name !== void 0 && !/^[a-zA-Z0-9-]+$/.test(e.name) && V(`cdcOutPlugin: name '${e.name}' must match [a-zA-Z0-9-]+ (it lands in the inspect URL)`);
|
|
367
309
|
let n = [
|
|
368
310
|
["maxAttempts", e.maxAttempts],
|
|
369
311
|
["backoffBaseMs", e.backoffBaseMs],
|
|
@@ -376,10 +318,10 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
376
318
|
["handoffBufferMs", e.handoffBufferMs],
|
|
377
319
|
["handoffBufferSize", e.handoffBufferSize]
|
|
378
320
|
];
|
|
379
|
-
for (let [e, t] of n) t !== void 0 && t < 1 &&
|
|
380
|
-
let r = e.leaseTtlMs ??
|
|
381
|
-
e.dedupWindowMs !== void 0 && e.dedupWindowMs <= r &&
|
|
382
|
-
},
|
|
321
|
+
for (let [e, t] of n) t !== void 0 && t < 1 && V(`cdcOutPlugin: ${e} must be >= 1`);
|
|
322
|
+
let r = e.leaseTtlMs ?? B;
|
|
323
|
+
e.dedupWindowMs !== void 0 && e.dedupWindowMs <= r && V(`cdcOutPlugin: dedupWindowMs (${e.dedupWindowMs}) must exceed leaseTtlMs (${r}) — a claim that expires inside a leadership handoff lets two replicas enqueue the same change`);
|
|
324
|
+
}, U = (e, t) => {
|
|
383
325
|
if (t.filter !== void 0 && !t.filter(e)) return null;
|
|
384
326
|
if (e.op === "delete") {
|
|
385
327
|
let t = String(e.old?.id ?? "");
|
|
@@ -399,7 +341,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
399
341
|
key: r,
|
|
400
342
|
data: t.map ? t.map(n) : n
|
|
401
343
|
};
|
|
402
|
-
},
|
|
344
|
+
}, W = (e, t) => `${e}:${t.table}:${t.sink.name}`, G = (e, t, n) => ({
|
|
403
345
|
id: O(),
|
|
404
346
|
pipe: t,
|
|
405
347
|
sourceTable: e.table,
|
|
@@ -414,10 +356,10 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
414
356
|
createdAt: new Date(n),
|
|
415
357
|
deliveredAt: null,
|
|
416
358
|
lastError: null
|
|
417
|
-
}),
|
|
418
|
-
let i =
|
|
359
|
+
}), K = async (e, t, n, r) => {
|
|
360
|
+
let i = W(r?.instanceName ?? "default", t), a = (r?.now ?? Date.now)(), o = n.flatMap((e) => {
|
|
419
361
|
let n = String(e.id ?? "");
|
|
420
|
-
return n === "" ? [] : [
|
|
362
|
+
return n === "" ? [] : [G({
|
|
421
363
|
table: t.table,
|
|
422
364
|
op: "insert",
|
|
423
365
|
key: n,
|
|
@@ -425,19 +367,19 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
425
367
|
}, i, a)];
|
|
426
368
|
});
|
|
427
369
|
return o.length > 0 && await e.insertMany(C, o), o.length;
|
|
428
|
-
},
|
|
429
|
-
|
|
430
|
-
let
|
|
431
|
-
base:
|
|
370
|
+
}, q = "@voltro/plugin-cdc-out", J = (i) => {
|
|
371
|
+
H(i);
|
|
372
|
+
let f = i.name ?? "default", p = o({
|
|
373
|
+
base: q,
|
|
432
374
|
alias: i.alias,
|
|
433
375
|
instance: i.name
|
|
434
|
-
}),
|
|
435
|
-
for (let e of i.sinks)
|
|
376
|
+
}), h = `cdcout-${Math.random().toString(36).slice(2, 10)}`, te = i.maxAttempts ?? se, _ = i.sweepIntervalMs ?? ue, v = i.leaseTtlMs ?? B, y = i.dedupWindowMs ?? Math.max(pe, 4 * v), b = i.handoffBufferMs ?? y, x = i.handoffBufferSize ?? me, S = /* @__PURE__ */ new Map();
|
|
377
|
+
for (let e of i.sinks) S.set(e.table, {
|
|
436
378
|
config: e,
|
|
437
379
|
enginePipe: {
|
|
438
|
-
key:
|
|
380
|
+
key: W(f, e),
|
|
439
381
|
sink: e.sink,
|
|
440
|
-
batchSize: e.batchSize ??
|
|
382
|
+
batchSize: e.batchSize ?? oe,
|
|
441
383
|
busy: !1,
|
|
442
384
|
stats: {
|
|
443
385
|
delivered: 0,
|
|
@@ -447,38 +389,38 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
447
389
|
}
|
|
448
390
|
}
|
|
449
391
|
});
|
|
450
|
-
let
|
|
451
|
-
key:
|
|
452
|
-
holder:
|
|
453
|
-
ttlMs:
|
|
454
|
-
}),
|
|
455
|
-
pipes: [...
|
|
456
|
-
replicaId:
|
|
457
|
-
maxAttempts:
|
|
458
|
-
backoffBaseMs: i.backoffBaseMs ??
|
|
459
|
-
backoffMaxMs: i.backoffMaxMs ??
|
|
460
|
-
deliveryTimeoutMs: i.deliveryTimeoutMs ??
|
|
461
|
-
isLeader: () =>
|
|
462
|
-
warn: (e, t) =>
|
|
463
|
-
}),
|
|
464
|
-
windowMs: 2 *
|
|
465
|
-
maxEntries: Math.max(
|
|
466
|
-
}),
|
|
467
|
-
windowMs:
|
|
468
|
-
maxEntries:
|
|
469
|
-
}),
|
|
470
|
-
|
|
392
|
+
let w = new N({
|
|
393
|
+
key: f,
|
|
394
|
+
holder: h,
|
|
395
|
+
ttlMs: v
|
|
396
|
+
}), E = new ae({
|
|
397
|
+
pipes: [...S.values()].map((e) => e.enginePipe),
|
|
398
|
+
replicaId: h,
|
|
399
|
+
maxAttempts: te,
|
|
400
|
+
backoffBaseMs: i.backoffBaseMs ?? ce,
|
|
401
|
+
backoffMaxMs: i.backoffMaxMs ?? le,
|
|
402
|
+
deliveryTimeoutMs: i.deliveryTimeoutMs ?? de,
|
|
403
|
+
isLeader: () => w.isLeader,
|
|
404
|
+
warn: (e, t) => O(e, t)
|
|
405
|
+
}), D = { current: null }, O = () => void 0, M = 0, F, I, L = new s({
|
|
406
|
+
windowMs: 2 * y,
|
|
407
|
+
maxEntries: Math.max(x, 1e3) * 4
|
|
408
|
+
}), R = new P({
|
|
409
|
+
windowMs: b,
|
|
410
|
+
maxEntries: x
|
|
411
|
+
}), z = !1, V = 0, K = !1, J = null, Y = 0, ge = !1, _e = !1, X = [], Z = 0;
|
|
412
|
+
ne({
|
|
471
413
|
source: "plugin",
|
|
472
414
|
table: C,
|
|
473
415
|
timeColumn: "createdAt",
|
|
474
|
-
ttlMs: i.retentionHours === void 0 ?
|
|
475
|
-
where:
|
|
416
|
+
ttlMs: i.retentionHours === void 0 ? re(process.env.CDCOUT_RETENTION_HOURS, fe) : i.retentionHours * 36e5,
|
|
417
|
+
where: m("status", ["delivered", "dead"]),
|
|
476
418
|
label: "cdc-out outbox (delivered/dead)"
|
|
477
|
-
}),
|
|
419
|
+
}), ne({
|
|
478
420
|
source: "plugin",
|
|
479
421
|
table: T,
|
|
480
422
|
timeColumn: "seenAt",
|
|
481
|
-
ttlMs:
|
|
423
|
+
ttlMs: y,
|
|
482
424
|
label: "cdc-out enqueue claims (dedup window)"
|
|
483
425
|
});
|
|
484
426
|
let ve = /* @__PURE__ */ new Set();
|
|
@@ -503,62 +445,62 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
503
445
|
return await Promise.all(be.map(async (r) => {
|
|
504
446
|
let i = await e.query({
|
|
505
447
|
table: C,
|
|
506
|
-
predicate:
|
|
448
|
+
predicate: c(d("pipe", t), d("status", r)),
|
|
507
449
|
order: [],
|
|
508
450
|
take: void 0,
|
|
509
451
|
skip: void 0,
|
|
510
452
|
projection: void 0,
|
|
511
|
-
aggregations: [
|
|
453
|
+
aggregations: [u("n")]
|
|
512
454
|
});
|
|
513
455
|
n[r] = Number(i[0]?.n ?? 0);
|
|
514
456
|
})), n;
|
|
515
457
|
}, Se = (e) => {
|
|
516
458
|
let t = Date.now();
|
|
517
|
-
t -
|
|
459
|
+
t - M > 6e4 && (M = t, O("cdc-out: no DataStore bound — change events are NOT being mirrored", { table: e }));
|
|
518
460
|
}, Ce = async (e, t, n) => (await e.insertIgnore(T, {
|
|
519
|
-
id:
|
|
461
|
+
id: ie(),
|
|
520
462
|
pipe: t,
|
|
521
463
|
changeKey: n,
|
|
522
|
-
claimedBy:
|
|
464
|
+
claimedBy: h,
|
|
523
465
|
seenAt: /* @__PURE__ */ new Date()
|
|
524
|
-
}, { conflictColumns: ["pipe", "changeKey"] })).claimedBy ===
|
|
525
|
-
if (
|
|
526
|
-
let e =
|
|
466
|
+
}, { conflictColumns: ["pipe", "changeKey"] })).claimedBy === h, Q = async () => {
|
|
467
|
+
if (z) return;
|
|
468
|
+
let e = D.current;
|
|
527
469
|
if (e !== null) {
|
|
528
|
-
|
|
470
|
+
z = !0;
|
|
529
471
|
try {
|
|
530
472
|
for (;;) {
|
|
531
|
-
let t =
|
|
532
|
-
if (t === void 0 || !
|
|
533
|
-
await Ce(e, t.pipe, t.changeKey) && await e.insert(C,
|
|
473
|
+
let t = R.nextPending();
|
|
474
|
+
if (t === void 0 || !w.isLeader) return;
|
|
475
|
+
await Ce(e, t.pipe, t.changeKey) && await e.insert(C, G(t.change, t.pipe, Date.now())), t.enqueued = !0;
|
|
534
476
|
}
|
|
535
477
|
} finally {
|
|
536
|
-
|
|
478
|
+
z = !1;
|
|
537
479
|
}
|
|
538
480
|
}
|
|
539
481
|
}, we = (e) => {
|
|
540
|
-
if (
|
|
541
|
-
for (X.push(e); X.length >
|
|
482
|
+
if (ge = !0, !K) {
|
|
483
|
+
for (X.push(e); X.length > x;) X.shift(), Z += 1;
|
|
542
484
|
return;
|
|
543
485
|
}
|
|
544
|
-
|
|
486
|
+
R.append({
|
|
545
487
|
pipe: e.pipe,
|
|
546
|
-
changeKey:
|
|
488
|
+
changeKey: l(e.digest, L.next(e.digest, e.observedAtMs)),
|
|
547
489
|
change: e.change,
|
|
548
490
|
observedAtMs: e.observedAtMs,
|
|
549
491
|
enqueued: !1
|
|
550
492
|
});
|
|
551
493
|
}, Te = (e, t) => {
|
|
552
|
-
|
|
494
|
+
L.seedFromChangeKeys(e, t), K = !0;
|
|
553
495
|
for (let e of X.splice(0, X.length)) we(e);
|
|
554
496
|
}, $ = async () => {
|
|
555
|
-
if (
|
|
556
|
-
let e =
|
|
497
|
+
if (K) return;
|
|
498
|
+
let e = D.current;
|
|
557
499
|
if (e !== null) {
|
|
558
|
-
if (
|
|
559
|
-
if (
|
|
560
|
-
let t = !
|
|
561
|
-
|
|
500
|
+
if (J === null) {
|
|
501
|
+
if (Y !== 0 && Date.now() - Y < he) return;
|
|
502
|
+
let t = !ge;
|
|
503
|
+
J = (async () => {
|
|
562
504
|
let n = await e.query({
|
|
563
505
|
table: T,
|
|
564
506
|
predicate: void 0,
|
|
@@ -566,28 +508,28 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
566
508
|
column: "seenAt",
|
|
567
509
|
direction: "desc"
|
|
568
510
|
}],
|
|
569
|
-
take: Math.max(
|
|
511
|
+
take: Math.max(x, 1e3),
|
|
570
512
|
skip: void 0,
|
|
571
513
|
projection: ["changeKey"]
|
|
572
514
|
});
|
|
573
|
-
!t && !
|
|
515
|
+
!t && !_e && (_e = !0, O("cdc-out: occurrence counters start from this replica's own stream — the claims snapshot was only reachable after it began observing, and counting it could duplicate a change", { replicaId: h })), Te(t ? n.flatMap((e) => typeof e.changeKey == "string" ? [e.changeKey] : []) : [], Date.now());
|
|
574
516
|
})().catch((e) => {
|
|
575
|
-
|
|
517
|
+
Y = Date.now(), O("cdc-out: could not seed change-occurrence counters — enqueue is held until it lands", { reason: String(e) });
|
|
576
518
|
}).finally(() => {
|
|
577
|
-
|
|
519
|
+
J = null;
|
|
578
520
|
});
|
|
579
521
|
}
|
|
580
|
-
await
|
|
522
|
+
await J;
|
|
581
523
|
}
|
|
582
524
|
}, Ee = (e) => {
|
|
583
|
-
let t = e -
|
|
525
|
+
let t = e - b;
|
|
584
526
|
for (; X.length > 0 && X[0].observedAtMs < t;) X.shift(), Z += 1;
|
|
585
527
|
}, De = async (e, t) => {
|
|
586
|
-
t - V < Math.max(1e3, Math.floor(
|
|
528
|
+
t - V < Math.max(1e3, Math.floor(y / 4)) || (V = t, await e.deleteMany(T, { where: g("seenAt", new Date(t - y)) }));
|
|
587
529
|
};
|
|
588
530
|
return a({
|
|
589
|
-
name:
|
|
590
|
-
baseName:
|
|
531
|
+
name: p,
|
|
532
|
+
baseName: q,
|
|
591
533
|
description: "Declarative reverse-ETL: mirror table changes outward to external sinks (webhook, Kafka via @voltro/plugin-queue's kafkaSink, plus a CdcSink interface for custom sinks) through a durable outbox — ordered per pipe, at-least-once from enqueue, retried with backoff, dead-lettered.",
|
|
592
534
|
permissions: ye,
|
|
593
535
|
declaredEnv: [{
|
|
@@ -598,23 +540,23 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
598
540
|
example: "72"
|
|
599
541
|
}],
|
|
600
542
|
extendSchema: { tables: [
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
543
|
+
k,
|
|
544
|
+
j,
|
|
545
|
+
A
|
|
604
546
|
] },
|
|
605
547
|
bindDataStore: (e) => {
|
|
606
|
-
|
|
548
|
+
D.current = e, $();
|
|
607
549
|
},
|
|
608
550
|
inspectEndpoints: [{
|
|
609
551
|
method: "GET",
|
|
610
552
|
path: "/sinks",
|
|
611
553
|
description: "Configured sinks + per-pipe outbox counts and delivery stats.",
|
|
612
554
|
handler: () => t.tryPromise(async () => {
|
|
613
|
-
let e =
|
|
555
|
+
let e = D.current;
|
|
614
556
|
return {
|
|
615
557
|
kind: "json",
|
|
616
558
|
data: {
|
|
617
|
-
sinks: await Promise.all([...
|
|
559
|
+
sinks: await Promise.all([...S.values()].map(async ({ config: t, enginePipe: n }) => {
|
|
618
560
|
let r = e === null ? {
|
|
619
561
|
pending: 0,
|
|
620
562
|
delivering: 0,
|
|
@@ -631,14 +573,14 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
631
573
|
};
|
|
632
574
|
})),
|
|
633
575
|
handoff: {
|
|
634
|
-
leader:
|
|
635
|
-
seeded:
|
|
576
|
+
leader: w.isLeader,
|
|
577
|
+
seeded: K,
|
|
636
578
|
awaitingSeed: X.length,
|
|
637
|
-
buffered:
|
|
638
|
-
pending:
|
|
639
|
-
dropped:
|
|
640
|
-
windowMs:
|
|
641
|
-
dedupWindowMs:
|
|
579
|
+
buffered: R.size,
|
|
580
|
+
pending: R.pendingCount(),
|
|
581
|
+
dropped: R.droppedPending + Z,
|
|
582
|
+
windowMs: b,
|
|
583
|
+
dedupWindowMs: y
|
|
642
584
|
}
|
|
643
585
|
}
|
|
644
586
|
};
|
|
@@ -648,7 +590,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
648
590
|
path: "/dead-letter",
|
|
649
591
|
description: "Outbox rows that exhausted maxAttempts (newest first, capped at 100).",
|
|
650
592
|
handler: () => t.tryPromise(async () => {
|
|
651
|
-
let e =
|
|
593
|
+
let e = D.current;
|
|
652
594
|
return e === null ? {
|
|
653
595
|
kind: "json",
|
|
654
596
|
data: { rows: [] }
|
|
@@ -656,7 +598,7 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
656
598
|
kind: "json",
|
|
657
599
|
data: { rows: (await e.query({
|
|
658
600
|
table: C,
|
|
659
|
-
predicate:
|
|
601
|
+
predicate: d("status", "dead"),
|
|
660
602
|
order: [{
|
|
661
603
|
column: "id",
|
|
662
604
|
direction: "desc"
|
|
@@ -678,12 +620,12 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
678
620
|
})
|
|
679
621
|
}],
|
|
680
622
|
onChangeEvent: (e) => {
|
|
681
|
-
let n =
|
|
623
|
+
let n = S.get(e.table);
|
|
682
624
|
if (n === void 0) return t.void;
|
|
683
|
-
let r =
|
|
625
|
+
let r = U(e, n.config);
|
|
684
626
|
if (r === null) return t.void;
|
|
685
627
|
if (e.changeScope === "fleet") {
|
|
686
|
-
let i = Date.now(), a =
|
|
628
|
+
let i = Date.now(), a = ee({
|
|
687
629
|
pipe: n.enginePipe.key,
|
|
688
630
|
op: r.op,
|
|
689
631
|
key: r.key,
|
|
@@ -695,44 +637,44 @@ var x = class extends i.TaggedError()("CdcConfigError", { message: i.String }) {
|
|
|
695
637
|
digest: a,
|
|
696
638
|
change: r,
|
|
697
639
|
observedAtMs: i
|
|
698
|
-
}),
|
|
640
|
+
}), D.current === null ? (Se(e.table), t.void) : t.promise(() => $()).pipe(t.flatMap(() => w.isLeader ? t.tryPromise(() => Q()) : t.void));
|
|
699
641
|
}
|
|
700
642
|
if (e.origin === "injected") return t.void;
|
|
701
|
-
let i =
|
|
643
|
+
let i = D.current;
|
|
702
644
|
if (i === null) return Se(e.table), t.void;
|
|
703
|
-
let a =
|
|
645
|
+
let a = G(r, n.enginePipe.key, Date.now());
|
|
704
646
|
return t.tryPromise(() => i.insert(C, a).then(() => void 0));
|
|
705
647
|
},
|
|
706
648
|
onActivate: (n) => t.sync(() => {
|
|
707
|
-
|
|
649
|
+
O = (e, t) => n.logger.warn(e, t);
|
|
708
650
|
let i = t.suspend(() => {
|
|
709
|
-
let e =
|
|
651
|
+
let e = D.current;
|
|
710
652
|
return e === null ? t.void : t.promise(async () => {
|
|
711
653
|
await $();
|
|
712
|
-
let t =
|
|
713
|
-
await
|
|
714
|
-
|
|
654
|
+
let t = w.isLeader;
|
|
655
|
+
await w.heartbeat(e) && !t && await Q().catch((e) => {
|
|
656
|
+
O("cdc-out: handoff drain failed (retried on the next sweep)", { reason: String(e) });
|
|
715
657
|
});
|
|
716
658
|
});
|
|
717
|
-
}).pipe(t.repeat(r.spaced(e.millis(Math.max(1, Math.floor(
|
|
718
|
-
let e =
|
|
659
|
+
}).pipe(t.repeat(r.spaced(e.millis(Math.max(1, Math.floor(v / 3)))))), a = t.suspend(() => {
|
|
660
|
+
let e = D.current;
|
|
719
661
|
return e === null ? t.void : t.tryPromise(async () => {
|
|
720
662
|
let t = Date.now();
|
|
721
|
-
await $(), Ee(t),
|
|
722
|
-
}).pipe(t.catchAll((e) => t.sync(() =>
|
|
723
|
-
}).pipe(t.repeat(r.spaced(e.millis(
|
|
724
|
-
|
|
725
|
-
tables: [...
|
|
726
|
-
replicaId:
|
|
663
|
+
await $(), Ee(t), R.prune(t), L.prune(t), await De(e, t), w.isLeader && await Q(), await E.tick(e);
|
|
664
|
+
}).pipe(t.catchAll((e) => t.sync(() => O("cdc-out worker sweep failed", { reason: String(e) }))));
|
|
665
|
+
}).pipe(t.repeat(r.spaced(e.millis(_))));
|
|
666
|
+
I = t.runFork(i), F = t.runFork(a), n.logger.info("cdc-out active", {
|
|
667
|
+
tables: [...S.keys()],
|
|
668
|
+
replicaId: h,
|
|
727
669
|
outbox: C
|
|
728
670
|
});
|
|
729
671
|
}),
|
|
730
672
|
onDeactivate: () => t.gen(function* () {
|
|
731
|
-
|
|
732
|
-
let e =
|
|
733
|
-
e !== null && (yield* t.promise(() =>
|
|
673
|
+
F !== void 0 && (yield* n.interrupt(F)), I !== void 0 && (yield* n.interrupt(I)), F = void 0, I = void 0;
|
|
674
|
+
let e = D.current;
|
|
675
|
+
e !== null && (yield* t.promise(() => w.release(e)));
|
|
734
676
|
})
|
|
735
677
|
});
|
|
736
678
|
};
|
|
737
679
|
//#endregion
|
|
738
|
-
export { T as CDCOUT_CLAIMS_TABLE, w as CDCOUT_LEASES_TABLE, C as CDCOUT_OUTBOX_TABLE, x as CdcConfigError, S as CdcDeliveryError,
|
|
680
|
+
export { T as CDCOUT_CLAIMS_TABLE, w as CDCOUT_LEASES_TABLE, C as CDCOUT_OUTBOX_TABLE, x as CdcConfigError, S as CdcDeliveryError, N as CdcLeaseManager, ae as CdcOutEngine, P as HandoffBuffer, A as cdcOutClaimsTable, j as cdcOutLeasesTable, k as cdcOutOutboxTable, J as cdcOutPlugin, F as deliverOnce, K as enqueueBackfill, U as mapChange, R as memorySink, ie as nextClaimId, O as nextOutboxId, G as outboxRowFor, W as pipeKey, L as retryDelayMs, H as validateCdcOutConfig, z as webhookSink };
|