@voltro/plugin-queue 0.83.1 → 0.85.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 +1191 -0
- package/THIRD-PARTY-NOTICES.md +71 -525
- package/dist/index.d.ts +2 -2
- package/dist/index.js +87 -87
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -114,7 +114,7 @@ export declare interface QueueConsumerDefinition<A = unknown> {
|
|
|
114
114
|
* to parse or decode goes STRAIGHT to the dead-letter topic with the
|
|
115
115
|
* reason — never into the retry loop (a decode failure is deterministic;
|
|
116
116
|
* retrying it is an infinite loop with extra steps). */
|
|
117
|
-
readonly schema: Schema.
|
|
117
|
+
readonly schema: Schema.Codec<A, any, never, never>;
|
|
118
118
|
/**
|
|
119
119
|
* The handler. Runs SERIALLY PER PARTITION; its offset commits only after
|
|
120
120
|
* it resolves. It MUST be idempotent: delivery is at-least-once, and a
|
|
@@ -247,7 +247,7 @@ export declare interface QueueRunnerLogger {
|
|
|
247
247
|
export declare class QueueService extends QueueService_base {
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
declare const QueueService_base: Context.
|
|
250
|
+
declare const QueueService_base: Context.ServiceClass<QueueService, "@voltro/plugin-queue/QueueService", QueueServiceApi>;
|
|
251
251
|
|
|
252
252
|
export declare interface QueueServiceApi {
|
|
253
253
|
/** Produce a batch to a topic (one transport round-trip). NOT transactional
|
package/dist/index.js
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
import { Cause as e, Context as t, Effect as n, Exit as r, Layer as i, Metric as a,
|
|
2
|
-
import { definePlugin as
|
|
3
|
-
import { Kafka as
|
|
4
|
-
import { externalSpanFromTraceparent as
|
|
1
|
+
import { Cause as e, Context as t, Effect as n, Exit as r, Layer as i, Metric as a, Result as o, Schema as s } from "effect";
|
|
2
|
+
import { definePlugin as c } from "@voltro/protocol";
|
|
3
|
+
import { Kafka as l, logLevel as u } from "kafkajs";
|
|
4
|
+
import { externalSpanFromTraceparent as d, withServerTracer as f } from "@voltro/runtime";
|
|
5
5
|
//#region src/consumer.ts
|
|
6
|
-
var
|
|
6
|
+
var p = Symbol.for("@voltro/plugin-queue:consumers"), m = () => {
|
|
7
7
|
let e = globalThis;
|
|
8
|
-
return e[
|
|
9
|
-
},
|
|
8
|
+
return e[p] ??= /* @__PURE__ */ new Map();
|
|
9
|
+
}, h = (e) => {
|
|
10
10
|
if (e.topic.trim() === "") throw Error("defineQueueConsumer: `topic` must be a non-empty name");
|
|
11
11
|
if (e.maxAttempts !== void 0 && e.maxAttempts < 1) throw Error(`defineQueueConsumer("${e.topic}"): maxAttempts must be >= 1`);
|
|
12
|
-
let t = `${e.topic}|${e.groupId ?? ""}`, n =
|
|
12
|
+
let t = `${e.topic}|${e.groupId ?? ""}`, n = m().get(t);
|
|
13
13
|
if (n !== void 0 && n !== e) throw Error(`defineQueueConsumer: topic '${e.topic}'${e.groupId ? ` group '${e.groupId}'` : ""} is declared twice — two consumers in one group on one topic would split its partitions unpredictably. Use a distinct groupId for an independent consumer.`);
|
|
14
|
-
return
|
|
15
|
-
},
|
|
16
|
-
|
|
17
|
-
},
|
|
14
|
+
return m().set(t, e), e;
|
|
15
|
+
}, g = () => [...m().values()], _ = () => {
|
|
16
|
+
m().clear();
|
|
17
|
+
}, v = () => Object.create(null), y = Symbol.for("@voltro/plugin-queue:metrics"), b = () => {
|
|
18
18
|
let e = globalThis;
|
|
19
|
-
return e[
|
|
20
|
-
consumed:
|
|
21
|
-
dlq:
|
|
22
|
-
retried:
|
|
23
|
-
lastError:
|
|
24
|
-
produced:
|
|
19
|
+
return e[y] ??= {
|
|
20
|
+
consumed: v(),
|
|
21
|
+
dlq: v(),
|
|
22
|
+
retried: v(),
|
|
23
|
+
lastError: v(),
|
|
24
|
+
produced: v()
|
|
25
25
|
};
|
|
26
|
-
},
|
|
26
|
+
}, x = (e, t, n = 1) => {
|
|
27
27
|
e[t] = (e[t] ?? 0) + n;
|
|
28
|
-
},
|
|
28
|
+
}, S = a.counter("voltro_queue_consumed_total", { description: "Messages a consumer finished with, by topic and outcome (ok | dead-lettered)." }), C = a.counter("voltro_queue_retries_total", { description: "In-process handler retries, by topic. A retry BLOCKS its partition." }), w = a.counter("voltro_queue_produced_total", { description: "Messages produced through the outbox bridge and QueueService, by topic." }), T = a.gauge("voltro_queue_lag_messages", { description: "Messages between the offset just picked up and the partition high watermark, by topic and partition. Sampled at pickup; holds its last value for an idle or revoked partition." }), E = t.empty(), D = /* @__PURE__ */ new Map(), O = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), A = /* @__PURE__ */ new Map(), j = (e, t, n) => {
|
|
29
29
|
let r = e.get(t);
|
|
30
30
|
if (r !== void 0) return r;
|
|
31
31
|
let i = n();
|
|
32
32
|
return e.set(t, i), i;
|
|
33
|
-
},
|
|
34
|
-
b(
|
|
35
|
-
},
|
|
36
|
-
let n =
|
|
37
|
-
|
|
38
|
-
},
|
|
39
|
-
b(
|
|
40
|
-
},
|
|
41
|
-
t <= 0 || (b(
|
|
42
|
-
},
|
|
33
|
+
}, M = (e) => {
|
|
34
|
+
x(b().consumed, e), j(D, `ok\u0000${e}`, () => a.withAttributes(a.withAttributes(S, { topic: e }), { outcome: "ok" })).updateUnsafe(1, E);
|
|
35
|
+
}, N = (e, t) => {
|
|
36
|
+
let n = b();
|
|
37
|
+
x(n.dlq, e), n.lastError[e] = t.slice(0, 500), j(D, `dlq\u0000${e}`, () => a.withAttributes(a.withAttributes(S, { topic: e }), { outcome: "dead-lettered" })).updateUnsafe(1, E);
|
|
38
|
+
}, P = (e) => {
|
|
39
|
+
x(b().retried, e), j(O, e, () => a.withAttributes(C, { topic: e })).updateUnsafe(1, E);
|
|
40
|
+
}, F = (e, t) => {
|
|
41
|
+
t <= 0 || (x(b().produced, e, t), j(k, e, () => a.withAttributes(w, { topic: e })).updateUnsafe(t, E));
|
|
42
|
+
}, I = (e, t, n, r) => {
|
|
43
43
|
let i;
|
|
44
44
|
try {
|
|
45
45
|
let e = BigInt(n) - BigInt(r) - 1n;
|
|
@@ -47,16 +47,16 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
47
47
|
} catch {
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
A
|
|
51
|
-
},
|
|
50
|
+
j(A, `${e}\u0000${t}`, () => a.withAttributes(a.withAttributes(T, { topic: e }), { partition: String(t) })).updateUnsafe(i, E);
|
|
51
|
+
}, L = (e) => {
|
|
52
52
|
let t = {};
|
|
53
53
|
for (let [n, r] of Object.entries(e ?? {})) r != null && (t[n] = Buffer.isBuffer(r) ? r.toString("utf8") : String(r));
|
|
54
54
|
return t;
|
|
55
|
-
},
|
|
56
|
-
let t = new
|
|
55
|
+
}, R = (e) => {
|
|
56
|
+
let t = new l({
|
|
57
57
|
clientId: e.clientId ?? "voltro",
|
|
58
58
|
brokers: [...e.brokers],
|
|
59
|
-
logLevel:
|
|
59
|
+
logLevel: u.ERROR,
|
|
60
60
|
...e.ssl === void 0 ? {} : { ssl: e.ssl },
|
|
61
61
|
...e.sasl === void 0 ? {} : { sasl: e.sasl }
|
|
62
62
|
}), n = null, r = /* @__PURE__ */ new Set();
|
|
@@ -113,10 +113,10 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
113
113
|
offset: s.offset,
|
|
114
114
|
key: s.key === null ? null : s.key.toString("utf8"),
|
|
115
115
|
value: s.value === null ? null : s.value.toString("utf8"),
|
|
116
|
-
headers:
|
|
116
|
+
headers: L(s.headers),
|
|
117
117
|
timestamp: s.timestamp
|
|
118
118
|
};
|
|
119
|
-
|
|
119
|
+
I(e.topic, e.partition, e.highWatermark, s.offset), await n.onMessage(c, {
|
|
120
120
|
heartbeat: i,
|
|
121
121
|
isStale: o
|
|
122
122
|
}), t(s.offset), await r();
|
|
@@ -127,9 +127,9 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
127
127
|
} };
|
|
128
128
|
}
|
|
129
129
|
};
|
|
130
|
-
},
|
|
131
|
-
let i =
|
|
132
|
-
return r.pipe(n.tap((e) => t === void 0 ? n.void : n.annotateCurrentSpan(t(e))), n.withSpan(
|
|
130
|
+
}, z = "queue.consume", B = (e, t) => (r) => {
|
|
131
|
+
let i = d(e.traceparent);
|
|
132
|
+
return r.pipe(n.tap((e) => t === void 0 ? n.void : n.annotateCurrentSpan(t(e))), n.withSpan(z, {
|
|
133
133
|
attributes: {
|
|
134
134
|
"messaging.system": e.system,
|
|
135
135
|
"messaging.operation": "process",
|
|
@@ -140,17 +140,17 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
140
140
|
},
|
|
141
141
|
...i ? { parent: i } : {}
|
|
142
142
|
}));
|
|
143
|
-
},
|
|
143
|
+
}, V = async (t, i, a) => {
|
|
144
144
|
let o, s = await n.runPromiseExit(n.tryPromise({
|
|
145
145
|
try: i,
|
|
146
146
|
catch: (e) => (o = { value: e }, e)
|
|
147
|
-
}).pipe(
|
|
147
|
+
}).pipe(B(t, a), f));
|
|
148
148
|
if (r.isSuccess(s)) return s.value;
|
|
149
149
|
throw o === void 0 ? e.squash(s.cause) : o.value;
|
|
150
|
-
},
|
|
150
|
+
}, H = (e) => new Promise((t) => setTimeout(t, e)), U = async (e) => {
|
|
151
151
|
let { provider: t, log: n } = e, r = { current: !1 }, i = /* @__PURE__ */ new Set(), a = async (r) => {
|
|
152
|
-
let i = r.groupId ?? `${e.appName}.${r.topic}`, a = r.dlqTopic ?? `${r.topic}.dlq`,
|
|
153
|
-
|
|
152
|
+
let i = r.groupId ?? `${e.appName}.${r.topic}`, a = r.dlqTopic ?? `${r.topic}.dlq`, c = r.maxAttempts ?? 3, l = s.decodeUnknownResult(r.schema), u = async (e, i, o) => {
|
|
153
|
+
N(r.topic, `${e}: ${i}`), n.warn("queue: dead-lettered", {
|
|
154
154
|
topic: r.topic,
|
|
155
155
|
dlqTopic: a,
|
|
156
156
|
reason: e,
|
|
@@ -168,12 +168,12 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
168
168
|
"x-voltro-dlq-source-offset": o.offset
|
|
169
169
|
}
|
|
170
170
|
}]);
|
|
171
|
-
},
|
|
171
|
+
}, d = await t.consume({
|
|
172
172
|
topic: r.topic,
|
|
173
173
|
groupId: i,
|
|
174
174
|
...r.fromBeginning === void 0 ? {} : { fromBeginning: r.fromBeginning },
|
|
175
|
-
onMessage: async (a,
|
|
176
|
-
await
|
|
175
|
+
onMessage: async (a, s) => {
|
|
176
|
+
await V({
|
|
177
177
|
system: t.name,
|
|
178
178
|
topic: r.topic,
|
|
179
179
|
groupId: i,
|
|
@@ -185,14 +185,14 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
185
185
|
try {
|
|
186
186
|
t = a.value === null ? null : JSON.parse(a.value);
|
|
187
187
|
} catch (e) {
|
|
188
|
-
return await
|
|
188
|
+
return await u("decode-failed", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`, a), "dead-lettered";
|
|
189
189
|
}
|
|
190
|
-
let i =
|
|
191
|
-
if (i
|
|
192
|
-
for (let t = 1; t <=
|
|
193
|
-
if (
|
|
190
|
+
let i = l(t);
|
|
191
|
+
if (o.isFailure(i)) return await u("decode-failed", String(i.failure), a), "dead-lettered";
|
|
192
|
+
for (let t = 1; t <= c; t++) {
|
|
193
|
+
if (s.isStale()) return "stale";
|
|
194
194
|
try {
|
|
195
|
-
return await r.handler(i.
|
|
195
|
+
return await r.handler(i.success, {
|
|
196
196
|
topic: a.topic,
|
|
197
197
|
partition: a.partition,
|
|
198
198
|
offset: a.offset,
|
|
@@ -205,16 +205,16 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
205
205
|
if (t === null) throw Error("queue: store not bound yet — the retry loop absorbs a message arriving before boot finished");
|
|
206
206
|
return t;
|
|
207
207
|
}
|
|
208
|
-
}),
|
|
208
|
+
}), M(r.topic), "ok";
|
|
209
209
|
} catch (i) {
|
|
210
|
-
let
|
|
211
|
-
if (t >=
|
|
212
|
-
|
|
210
|
+
let o = i instanceof Error ? i.message : String(i);
|
|
211
|
+
if (t >= c) return await u("handler-failed", o, a), "dead-lettered";
|
|
212
|
+
P(r.topic), n.warn("queue: handler failed, retrying", {
|
|
213
213
|
topic: r.topic,
|
|
214
214
|
attempt: t,
|
|
215
|
-
maxAttempts:
|
|
216
|
-
detail:
|
|
217
|
-
}), await
|
|
215
|
+
maxAttempts: c,
|
|
216
|
+
detail: o
|
|
217
|
+
}), await s.heartbeat().catch(() => {}), await H((e.retryBaseMs ?? 1e3) * 2 ** (t - 1));
|
|
218
218
|
}
|
|
219
219
|
}
|
|
220
220
|
return "stale";
|
|
@@ -225,8 +225,8 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
225
225
|
topic: r.topic,
|
|
226
226
|
groupId: i,
|
|
227
227
|
dlqTopic: a
|
|
228
|
-
}),
|
|
229
|
-
},
|
|
228
|
+
}), d;
|
|
229
|
+
}, c = e.consumers.map((e) => {
|
|
230
230
|
let t = { handle: null }, o = () => {
|
|
231
231
|
a(e).then((e) => {
|
|
232
232
|
if (r.current) {
|
|
@@ -251,10 +251,10 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
251
251
|
return { stop: async () => {
|
|
252
252
|
r.current = !0;
|
|
253
253
|
for (let e of i) clearTimeout(e);
|
|
254
|
-
i.clear(), await Promise.all(
|
|
254
|
+
i.clear(), await Promise.all(c.map((e) => e.handle?.stop() ?? Promise.resolve()));
|
|
255
255
|
} };
|
|
256
|
-
},
|
|
257
|
-
let t = () => e.provider ??
|
|
256
|
+
}, W = (e) => {
|
|
257
|
+
let t = () => e.provider ?? J();
|
|
258
258
|
return {
|
|
259
259
|
name: `kafka(${e.topic})`,
|
|
260
260
|
...e.provider !== void 0 && e.provider.hosts[0] !== void 0 ? { outboundHost: e.provider.hosts[0] } : {},
|
|
@@ -271,43 +271,43 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
271
271
|
})));
|
|
272
272
|
}
|
|
273
273
|
};
|
|
274
|
-
},
|
|
274
|
+
}, G = class extends t.Service()("@voltro/plugin-queue/QueueService") {}, K = Symbol.for("@voltro/plugin-queue:active-provider"), q = () => {
|
|
275
275
|
let e = globalThis;
|
|
276
|
-
return e[
|
|
277
|
-
},
|
|
278
|
-
let e =
|
|
276
|
+
return e[K] ??= { current: null };
|
|
277
|
+
}, J = () => {
|
|
278
|
+
let e = q().current;
|
|
279
279
|
if (e === null) throw Error("[voltro] no active queue provider — is queuePlugin({...}) in this app's plugins? The outbox handler and kafkaSink need the plugin's connected provider.");
|
|
280
280
|
return e;
|
|
281
|
-
},
|
|
281
|
+
}, Y = () => ({
|
|
282
282
|
effect: "queue.produce",
|
|
283
283
|
maxAttempts: 8,
|
|
284
284
|
handler: async ({ payload: e }) => {
|
|
285
285
|
let t = e.topic, n = e.messages;
|
|
286
286
|
if (typeof t != "string" || !Array.isArray(n)) throw Error("queue.produce payload must be { topic: string, messages: QueueOutgoingMessage[] }");
|
|
287
|
-
await
|
|
287
|
+
await J().produce(t, n), F(t, n.length);
|
|
288
288
|
}
|
|
289
|
-
}),
|
|
290
|
-
let t = e.name ?? "queue", r =
|
|
291
|
-
return
|
|
289
|
+
}), X = (e) => {
|
|
290
|
+
let t = e.name ?? "queue", r = R(e), a = ["inspect:read", ...r.hosts.map((e) => `network:outbound:${e}`)], o = null, s = { current: null };
|
|
291
|
+
return c({
|
|
292
292
|
name: t,
|
|
293
293
|
baseName: "queue",
|
|
294
294
|
description: "Queue interop — consume and produce against an existing Kafka: Schema-decoded consumers (at-least-once, serial per partition, retry + dead-letter), batched producing via a handler service or transactionally through the outbox, and a kafkaSink for plugin-cdc-out.",
|
|
295
295
|
permissions: a,
|
|
296
|
-
services: i.succeed(
|
|
297
|
-
await r.produce(e, t),
|
|
296
|
+
services: i.succeed(G, { produce: async (e, t) => {
|
|
297
|
+
await r.produce(e, t), F(e, t.length);
|
|
298
298
|
} }),
|
|
299
299
|
bindDataStore: (e) => {
|
|
300
|
-
|
|
300
|
+
s.current = e;
|
|
301
301
|
},
|
|
302
302
|
onActivate: (t) => n.tryPromise(async () => {
|
|
303
|
-
await r.connect(),
|
|
304
|
-
let n =
|
|
305
|
-
o = await
|
|
303
|
+
await r.connect(), q().current = r;
|
|
304
|
+
let n = g();
|
|
305
|
+
o = await U({
|
|
306
306
|
provider: r,
|
|
307
307
|
consumers: n,
|
|
308
308
|
appName: e.groupPrefix ?? t.app.name,
|
|
309
309
|
log: t.logger,
|
|
310
|
-
store: () =>
|
|
310
|
+
store: () => s.current,
|
|
311
311
|
...e.retryBaseMs === void 0 ? {} : { retryBaseMs: e.retryBaseMs }
|
|
312
312
|
}), t.logger.info("queue: connected", {
|
|
313
313
|
brokers: r.hosts,
|
|
@@ -315,7 +315,7 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
315
315
|
});
|
|
316
316
|
}),
|
|
317
317
|
onDeactivate: () => n.tryPromise(async () => {
|
|
318
|
-
await o?.stop(), o = null,
|
|
318
|
+
await o?.stop(), o = null, q().current === r && (q().current = null), await r.disconnect();
|
|
319
319
|
}),
|
|
320
320
|
inspectEndpoints: [{
|
|
321
321
|
method: "GET",
|
|
@@ -323,13 +323,13 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
323
323
|
scope: "process",
|
|
324
324
|
description: "Registered consumers + per-topic consumed/retried/dead-lettered/produced counters (this replica).",
|
|
325
325
|
handler: () => n.sync(() => {
|
|
326
|
-
let e =
|
|
326
|
+
let e = b();
|
|
327
327
|
return {
|
|
328
328
|
kind: "json",
|
|
329
329
|
data: {
|
|
330
|
-
producerSelected:
|
|
330
|
+
producerSelected: q().current === r,
|
|
331
331
|
brokers: r.hosts,
|
|
332
|
-
consumers:
|
|
332
|
+
consumers: g().map((t) => ({
|
|
333
333
|
topic: t.topic,
|
|
334
334
|
groupId: t.groupId ?? null,
|
|
335
335
|
dlqTopic: t.dlqTopic ?? `${t.topic}.dlq`,
|
|
@@ -347,4 +347,4 @@ var f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
|
|
|
347
347
|
});
|
|
348
348
|
};
|
|
349
349
|
//#endregion
|
|
350
|
-
export {
|
|
350
|
+
export { G as QueueService, J as activeQueueProvider, h as defineQueueConsumer, R as kafkaProvider, W as kafkaSink, b as queueMetrics, Y as queueOutboxHandler, X as queuePlugin, g as registeredQueueConsumers, _ as resetQueueConsumersForTest, U as startQueueConsumers };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-queue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.85.0",
|
|
4
4
|
"description": "Queue interop — consume and produce against an existing Kafka: Schema-decoded consumers via defineQueueConsumer (*.consumer.ts; at-least-once, serial per partition, retry + dead-letter), batched producing via QueueService or transactionally through the outbox (queueOutboxHandler), and a kafkaSink for plugin-cdc-out.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -33,13 +33,13 @@
|
|
|
33
33
|
"node": ">=24.0.0"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@voltro/database": "0.
|
|
37
|
-
"@voltro/protocol": "0.
|
|
38
|
-
"@voltro/runtime": "0.
|
|
36
|
+
"@voltro/database": "0.85.0",
|
|
37
|
+
"@voltro/protocol": "0.85.0",
|
|
38
|
+
"@voltro/runtime": "0.85.0",
|
|
39
39
|
"kafkajs": "^2.2.4"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
|
-
"effect": "
|
|
42
|
+
"effect": "4.0.1"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|