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