@burdenoff/be-sdk 2026.514.2 → 2026.516.2

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.
Files changed (66) hide show
  1. package/dist/EventQueueFactory-6UJ3UIP4.js +6 -0
  2. package/dist/{EventQueueFactory-DGHBOV7I.js.map → EventQueueFactory-6UJ3UIP4.js.map} +1 -1
  3. package/dist/cache/index.d.ts +1 -1
  4. package/dist/cache/index.js +1 -1
  5. package/dist/{chunk-FT74HIIG.js → chunk-24VR7PF3.js} +2 -2
  6. package/dist/{chunk-FT74HIIG.js.map → chunk-24VR7PF3.js.map} +1 -1
  7. package/dist/chunk-75VTKG4Q.js +243 -0
  8. package/dist/chunk-75VTKG4Q.js.map +1 -0
  9. package/dist/{chunk-M3PYLAPT.js → chunk-7Y3RNUS3.js} +4 -4
  10. package/dist/{chunk-M3PYLAPT.js.map → chunk-7Y3RNUS3.js.map} +1 -1
  11. package/dist/{chunk-FSTWNBRX.js → chunk-MQ6OE5QR.js} +10 -33
  12. package/dist/chunk-MQ6OE5QR.js.map +1 -0
  13. package/dist/{chunk-N52W3RQA.js → chunk-MVHOP4IG.js} +3 -174
  14. package/dist/chunk-MVHOP4IG.js.map +1 -0
  15. package/dist/{chunk-S74C6CEN.js → chunk-Q6N74R6S.js} +61 -68
  16. package/dist/chunk-Q6N74R6S.js.map +1 -0
  17. package/dist/chunk-RRFNSBDZ.js +181 -0
  18. package/dist/chunk-RRFNSBDZ.js.map +1 -0
  19. package/dist/{chunk-D35GTG7Y.js → chunk-SNW4BLF2.js} +3 -3
  20. package/dist/{chunk-D35GTG7Y.js.map → chunk-SNW4BLF2.js.map} +1 -1
  21. package/dist/chunk-TM5RQ6P2.js +925 -0
  22. package/dist/chunk-TM5RQ6P2.js.map +1 -0
  23. package/dist/{chunk-IX5QCHWE.js → chunk-U455UF4O.js} +3 -3
  24. package/dist/{chunk-IX5QCHWE.js.map → chunk-U455UF4O.js.map} +1 -1
  25. package/dist/{chunk-CCV2AVAI.js → chunk-V4RLPTTT.js} +3 -3
  26. package/dist/{chunk-CCV2AVAI.js.map → chunk-V4RLPTTT.js.map} +1 -1
  27. package/dist/{chunk-KLYAIEAX.js → chunk-VKSB2K5E.js} +15 -30
  28. package/dist/chunk-VKSB2K5E.js.map +1 -0
  29. package/dist/event-queue/index.d.ts +2 -2
  30. package/dist/event-queue/index.js +4 -6
  31. package/dist/export/index.js +3 -3
  32. package/dist/gateway/index.js +2 -2
  33. package/dist/gateway/security/index.js +1 -1
  34. package/dist/graph/index.d.ts +2 -2
  35. package/dist/graph/index.js +5 -6
  36. package/dist/{graph-emitter-Bk0FqD53.d.ts → graph-emitter-Dp7UE1Yd.d.ts} +3 -2
  37. package/dist/graphql/index.d.ts +1 -1
  38. package/dist/graphql/index.js +2 -2
  39. package/dist/{index-C1ojTQq0.d.ts → index-B6IwtUb1.d.ts} +2 -0
  40. package/dist/{index-DZO2nvC9.d.ts → index-Bo58dlMU.d.ts} +2 -2
  41. package/dist/{index-syMAjErz.d.ts → index-D_Nc4UHi.d.ts} +6 -84
  42. package/dist/index-joONjwLt.d.ts +33 -0
  43. package/dist/{index-XY9x2I6b.d.ts → index-tJYziwMs.d.ts} +1 -0
  44. package/dist/index.d.ts +8 -8
  45. package/dist/index.js +14 -15
  46. package/dist/index.js.map +1 -1
  47. package/dist/init/index.d.ts +1 -1
  48. package/dist/init/index.js +3 -6
  49. package/dist/prisma/index.js +4 -6
  50. package/package.json +1 -6
  51. package/dist/EventQueueFactory-DGHBOV7I.js +0 -8
  52. package/dist/chunk-7V6GUNGP.js +0 -438
  53. package/dist/chunk-7V6GUNGP.js.map +0 -1
  54. package/dist/chunk-FSTWNBRX.js.map +0 -1
  55. package/dist/chunk-KLYAIEAX.js.map +0 -1
  56. package/dist/chunk-LRGXT37B.js +0 -1966
  57. package/dist/chunk-LRGXT37B.js.map +0 -1
  58. package/dist/chunk-N52W3RQA.js.map +0 -1
  59. package/dist/chunk-Q4HKXQVD.js +0 -478
  60. package/dist/chunk-Q4HKXQVD.js.map +0 -1
  61. package/dist/chunk-S74C6CEN.js.map +0 -1
  62. package/dist/chunk-UVYDP6P6.js +0 -13
  63. package/dist/chunk-UVYDP6P6.js.map +0 -1
  64. package/dist/config-QVYSAUMT.js +0 -4
  65. package/dist/config-QVYSAUMT.js.map +0 -1
  66. package/dist/index-CDFQRL-h.d.ts +0 -66
