@voltro/plugin-queue 0.84.0 → 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/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.Schema<A, any, never>;
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.TagClass<QueueService, "@voltro/plugin-queue/QueueService", QueueServiceApi>;
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, Schema as o } from "effect";
2
- import { definePlugin as s } from "@voltro/protocol";
3
- import { Kafka as c, logLevel as l } from "kafkajs";
4
- import { externalSpanFromTraceparent as u, withServerTracer as d } from "@voltro/runtime";
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 f = Symbol.for("@voltro/plugin-queue:consumers"), p = () => {
6
+ var p = Symbol.for("@voltro/plugin-queue:consumers"), m = () => {
7
7
  let e = globalThis;
8
- return e[f] ??= /* @__PURE__ */ new Map();
9
- }, m = (e) => {
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 = p().get(t);
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 p().set(t, e), e;
15
- }, h = () => [...p().values()], g = () => {
16
- p().clear();
17
- }, _ = () => Object.create(null), v = Symbol.for("@voltro/plugin-queue:metrics"), y = () => {
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[v] ??= {
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
- }, b = (e, t, n = 1) => {
26
+ }, x = (e, t, n = 1) => {
27
27
  e[t] = (e[t] ?? 0) + n;
28
- }, x = a.counter("voltro_queue_consumed_total", { description: "Messages a consumer finished with, by topic and outcome (ok | dead-lettered)." }), S = a.counter("voltro_queue_retries_total", { description: "In-process handler retries, by topic. A retry BLOCKS its partition." }), C = a.counter("voltro_queue_produced_total", { description: "Messages produced through the outbox bridge and QueueService, by topic." }), w = 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." }), T = [], E = /* @__PURE__ */ new Map(), D = /* @__PURE__ */ new Map(), O = /* @__PURE__ */ new Map(), k = /* @__PURE__ */ new Map(), A = (e, t, n) => {
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
- }, j = (e) => {
34
- b(y().consumed, e), A(E, `ok\u0000${e}`, () => a.tagged(a.tagged(x, "topic", e), "outcome", "ok")).unsafeUpdate(1, T);
35
- }, M = (e, t) => {
36
- let n = y();
37
- b(n.dlq, e), n.lastError[e] = t.slice(0, 500), A(E, `dlq\u0000${e}`, () => a.tagged(a.tagged(x, "topic", e), "outcome", "dead-lettered")).unsafeUpdate(1, T);
38
- }, N = (e) => {
39
- b(y().retried, e), A(D, e, () => a.tagged(S, "topic", e)).unsafeUpdate(1, T);
40
- }, P = (e, t) => {
41
- t <= 0 || (b(y().produced, e, t), A(O, e, () => a.tagged(C, "topic", e)).unsafeUpdate(t, T));
42
- }, F = (e, t, n, r) => {
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(k, `${e}\u0000${t}`, () => a.tagged(a.tagged(w, "topic", e), "partition", String(t))).unsafeUpdate(i, T);
51
- }, I = (e) => {
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
- }, L = (e) => {
56
- let t = new c({
55
+ }, R = (e) => {
56
+ let t = new l({
57
57
  clientId: e.clientId ?? "voltro",
58
58
  brokers: [...e.brokers],
59
- logLevel: l.ERROR,
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: I(s.headers),
116
+ headers: L(s.headers),
117
117
  timestamp: s.timestamp
118
118
  };
119
- F(e.topic, e.partition, e.highWatermark, s.offset), await n.onMessage(c, {
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
- }, R = "queue.consume", z = (e, t) => (r) => {
131
- let i = u(e.traceparent);
132
- return r.pipe(n.tap((e) => t === void 0 ? n.void : n.annotateCurrentSpan(t(e))), n.withSpan(R, {
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
- }, B = async (t, i, a) => {
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(z(t, a), d));
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
- }, V = (e) => new Promise((t) => setTimeout(t, e)), H = async (e) => {
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`, s = r.maxAttempts ?? 3, c = o.decodeUnknownEither(r.schema), l = async (e, i, o) => {
153
- M(r.topic, `${e}: ${i}`), n.warn("queue: dead-lettered", {
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
- }, u = await t.consume({
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, o) => {
176
- await B({
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 l("decode-failed", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`, a), "dead-lettered";
188
+ return await u("decode-failed", `invalid JSON: ${e instanceof Error ? e.message : String(e)}`, a), "dead-lettered";
189
189
  }
190
- let i = c(t);
191
- if (i._tag === "Left") return await l("decode-failed", String(i.left), a), "dead-lettered";
192
- for (let t = 1; t <= s; t++) {
193
- if (o.isStale()) return "stale";
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.right, {
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
- }), j(r.topic), "ok";
208
+ }), M(r.topic), "ok";
209
209
  } catch (i) {
210
- let c = i instanceof Error ? i.message : String(i);
211
- if (t >= s) return await l("handler-failed", c, a), "dead-lettered";
212
- N(r.topic), n.warn("queue: handler failed, retrying", {
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: s,
216
- detail: c
217
- }), await o.heartbeat().catch(() => {}), await V((e.retryBaseMs ?? 1e3) * 2 ** (t - 1));
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
- }), u;
229
- }, s = e.consumers.map((e) => {
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(s.map((e) => e.handle?.stop() ?? Promise.resolve()));
254
+ i.clear(), await Promise.all(c.map((e) => e.handle?.stop() ?? Promise.resolve()));
255
255
  } };
256
- }, U = (e) => {
257
- let t = () => e.provider ?? q();
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
- }, W = class extends t.Tag("@voltro/plugin-queue/QueueService")() {}, G = Symbol.for("@voltro/plugin-queue:active-provider"), K = () => {
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[G] ??= { current: null };
277
- }, q = () => {
278
- let e = K().current;
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
- }, J = () => ({
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 q().produce(t, n), P(t, n.length);
287
+ await J().produce(t, n), F(t, n.length);
288
288
  }
289
- }), Y = (e) => {
290
- let t = e.name ?? "queue", r = L(e), a = ["inspect:read", ...r.hosts.map((e) => `network:outbound:${e}`)], o = null, c = { current: null };
291
- return s({
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(W, { produce: async (e, t) => {
297
- await r.produce(e, t), P(e, t.length);
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
- c.current = e;
300
+ s.current = e;
301
301
  },
302
302
  onActivate: (t) => n.tryPromise(async () => {
303
- await r.connect(), K().current = r;
304
- let n = h();
305
- o = await H({
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: () => c.current,
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, K().current === r && (K().current = null), await r.disconnect();
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 = y();
326
+ let e = b();
327
327
  return {
328
328
  kind: "json",
329
329
  data: {
330
- producerSelected: K().current === r,
330
+ producerSelected: q().current === r,
331
331
  brokers: r.hosts,
332
- consumers: h().map((t) => ({
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 { W as QueueService, q as activeQueueProvider, m as defineQueueConsumer, L as kafkaProvider, U as kafkaSink, y as queueMetrics, J as queueOutboxHandler, Y as queuePlugin, h as registeredQueueConsumers, g as resetQueueConsumersForTest, H as startQueueConsumers };
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.84.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.84.0",
37
- "@voltro/protocol": "0.84.0",
38
- "@voltro/runtime": "0.84.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": "^3.22.2"
42
+ "effect": "4.0.1"
43
43
  },
44
44
  "publishConfig": {
45
45
  "access": "public"