@@ -0,0 +1,925 @@
1
+ import { RedisStreamsConnection, isWildcard, streamKey, boundaryOf, dlqKey, registryKey, delayQueueKey, delayLockKey, dedupKey, replyKey, matchSubject } from './chunk-MVHOP4IG.js';
2
+ import { logger } from './chunk-CBZNP6RF.js';
3
+ import { __name } from './chunk-XE4OXN2W.js';
4
+ import { randomUUID } from 'crypto';
5
+ import { hostname } from 'os';
6
+
7
+ // src/event-queue/implementations/redis-streams/trace-context.ts
8
+ var otelApiPromise = null;
9
+ async function loadOtelApi() {
10
+ if (otelApiPromise) return otelApiPromise;
11
+ otelApiPromise = (async () => {
12
+ try {
13
+ const mod = await import('./esm-M2M7HGV4.js');
14
+ return mod;
15
+ } catch {
16
+ logger.debug("[redis-streams/trace-context] @opentelemetry/api not available; tracing disabled");
17
+ return null;
18
+ }
19
+ })();
20
+ return otelApiPromise;
21
+ }
22
+ __name(loadOtelApi, "loadOtelApi");
23
+ var TRACE_KEYS = [
24
+ "traceparent",
25
+ "tracestate",
26
+ "baggage"
27
+ ];
28
+ async function injectTraceFields() {
29
+ const api = await loadOtelApi();
30
+ if (!api) return {};
31
+ const carrier = {};
32
+ try {
33
+ api.propagation.inject(api.context.active(), carrier, {
34
+ set(c, k, v) {
35
+ c[k] = v;
36
+ }
37
+ });
38
+ } catch (err) {
39
+ logger.debug("[redis-streams/trace-context] inject failed", {
40
+ error: err instanceof Error ? err.message : String(err)
41
+ });
42
+ }
43
+ return carrier;
44
+ }
45
+ __name(injectTraceFields, "injectTraceFields");
46
+ async function extractTraceContext(fields) {
47
+ const api = await loadOtelApi();
48
+ if (!api) return void 0;
49
+ const carrier = {};
50
+ for (const key of TRACE_KEYS) {
51
+ const value = fields[key];
52
+ if (value) carrier[key] = value;
53
+ }
54
+ try {
55
+ return api.propagation.extract(api.context.active(), carrier, {
56
+ get(c, k) {
57
+ return c[k];
58
+ },
59
+ keys(c) {
60
+ return Object.keys(c);
61
+ }
62
+ });
63
+ } catch {
64
+ return void 0;
65
+ }
66
+ }
67
+ __name(extractTraceContext, "extractTraceContext");
68
+ async function runWithConsumerSpan(subject, fields, handler) {
69
+ const api = await loadOtelApi();
70
+ if (!api) return handler();
71
+ try {
72
+ const parentCtx = await extractTraceContext(fields);
73
+ const tracer = api.trace.getTracer("@burdenoff/be-sdk/redis-streams");
74
+ return await new Promise((resolve, reject) => {
75
+ tracer.startActiveSpan(`redis-streams.consume ${subject}`, {
76
+ kind: 5,
77
+ attributes: {
78
+ "messaging.system": "redis",
79
+ "messaging.destination.name": subject
80
+ }
81
+ }, parentCtx, async (span) => {
82
+ try {
83
+ const result = await handler();
84
+ span.setStatus({
85
+ code: 1
86
+ });
87
+ span.end();
88
+ resolve(result);
89
+ } catch (err) {
90
+ span.recordException(err);
91
+ span.setStatus({
92
+ code: 2,
93
+ message: err instanceof Error ? err.message : String(err)
94
+ });
95
+ span.end();
96
+ reject(err);
97
+ }
98
+ });
99
+ });
100
+ } catch {
101
+ return handler();
102
+ }
103
+ }
104
+ __name(runWithConsumerSpan, "runWithConsumerSpan");
105
+
106
+ // src/event-queue/implementations/redis-streams/RedisStreamsEventConsumer.ts
107
+ var DEFAULT_BLOCK_MS = 5e3;
108
+ var DEFAULT_BATCH = 10;
109
+ var REGISTRY_REFRESH_MS = 1e4;
110
+ var DEFAULT_ACK_WAIT_MS = 3e4;
111
+ var DEFAULT_MAX_DELIVER = 3;
112
+ var SWEEP_INTERVAL_MS = 15e3;
113
+ var DELAYQ_INTERVAL_MS = 1e3;
114
+ var RedisStreamsSubscription = class RedisStreamsSubscription2 {
115
+ static {
116
+ __name(this, "RedisStreamsSubscription");
117
+ }
118
+ subject;
119
+ unsubscribeFn;
120
+ id = randomUUID();
121
+ active = true;
122
+ receivedCount = 0;
123
+ processedCount = 0;
124
+ failedCount = 0;
125
+ lastActivity = (/* @__PURE__ */ new Date()).toISOString();
126
+ constructor(subject, unsubscribeFn) {
127
+ this.subject = subject;
128
+ this.unsubscribeFn = unsubscribeFn;
129
+ }
130
+ async unsubscribe() {
131
+ if (!this.active) return;
132
+ this.active = false;
133
+ await this.unsubscribeFn();
134
+ }
135
+ isActive() {
136
+ return this.active;
137
+ }
138
+ getStats() {
139
+ return {
140
+ messagesReceived: this.receivedCount,
141
+ messagesProcessed: this.processedCount,
142
+ messagesFailed: this.failedCount,
143
+ lastActivity: this.lastActivity
144
+ };
145
+ }
146
+ recordReceived() {
147
+ this.receivedCount++;
148
+ this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
149
+ }
150
+ recordProcessed() {
151
+ this.processedCount++;
152
+ this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
153
+ }
154
+ recordFailed() {
155
+ this.failedCount++;
156
+ this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
157
+ }
158
+ };
159
+ var RedisStreamsEventConsumer = class {
160
+ static {
161
+ __name(this, "RedisStreamsEventConsumer");
162
+ }
163
+ connection;
164
+ subscriptions = [];
165
+ subscriptionLoops = /* @__PURE__ */ new Set();
166
+ keyPrefix;
167
+ consumerName;
168
+ group;
169
+ maxDeliver;
170
+ ackWaitMs;
171
+ maxConcurrent;
172
+ consumedCount = 0;
173
+ failedCount = 0;
174
+ lastActivity = null;
175
+ stopped = false;
176
+ sweepTimer = null;
177
+ delayqTimer = null;
178
+ knownStreams = /* @__PURE__ */ new Map();
179
+ constructor(config) {
180
+ this.connection = new RedisStreamsConnection({
181
+ type: "redis-streams",
182
+ url: config.url ?? process.env["REDIS_URL"] ?? "redis://localhost:6379",
183
+ user: config.user,
184
+ password: config.password,
185
+ name: config.name,
186
+ connectionTimeout: config.connectionTimeout
187
+ });
188
+ this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
189
+ this.group = config.consumer || config.name || "default-consumer";
190
+ this.consumerName = `${this.group}:${hostname()}:${process.pid}`;
191
+ this.maxDeliver = config.maxDeliver ?? DEFAULT_MAX_DELIVER;
192
+ this.ackWaitMs = config.ackWait ? Math.max(1e3, Math.round(config.ackWait / 1e6)) : DEFAULT_ACK_WAIT_MS;
193
+ this.maxConcurrent = config.maxConcurrent ?? 5;
194
+ }
195
+ async connect() {
196
+ await this.connection.connect();
197
+ if (!this.sweepTimer) {
198
+ const t = setInterval(() => {
199
+ void this.sweepPending().catch((err) => {
200
+ logger.debug("[RedisStreamsEventConsumer] sweep error", {
201
+ error: err instanceof Error ? err.message : String(err)
202
+ });
203
+ });
204
+ }, SWEEP_INTERVAL_MS);
205
+ t.unref?.();
206
+ this.sweepTimer = t;
207
+ }
208
+ if (!this.delayqTimer) {
209
+ const t = setInterval(() => {
210
+ void this.drainDelayQueues().catch(() => void 0);
211
+ }, DELAYQ_INTERVAL_MS);
212
+ t.unref?.();
213
+ this.delayqTimer = t;
214
+ }
215
+ }
216
+ async disconnect() {
217
+ this.stopped = true;
218
+ if (this.sweepTimer) clearInterval(this.sweepTimer);
219
+ if (this.delayqTimer) clearInterval(this.delayqTimer);
220
+ await this.unsubscribeAll();
221
+ await this.connection.disconnect();
222
+ }
223
+ isConnected() {
224
+ return this.connection.isConnected();
225
+ }
226
+ async healthCheck() {
227
+ try {
228
+ const t0 = Date.now();
229
+ const pong = await this.connection.ping();
230
+ return {
231
+ status: pong === "PONG" ? "healthy" : "degraded",
232
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
233
+ connected: this.isConnected(),
234
+ latencyMs: Date.now() - t0,
235
+ details: {
236
+ type: "redis-streams",
237
+ subscriptions: this.subscriptions.length
238
+ }
239
+ };
240
+ } catch (err) {
241
+ return {
242
+ status: "unhealthy",
243
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
244
+ connected: false,
245
+ details: {
246
+ type: "redis-streams"
247
+ },
248
+ error: err instanceof Error ? err.message : String(err)
249
+ };
250
+ }
251
+ }
252
+ async getStatistics() {
253
+ return Promise.resolve({
254
+ messagesConsumed: this.consumedCount,
255
+ messagesFailed: this.failedCount,
256
+ messagesPublished: 0,
257
+ activeConsumers: this.subscriptions.length,
258
+ lastActivity: this.lastActivity?.toISOString() ?? null,
259
+ details: {
260
+ type: "redis-streams",
261
+ group: this.group
262
+ }
263
+ });
264
+ }
265
+ async ensureStream(_config) {
266
+ return Promise.resolve();
267
+ }
268
+ async ensureConsumer(_config) {
269
+ return Promise.resolve();
270
+ }
271
+ async subscribe(opts) {
272
+ await this.connect();
273
+ const sub = new RedisStreamsSubscription(opts.subject, async () => {
274
+ this.subscriptions = this.subscriptions.filter((s) => s !== sub);
275
+ });
276
+ this.subscriptions.push(sub);
277
+ const startId = this.resolveStartId(opts.options?.deliverPolicy);
278
+ const wildcard = isWildcard(opts.subject);
279
+ const handlerFn = opts.handler;
280
+ if (!wildcard) {
281
+ const stream = streamKey(opts.subject, {
282
+ prefix: this.keyPrefix
283
+ });
284
+ await this.ensureGroup(stream, startId);
285
+ this.knownStreams.set(stream, opts.subject);
286
+ this.spawnLoop(sub, stream, opts.subject, handlerFn);
287
+ } else {
288
+ const boundary = boundaryOf(opts.subject);
289
+ const refresh = /* @__PURE__ */ __name(async () => {
290
+ const client = this.connection.getClient();
291
+ const subjects = await client.smembers(registryKey(boundary));
292
+ for (const s of subjects) {
293
+ if (!matchSubject(opts.subject, s)) continue;
294
+ const stream = streamKey(s, {
295
+ prefix: this.keyPrefix
296
+ });
297
+ if (this.knownStreams.has(stream)) continue;
298
+ await this.ensureGroup(stream, startId);
299
+ this.knownStreams.set(stream, s);
300
+ this.spawnLoop(sub, stream, s, handlerFn);
301
+ }
302
+ }, "refresh");
303
+ await refresh();
304
+ const t = setInterval(() => {
305
+ if (this.stopped || !sub.isActive()) {
306
+ clearInterval(t);
307
+ return;
308
+ }
309
+ void refresh().catch(() => void 0);
310
+ }, REGISTRY_REFRESH_MS);
311
+ t.unref?.();
312
+ }
313
+ return sub;
314
+ }
315
+ async subscribeMultiple(subjects, handler, options) {
316
+ const out = [];
317
+ for (const subject of subjects) {
318
+ out.push(await this.subscribe({
319
+ subject,
320
+ handler,
321
+ options
322
+ }));
323
+ }
324
+ return out;
325
+ }
326
+ async pull(opts) {
327
+ await this.connect();
328
+ const stream = streamKey(opts.subject, {
329
+ prefix: this.keyPrefix
330
+ });
331
+ await this.ensureGroup(stream, "$");
332
+ const client = this.connection.getClient();
333
+ const raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", opts.batch ?? DEFAULT_BATCH, "BLOCK", opts.timeout ?? DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
334
+ if (!raw) return [];
335
+ const out = [];
336
+ for (const [streamName, entries] of raw) {
337
+ for (const [id, rawFields] of entries) {
338
+ const parsed = this.parseEntry(streamName, id, rawFields);
339
+ out.push(this.toAcknowledgeable(streamName, id, parsed));
340
+ }
341
+ }
342
+ return out;
343
+ }
344
+ async unsubscribeAll() {
345
+ for (const sub of [
346
+ ...this.subscriptions
347
+ ]) {
348
+ await sub.unsubscribe();
349
+ }
350
+ this.subscriptions = [];
351
+ this.subscriptionLoops.clear();
352
+ }
353
+ getSubscriptions() {
354
+ return [
355
+ ...this.subscriptions
356
+ ];
357
+ }
358
+ async processDLQ(handler) {
359
+ await this.connect();
360
+ const client = this.connection.getClient();
361
+ const dlqGroup = `${this.group}-dlq`;
362
+ for (const [stream] of this.knownStreams) {
363
+ const dlq = dlqKey(stream);
364
+ try {
365
+ await client.xgroup("CREATE", dlq, dlqGroup, "0", "MKSTREAM");
366
+ } catch {
367
+ }
368
+ const res = await client.xreadgroup("GROUP", dlqGroup, this.consumerName, "COUNT", 50, "BLOCK", 100, "STREAMS", dlq, ">");
369
+ if (!res) continue;
370
+ for (const [, entries] of res) {
371
+ for (const [id, rawFields] of entries) {
372
+ const parsed = this.parseEntry(dlq, id, rawFields);
373
+ const evt = this.toAcknowledgeable(dlq, id, parsed, dlqGroup);
374
+ try {
375
+ await handler(evt);
376
+ } catch (err) {
377
+ logger.error("[RedisStreamsEventConsumer] DLQ handler error", {
378
+ error: err instanceof Error ? err.message : String(err)
379
+ });
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ async listSubjects(_stream) {
386
+ await this.connect();
387
+ const client = this.connection.getClient();
388
+ const seen = /* @__PURE__ */ new Set();
389
+ for (const subject of this.knownStreams.values()) {
390
+ const members = await client.smembers(registryKey(boundaryOf(subject)));
391
+ for (const m of members) seen.add(m);
392
+ }
393
+ return [
394
+ ...seen
395
+ ];
396
+ }
397
+ // ---- internals ----
398
+ resolveStartId(policy) {
399
+ switch (policy) {
400
+ case "all":
401
+ return "0";
402
+ case "new":
403
+ return "$";
404
+ case "last":
405
+ return "$";
406
+ default:
407
+ return "$";
408
+ }
409
+ }
410
+ async ensureGroup(stream, startId) {
411
+ const client = this.connection.getClient();
412
+ try {
413
+ await client.xgroup("CREATE", stream, this.group, startId, "MKSTREAM");
414
+ logger.debug("[RedisStreamsEventConsumer] group created", {
415
+ stream,
416
+ group: this.group,
417
+ startId
418
+ });
419
+ } catch (err) {
420
+ const msg = err instanceof Error ? err.message : String(err);
421
+ if (!msg.includes("BUSYGROUP")) {
422
+ throw err;
423
+ }
424
+ }
425
+ }
426
+ spawnLoop(sub, stream, subject, handler) {
427
+ const loop = /* @__PURE__ */ __name(async () => {
428
+ const client = await this.connection.makeBlockingClient();
429
+ try {
430
+ while (!this.stopped && sub.isActive()) {
431
+ let raw = null;
432
+ try {
433
+ raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", this.maxConcurrent, "BLOCK", DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
434
+ } catch (err) {
435
+ const msg = err instanceof Error ? err.message : String(err);
436
+ if (msg.includes("NOGROUP")) {
437
+ await this.ensureGroup(stream, "$");
438
+ continue;
439
+ }
440
+ logger.warn("[RedisStreamsEventConsumer] read error", {
441
+ stream,
442
+ error: msg
443
+ });
444
+ await new Promise((res) => setTimeout(res, 1e3));
445
+ continue;
446
+ }
447
+ if (!raw) continue;
448
+ for (const [, entries] of raw) {
449
+ for (const [id, rawFields] of entries) {
450
+ sub.recordReceived();
451
+ this.consumedCount++;
452
+ this.lastActivity = /* @__PURE__ */ new Date();
453
+ const parsed = this.parseEntry(stream, id, rawFields);
454
+ const evt = this.toAcknowledgeable(stream, id, parsed);
455
+ try {
456
+ await runWithConsumerSpan(subject, parsed.fields, () => handler(evt));
457
+ sub.recordProcessed();
458
+ } catch (err) {
459
+ sub.recordFailed();
460
+ this.failedCount++;
461
+ logger.error("[RedisStreamsEventConsumer] handler error", {
462
+ subject,
463
+ id,
464
+ error: err instanceof Error ? err.message : String(err)
465
+ });
466
+ try {
467
+ await evt.nak();
468
+ } catch {
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ } finally {
475
+ try {
476
+ client.disconnect();
477
+ } catch {
478
+ }
479
+ }
480
+ }, "loop");
481
+ void loop().catch((err) => {
482
+ logger.error("[RedisStreamsEventConsumer] loop crashed", {
483
+ stream,
484
+ error: err instanceof Error ? err.message : String(err)
485
+ });
486
+ });
487
+ }
488
+ parseEntry(stream, id, rawFields) {
489
+ const fields = {};
490
+ for (let i = 0; i + 1 < rawFields.length; i += 2) {
491
+ const key = rawFields[i];
492
+ const val = rawFields[i + 1];
493
+ if (key === void 0 || val === void 0) continue;
494
+ fields[key] = val;
495
+ }
496
+ let data;
497
+ try {
498
+ data = JSON.parse(fields["payload"] ?? "null");
499
+ } catch {
500
+ data = fields["payload"];
501
+ }
502
+ let metadata;
503
+ try {
504
+ metadata = JSON.parse(fields["metadata"] ?? "{}");
505
+ } catch {
506
+ metadata = {
507
+ correlationId: fields["correlation-id"] ?? ""
508
+ };
509
+ }
510
+ if (!metadata.correlationId) metadata.correlationId = fields["correlation-id"] ?? "";
511
+ return {
512
+ id,
513
+ stream,
514
+ subject: fields["subject"] ?? "",
515
+ data,
516
+ metadata,
517
+ timestamp: fields["timestamp"] ?? (/* @__PURE__ */ new Date()).toISOString(),
518
+ fields,
519
+ deliveryCount: 1
520
+ };
521
+ }
522
+ toAcknowledgeable(stream, id, parsed, group = this.group) {
523
+ const connection = this.connection;
524
+ let acked = false;
525
+ return {
526
+ id,
527
+ subject: parsed.subject,
528
+ data: parsed.data,
529
+ metadata: parsed.metadata,
530
+ timestamp: parsed.timestamp,
531
+ async ack() {
532
+ if (acked) return;
533
+ acked = true;
534
+ const client = connection.getClient();
535
+ await client.xack(stream, group, id);
536
+ await client.xdel(stream, id).catch(() => void 0);
537
+ },
538
+ async nak(delay = 0) {
539
+ if (acked) return;
540
+ acked = true;
541
+ const client = connection.getClient();
542
+ if (delay > 0) {
543
+ const score = Date.now() + delay;
544
+ const payload = JSON.stringify(parsed.fields);
545
+ await client.zadd(delayQueueKey(stream), score, `${id}|${payload}`);
546
+ await client.xack(stream, group, id);
547
+ await client.xdel(stream, id).catch(() => void 0);
548
+ }
549
+ },
550
+ async term() {
551
+ if (acked) return;
552
+ acked = true;
553
+ const client = connection.getClient();
554
+ const dlq = dlqKey(stream);
555
+ const flat = [];
556
+ for (const [k, v] of Object.entries(parsed.fields)) flat.push(k, v);
557
+ await client.xadd(dlq, "MAXLEN", "~", 1e4, "*", ...flat);
558
+ await client.xack(stream, group, id);
559
+ await client.xdel(stream, id).catch(() => void 0);
560
+ },
561
+ isRedelivered() {
562
+ return parsed.deliveryCount > 1;
563
+ },
564
+ getDeliveryAttempt() {
565
+ return parsed.deliveryCount;
566
+ }
567
+ };
568
+ }
569
+ async sweepPending() {
570
+ if (this.stopped || this.knownStreams.size === 0) return;
571
+ const client = this.connection.getClient();
572
+ for (const [stream] of this.knownStreams) {
573
+ try {
574
+ const claimed = await client.xautoclaim(stream, this.group, this.consumerName, this.ackWaitMs, "0", "COUNT", 50);
575
+ if (!claimed) continue;
576
+ const entries = claimed[1] ?? [];
577
+ for (const [id, rawFields] of entries) {
578
+ const pending = await client.xpending(stream, this.group, "IDLE", 0, "-", "+", 1, this.consumerName);
579
+ const deliveryCount = Array.isArray(pending) && pending[0] ? pending[0][3] : 1;
580
+ if (deliveryCount > this.maxDeliver) {
581
+ const flat = [];
582
+ for (let i = 0; i + 1 < rawFields.length; i += 2) {
583
+ flat.push(rawFields[i], rawFields[i + 1]);
584
+ }
585
+ flat.push("dlq-reason", "max-deliver-exceeded");
586
+ flat.push("dlq-deliveries", String(deliveryCount));
587
+ await client.xadd(dlqKey(stream), "MAXLEN", "~", 1e4, "*", ...flat);
588
+ await client.xack(stream, this.group, id);
589
+ await client.xdel(stream, id).catch(() => void 0);
590
+ logger.warn("[RedisStreamsEventConsumer] message moved to DLQ", {
591
+ stream,
592
+ id,
593
+ deliveryCount
594
+ });
595
+ }
596
+ }
597
+ } catch (err) {
598
+ const msg = err instanceof Error ? err.message : String(err);
599
+ if (msg.includes("NOGROUP")) continue;
600
+ logger.debug("[RedisStreamsEventConsumer] sweep iteration error", {
601
+ stream,
602
+ error: msg
603
+ });
604
+ }
605
+ }
606
+ }
607
+ async drainDelayQueues() {
608
+ if (this.stopped || this.knownStreams.size === 0) return;
609
+ const client = this.connection.getClient();
610
+ for (const [stream] of this.knownStreams) {
611
+ const lockKey = delayLockKey(stream);
612
+ const got = await client.set(lockKey, this.consumerName, "NX", "EX", 30).catch(() => null);
613
+ if (got !== "OK") continue;
614
+ try {
615
+ const due = await client.zrangebyscore(delayQueueKey(stream), 0, Date.now(), "LIMIT", 0, 50);
616
+ for (const member of due) {
617
+ const split = member.indexOf("|");
618
+ if (split < 0) continue;
619
+ const payload = member.slice(split + 1);
620
+ let fields = {};
621
+ try {
622
+ fields = JSON.parse(payload);
623
+ } catch {
624
+ continue;
625
+ }
626
+ const flat = [];
627
+ for (const [k, v] of Object.entries(fields)) flat.push(k, v);
628
+ await client.xadd(stream, "MAXLEN", "~", 1e5, "*", ...flat);
629
+ await client.zrem(delayQueueKey(stream), member);
630
+ }
631
+ } finally {
632
+ await client.del(lockKey).catch(() => void 0);
633
+ }
634
+ }
635
+ }
636
+ };
637
+ var RedisStreamsEventPublisher = class {
638
+ static {
639
+ __name(this, "RedisStreamsEventPublisher");
640
+ }
641
+ config;
642
+ connection;
643
+ publishCount = 0;
644
+ lastPublishTime = null;
645
+ maxLen;
646
+ dedupTtl;
647
+ keyPrefix;
648
+ constructor(config) {
649
+ this.config = config;
650
+ this.connection = new RedisStreamsConnection(config);
651
+ this.maxLen = config.maxLen ?? parseInt(process.env["REDIS_STREAMS_MAXLEN"] ?? "100000", 10);
652
+ this.dedupTtl = config.dedupTtlSeconds ?? parseInt(process.env["REDIS_STREAMS_DEDUP_TTL"] ?? "120", 10);
653
+ this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
654
+ }
655
+ async connect() {
656
+ await this.connection.connect();
657
+ }
658
+ async disconnect() {
659
+ await this.connection.disconnect();
660
+ }
661
+ isConnected() {
662
+ return this.connection.isConnected();
663
+ }
664
+ async healthCheck() {
665
+ try {
666
+ const t0 = Date.now();
667
+ const pong = await this.connection.ping();
668
+ return {
669
+ status: pong === "PONG" ? "healthy" : "degraded",
670
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
671
+ connected: this.isConnected(),
672
+ latencyMs: Date.now() - t0,
673
+ details: {
674
+ type: "redis-streams",
675
+ url: this.config.url
676
+ }
677
+ };
678
+ } catch (err) {
679
+ return {
680
+ status: "unhealthy",
681
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
682
+ connected: false,
683
+ details: {
684
+ type: "redis-streams"
685
+ },
686
+ error: err instanceof Error ? err.message : String(err)
687
+ };
688
+ }
689
+ }
690
+ async getStatistics() {
691
+ return {
692
+ messagesPublished: this.publishCount,
693
+ messagesConsumed: 0,
694
+ messagesFailed: 0,
695
+ activePublishers: 1,
696
+ activeConsumers: 0,
697
+ lastActivity: this.lastPublishTime?.toISOString() ?? null,
698
+ details: {
699
+ type: "redis-streams"
700
+ }
701
+ };
702
+ }
703
+ async ensureStream(_config) {
704
+ return Promise.resolve();
705
+ }
706
+ async publish(options) {
707
+ return this.publishInternal(options);
708
+ }
709
+ async publishInternal(opts) {
710
+ await this.connect();
711
+ const client = this.connection.getClient();
712
+ const stream = streamKey(opts.subject, {
713
+ prefix: this.keyPrefix
714
+ });
715
+ const msgId = opts.options?.messageId ?? randomUUID();
716
+ const correlationId = opts.metadata?.correlationId ?? randomUUID();
717
+ const enableDedup = this.config.enableDeduplication === true || opts.options?.messageId !== void 0;
718
+ if (enableDedup) {
719
+ const ok = await client.set(dedupKey(stream, msgId), "1", "NX", "EX", this.dedupTtl);
720
+ if (ok !== "OK") {
721
+ logger.debug("[RedisStreamsEventPublisher] dedup hit; skipping publish", {
722
+ subject: opts.subject,
723
+ msgId
724
+ });
725
+ return msgId;
726
+ }
727
+ }
728
+ const trace = await injectTraceFields();
729
+ const fields = {
730
+ "msg-id": msgId,
731
+ subject: opts.subject,
732
+ "correlation-id": correlationId,
733
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
734
+ priority: opts.options?.priority ?? this.config.defaultPriority ?? "normal",
735
+ source: opts.metadata?.source ?? this.config.name ?? "unknown",
736
+ payload: JSON.stringify(opts.data ?? null),
737
+ metadata: JSON.stringify(opts.metadata ?? {}),
738
+ ...trace
739
+ };
740
+ if (opts.metadata?.userId) fields["user-id"] = String(opts.metadata.userId);
741
+ if (opts.metadata?.workspaceId) fields["workspace-id"] = String(opts.metadata.workspaceId);
742
+ if (opts.metadata?.tenantId) fields["tenant-id"] = String(opts.metadata.tenantId);
743
+ const flat = [];
744
+ for (const [k, v] of Object.entries(fields)) {
745
+ flat.push(k, v);
746
+ }
747
+ const id = await client.xadd(stream, "MAXLEN", "~", this.maxLen, "*", ...flat);
748
+ void client.sadd(registryKey(boundaryOf(opts.subject)), opts.subject).catch(() => void 0);
749
+ this.publishCount++;
750
+ this.lastPublishTime = /* @__PURE__ */ new Date();
751
+ logger.debug("[RedisStreamsEventPublisher] published", {
752
+ subject: opts.subject,
753
+ stream,
754
+ id
755
+ });
756
+ return id;
757
+ }
758
+ async publishBatch(events) {
759
+ const out = [];
760
+ for (const e of events) {
761
+ out.push(await this.publishInternal(e));
762
+ }
763
+ return out;
764
+ }
765
+ /**
766
+ * Emulated request/response: writer XADDs onto its own subject, then
767
+ * blocks on a one-shot reply stream keyed by correlationId.
768
+ */
769
+ async publishWithAck(opts) {
770
+ await this.connect();
771
+ const client = this.connection.getClient();
772
+ const correlationId = opts.metadata?.correlationId ?? randomUUID();
773
+ const reply = replyKey(correlationId);
774
+ await this.publishInternal({
775
+ subject: opts.subject,
776
+ data: opts.data,
777
+ metadata: {
778
+ ...opts.metadata,
779
+ correlationId,
780
+ replyTo: reply
781
+ }
782
+ });
783
+ const timeoutMs = opts.timeout ?? 5e3;
784
+ const t0 = Date.now();
785
+ while (Date.now() - t0 < timeoutMs) {
786
+ const entries = await client.xrange(reply, "-", "+");
787
+ if (Array.isArray(entries) && entries.length > 0) {
788
+ const last = entries[entries.length - 1];
789
+ if (!last) break;
790
+ const rawFields = last[1];
791
+ const payloadIdx = rawFields.indexOf("payload");
792
+ const payload = payloadIdx >= 0 ? rawFields[payloadIdx + 1] : "{}";
793
+ await client.del(reply);
794
+ return JSON.parse(payload ?? "{}");
795
+ }
796
+ await new Promise((res) => setTimeout(res, 50));
797
+ }
798
+ await client.del(reply).catch(() => void 0);
799
+ throw new Error(`[RedisStreamsEventPublisher] publishWithAck timed out for subject=${opts.subject}`);
800
+ }
801
+ /** Internal helper used by the consumer's reply path. */
802
+ async sendReply(correlationId, data) {
803
+ await this.connect();
804
+ const client = this.connection.getClient();
805
+ const key = replyKey(correlationId);
806
+ await client.xadd(key, "*", "payload", JSON.stringify(data ?? null));
807
+ await client.expire(key, 60);
808
+ }
809
+ };
810
+
811
+ // src/event-queue/types/config.ts
812
+ var QueueType = /* @__PURE__ */ (function(QueueType2) {
813
+ QueueType2["REDIS_STREAMS"] = "redis-streams";
814
+ QueueType2["RABBITMQ"] = "rabbitmq";
815
+ QueueType2["KAFKA"] = "kafka";
816
+ QueueType2["SQS"] = "sqs";
817
+ return QueueType2;
818
+ })({});
819
+
820
+ // src/event-queue/factory/EventQueueFactory.ts
821
+ function resolveQueueType() {
822
+ return QueueType.REDIS_STREAMS;
823
+ }
824
+ __name(resolveQueueType, "resolveQueueType");
825
+ var EventQueueFactory = class _EventQueueFactory {
826
+ static {
827
+ __name(this, "EventQueueFactory");
828
+ }
829
+ /**
830
+ * Create an event publisher based on configuration
831
+ */
832
+ static createPublisher(config) {
833
+ logger.info("[EventQueueFactory] Creating event publisher", {
834
+ type: config.type,
835
+ url: config.url
836
+ });
837
+ if (config.type === QueueType.REDIS_STREAMS) {
838
+ return new RedisStreamsEventPublisher(config);
839
+ }
840
+ throw new Error(`Unsupported queue type: ${config.type}. NATS was retired on 2026-05-16; use 'redis-streams'.`);
841
+ }
842
+ /**
843
+ * Create an event consumer based on configuration
844
+ */
845
+ static createConsumer(config) {
846
+ logger.info("[EventQueueFactory] Creating event consumer", {
847
+ type: config.type,
848
+ url: config.url,
849
+ stream: config.stream,
850
+ consumer: config.consumer
851
+ });
852
+ if (config.type === QueueType.REDIS_STREAMS) {
853
+ return new RedisStreamsEventConsumer(config);
854
+ }
855
+ throw new Error(`Unsupported queue type: ${config.type}. NATS was retired on 2026-05-16; use 'redis-streams'.`);
856
+ }
857
+ /**
858
+ * Create a publisher from environment variables.
859
+ * Reads `REDIS_URL`, `REDIS_PASSWORD`, and standard queue options.
860
+ */
861
+ static createPublisherFromEnv() {
862
+ return _EventQueueFactory.createPublisher({
863
+ type: QueueType.REDIS_STREAMS,
864
+ url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
865
+ password: process.env["REDIS_PASSWORD"],
866
+ name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? "be-sdk-publisher",
867
+ enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
868
+ defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] ?? "normal"
869
+ });
870
+ }
871
+ /**
872
+ * Create a consumer from environment variables.
873
+ */
874
+ static createConsumerFromEnv(stream, consumer) {
875
+ return _EventQueueFactory.createConsumer({
876
+ type: QueueType.REDIS_STREAMS,
877
+ url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
878
+ password: process.env["REDIS_PASSWORD"],
879
+ name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? consumer,
880
+ stream,
881
+ consumer,
882
+ maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
883
+ maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
884
+ ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
885
+ });
886
+ }
887
+ // ========================================================================
888
+ // Layer-Aware Factory Methods
889
+ // ========================================================================
890
+ /**
891
+ * Create a publisher for a specific layer.
892
+ * The layer is preserved on the publisher name; otherwise identical to
893
+ * `createPublisherFromEnv()`.
894
+ */
895
+ static createPublisherForLayer(_layer, serviceName) {
896
+ return _EventQueueFactory.createPublisher({
897
+ type: QueueType.REDIS_STREAMS,
898
+ url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
899
+ password: process.env["REDIS_PASSWORD"],
900
+ name: serviceName,
901
+ enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
902
+ defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] ?? "normal"
903
+ });
904
+ }
905
+ /**
906
+ * Create a consumer for a specific layer.
907
+ */
908
+ static createConsumerForLayer(_layer, serviceName, stream, consumer) {
909
+ return _EventQueueFactory.createConsumer({
910
+ type: QueueType.REDIS_STREAMS,
911
+ url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
912
+ password: process.env["REDIS_PASSWORD"],
913
+ name: serviceName,
914
+ stream,
915
+ consumer,
916
+ maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
917
+ maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
918
+ ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
919
+ });
920
+ }
921
+ };
922
+
923
+ export { EventQueueFactory, QueueType, RedisStreamsEventConsumer, RedisStreamsEventPublisher, resolveQueueType };
924
+ //# sourceMappingURL=chunk-TM5RQ6P2.js.map
925
+ //# sourceMappingURL=chunk-TM5RQ6P2.js.map