@burdenoff/be-sdk 2026.512.103 → 2026.513.1
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/EventQueueFactory-JQHBTQG3.js +8 -0
- package/dist/{EventQueueFactory-FNE7H66N.js.map → EventQueueFactory-JQHBTQG3.js.map} +1 -1
- package/dist/{chunk-WPGSKLAG.js → chunk-4YCH5SUK.js} +23 -5
- package/dist/chunk-4YCH5SUK.js.map +1 -0
- package/dist/chunk-BQRXB4CW.js +2183 -0
- package/dist/chunk-BQRXB4CW.js.map +1 -0
- package/dist/{chunk-DR5AQJT6.js → chunk-EODPHHEH.js} +3 -3
- package/dist/{chunk-DR5AQJT6.js.map → chunk-EODPHHEH.js.map} +1 -1
- package/dist/{chunk-S52PJKSW.js → chunk-I2PO7JBF.js} +3 -3
- package/dist/{chunk-S52PJKSW.js.map → chunk-I2PO7JBF.js.map} +1 -1
- package/dist/{chunk-LRL6GKXI.js → chunk-LIOZLSID.js} +3 -3
- package/dist/{chunk-LRL6GKXI.js.map → chunk-LIOZLSID.js.map} +1 -1
- package/dist/{chunk-SXIL2AJF.js → chunk-ORMCFQNW.js} +5 -5
- package/dist/{chunk-SXIL2AJF.js.map → chunk-ORMCFQNW.js.map} +1 -1
- package/dist/{chunk-VOFKF27Q.js → chunk-UVYDP6P6.js} +3 -2
- package/dist/chunk-UVYDP6P6.js.map +1 -0
- package/dist/config-QVYSAUMT.js +4 -0
- package/dist/{config-7U7J3QXQ.js.map → config-QVYSAUMT.js.map} +1 -1
- package/dist/esm-M2M7HGV4.js +1356 -0
- package/dist/esm-M2M7HGV4.js.map +1 -0
- package/dist/event-queue/index.d.ts +1 -1
- package/dist/event-queue/index.js +4 -4
- package/dist/graph/index.js +5 -5
- package/dist/{index-BF2uh_h9.d.ts → index-eUvoVdr7.d.ts} +158 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -9
- package/dist/init/index.js +3 -3
- package/dist/prisma/index.js +4 -4
- package/package.json +9 -1
- package/dist/EventQueueFactory-FNE7H66N.js +0 -8
- package/dist/chunk-KSO6QQVD.js +0 -1094
- package/dist/chunk-KSO6QQVD.js.map +0 -1
- package/dist/chunk-VOFKF27Q.js.map +0 -1
- package/dist/chunk-WPGSKLAG.js.map +0 -1
- package/dist/config-7U7J3QXQ.js +0 -4
- package/dist/esm-K6H7EIWK.js +0 -1450
- package/dist/esm-K6H7EIWK.js.map +0 -1
|
@@ -0,0 +1,2183 @@
|
|
|
1
|
+
import { QueueType } from './chunk-UVYDP6P6.js';
|
|
2
|
+
import { NatsConnectionManager } from './chunk-COSUYUCB.js';
|
|
3
|
+
import { DEFAULT_LAYER_RESOURCES } from './chunk-NGMEI42S.js';
|
|
4
|
+
import { logger } from './chunk-CBZNP6RF.js';
|
|
5
|
+
import { __name } from './chunk-XE4OXN2W.js';
|
|
6
|
+
import { RetentionPolicy, StorageType, AckPolicy, DeliverPolicy, headers } from 'nats';
|
|
7
|
+
import { randomUUID } from 'crypto';
|
|
8
|
+
import { hostname } from 'os';
|
|
9
|
+
|
|
10
|
+
// src/event-queue/implementations/nats/trace-context.ts
|
|
11
|
+
var otelApiPromise = null;
|
|
12
|
+
async function loadOtelApi() {
|
|
13
|
+
if (otelApiPromise) {
|
|
14
|
+
return otelApiPromise;
|
|
15
|
+
}
|
|
16
|
+
otelApiPromise = (async () => {
|
|
17
|
+
try {
|
|
18
|
+
const mod = await import('./esm-M2M7HGV4.js');
|
|
19
|
+
return mod;
|
|
20
|
+
} catch {
|
|
21
|
+
logger.debug("[trace-context] @opentelemetry/api not available; NATS traces disabled");
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
})();
|
|
25
|
+
return otelApiPromise;
|
|
26
|
+
}
|
|
27
|
+
__name(loadOtelApi, "loadOtelApi");
|
|
28
|
+
var setter = {
|
|
29
|
+
set(carrier, key, value) {
|
|
30
|
+
carrier[key] = value;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var getter = {
|
|
34
|
+
get(carrier, key) {
|
|
35
|
+
return carrier[key];
|
|
36
|
+
},
|
|
37
|
+
keys(carrier) {
|
|
38
|
+
return Object.keys(carrier);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
async function injectW3CTraceContext(hdrs) {
|
|
42
|
+
const api = await loadOtelApi();
|
|
43
|
+
if (!api) return;
|
|
44
|
+
try {
|
|
45
|
+
const carrier = {};
|
|
46
|
+
api.propagation.inject(api.context.active(), carrier, setter);
|
|
47
|
+
for (const [key, value] of Object.entries(carrier)) {
|
|
48
|
+
if (value) hdrs.set(key, value);
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.debug("[trace-context] injection failed", {
|
|
52
|
+
error: err instanceof Error ? err.message : String(err)
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
__name(injectW3CTraceContext, "injectW3CTraceContext");
|
|
57
|
+
async function extractW3CTraceContext(hdrs) {
|
|
58
|
+
const api = await loadOtelApi();
|
|
59
|
+
if (!api || !hdrs) return void 0;
|
|
60
|
+
try {
|
|
61
|
+
const carrier = {};
|
|
62
|
+
for (const key of [
|
|
63
|
+
"traceparent",
|
|
64
|
+
"tracestate",
|
|
65
|
+
"baggage"
|
|
66
|
+
]) {
|
|
67
|
+
const value = hdrs.get(key);
|
|
68
|
+
if (value) carrier[key] = value;
|
|
69
|
+
}
|
|
70
|
+
return api.propagation.extract(api.context.active(), carrier, getter);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
logger.debug("[trace-context] extraction failed", {
|
|
73
|
+
error: err instanceof Error ? err.message : String(err)
|
|
74
|
+
});
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
__name(extractW3CTraceContext, "extractW3CTraceContext");
|
|
79
|
+
async function runWithConsumerSpan(subject, hdrs, handler) {
|
|
80
|
+
const api = await loadOtelApi();
|
|
81
|
+
if (!api) return handler();
|
|
82
|
+
try {
|
|
83
|
+
const parentCtx = await extractW3CTraceContext(hdrs);
|
|
84
|
+
const tracer = api.trace.getTracer("@burdenoff/be-sdk/nats");
|
|
85
|
+
return await new Promise((resolve, reject) => {
|
|
86
|
+
tracer.startActiveSpan(`nats.consume ${subject}`, {
|
|
87
|
+
kind: 5,
|
|
88
|
+
attributes: {
|
|
89
|
+
"messaging.system": "nats",
|
|
90
|
+
"messaging.destination.name": subject
|
|
91
|
+
}
|
|
92
|
+
}, parentCtx, async (span) => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await handler();
|
|
95
|
+
span.setStatus({
|
|
96
|
+
code: 1
|
|
97
|
+
});
|
|
98
|
+
span.end();
|
|
99
|
+
resolve(result);
|
|
100
|
+
} catch (err) {
|
|
101
|
+
span.recordException(err);
|
|
102
|
+
span.setStatus({
|
|
103
|
+
code: 2,
|
|
104
|
+
message: err instanceof Error ? err.message : String(err)
|
|
105
|
+
});
|
|
106
|
+
span.end();
|
|
107
|
+
reject(err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
} catch (err) {
|
|
112
|
+
logger.debug("[trace-context] consumer span failed to start; running without span", {
|
|
113
|
+
error: err instanceof Error ? err.message : String(err)
|
|
114
|
+
});
|
|
115
|
+
return handler();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
__name(runWithConsumerSpan, "runWithConsumerSpan");
|
|
119
|
+
var DEFAULT_STREAM_MAX_BYTES = 10 * 1024 * 1024;
|
|
120
|
+
var NatsSubscription = class NatsSubscription2 {
|
|
121
|
+
static {
|
|
122
|
+
__name(this, "NatsSubscription");
|
|
123
|
+
}
|
|
124
|
+
id;
|
|
125
|
+
subject;
|
|
126
|
+
stopFn;
|
|
127
|
+
active = true;
|
|
128
|
+
stats = {
|
|
129
|
+
messagesReceived: 0,
|
|
130
|
+
messagesProcessed: 0,
|
|
131
|
+
messagesFailed: 0,
|
|
132
|
+
lastActivity: (/* @__PURE__ */ new Date()).toISOString()
|
|
133
|
+
};
|
|
134
|
+
constructor(id, subject, stopFn) {
|
|
135
|
+
this.id = id;
|
|
136
|
+
this.subject = subject;
|
|
137
|
+
this.stopFn = stopFn;
|
|
138
|
+
}
|
|
139
|
+
async unsubscribe() {
|
|
140
|
+
if (!this.active) {
|
|
141
|
+
return Promise.resolve();
|
|
142
|
+
}
|
|
143
|
+
this.active = false;
|
|
144
|
+
this.stopFn();
|
|
145
|
+
return Promise.resolve();
|
|
146
|
+
}
|
|
147
|
+
isActive() {
|
|
148
|
+
return this.active;
|
|
149
|
+
}
|
|
150
|
+
getStats() {
|
|
151
|
+
return {
|
|
152
|
+
messagesReceived: this.stats.messagesReceived,
|
|
153
|
+
messagesProcessed: this.stats.messagesProcessed,
|
|
154
|
+
messagesFailed: this.stats.messagesFailed,
|
|
155
|
+
lastActivity: this.stats.lastActivity
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
incrementReceived() {
|
|
159
|
+
this.stats.messagesReceived++;
|
|
160
|
+
this.stats.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
161
|
+
}
|
|
162
|
+
incrementProcessed() {
|
|
163
|
+
this.stats.messagesProcessed++;
|
|
164
|
+
this.stats.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
165
|
+
}
|
|
166
|
+
incrementFailed() {
|
|
167
|
+
this.stats.messagesFailed++;
|
|
168
|
+
this.stats.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
var NatsEventConsumer = class {
|
|
172
|
+
static {
|
|
173
|
+
__name(this, "NatsEventConsumer");
|
|
174
|
+
}
|
|
175
|
+
config;
|
|
176
|
+
connectionManager;
|
|
177
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
178
|
+
constructor(config) {
|
|
179
|
+
this.config = config;
|
|
180
|
+
const baseConfig = {
|
|
181
|
+
type: config.type || "nats",
|
|
182
|
+
url: config.url || "nats://localhost:4222",
|
|
183
|
+
user: config.user,
|
|
184
|
+
password: config.password,
|
|
185
|
+
token: config.token,
|
|
186
|
+
creds: config.creds,
|
|
187
|
+
credsFile: config.credsFile,
|
|
188
|
+
maxReconnectAttempts: config.maxReconnectAttempts,
|
|
189
|
+
reconnectTimeWait: config.reconnectTimeWait,
|
|
190
|
+
connectionTimeout: config.connectionTimeout,
|
|
191
|
+
name: config.name
|
|
192
|
+
};
|
|
193
|
+
this.connectionManager = new NatsConnectionManager(baseConfig);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Connect to NATS server
|
|
197
|
+
*/
|
|
198
|
+
async connect() {
|
|
199
|
+
await this.connectionManager.connect();
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Disconnect from NATS server
|
|
203
|
+
*/
|
|
204
|
+
async disconnect() {
|
|
205
|
+
await this.unsubscribeAll();
|
|
206
|
+
await this.connectionManager.disconnect();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Check if connected
|
|
210
|
+
*/
|
|
211
|
+
isConnected() {
|
|
212
|
+
return this.connectionManager.isConnected();
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Health check
|
|
216
|
+
*/
|
|
217
|
+
async healthCheck() {
|
|
218
|
+
await Promise.resolve();
|
|
219
|
+
const isConnected = this.isConnected();
|
|
220
|
+
return {
|
|
221
|
+
status: isConnected ? "healthy" : "unhealthy",
|
|
222
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
223
|
+
connected: isConnected,
|
|
224
|
+
details: {
|
|
225
|
+
type: "nats",
|
|
226
|
+
url: this.config.url,
|
|
227
|
+
activeSubscriptions: this.subscriptions.size
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Get queue statistics
|
|
233
|
+
*/
|
|
234
|
+
async getStatistics() {
|
|
235
|
+
await Promise.resolve();
|
|
236
|
+
let totalReceived = 0;
|
|
237
|
+
let totalProcessed = 0;
|
|
238
|
+
let totalFailed = 0;
|
|
239
|
+
let lastActivity = null;
|
|
240
|
+
for (const subscription of this.subscriptions.values()) {
|
|
241
|
+
const stats = subscription.getStats();
|
|
242
|
+
totalReceived += stats.messagesReceived;
|
|
243
|
+
totalProcessed += stats.messagesProcessed;
|
|
244
|
+
totalFailed += stats.messagesFailed;
|
|
245
|
+
if (stats.lastActivity && (!lastActivity || stats.lastActivity > lastActivity)) {
|
|
246
|
+
lastActivity = stats.lastActivity;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
messagesPublished: 0,
|
|
251
|
+
messagesConsumed: totalProcessed,
|
|
252
|
+
messagesFailed: totalFailed,
|
|
253
|
+
activeConsumers: this.subscriptions.size,
|
|
254
|
+
activePublishers: 0,
|
|
255
|
+
queueSize: 0,
|
|
256
|
+
lastActivity,
|
|
257
|
+
details: {
|
|
258
|
+
totalReceived,
|
|
259
|
+
type: "nats"
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Ensure a stream exists
|
|
265
|
+
*/
|
|
266
|
+
async ensureStream(config) {
|
|
267
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
268
|
+
try {
|
|
269
|
+
try {
|
|
270
|
+
await jsm.streams.info(config.name);
|
|
271
|
+
logger.debug("[NatsEventConsumer] Stream already exists", {
|
|
272
|
+
stream: config.name
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
const streamConfig = {
|
|
278
|
+
name: config.name,
|
|
279
|
+
subjects: config.subjects,
|
|
280
|
+
retention: this.mapRetentionPolicy(config.retention),
|
|
281
|
+
storage: this.mapStorageType(config.storage),
|
|
282
|
+
max_consumers: config.maxConsumers,
|
|
283
|
+
max_msgs: config.maxMsgs,
|
|
284
|
+
max_bytes: config.maxBytes ?? DEFAULT_STREAM_MAX_BYTES,
|
|
285
|
+
max_age: config.maxAge,
|
|
286
|
+
duplicate_window: config.duplicateWindow
|
|
287
|
+
};
|
|
288
|
+
await jsm.streams.add(streamConfig);
|
|
289
|
+
logger.info("[NatsEventConsumer] Stream created successfully", {
|
|
290
|
+
stream: config.name,
|
|
291
|
+
subjects: config.subjects
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
logger.error("[NatsEventConsumer] Failed to ensure stream", {
|
|
295
|
+
stream: config.name,
|
|
296
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
297
|
+
});
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Ensure a consumer exists
|
|
303
|
+
*/
|
|
304
|
+
async ensureConsumer(config) {
|
|
305
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
306
|
+
try {
|
|
307
|
+
await jsm.consumers.add(config.stream, {
|
|
308
|
+
name: config.consumer,
|
|
309
|
+
durable_name: config.consumer,
|
|
310
|
+
filter_subject: config.filterSubject,
|
|
311
|
+
ack_policy: this.mapAckPolicy(config.ackPolicy),
|
|
312
|
+
deliver_policy: this.mapDeliverPolicy(config.deliverPolicy),
|
|
313
|
+
max_deliver: config.maxDeliver || 3,
|
|
314
|
+
ack_wait: config.ackWait || 3e10,
|
|
315
|
+
max_ack_pending: config.maxAckPending || 100
|
|
316
|
+
});
|
|
317
|
+
logger.info("[NatsEventConsumer] Consumer created successfully", {
|
|
318
|
+
stream: config.stream,
|
|
319
|
+
consumer: config.consumer
|
|
320
|
+
});
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof Error && error.message?.includes("already exists")) {
|
|
323
|
+
logger.debug("[NatsEventConsumer] Consumer already exists", {
|
|
324
|
+
stream: config.stream,
|
|
325
|
+
consumer: config.consumer
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
logger.error("[NatsEventConsumer] Failed to ensure consumer", {
|
|
329
|
+
stream: config.stream,
|
|
330
|
+
consumer: config.consumer,
|
|
331
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
332
|
+
});
|
|
333
|
+
throw error;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Derive stream subject pattern from subscription subject
|
|
339
|
+
* Examples:
|
|
340
|
+
* - "channel.conversation.message" → ["channel.>"]
|
|
341
|
+
* - "wspace.tags.created" → ["wspace.>"]
|
|
342
|
+
* - "global.auth.login" → ["global.>"]
|
|
343
|
+
*/
|
|
344
|
+
deriveStreamSubjects(subject) {
|
|
345
|
+
const firstSegment = subject.split(".")[0];
|
|
346
|
+
if (firstSegment) {
|
|
347
|
+
return [
|
|
348
|
+
`${firstSegment}.>`
|
|
349
|
+
];
|
|
350
|
+
}
|
|
351
|
+
return [
|
|
352
|
+
`${subject.replace(/\.[^.]+$/, ".>")}`
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Subscribe to a subject
|
|
357
|
+
*/
|
|
358
|
+
async subscribe(options) {
|
|
359
|
+
const js = this.connectionManager.getJetStream();
|
|
360
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
361
|
+
try {
|
|
362
|
+
let streamName = options.options?.stream || this.config.stream;
|
|
363
|
+
const consumerName = options.options?.consumer || `${this.config.consumer}-${options.subject.replace(/\./g, "-")}`;
|
|
364
|
+
const streamSubjects = options.options?.streamSubjects || this.deriveStreamSubjects(options.subject);
|
|
365
|
+
try {
|
|
366
|
+
await this.ensureStream({
|
|
367
|
+
name: streamName,
|
|
368
|
+
subjects: streamSubjects,
|
|
369
|
+
retention: options.options?.retention || "limits",
|
|
370
|
+
storage: options.options?.storage || "file"
|
|
371
|
+
});
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
374
|
+
if (!errorMessage.includes("subjects overlap")) {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
streamName = await jsm.streams.find(options.subject);
|
|
378
|
+
logger.info("[NatsEventConsumer] Using existing stream for overlapping subject", {
|
|
379
|
+
requestedStream: options.options?.stream || this.config.stream,
|
|
380
|
+
stream: streamName,
|
|
381
|
+
subject: options.subject
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
await this.ensureConsumer({
|
|
385
|
+
type: this.config.type,
|
|
386
|
+
url: this.config.url,
|
|
387
|
+
consumer: consumerName,
|
|
388
|
+
stream: streamName,
|
|
389
|
+
filterSubject: options.subject,
|
|
390
|
+
ackPolicy: options.options?.ackPolicy,
|
|
391
|
+
deliverPolicy: options.options?.deliverPolicy,
|
|
392
|
+
maxDeliver: options.options?.maxDeliver || this.config.maxDeliver,
|
|
393
|
+
ackWait: options.options?.ackWait || this.config.ackWait,
|
|
394
|
+
maxAckPending: options.options?.maxAckPending
|
|
395
|
+
});
|
|
396
|
+
const consumer = await js.consumers.get(streamName, consumerName);
|
|
397
|
+
const subscriptionId = `sub_${Date.now()}_${crypto.randomUUID().substring(0, 7)}`;
|
|
398
|
+
let stopRequested = false;
|
|
399
|
+
const stopFn = /* @__PURE__ */ __name(() => {
|
|
400
|
+
stopRequested = true;
|
|
401
|
+
}, "stopFn");
|
|
402
|
+
const subscription = new NatsSubscription(subscriptionId, options.subject, stopFn);
|
|
403
|
+
this.subscriptions.set(subscriptionId, subscription);
|
|
404
|
+
this.consumeMessages(consumer, subscription, options.handler, options.options?.maxConcurrent || this.config.maxConcurrent || 1, () => stopRequested).catch((error) => {
|
|
405
|
+
logger.error("[NatsEventConsumer] Error in message consumption loop", {
|
|
406
|
+
subject: options.subject,
|
|
407
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
logger.info("[NatsEventConsumer] Subscription created", {
|
|
411
|
+
subscriptionId,
|
|
412
|
+
subject: options.subject
|
|
413
|
+
});
|
|
414
|
+
return subscription;
|
|
415
|
+
} catch (error) {
|
|
416
|
+
logger.error("[NatsEventConsumer] Failed to subscribe", {
|
|
417
|
+
subject: options.subject,
|
|
418
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
419
|
+
});
|
|
420
|
+
throw error;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Subscribe to multiple subjects
|
|
425
|
+
*/
|
|
426
|
+
async subscribeMultiple(subjects, handler, options) {
|
|
427
|
+
const subscriptions = [];
|
|
428
|
+
for (const subject of subjects) {
|
|
429
|
+
const subscription = await this.subscribe({
|
|
430
|
+
subject,
|
|
431
|
+
handler,
|
|
432
|
+
options
|
|
433
|
+
});
|
|
434
|
+
subscriptions.push(subscription);
|
|
435
|
+
}
|
|
436
|
+
return subscriptions;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Pull messages manually
|
|
440
|
+
*/
|
|
441
|
+
async pull(options) {
|
|
442
|
+
const js = this.connectionManager.getJetStream();
|
|
443
|
+
try {
|
|
444
|
+
const consumer = await js.consumers.get(this.config.stream, this.config.consumer);
|
|
445
|
+
const messages = await consumer.fetch({
|
|
446
|
+
max_messages: options.batch || 10,
|
|
447
|
+
expires: options.timeout || 3e4
|
|
448
|
+
});
|
|
449
|
+
const events = [];
|
|
450
|
+
for await (const msg of messages) {
|
|
451
|
+
const event = this.wrapMessage(msg);
|
|
452
|
+
events.push(event);
|
|
453
|
+
}
|
|
454
|
+
return events;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
logger.error("[NatsEventConsumer] Failed to pull messages", {
|
|
457
|
+
subject: options.subject,
|
|
458
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
459
|
+
});
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Unsubscribe from all active subscriptions
|
|
465
|
+
*/
|
|
466
|
+
async unsubscribeAll() {
|
|
467
|
+
const unsubscribePromises = Array.from(this.subscriptions.values()).map(async (subscription) => subscription.unsubscribe());
|
|
468
|
+
await Promise.all(unsubscribePromises);
|
|
469
|
+
this.subscriptions.clear();
|
|
470
|
+
logger.info("[NatsEventConsumer] All subscriptions unsubscribed");
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Get all active subscriptions
|
|
474
|
+
*/
|
|
475
|
+
getSubscriptions() {
|
|
476
|
+
return Array.from(this.subscriptions.values());
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Process pending messages in dead letter queue
|
|
480
|
+
*/
|
|
481
|
+
async processDLQ(_handler) {
|
|
482
|
+
await Promise.resolve();
|
|
483
|
+
logger.warn("[NatsEventConsumer] DLQ processing not yet implemented");
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* List subjects for the configured stream
|
|
487
|
+
*/
|
|
488
|
+
async listSubjects(streamName) {
|
|
489
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
490
|
+
const info = await jsm.streams.info(streamName || this.config.stream);
|
|
491
|
+
return info.config.subjects || [];
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Consume messages from consumer
|
|
495
|
+
*/
|
|
496
|
+
async consumeMessages(consumer, subscription, handler, maxConcurrent, stopRequested) {
|
|
497
|
+
try {
|
|
498
|
+
const messages = await consumer.consume({
|
|
499
|
+
max_messages: maxConcurrent,
|
|
500
|
+
expires: 3e4
|
|
501
|
+
});
|
|
502
|
+
for await (const msg of messages) {
|
|
503
|
+
if (stopRequested()) {
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
subscription.incrementReceived();
|
|
507
|
+
try {
|
|
508
|
+
const event = this.wrapMessage(msg);
|
|
509
|
+
await runWithConsumerSpan(msg.subject, msg.headers, async () => handler(event));
|
|
510
|
+
subscription.incrementProcessed();
|
|
511
|
+
} catch (error) {
|
|
512
|
+
subscription.incrementFailed();
|
|
513
|
+
logger.error("[NatsEventConsumer] Error processing message", {
|
|
514
|
+
subject: msg.subject,
|
|
515
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
516
|
+
});
|
|
517
|
+
msg.nak();
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} catch (error) {
|
|
521
|
+
logger.error("[NatsEventConsumer] Error consuming messages", {
|
|
522
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* Wrap NATS message in AcknowledgeableEvent
|
|
528
|
+
*/
|
|
529
|
+
wrapMessage(msg) {
|
|
530
|
+
const eventMessage = JSON.parse(msg.data.toString());
|
|
531
|
+
return {
|
|
532
|
+
...eventMessage,
|
|
533
|
+
sequence: msg.seq,
|
|
534
|
+
ack: /* @__PURE__ */ __name(async () => {
|
|
535
|
+
await Promise.resolve();
|
|
536
|
+
msg.ack();
|
|
537
|
+
}, "ack"),
|
|
538
|
+
nak: /* @__PURE__ */ __name(async (delay) => {
|
|
539
|
+
await Promise.resolve();
|
|
540
|
+
if (delay) {
|
|
541
|
+
msg.nak(delay);
|
|
542
|
+
} else {
|
|
543
|
+
msg.nak();
|
|
544
|
+
}
|
|
545
|
+
}, "nak"),
|
|
546
|
+
term: /* @__PURE__ */ __name(async () => {
|
|
547
|
+
await Promise.resolve();
|
|
548
|
+
msg.term();
|
|
549
|
+
}, "term"),
|
|
550
|
+
isRedelivered: /* @__PURE__ */ __name(() => {
|
|
551
|
+
return msg.redelivered;
|
|
552
|
+
}, "isRedelivered"),
|
|
553
|
+
getDeliveryAttempt: /* @__PURE__ */ __name(() => {
|
|
554
|
+
return msg.info.deliveryCount;
|
|
555
|
+
}, "getDeliveryAttempt")
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
/**
|
|
559
|
+
* Map retention policy string to NATS enum
|
|
560
|
+
*/
|
|
561
|
+
mapRetentionPolicy(policy) {
|
|
562
|
+
switch (policy?.toLowerCase()) {
|
|
563
|
+
case "limits":
|
|
564
|
+
return RetentionPolicy.Limits;
|
|
565
|
+
case "interest":
|
|
566
|
+
return RetentionPolicy.Interest;
|
|
567
|
+
case "workqueue":
|
|
568
|
+
default:
|
|
569
|
+
return RetentionPolicy.Workqueue;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Map storage type string to NATS enum
|
|
574
|
+
*/
|
|
575
|
+
mapStorageType(storage) {
|
|
576
|
+
switch (storage?.toLowerCase()) {
|
|
577
|
+
case "memory":
|
|
578
|
+
return StorageType.Memory;
|
|
579
|
+
case "file":
|
|
580
|
+
default:
|
|
581
|
+
return StorageType.File;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Map ack policy string to NATS enum
|
|
586
|
+
*/
|
|
587
|
+
mapAckPolicy(policy) {
|
|
588
|
+
switch (policy?.toLowerCase()) {
|
|
589
|
+
case "none":
|
|
590
|
+
return AckPolicy.None;
|
|
591
|
+
case "all":
|
|
592
|
+
return AckPolicy.All;
|
|
593
|
+
case "explicit":
|
|
594
|
+
default:
|
|
595
|
+
return AckPolicy.Explicit;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Map deliver policy string to NATS enum
|
|
600
|
+
*/
|
|
601
|
+
mapDeliverPolicy(policy) {
|
|
602
|
+
switch (policy?.toLowerCase()) {
|
|
603
|
+
case "new":
|
|
604
|
+
return DeliverPolicy.New;
|
|
605
|
+
case "last":
|
|
606
|
+
return DeliverPolicy.Last;
|
|
607
|
+
case "lastpersubject":
|
|
608
|
+
return DeliverPolicy.LastPerSubject;
|
|
609
|
+
case "all":
|
|
610
|
+
default:
|
|
611
|
+
return DeliverPolicy.All;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
var DEFAULT_STREAM_MAX_BYTES2 = 10 * 1024 * 1024;
|
|
616
|
+
var NatsEventPublisher = class {
|
|
617
|
+
static {
|
|
618
|
+
__name(this, "NatsEventPublisher");
|
|
619
|
+
}
|
|
620
|
+
config;
|
|
621
|
+
connectionManager;
|
|
622
|
+
publishCount = 0;
|
|
623
|
+
lastPublishTime = null;
|
|
624
|
+
constructor(config) {
|
|
625
|
+
this.config = config;
|
|
626
|
+
this.connectionManager = new NatsConnectionManager(config);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Connect to NATS server
|
|
630
|
+
*/
|
|
631
|
+
async connect() {
|
|
632
|
+
await this.connectionManager.connect();
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* Disconnect from NATS server
|
|
636
|
+
*/
|
|
637
|
+
async disconnect() {
|
|
638
|
+
await this.connectionManager.disconnect();
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Check if connected
|
|
642
|
+
*/
|
|
643
|
+
isConnected() {
|
|
644
|
+
return this.connectionManager.isConnected();
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Health check
|
|
648
|
+
*/
|
|
649
|
+
async healthCheck() {
|
|
650
|
+
await Promise.resolve();
|
|
651
|
+
const isConnected = this.isConnected();
|
|
652
|
+
return {
|
|
653
|
+
status: isConnected ? "healthy" : "unhealthy",
|
|
654
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
655
|
+
details: {
|
|
656
|
+
connected: isConnected,
|
|
657
|
+
type: "nats",
|
|
658
|
+
url: this.config.url
|
|
659
|
+
}
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Get queue statistics
|
|
664
|
+
*/
|
|
665
|
+
async getStatistics() {
|
|
666
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
667
|
+
try {
|
|
668
|
+
const streams = await jsm.streams.list().next();
|
|
669
|
+
const streamCount = streams.length;
|
|
670
|
+
return {
|
|
671
|
+
messagesPublished: this.publishCount,
|
|
672
|
+
messagesConsumed: 0,
|
|
673
|
+
messagesFailed: 0,
|
|
674
|
+
activeConsumers: 0,
|
|
675
|
+
activePublishers: 1,
|
|
676
|
+
queueSize: 0,
|
|
677
|
+
lastActivity: this.lastPublishTime?.toISOString() || null,
|
|
678
|
+
details: {
|
|
679
|
+
streamCount,
|
|
680
|
+
type: "nats"
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.error("[NatsEventPublisher] Failed to get statistics", {
|
|
685
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
686
|
+
});
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Ensure a stream exists
|
|
692
|
+
*/
|
|
693
|
+
async ensureStream(config) {
|
|
694
|
+
const jsm = this.connectionManager.getJetStreamManager();
|
|
695
|
+
try {
|
|
696
|
+
try {
|
|
697
|
+
await jsm.streams.info(config.name);
|
|
698
|
+
logger.debug("[NatsEventPublisher] Stream already exists", {
|
|
699
|
+
stream: config.name
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
const streamConfig = {
|
|
705
|
+
name: config.name,
|
|
706
|
+
subjects: config.subjects,
|
|
707
|
+
retention: this.mapRetentionPolicy(config.retention),
|
|
708
|
+
storage: this.mapStorageType(config.storage),
|
|
709
|
+
max_consumers: config.maxConsumers,
|
|
710
|
+
max_msgs: config.maxMsgs,
|
|
711
|
+
max_bytes: config.maxBytes ?? DEFAULT_STREAM_MAX_BYTES2,
|
|
712
|
+
max_age: config.maxAge,
|
|
713
|
+
duplicate_window: config.duplicateWindow
|
|
714
|
+
};
|
|
715
|
+
await jsm.streams.add(streamConfig);
|
|
716
|
+
logger.info("[NatsEventPublisher] Stream created successfully", {
|
|
717
|
+
stream: config.name,
|
|
718
|
+
subjects: config.subjects
|
|
719
|
+
});
|
|
720
|
+
} catch (error) {
|
|
721
|
+
logger.error("[NatsEventPublisher] Failed to ensure stream", {
|
|
722
|
+
stream: config.name,
|
|
723
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
724
|
+
});
|
|
725
|
+
throw error;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Derive stream subject pattern from subscription subject
|
|
730
|
+
* Examples:
|
|
731
|
+
* - "channel.conversation.message" → ["channel.>"]
|
|
732
|
+
* - "wspace.tags.created" → ["wspace.>"]
|
|
733
|
+
* - "global.auth.login" → ["global.>"]
|
|
734
|
+
*/
|
|
735
|
+
deriveStreamSubjects(subject) {
|
|
736
|
+
const firstSegment = subject.split(".")[0];
|
|
737
|
+
if (firstSegment) {
|
|
738
|
+
return [
|
|
739
|
+
`${firstSegment}.>`
|
|
740
|
+
];
|
|
741
|
+
}
|
|
742
|
+
return [
|
|
743
|
+
`${subject.replace(/\.[^.]+$/, ".>")}`
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Derive stream name from subject
|
|
748
|
+
* Examples:
|
|
749
|
+
* - "channel.conversation.message" → "CHANNEL_EVENTS"
|
|
750
|
+
* - "wspace.tags.created" → "WSPACE_EVENTS"
|
|
751
|
+
* - "global.auth.login" → "GLOBAL_EVENTS"
|
|
752
|
+
*/
|
|
753
|
+
deriveStreamName(subject) {
|
|
754
|
+
const firstSegment = subject.split(".")[0];
|
|
755
|
+
if (firstSegment) {
|
|
756
|
+
return `${firstSegment.toUpperCase()}_EVENTS`;
|
|
757
|
+
}
|
|
758
|
+
return "EVENTS";
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Publish an event to a subject
|
|
762
|
+
*/
|
|
763
|
+
async publish(options) {
|
|
764
|
+
const js = this.connectionManager.getJetStream();
|
|
765
|
+
try {
|
|
766
|
+
const streamName = options.options?.stream || this.deriveStreamName(options.subject);
|
|
767
|
+
const streamSubjects = options.options?.streamSubjects || this.deriveStreamSubjects(options.subject);
|
|
768
|
+
await this.ensureStream({
|
|
769
|
+
name: streamName,
|
|
770
|
+
subjects: streamSubjects,
|
|
771
|
+
retention: options.options?.retention || "limits",
|
|
772
|
+
storage: options.options?.storage || "file"
|
|
773
|
+
});
|
|
774
|
+
const message = {
|
|
775
|
+
id: options.options?.messageId || this.generateMessageId(),
|
|
776
|
+
subject: options.subject,
|
|
777
|
+
data: options.data,
|
|
778
|
+
metadata: {
|
|
779
|
+
source: options.metadata?.source || "workspaces-backend",
|
|
780
|
+
correlationId: options.metadata?.correlationId || this.generateCorrelationId(),
|
|
781
|
+
userId: options.metadata?.userId,
|
|
782
|
+
workspaceId: options.metadata?.workspaceId,
|
|
783
|
+
priority: options.options?.priority || this.config.defaultPriority || "normal",
|
|
784
|
+
...options.metadata
|
|
785
|
+
},
|
|
786
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
787
|
+
};
|
|
788
|
+
const hdrs = headers();
|
|
789
|
+
hdrs.set("message-id", message.id);
|
|
790
|
+
hdrs.set("correlation-id", message.metadata.correlationId);
|
|
791
|
+
if (message.metadata.source) {
|
|
792
|
+
hdrs.set("source", message.metadata.source);
|
|
793
|
+
}
|
|
794
|
+
if (message.metadata["priority"] && typeof message.metadata["priority"] === "string") {
|
|
795
|
+
hdrs.set("priority", message.metadata["priority"]);
|
|
796
|
+
}
|
|
797
|
+
if (message.metadata.userId) {
|
|
798
|
+
hdrs.set("user-id", message.metadata.userId);
|
|
799
|
+
}
|
|
800
|
+
if (message.metadata.workspaceId) {
|
|
801
|
+
hdrs.set("workspace-id", message.metadata.workspaceId);
|
|
802
|
+
}
|
|
803
|
+
await injectW3CTraceContext(hdrs);
|
|
804
|
+
const publishOpts = {
|
|
805
|
+
msgID: this.config.enableDeduplication ? message.id : void 0,
|
|
806
|
+
headers: hdrs
|
|
807
|
+
};
|
|
808
|
+
if (options.options?.ttl) {
|
|
809
|
+
logger.warn("[NatsEventPublisher] Per-message TTL not supported in NATS", {
|
|
810
|
+
subject: options.subject,
|
|
811
|
+
ttl: options.options.ttl
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
if (options.options?.delay) {
|
|
815
|
+
logger.warn("[NatsEventPublisher] Message delay not supported in NATS", {
|
|
816
|
+
subject: options.subject,
|
|
817
|
+
delay: options.options.delay
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
const ack = await js.publish(options.subject, JSON.stringify(message), publishOpts);
|
|
821
|
+
this.publishCount++;
|
|
822
|
+
this.lastPublishTime = /* @__PURE__ */ new Date();
|
|
823
|
+
logger.debug("[NatsEventPublisher] Event published", {
|
|
824
|
+
subject: options.subject,
|
|
825
|
+
messageId: message.id,
|
|
826
|
+
sequence: ack.seq
|
|
827
|
+
});
|
|
828
|
+
return ack.seq;
|
|
829
|
+
} catch (error) {
|
|
830
|
+
logger.error("[NatsEventPublisher] Failed to publish event", {
|
|
831
|
+
subject: options.subject,
|
|
832
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
833
|
+
});
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/**
|
|
838
|
+
* Publish multiple events in a batch
|
|
839
|
+
*/
|
|
840
|
+
async publishBatch(events) {
|
|
841
|
+
const results = [];
|
|
842
|
+
for (const event of events) {
|
|
843
|
+
const seq = await this.publish(event);
|
|
844
|
+
results.push(seq);
|
|
845
|
+
}
|
|
846
|
+
return results;
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* Publish an event and wait for acknowledgment
|
|
850
|
+
* Note: This is not natively supported by NATS JetStream
|
|
851
|
+
* Would require implementing request-reply pattern
|
|
852
|
+
*/
|
|
853
|
+
async publishWithAck(options) {
|
|
854
|
+
const nc = this.connectionManager.getNatsConnection();
|
|
855
|
+
try {
|
|
856
|
+
const message = {
|
|
857
|
+
id: this.generateMessageId(),
|
|
858
|
+
subject: options.subject,
|
|
859
|
+
data: options.data,
|
|
860
|
+
metadata: {
|
|
861
|
+
source: options.metadata?.source || "workspaces-backend",
|
|
862
|
+
correlationId: options.metadata?.correlationId || this.generateCorrelationId(),
|
|
863
|
+
userId: options.metadata?.userId,
|
|
864
|
+
workspaceId: options.metadata?.workspaceId,
|
|
865
|
+
...options.metadata
|
|
866
|
+
},
|
|
867
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
868
|
+
};
|
|
869
|
+
const response = await nc.request(options.subject, JSON.stringify(message), {
|
|
870
|
+
timeout: options.timeout || 3e4
|
|
871
|
+
});
|
|
872
|
+
const result = JSON.parse(response.data.toString());
|
|
873
|
+
logger.debug("[NatsEventPublisher] Event published with ack", {
|
|
874
|
+
subject: options.subject,
|
|
875
|
+
messageId: message.id
|
|
876
|
+
});
|
|
877
|
+
return result;
|
|
878
|
+
} catch (error) {
|
|
879
|
+
logger.error("[NatsEventPublisher] Failed to publish event with ack", {
|
|
880
|
+
subject: options.subject,
|
|
881
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
882
|
+
});
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* Map retention policy string to NATS enum
|
|
888
|
+
*/
|
|
889
|
+
mapRetentionPolicy(policy) {
|
|
890
|
+
switch (policy?.toLowerCase()) {
|
|
891
|
+
case "limits":
|
|
892
|
+
return RetentionPolicy.Limits;
|
|
893
|
+
case "interest":
|
|
894
|
+
return RetentionPolicy.Interest;
|
|
895
|
+
case "workqueue":
|
|
896
|
+
default:
|
|
897
|
+
return RetentionPolicy.Workqueue;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Map storage type string to NATS enum
|
|
902
|
+
*/
|
|
903
|
+
mapStorageType(storage) {
|
|
904
|
+
switch (storage?.toLowerCase()) {
|
|
905
|
+
case "memory":
|
|
906
|
+
return StorageType.Memory;
|
|
907
|
+
case "file":
|
|
908
|
+
default:
|
|
909
|
+
return StorageType.File;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
/**
|
|
913
|
+
* Generate unique message ID
|
|
914
|
+
*/
|
|
915
|
+
generateMessageId() {
|
|
916
|
+
return `msg_${Date.now()}_${crypto.randomUUID().substring(0, 13)}`;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Generate correlation ID
|
|
920
|
+
*/
|
|
921
|
+
generateCorrelationId() {
|
|
922
|
+
return `corr_${Date.now()}_${crypto.randomUUID().substring(0, 13)}`;
|
|
923
|
+
}
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
// src/event-queue/implementations/redis-streams/RedisStreamsConnection.ts
|
|
927
|
+
var ioredisModulePromise = null;
|
|
928
|
+
async function loadIoRedis() {
|
|
929
|
+
if (!ioredisModulePromise) {
|
|
930
|
+
ioredisModulePromise = (async () => {
|
|
931
|
+
try {
|
|
932
|
+
const mod = await import('ioredis');
|
|
933
|
+
return mod.default ?? mod.Redis ?? null;
|
|
934
|
+
} catch (err) {
|
|
935
|
+
logger.error("[RedisStreamsConnection] ioredis not installed", {
|
|
936
|
+
error: err instanceof Error ? err.message : String(err)
|
|
937
|
+
});
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
})();
|
|
941
|
+
}
|
|
942
|
+
const ctor = await ioredisModulePromise;
|
|
943
|
+
if (!ctor) {
|
|
944
|
+
throw new Error("[RedisStreamsConnection] ioredis is required for the redis-streams event-queue backend. Install it with: bun add ioredis (or npm i ioredis)");
|
|
945
|
+
}
|
|
946
|
+
return ctor;
|
|
947
|
+
}
|
|
948
|
+
__name(loadIoRedis, "loadIoRedis");
|
|
949
|
+
function normaliseUrl(rawUrl) {
|
|
950
|
+
let url = rawUrl;
|
|
951
|
+
let tls = false;
|
|
952
|
+
if (/^valkeys:\/\//.test(url)) {
|
|
953
|
+
tls = true;
|
|
954
|
+
url = url.replace(/^valkeys:\/\//, "rediss://");
|
|
955
|
+
} else if (/^valkey:\/\//.test(url)) {
|
|
956
|
+
url = url.replace(/^valkey:\/\//, "redis://");
|
|
957
|
+
} else if (/^rediss:\/\//.test(url)) {
|
|
958
|
+
tls = true;
|
|
959
|
+
}
|
|
960
|
+
let host = "localhost";
|
|
961
|
+
try {
|
|
962
|
+
const parsed = new URL(url);
|
|
963
|
+
host = parsed.hostname || host;
|
|
964
|
+
} catch {
|
|
965
|
+
}
|
|
966
|
+
return {
|
|
967
|
+
url,
|
|
968
|
+
tls,
|
|
969
|
+
host
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
__name(normaliseUrl, "normaliseUrl");
|
|
973
|
+
var RedisStreamsConnection = class {
|
|
974
|
+
static {
|
|
975
|
+
__name(this, "RedisStreamsConnection");
|
|
976
|
+
}
|
|
977
|
+
config;
|
|
978
|
+
client = null;
|
|
979
|
+
connecting = null;
|
|
980
|
+
tls;
|
|
981
|
+
host;
|
|
982
|
+
normalisedUrl;
|
|
983
|
+
constructor(config) {
|
|
984
|
+
this.config = config;
|
|
985
|
+
const { url, tls, host } = normaliseUrl(config.url);
|
|
986
|
+
this.normalisedUrl = url;
|
|
987
|
+
this.tls = tls || process.env["REDIS_TLS"] === "true";
|
|
988
|
+
this.host = host;
|
|
989
|
+
}
|
|
990
|
+
/** True once `connect()` has resolved and ioredis is in `ready` state. */
|
|
991
|
+
isConnected() {
|
|
992
|
+
return this.client !== null && this.client.status === "ready";
|
|
993
|
+
}
|
|
994
|
+
/** Connect lazily; subsequent calls await the in-flight promise. */
|
|
995
|
+
async connect() {
|
|
996
|
+
if (this.isConnected()) return;
|
|
997
|
+
if (this.connecting) {
|
|
998
|
+
await this.connecting;
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
this.connecting = this.doConnect().finally(() => {
|
|
1002
|
+
this.connecting = null;
|
|
1003
|
+
});
|
|
1004
|
+
await this.connecting;
|
|
1005
|
+
}
|
|
1006
|
+
async doConnect() {
|
|
1007
|
+
const Redis = await loadIoRedis();
|
|
1008
|
+
const opts = {
|
|
1009
|
+
lazyConnect: true,
|
|
1010
|
+
maxRetriesPerRequest: null,
|
|
1011
|
+
enableReadyCheck: true,
|
|
1012
|
+
connectTimeout: this.config.connectionTimeout ?? 15e3,
|
|
1013
|
+
reconnectOnError: /* @__PURE__ */ __name(() => true, "reconnectOnError"),
|
|
1014
|
+
retryStrategy: /* @__PURE__ */ __name((times) => Math.min(times * 200, 2e3), "retryStrategy")
|
|
1015
|
+
};
|
|
1016
|
+
if (this.config.password) opts["password"] = this.config.password;
|
|
1017
|
+
if (this.config.user) opts["username"] = this.config.user;
|
|
1018
|
+
if (this.tls) {
|
|
1019
|
+
opts["tls"] = {
|
|
1020
|
+
servername: this.host
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
this.client = new Redis(this.normalisedUrl, opts);
|
|
1024
|
+
this.client.on("error", (...args) => {
|
|
1025
|
+
const err = args[0];
|
|
1026
|
+
logger.warn("[RedisStreamsConnection] redis error", {
|
|
1027
|
+
error: err?.message ?? "unknown"
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
1030
|
+
this.client.on("reconnecting", () => {
|
|
1031
|
+
logger.info("[RedisStreamsConnection] reconnecting");
|
|
1032
|
+
});
|
|
1033
|
+
await this.client.connect();
|
|
1034
|
+
logger.info("[RedisStreamsConnection] connected", {
|
|
1035
|
+
url: this.normalisedUrl.replace(/:[^:@/]*@/, ":***@"),
|
|
1036
|
+
tls: this.tls
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
async disconnect() {
|
|
1040
|
+
if (!this.client) return;
|
|
1041
|
+
try {
|
|
1042
|
+
await this.client.quit();
|
|
1043
|
+
} catch {
|
|
1044
|
+
this.client.disconnect();
|
|
1045
|
+
}
|
|
1046
|
+
this.client = null;
|
|
1047
|
+
}
|
|
1048
|
+
/** The shared command client. Throws if not connected. */
|
|
1049
|
+
getClient() {
|
|
1050
|
+
if (!this.client) {
|
|
1051
|
+
throw new Error("[RedisStreamsConnection] not connected \u2014 call connect() first");
|
|
1052
|
+
}
|
|
1053
|
+
return this.client;
|
|
1054
|
+
}
|
|
1055
|
+
/** Make a dedicated client (e.g. for a blocking XREADGROUP loop). */
|
|
1056
|
+
async makeBlockingClient() {
|
|
1057
|
+
await this.connect();
|
|
1058
|
+
const dup = this.getClient().duplicate();
|
|
1059
|
+
if ("connect" in dup && typeof dup.connect === "function") {
|
|
1060
|
+
try {
|
|
1061
|
+
await dup.connect();
|
|
1062
|
+
} catch {
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
return dup;
|
|
1066
|
+
}
|
|
1067
|
+
async ping() {
|
|
1068
|
+
await this.connect();
|
|
1069
|
+
return this.getClient().ping();
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
// src/event-queue/implementations/redis-streams/keyspace.ts
|
|
1074
|
+
var DEFAULT_PREFIX = "boff:events";
|
|
1075
|
+
var REGISTRY_PREFIX = "boff:registry:subjects";
|
|
1076
|
+
var DELAYQ_PREFIX = "boff:delayq";
|
|
1077
|
+
var DELAYQ_LOCK_PREFIX = "boff:lock:delayq";
|
|
1078
|
+
var DLQ_PREFIX = "boff:dlq";
|
|
1079
|
+
var DEDUP_PREFIX = "boff:dedup";
|
|
1080
|
+
var REPLY_PREFIX = "boff:replies";
|
|
1081
|
+
function subjectToKey(subject) {
|
|
1082
|
+
return subject.replace(/\./g, ":");
|
|
1083
|
+
}
|
|
1084
|
+
__name(subjectToKey, "subjectToKey");
|
|
1085
|
+
function streamKey(subject, opts = {}) {
|
|
1086
|
+
const prefix = opts.prefix ?? DEFAULT_PREFIX;
|
|
1087
|
+
return `${prefix}:${subjectToKey(subject)}`;
|
|
1088
|
+
}
|
|
1089
|
+
__name(streamKey, "streamKey");
|
|
1090
|
+
function registryKey(boundary) {
|
|
1091
|
+
return `${REGISTRY_PREFIX}:${boundary}`;
|
|
1092
|
+
}
|
|
1093
|
+
__name(registryKey, "registryKey");
|
|
1094
|
+
function boundaryOf(subject) {
|
|
1095
|
+
const first = subject.split(".")[0];
|
|
1096
|
+
return first ?? subject;
|
|
1097
|
+
}
|
|
1098
|
+
__name(boundaryOf, "boundaryOf");
|
|
1099
|
+
function delayQueueKey(stream) {
|
|
1100
|
+
return `${DELAYQ_PREFIX}:${stream}`;
|
|
1101
|
+
}
|
|
1102
|
+
__name(delayQueueKey, "delayQueueKey");
|
|
1103
|
+
function delayLockKey(stream) {
|
|
1104
|
+
return `${DELAYQ_LOCK_PREFIX}:${stream}`;
|
|
1105
|
+
}
|
|
1106
|
+
__name(delayLockKey, "delayLockKey");
|
|
1107
|
+
function dlqKey(stream) {
|
|
1108
|
+
return `${DLQ_PREFIX}:${stream}`;
|
|
1109
|
+
}
|
|
1110
|
+
__name(dlqKey, "dlqKey");
|
|
1111
|
+
function dedupKey(stream, msgId) {
|
|
1112
|
+
return `${DEDUP_PREFIX}:${stream}:${msgId}`;
|
|
1113
|
+
}
|
|
1114
|
+
__name(dedupKey, "dedupKey");
|
|
1115
|
+
function replyKey(correlationId) {
|
|
1116
|
+
return `${REPLY_PREFIX}:${correlationId}`;
|
|
1117
|
+
}
|
|
1118
|
+
__name(replyKey, "replyKey");
|
|
1119
|
+
function matchSubject(pattern, subject) {
|
|
1120
|
+
if (pattern === subject) return true;
|
|
1121
|
+
const pParts = pattern.split(".");
|
|
1122
|
+
const sParts = subject.split(".");
|
|
1123
|
+
const last = pParts[pParts.length - 1];
|
|
1124
|
+
if (last === ">") {
|
|
1125
|
+
if (sParts.length < pParts.length) return false;
|
|
1126
|
+
for (let i = 0; i < pParts.length - 1; i++) {
|
|
1127
|
+
if (pParts[i] !== sParts[i]) return false;
|
|
1128
|
+
}
|
|
1129
|
+
return true;
|
|
1130
|
+
}
|
|
1131
|
+
if (pParts.length !== sParts.length) return false;
|
|
1132
|
+
for (let i = 0; i < pParts.length; i++) {
|
|
1133
|
+
if (pParts[i] !== "*" && pParts[i] !== sParts[i]) return false;
|
|
1134
|
+
}
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
__name(matchSubject, "matchSubject");
|
|
1138
|
+
function isWildcard(pattern) {
|
|
1139
|
+
return pattern.includes("*") || pattern.includes(">");
|
|
1140
|
+
}
|
|
1141
|
+
__name(isWildcard, "isWildcard");
|
|
1142
|
+
|
|
1143
|
+
// src/event-queue/implementations/redis-streams/trace-context.ts
|
|
1144
|
+
var otelApiPromise2 = null;
|
|
1145
|
+
async function loadOtelApi2() {
|
|
1146
|
+
if (otelApiPromise2) return otelApiPromise2;
|
|
1147
|
+
otelApiPromise2 = (async () => {
|
|
1148
|
+
try {
|
|
1149
|
+
const mod = await import('./esm-M2M7HGV4.js');
|
|
1150
|
+
return mod;
|
|
1151
|
+
} catch {
|
|
1152
|
+
logger.debug("[redis-streams/trace-context] @opentelemetry/api not available; tracing disabled");
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
})();
|
|
1156
|
+
return otelApiPromise2;
|
|
1157
|
+
}
|
|
1158
|
+
__name(loadOtelApi2, "loadOtelApi");
|
|
1159
|
+
var TRACE_KEYS = [
|
|
1160
|
+
"traceparent",
|
|
1161
|
+
"tracestate",
|
|
1162
|
+
"baggage"
|
|
1163
|
+
];
|
|
1164
|
+
async function injectTraceFields() {
|
|
1165
|
+
const api = await loadOtelApi2();
|
|
1166
|
+
if (!api) return {};
|
|
1167
|
+
const carrier = {};
|
|
1168
|
+
try {
|
|
1169
|
+
api.propagation.inject(api.context.active(), carrier, {
|
|
1170
|
+
set(c, k, v) {
|
|
1171
|
+
c[k] = v;
|
|
1172
|
+
}
|
|
1173
|
+
});
|
|
1174
|
+
} catch (err) {
|
|
1175
|
+
logger.debug("[redis-streams/trace-context] inject failed", {
|
|
1176
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1177
|
+
});
|
|
1178
|
+
}
|
|
1179
|
+
return carrier;
|
|
1180
|
+
}
|
|
1181
|
+
__name(injectTraceFields, "injectTraceFields");
|
|
1182
|
+
async function extractTraceContext(fields) {
|
|
1183
|
+
const api = await loadOtelApi2();
|
|
1184
|
+
if (!api) return void 0;
|
|
1185
|
+
const carrier = {};
|
|
1186
|
+
for (const key of TRACE_KEYS) {
|
|
1187
|
+
const value = fields[key];
|
|
1188
|
+
if (value) carrier[key] = value;
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
return api.propagation.extract(api.context.active(), carrier, {
|
|
1192
|
+
get(c, k) {
|
|
1193
|
+
return c[k];
|
|
1194
|
+
},
|
|
1195
|
+
keys(c) {
|
|
1196
|
+
return Object.keys(c);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
} catch {
|
|
1200
|
+
return void 0;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
__name(extractTraceContext, "extractTraceContext");
|
|
1204
|
+
async function runWithConsumerSpan2(subject, fields, handler) {
|
|
1205
|
+
const api = await loadOtelApi2();
|
|
1206
|
+
if (!api) return handler();
|
|
1207
|
+
try {
|
|
1208
|
+
const parentCtx = await extractTraceContext(fields);
|
|
1209
|
+
const tracer = api.trace.getTracer("@burdenoff/be-sdk/redis-streams");
|
|
1210
|
+
return await new Promise((resolve, reject) => {
|
|
1211
|
+
tracer.startActiveSpan(`redis-streams.consume ${subject}`, {
|
|
1212
|
+
kind: 5,
|
|
1213
|
+
attributes: {
|
|
1214
|
+
"messaging.system": "redis",
|
|
1215
|
+
"messaging.destination.name": subject
|
|
1216
|
+
}
|
|
1217
|
+
}, parentCtx, async (span) => {
|
|
1218
|
+
try {
|
|
1219
|
+
const result = await handler();
|
|
1220
|
+
span.setStatus({
|
|
1221
|
+
code: 1
|
|
1222
|
+
});
|
|
1223
|
+
span.end();
|
|
1224
|
+
resolve(result);
|
|
1225
|
+
} catch (err) {
|
|
1226
|
+
span.recordException(err);
|
|
1227
|
+
span.setStatus({
|
|
1228
|
+
code: 2,
|
|
1229
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1230
|
+
});
|
|
1231
|
+
span.end();
|
|
1232
|
+
reject(err);
|
|
1233
|
+
}
|
|
1234
|
+
});
|
|
1235
|
+
});
|
|
1236
|
+
} catch {
|
|
1237
|
+
return handler();
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
__name(runWithConsumerSpan2, "runWithConsumerSpan");
|
|
1241
|
+
|
|
1242
|
+
// src/event-queue/implementations/redis-streams/RedisStreamsEventConsumer.ts
|
|
1243
|
+
var DEFAULT_BLOCK_MS = 5e3;
|
|
1244
|
+
var DEFAULT_BATCH = 10;
|
|
1245
|
+
var REGISTRY_REFRESH_MS = 1e4;
|
|
1246
|
+
var DEFAULT_ACK_WAIT_MS = 3e4;
|
|
1247
|
+
var DEFAULT_MAX_DELIVER = 3;
|
|
1248
|
+
var SWEEP_INTERVAL_MS = 15e3;
|
|
1249
|
+
var DELAYQ_INTERVAL_MS = 1e3;
|
|
1250
|
+
var RedisStreamsSubscription = class RedisStreamsSubscription2 {
|
|
1251
|
+
static {
|
|
1252
|
+
__name(this, "RedisStreamsSubscription");
|
|
1253
|
+
}
|
|
1254
|
+
subject;
|
|
1255
|
+
unsubscribeFn;
|
|
1256
|
+
id = randomUUID();
|
|
1257
|
+
active = true;
|
|
1258
|
+
receivedCount = 0;
|
|
1259
|
+
processedCount = 0;
|
|
1260
|
+
failedCount = 0;
|
|
1261
|
+
lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1262
|
+
constructor(subject, unsubscribeFn) {
|
|
1263
|
+
this.subject = subject;
|
|
1264
|
+
this.unsubscribeFn = unsubscribeFn;
|
|
1265
|
+
}
|
|
1266
|
+
async unsubscribe() {
|
|
1267
|
+
if (!this.active) return;
|
|
1268
|
+
this.active = false;
|
|
1269
|
+
await this.unsubscribeFn();
|
|
1270
|
+
}
|
|
1271
|
+
isActive() {
|
|
1272
|
+
return this.active;
|
|
1273
|
+
}
|
|
1274
|
+
getStats() {
|
|
1275
|
+
return {
|
|
1276
|
+
messagesReceived: this.receivedCount,
|
|
1277
|
+
messagesProcessed: this.processedCount,
|
|
1278
|
+
messagesFailed: this.failedCount,
|
|
1279
|
+
lastActivity: this.lastActivity
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
recordReceived() {
|
|
1283
|
+
this.receivedCount++;
|
|
1284
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1285
|
+
}
|
|
1286
|
+
recordProcessed() {
|
|
1287
|
+
this.processedCount++;
|
|
1288
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1289
|
+
}
|
|
1290
|
+
recordFailed() {
|
|
1291
|
+
this.failedCount++;
|
|
1292
|
+
this.lastActivity = (/* @__PURE__ */ new Date()).toISOString();
|
|
1293
|
+
}
|
|
1294
|
+
};
|
|
1295
|
+
var RedisStreamsEventConsumer = class {
|
|
1296
|
+
static {
|
|
1297
|
+
__name(this, "RedisStreamsEventConsumer");
|
|
1298
|
+
}
|
|
1299
|
+
connection;
|
|
1300
|
+
subscriptions = [];
|
|
1301
|
+
subscriptionLoops = /* @__PURE__ */ new Set();
|
|
1302
|
+
keyPrefix;
|
|
1303
|
+
consumerName;
|
|
1304
|
+
group;
|
|
1305
|
+
maxDeliver;
|
|
1306
|
+
ackWaitMs;
|
|
1307
|
+
maxConcurrent;
|
|
1308
|
+
consumedCount = 0;
|
|
1309
|
+
failedCount = 0;
|
|
1310
|
+
lastActivity = null;
|
|
1311
|
+
stopped = false;
|
|
1312
|
+
sweepTimer = null;
|
|
1313
|
+
delayqTimer = null;
|
|
1314
|
+
knownStreams = /* @__PURE__ */ new Map();
|
|
1315
|
+
constructor(config) {
|
|
1316
|
+
this.connection = new RedisStreamsConnection({
|
|
1317
|
+
type: "redis-streams",
|
|
1318
|
+
url: config.url ?? process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
1319
|
+
user: config.user,
|
|
1320
|
+
password: config.password,
|
|
1321
|
+
name: config.name,
|
|
1322
|
+
connectionTimeout: config.connectionTimeout
|
|
1323
|
+
});
|
|
1324
|
+
this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
|
|
1325
|
+
this.group = config.consumer || config.name || "default-consumer";
|
|
1326
|
+
this.consumerName = `${this.group}:${hostname()}:${process.pid}`;
|
|
1327
|
+
this.maxDeliver = config.maxDeliver ?? DEFAULT_MAX_DELIVER;
|
|
1328
|
+
this.ackWaitMs = config.ackWait ? Math.max(1e3, Math.round(config.ackWait / 1e6)) : DEFAULT_ACK_WAIT_MS;
|
|
1329
|
+
this.maxConcurrent = config.maxConcurrent ?? 5;
|
|
1330
|
+
}
|
|
1331
|
+
async connect() {
|
|
1332
|
+
await this.connection.connect();
|
|
1333
|
+
if (!this.sweepTimer) {
|
|
1334
|
+
const t = setInterval(() => {
|
|
1335
|
+
void this.sweepPending().catch((err) => {
|
|
1336
|
+
logger.debug("[RedisStreamsEventConsumer] sweep error", {
|
|
1337
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1338
|
+
});
|
|
1339
|
+
});
|
|
1340
|
+
}, SWEEP_INTERVAL_MS);
|
|
1341
|
+
t.unref?.();
|
|
1342
|
+
this.sweepTimer = t;
|
|
1343
|
+
}
|
|
1344
|
+
if (!this.delayqTimer) {
|
|
1345
|
+
const t = setInterval(() => {
|
|
1346
|
+
void this.drainDelayQueues().catch(() => void 0);
|
|
1347
|
+
}, DELAYQ_INTERVAL_MS);
|
|
1348
|
+
t.unref?.();
|
|
1349
|
+
this.delayqTimer = t;
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
async disconnect() {
|
|
1353
|
+
this.stopped = true;
|
|
1354
|
+
if (this.sweepTimer) clearInterval(this.sweepTimer);
|
|
1355
|
+
if (this.delayqTimer) clearInterval(this.delayqTimer);
|
|
1356
|
+
await this.unsubscribeAll();
|
|
1357
|
+
await this.connection.disconnect();
|
|
1358
|
+
}
|
|
1359
|
+
isConnected() {
|
|
1360
|
+
return this.connection.isConnected();
|
|
1361
|
+
}
|
|
1362
|
+
async healthCheck() {
|
|
1363
|
+
try {
|
|
1364
|
+
const t0 = Date.now();
|
|
1365
|
+
const pong = await this.connection.ping();
|
|
1366
|
+
return {
|
|
1367
|
+
status: pong === "PONG" ? "healthy" : "degraded",
|
|
1368
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1369
|
+
connected: this.isConnected(),
|
|
1370
|
+
latencyMs: Date.now() - t0,
|
|
1371
|
+
details: {
|
|
1372
|
+
type: "redis-streams",
|
|
1373
|
+
subscriptions: this.subscriptions.length
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
} catch (err) {
|
|
1377
|
+
return {
|
|
1378
|
+
status: "unhealthy",
|
|
1379
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1380
|
+
connected: false,
|
|
1381
|
+
details: {
|
|
1382
|
+
type: "redis-streams"
|
|
1383
|
+
},
|
|
1384
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1385
|
+
};
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
async getStatistics() {
|
|
1389
|
+
return Promise.resolve({
|
|
1390
|
+
messagesConsumed: this.consumedCount,
|
|
1391
|
+
messagesFailed: this.failedCount,
|
|
1392
|
+
messagesPublished: 0,
|
|
1393
|
+
activeConsumers: this.subscriptions.length,
|
|
1394
|
+
lastActivity: this.lastActivity?.toISOString() ?? null,
|
|
1395
|
+
details: {
|
|
1396
|
+
type: "redis-streams",
|
|
1397
|
+
group: this.group
|
|
1398
|
+
}
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
async ensureStream(_config) {
|
|
1402
|
+
return Promise.resolve();
|
|
1403
|
+
}
|
|
1404
|
+
async ensureConsumer(_config) {
|
|
1405
|
+
return Promise.resolve();
|
|
1406
|
+
}
|
|
1407
|
+
async subscribe(opts) {
|
|
1408
|
+
await this.connect();
|
|
1409
|
+
const sub = new RedisStreamsSubscription(opts.subject, async () => {
|
|
1410
|
+
this.subscriptions = this.subscriptions.filter((s) => s !== sub);
|
|
1411
|
+
});
|
|
1412
|
+
this.subscriptions.push(sub);
|
|
1413
|
+
const startId = this.resolveStartId(opts.options?.deliverPolicy);
|
|
1414
|
+
const wildcard = isWildcard(opts.subject);
|
|
1415
|
+
const handlerFn = opts.handler;
|
|
1416
|
+
if (!wildcard) {
|
|
1417
|
+
const stream = streamKey(opts.subject, {
|
|
1418
|
+
prefix: this.keyPrefix
|
|
1419
|
+
});
|
|
1420
|
+
await this.ensureGroup(stream, startId);
|
|
1421
|
+
this.knownStreams.set(stream, opts.subject);
|
|
1422
|
+
this.spawnLoop(sub, stream, opts.subject, handlerFn);
|
|
1423
|
+
} else {
|
|
1424
|
+
const boundary = boundaryOf(opts.subject);
|
|
1425
|
+
const refresh = /* @__PURE__ */ __name(async () => {
|
|
1426
|
+
const client = this.connection.getClient();
|
|
1427
|
+
const subjects = await client.smembers(registryKey(boundary));
|
|
1428
|
+
for (const s of subjects) {
|
|
1429
|
+
if (!matchSubject(opts.subject, s)) continue;
|
|
1430
|
+
const stream = streamKey(s, {
|
|
1431
|
+
prefix: this.keyPrefix
|
|
1432
|
+
});
|
|
1433
|
+
if (this.knownStreams.has(stream)) continue;
|
|
1434
|
+
await this.ensureGroup(stream, startId);
|
|
1435
|
+
this.knownStreams.set(stream, s);
|
|
1436
|
+
this.spawnLoop(sub, stream, s, handlerFn);
|
|
1437
|
+
}
|
|
1438
|
+
}, "refresh");
|
|
1439
|
+
await refresh();
|
|
1440
|
+
const t = setInterval(() => {
|
|
1441
|
+
if (this.stopped || !sub.isActive()) {
|
|
1442
|
+
clearInterval(t);
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
void refresh().catch(() => void 0);
|
|
1446
|
+
}, REGISTRY_REFRESH_MS);
|
|
1447
|
+
t.unref?.();
|
|
1448
|
+
}
|
|
1449
|
+
return sub;
|
|
1450
|
+
}
|
|
1451
|
+
async subscribeMultiple(subjects, handler, options) {
|
|
1452
|
+
const out = [];
|
|
1453
|
+
for (const subject of subjects) {
|
|
1454
|
+
out.push(await this.subscribe({
|
|
1455
|
+
subject,
|
|
1456
|
+
handler,
|
|
1457
|
+
options
|
|
1458
|
+
}));
|
|
1459
|
+
}
|
|
1460
|
+
return out;
|
|
1461
|
+
}
|
|
1462
|
+
async pull(opts) {
|
|
1463
|
+
await this.connect();
|
|
1464
|
+
const stream = streamKey(opts.subject, {
|
|
1465
|
+
prefix: this.keyPrefix
|
|
1466
|
+
});
|
|
1467
|
+
await this.ensureGroup(stream, "$");
|
|
1468
|
+
const client = this.connection.getClient();
|
|
1469
|
+
const raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", opts.batch ?? DEFAULT_BATCH, "BLOCK", opts.timeout ?? DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
|
|
1470
|
+
if (!raw) return [];
|
|
1471
|
+
const out = [];
|
|
1472
|
+
for (const [streamName, entries] of raw) {
|
|
1473
|
+
for (const [id, rawFields] of entries) {
|
|
1474
|
+
const parsed = this.parseEntry(streamName, id, rawFields);
|
|
1475
|
+
out.push(this.toAcknowledgeable(streamName, id, parsed));
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return out;
|
|
1479
|
+
}
|
|
1480
|
+
async unsubscribeAll() {
|
|
1481
|
+
for (const sub of [
|
|
1482
|
+
...this.subscriptions
|
|
1483
|
+
]) {
|
|
1484
|
+
await sub.unsubscribe();
|
|
1485
|
+
}
|
|
1486
|
+
this.subscriptions = [];
|
|
1487
|
+
this.subscriptionLoops.clear();
|
|
1488
|
+
}
|
|
1489
|
+
getSubscriptions() {
|
|
1490
|
+
return [
|
|
1491
|
+
...this.subscriptions
|
|
1492
|
+
];
|
|
1493
|
+
}
|
|
1494
|
+
async processDLQ(handler) {
|
|
1495
|
+
await this.connect();
|
|
1496
|
+
const client = this.connection.getClient();
|
|
1497
|
+
const dlqGroup = `${this.group}-dlq`;
|
|
1498
|
+
for (const [stream] of this.knownStreams) {
|
|
1499
|
+
const dlq = dlqKey(stream);
|
|
1500
|
+
try {
|
|
1501
|
+
await client.xgroup("CREATE", dlq, dlqGroup, "0", "MKSTREAM");
|
|
1502
|
+
} catch {
|
|
1503
|
+
}
|
|
1504
|
+
const res = await client.xreadgroup("GROUP", dlqGroup, this.consumerName, "COUNT", 50, "BLOCK", 100, "STREAMS", dlq, ">");
|
|
1505
|
+
if (!res) continue;
|
|
1506
|
+
for (const [, entries] of res) {
|
|
1507
|
+
for (const [id, rawFields] of entries) {
|
|
1508
|
+
const parsed = this.parseEntry(dlq, id, rawFields);
|
|
1509
|
+
const evt = this.toAcknowledgeable(dlq, id, parsed, dlqGroup);
|
|
1510
|
+
try {
|
|
1511
|
+
await handler(evt);
|
|
1512
|
+
} catch (err) {
|
|
1513
|
+
logger.error("[RedisStreamsEventConsumer] DLQ handler error", {
|
|
1514
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
async listSubjects(_stream) {
|
|
1522
|
+
await this.connect();
|
|
1523
|
+
const client = this.connection.getClient();
|
|
1524
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1525
|
+
for (const subject of this.knownStreams.values()) {
|
|
1526
|
+
const members = await client.smembers(registryKey(boundaryOf(subject)));
|
|
1527
|
+
for (const m of members) seen.add(m);
|
|
1528
|
+
}
|
|
1529
|
+
return [
|
|
1530
|
+
...seen
|
|
1531
|
+
];
|
|
1532
|
+
}
|
|
1533
|
+
// ---- internals ----
|
|
1534
|
+
resolveStartId(policy) {
|
|
1535
|
+
switch (policy) {
|
|
1536
|
+
case "all":
|
|
1537
|
+
return "0";
|
|
1538
|
+
case "new":
|
|
1539
|
+
return "$";
|
|
1540
|
+
case "last":
|
|
1541
|
+
return "$";
|
|
1542
|
+
default:
|
|
1543
|
+
return "$";
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
async ensureGroup(stream, startId) {
|
|
1547
|
+
const client = this.connection.getClient();
|
|
1548
|
+
try {
|
|
1549
|
+
await client.xgroup("CREATE", stream, this.group, startId, "MKSTREAM");
|
|
1550
|
+
logger.debug("[RedisStreamsEventConsumer] group created", {
|
|
1551
|
+
stream,
|
|
1552
|
+
group: this.group,
|
|
1553
|
+
startId
|
|
1554
|
+
});
|
|
1555
|
+
} catch (err) {
|
|
1556
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1557
|
+
if (!msg.includes("BUSYGROUP")) {
|
|
1558
|
+
throw err;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
spawnLoop(sub, stream, subject, handler) {
|
|
1563
|
+
const loop = /* @__PURE__ */ __name(async () => {
|
|
1564
|
+
const client = await this.connection.makeBlockingClient();
|
|
1565
|
+
try {
|
|
1566
|
+
while (!this.stopped && sub.isActive()) {
|
|
1567
|
+
let raw = null;
|
|
1568
|
+
try {
|
|
1569
|
+
raw = await client.xreadgroup("GROUP", this.group, this.consumerName, "COUNT", this.maxConcurrent, "BLOCK", DEFAULT_BLOCK_MS, "STREAMS", stream, ">");
|
|
1570
|
+
} catch (err) {
|
|
1571
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1572
|
+
if (msg.includes("NOGROUP")) {
|
|
1573
|
+
await this.ensureGroup(stream, "$");
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
logger.warn("[RedisStreamsEventConsumer] read error", {
|
|
1577
|
+
stream,
|
|
1578
|
+
error: msg
|
|
1579
|
+
});
|
|
1580
|
+
await new Promise((res) => setTimeout(res, 1e3));
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
if (!raw) continue;
|
|
1584
|
+
for (const [, entries] of raw) {
|
|
1585
|
+
for (const [id, rawFields] of entries) {
|
|
1586
|
+
sub.recordReceived();
|
|
1587
|
+
this.consumedCount++;
|
|
1588
|
+
this.lastActivity = /* @__PURE__ */ new Date();
|
|
1589
|
+
const parsed = this.parseEntry(stream, id, rawFields);
|
|
1590
|
+
const evt = this.toAcknowledgeable(stream, id, parsed);
|
|
1591
|
+
try {
|
|
1592
|
+
await runWithConsumerSpan2(subject, parsed.fields, () => handler(evt));
|
|
1593
|
+
sub.recordProcessed();
|
|
1594
|
+
} catch (err) {
|
|
1595
|
+
sub.recordFailed();
|
|
1596
|
+
this.failedCount++;
|
|
1597
|
+
logger.error("[RedisStreamsEventConsumer] handler error", {
|
|
1598
|
+
subject,
|
|
1599
|
+
id,
|
|
1600
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1601
|
+
});
|
|
1602
|
+
try {
|
|
1603
|
+
await evt.nak();
|
|
1604
|
+
} catch {
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
} finally {
|
|
1611
|
+
try {
|
|
1612
|
+
client.disconnect();
|
|
1613
|
+
} catch {
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}, "loop");
|
|
1617
|
+
void loop().catch((err) => {
|
|
1618
|
+
logger.error("[RedisStreamsEventConsumer] loop crashed", {
|
|
1619
|
+
stream,
|
|
1620
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1621
|
+
});
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
parseEntry(stream, id, rawFields) {
|
|
1625
|
+
const fields = {};
|
|
1626
|
+
for (let i = 0; i + 1 < rawFields.length; i += 2) {
|
|
1627
|
+
const key = rawFields[i];
|
|
1628
|
+
const val = rawFields[i + 1];
|
|
1629
|
+
if (key === void 0 || val === void 0) continue;
|
|
1630
|
+
fields[key] = val;
|
|
1631
|
+
}
|
|
1632
|
+
let data;
|
|
1633
|
+
try {
|
|
1634
|
+
data = JSON.parse(fields["payload"] ?? "null");
|
|
1635
|
+
} catch {
|
|
1636
|
+
data = fields["payload"];
|
|
1637
|
+
}
|
|
1638
|
+
let metadata;
|
|
1639
|
+
try {
|
|
1640
|
+
metadata = JSON.parse(fields["metadata"] ?? "{}");
|
|
1641
|
+
} catch {
|
|
1642
|
+
metadata = {
|
|
1643
|
+
correlationId: fields["correlation-id"] ?? ""
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
if (!metadata.correlationId) metadata.correlationId = fields["correlation-id"] ?? "";
|
|
1647
|
+
return {
|
|
1648
|
+
id,
|
|
1649
|
+
stream,
|
|
1650
|
+
subject: fields["subject"] ?? "",
|
|
1651
|
+
data,
|
|
1652
|
+
metadata,
|
|
1653
|
+
timestamp: fields["timestamp"] ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1654
|
+
fields,
|
|
1655
|
+
deliveryCount: 1
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
toAcknowledgeable(stream, id, parsed, group = this.group) {
|
|
1659
|
+
const connection = this.connection;
|
|
1660
|
+
let acked = false;
|
|
1661
|
+
return {
|
|
1662
|
+
id,
|
|
1663
|
+
subject: parsed.subject,
|
|
1664
|
+
data: parsed.data,
|
|
1665
|
+
metadata: parsed.metadata,
|
|
1666
|
+
timestamp: parsed.timestamp,
|
|
1667
|
+
async ack() {
|
|
1668
|
+
if (acked) return;
|
|
1669
|
+
acked = true;
|
|
1670
|
+
const client = connection.getClient();
|
|
1671
|
+
await client.xack(stream, group, id);
|
|
1672
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1673
|
+
},
|
|
1674
|
+
async nak(delay = 0) {
|
|
1675
|
+
if (acked) return;
|
|
1676
|
+
acked = true;
|
|
1677
|
+
const client = connection.getClient();
|
|
1678
|
+
if (delay > 0) {
|
|
1679
|
+
const score = Date.now() + delay;
|
|
1680
|
+
const payload = JSON.stringify(parsed.fields);
|
|
1681
|
+
await client.zadd(delayQueueKey(stream), score, `${id}|${payload}`);
|
|
1682
|
+
await client.xack(stream, group, id);
|
|
1683
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
async term() {
|
|
1687
|
+
if (acked) return;
|
|
1688
|
+
acked = true;
|
|
1689
|
+
const client = connection.getClient();
|
|
1690
|
+
const dlq = dlqKey(stream);
|
|
1691
|
+
const flat = [];
|
|
1692
|
+
for (const [k, v] of Object.entries(parsed.fields)) flat.push(k, v);
|
|
1693
|
+
await client.xadd(dlq, "MAXLEN", "~", 1e4, "*", ...flat);
|
|
1694
|
+
await client.xack(stream, group, id);
|
|
1695
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1696
|
+
},
|
|
1697
|
+
isRedelivered() {
|
|
1698
|
+
return parsed.deliveryCount > 1;
|
|
1699
|
+
},
|
|
1700
|
+
getDeliveryAttempt() {
|
|
1701
|
+
return parsed.deliveryCount;
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
async sweepPending() {
|
|
1706
|
+
if (this.stopped || this.knownStreams.size === 0) return;
|
|
1707
|
+
const client = this.connection.getClient();
|
|
1708
|
+
for (const [stream] of this.knownStreams) {
|
|
1709
|
+
try {
|
|
1710
|
+
const claimed = await client.xautoclaim(stream, this.group, this.consumerName, this.ackWaitMs, "0", "COUNT", 50);
|
|
1711
|
+
if (!claimed) continue;
|
|
1712
|
+
const entries = claimed[1] ?? [];
|
|
1713
|
+
for (const [id, rawFields] of entries) {
|
|
1714
|
+
const pending = await client.xpending(stream, this.group, "IDLE", 0, "-", "+", 1, this.consumerName);
|
|
1715
|
+
const deliveryCount = Array.isArray(pending) && pending[0] ? pending[0][3] : 1;
|
|
1716
|
+
if (deliveryCount > this.maxDeliver) {
|
|
1717
|
+
const flat = [];
|
|
1718
|
+
for (let i = 0; i + 1 < rawFields.length; i += 2) {
|
|
1719
|
+
flat.push(rawFields[i], rawFields[i + 1]);
|
|
1720
|
+
}
|
|
1721
|
+
flat.push("dlq-reason", "max-deliver-exceeded");
|
|
1722
|
+
flat.push("dlq-deliveries", String(deliveryCount));
|
|
1723
|
+
await client.xadd(dlqKey(stream), "MAXLEN", "~", 1e4, "*", ...flat);
|
|
1724
|
+
await client.xack(stream, this.group, id);
|
|
1725
|
+
await client.xdel(stream, id).catch(() => void 0);
|
|
1726
|
+
logger.warn("[RedisStreamsEventConsumer] message moved to DLQ", {
|
|
1727
|
+
stream,
|
|
1728
|
+
id,
|
|
1729
|
+
deliveryCount
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
} catch (err) {
|
|
1734
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1735
|
+
if (msg.includes("NOGROUP")) continue;
|
|
1736
|
+
logger.debug("[RedisStreamsEventConsumer] sweep iteration error", {
|
|
1737
|
+
stream,
|
|
1738
|
+
error: msg
|
|
1739
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
async drainDelayQueues() {
|
|
1744
|
+
if (this.stopped || this.knownStreams.size === 0) return;
|
|
1745
|
+
const client = this.connection.getClient();
|
|
1746
|
+
for (const [stream] of this.knownStreams) {
|
|
1747
|
+
const lockKey = delayLockKey(stream);
|
|
1748
|
+
const got = await client.set(lockKey, this.consumerName, "NX", "EX", 30).catch(() => null);
|
|
1749
|
+
if (got !== "OK") continue;
|
|
1750
|
+
try {
|
|
1751
|
+
const due = await client.zrangebyscore(delayQueueKey(stream), 0, Date.now(), "LIMIT", 0, 50);
|
|
1752
|
+
for (const member of due) {
|
|
1753
|
+
const split = member.indexOf("|");
|
|
1754
|
+
if (split < 0) continue;
|
|
1755
|
+
const payload = member.slice(split + 1);
|
|
1756
|
+
let fields = {};
|
|
1757
|
+
try {
|
|
1758
|
+
fields = JSON.parse(payload);
|
|
1759
|
+
} catch {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const flat = [];
|
|
1763
|
+
for (const [k, v] of Object.entries(fields)) flat.push(k, v);
|
|
1764
|
+
await client.xadd(stream, "MAXLEN", "~", 1e5, "*", ...flat);
|
|
1765
|
+
await client.zrem(delayQueueKey(stream), member);
|
|
1766
|
+
}
|
|
1767
|
+
} finally {
|
|
1768
|
+
await client.del(lockKey).catch(() => void 0);
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
var RedisStreamsEventPublisher = class {
|
|
1774
|
+
static {
|
|
1775
|
+
__name(this, "RedisStreamsEventPublisher");
|
|
1776
|
+
}
|
|
1777
|
+
config;
|
|
1778
|
+
connection;
|
|
1779
|
+
publishCount = 0;
|
|
1780
|
+
lastPublishTime = null;
|
|
1781
|
+
maxLen;
|
|
1782
|
+
dedupTtl;
|
|
1783
|
+
keyPrefix;
|
|
1784
|
+
constructor(config) {
|
|
1785
|
+
this.config = config;
|
|
1786
|
+
this.connection = new RedisStreamsConnection(config);
|
|
1787
|
+
this.maxLen = config.maxLen ?? parseInt(process.env["REDIS_STREAMS_MAXLEN"] ?? "100000", 10);
|
|
1788
|
+
this.dedupTtl = config.dedupTtlSeconds ?? parseInt(process.env["REDIS_STREAMS_DEDUP_TTL"] ?? "120", 10);
|
|
1789
|
+
this.keyPrefix = config.keyPrefix ?? process.env["REDIS_STREAMS_KEY_PREFIX"] ?? "boff:events";
|
|
1790
|
+
}
|
|
1791
|
+
async connect() {
|
|
1792
|
+
await this.connection.connect();
|
|
1793
|
+
}
|
|
1794
|
+
async disconnect() {
|
|
1795
|
+
await this.connection.disconnect();
|
|
1796
|
+
}
|
|
1797
|
+
isConnected() {
|
|
1798
|
+
return this.connection.isConnected();
|
|
1799
|
+
}
|
|
1800
|
+
async healthCheck() {
|
|
1801
|
+
try {
|
|
1802
|
+
const t0 = Date.now();
|
|
1803
|
+
const pong = await this.connection.ping();
|
|
1804
|
+
return {
|
|
1805
|
+
status: pong === "PONG" ? "healthy" : "degraded",
|
|
1806
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1807
|
+
connected: this.isConnected(),
|
|
1808
|
+
latencyMs: Date.now() - t0,
|
|
1809
|
+
details: {
|
|
1810
|
+
type: "redis-streams",
|
|
1811
|
+
url: this.config.url
|
|
1812
|
+
}
|
|
1813
|
+
};
|
|
1814
|
+
} catch (err) {
|
|
1815
|
+
return {
|
|
1816
|
+
status: "unhealthy",
|
|
1817
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1818
|
+
connected: false,
|
|
1819
|
+
details: {
|
|
1820
|
+
type: "redis-streams"
|
|
1821
|
+
},
|
|
1822
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
async getStatistics() {
|
|
1827
|
+
return {
|
|
1828
|
+
messagesPublished: this.publishCount,
|
|
1829
|
+
messagesConsumed: 0,
|
|
1830
|
+
messagesFailed: 0,
|
|
1831
|
+
activePublishers: 1,
|
|
1832
|
+
activeConsumers: 0,
|
|
1833
|
+
lastActivity: this.lastPublishTime?.toISOString() ?? null,
|
|
1834
|
+
details: {
|
|
1835
|
+
type: "redis-streams"
|
|
1836
|
+
}
|
|
1837
|
+
};
|
|
1838
|
+
}
|
|
1839
|
+
async ensureStream(_config) {
|
|
1840
|
+
return Promise.resolve();
|
|
1841
|
+
}
|
|
1842
|
+
async publish(options) {
|
|
1843
|
+
return this.publishInternal(options);
|
|
1844
|
+
}
|
|
1845
|
+
async publishInternal(opts) {
|
|
1846
|
+
await this.connect();
|
|
1847
|
+
const client = this.connection.getClient();
|
|
1848
|
+
const stream = streamKey(opts.subject, {
|
|
1849
|
+
prefix: this.keyPrefix
|
|
1850
|
+
});
|
|
1851
|
+
const msgId = opts.options?.messageId ?? randomUUID();
|
|
1852
|
+
const correlationId = opts.metadata?.correlationId ?? randomUUID();
|
|
1853
|
+
const enableDedup = this.config.enableDeduplication === true || opts.options?.messageId !== void 0;
|
|
1854
|
+
if (enableDedup) {
|
|
1855
|
+
const ok = await client.set(dedupKey(stream, msgId), "1", "NX", "EX", this.dedupTtl);
|
|
1856
|
+
if (ok !== "OK") {
|
|
1857
|
+
logger.debug("[RedisStreamsEventPublisher] dedup hit; skipping publish", {
|
|
1858
|
+
subject: opts.subject,
|
|
1859
|
+
msgId
|
|
1860
|
+
});
|
|
1861
|
+
return msgId;
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
const trace = await injectTraceFields();
|
|
1865
|
+
const fields = {
|
|
1866
|
+
"msg-id": msgId,
|
|
1867
|
+
subject: opts.subject,
|
|
1868
|
+
"correlation-id": correlationId,
|
|
1869
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1870
|
+
priority: opts.options?.priority ?? this.config.defaultPriority ?? "normal",
|
|
1871
|
+
source: opts.metadata?.source ?? this.config.name ?? "unknown",
|
|
1872
|
+
payload: JSON.stringify(opts.data ?? null),
|
|
1873
|
+
metadata: JSON.stringify(opts.metadata ?? {}),
|
|
1874
|
+
...trace
|
|
1875
|
+
};
|
|
1876
|
+
if (opts.metadata?.userId) fields["user-id"] = String(opts.metadata.userId);
|
|
1877
|
+
if (opts.metadata?.workspaceId) fields["workspace-id"] = String(opts.metadata.workspaceId);
|
|
1878
|
+
if (opts.metadata?.tenantId) fields["tenant-id"] = String(opts.metadata.tenantId);
|
|
1879
|
+
const flat = [];
|
|
1880
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1881
|
+
flat.push(k, v);
|
|
1882
|
+
}
|
|
1883
|
+
const id = await client.xadd(stream, "MAXLEN", "~", this.maxLen, "*", ...flat);
|
|
1884
|
+
void client.sadd(registryKey(boundaryOf(opts.subject)), opts.subject).catch(() => void 0);
|
|
1885
|
+
this.publishCount++;
|
|
1886
|
+
this.lastPublishTime = /* @__PURE__ */ new Date();
|
|
1887
|
+
logger.debug("[RedisStreamsEventPublisher] published", {
|
|
1888
|
+
subject: opts.subject,
|
|
1889
|
+
stream,
|
|
1890
|
+
id
|
|
1891
|
+
});
|
|
1892
|
+
return id;
|
|
1893
|
+
}
|
|
1894
|
+
async publishBatch(events) {
|
|
1895
|
+
const out = [];
|
|
1896
|
+
for (const e of events) {
|
|
1897
|
+
out.push(await this.publishInternal(e));
|
|
1898
|
+
}
|
|
1899
|
+
return out;
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Emulated request/response: writer XADDs onto its own subject, then
|
|
1903
|
+
* blocks on a one-shot reply stream keyed by correlationId.
|
|
1904
|
+
*/
|
|
1905
|
+
async publishWithAck(opts) {
|
|
1906
|
+
await this.connect();
|
|
1907
|
+
const client = this.connection.getClient();
|
|
1908
|
+
const correlationId = opts.metadata?.correlationId ?? randomUUID();
|
|
1909
|
+
const reply = replyKey(correlationId);
|
|
1910
|
+
await this.publishInternal({
|
|
1911
|
+
subject: opts.subject,
|
|
1912
|
+
data: opts.data,
|
|
1913
|
+
metadata: {
|
|
1914
|
+
...opts.metadata,
|
|
1915
|
+
correlationId,
|
|
1916
|
+
replyTo: reply
|
|
1917
|
+
}
|
|
1918
|
+
});
|
|
1919
|
+
const timeoutMs = opts.timeout ?? 5e3;
|
|
1920
|
+
const t0 = Date.now();
|
|
1921
|
+
while (Date.now() - t0 < timeoutMs) {
|
|
1922
|
+
const entries = await client.xrange(reply, "-", "+");
|
|
1923
|
+
if (Array.isArray(entries) && entries.length > 0) {
|
|
1924
|
+
const last = entries[entries.length - 1];
|
|
1925
|
+
if (!last) break;
|
|
1926
|
+
const rawFields = last[1];
|
|
1927
|
+
const payloadIdx = rawFields.indexOf("payload");
|
|
1928
|
+
const payload = payloadIdx >= 0 ? rawFields[payloadIdx + 1] : "{}";
|
|
1929
|
+
await client.del(reply);
|
|
1930
|
+
return JSON.parse(payload ?? "{}");
|
|
1931
|
+
}
|
|
1932
|
+
await new Promise((res) => setTimeout(res, 50));
|
|
1933
|
+
}
|
|
1934
|
+
await client.del(reply).catch(() => void 0);
|
|
1935
|
+
throw new Error(`[RedisStreamsEventPublisher] publishWithAck timed out for subject=${opts.subject}`);
|
|
1936
|
+
}
|
|
1937
|
+
/** Internal helper used by the consumer's reply path. */
|
|
1938
|
+
async sendReply(correlationId, data) {
|
|
1939
|
+
await this.connect();
|
|
1940
|
+
const client = this.connection.getClient();
|
|
1941
|
+
const key = replyKey(correlationId);
|
|
1942
|
+
await client.xadd(key, "*", "payload", JSON.stringify(data ?? null));
|
|
1943
|
+
await client.expire(key, 60);
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
|
|
1947
|
+
// src/event-queue/factory/EventQueueFactory.ts
|
|
1948
|
+
function resolveQueueType() {
|
|
1949
|
+
const raw = (process.env["EVENT_QUEUE_TYPE"] ?? process.env["QUEUE_TYPE"] ?? "").toLowerCase();
|
|
1950
|
+
if (raw === "redis-streams" || raw === "redis" || raw === "streams") return QueueType.REDIS_STREAMS;
|
|
1951
|
+
if (raw === "nats") return QueueType.NATS;
|
|
1952
|
+
if (!process.env["NATS_URL"] && !process.env["NATS_SERVERS"] && process.env["REDIS_URL"]) {
|
|
1953
|
+
return QueueType.REDIS_STREAMS;
|
|
1954
|
+
}
|
|
1955
|
+
return QueueType.NATS;
|
|
1956
|
+
}
|
|
1957
|
+
__name(resolveQueueType, "resolveQueueType");
|
|
1958
|
+
var EventQueueFactory = class _EventQueueFactory {
|
|
1959
|
+
static {
|
|
1960
|
+
__name(this, "EventQueueFactory");
|
|
1961
|
+
}
|
|
1962
|
+
/**
|
|
1963
|
+
* Create an event publisher based on configuration
|
|
1964
|
+
*/
|
|
1965
|
+
static createPublisher(config) {
|
|
1966
|
+
logger.info("[EventQueueFactory] Creating event publisher", {
|
|
1967
|
+
type: config.type,
|
|
1968
|
+
url: config.url
|
|
1969
|
+
});
|
|
1970
|
+
if (config.type === QueueType.NATS) {
|
|
1971
|
+
return new NatsEventPublisher(config);
|
|
1972
|
+
}
|
|
1973
|
+
if (config.type === QueueType.REDIS_STREAMS) {
|
|
1974
|
+
return new RedisStreamsEventPublisher(config);
|
|
1975
|
+
}
|
|
1976
|
+
throw new Error(`Unsupported queue type: ${config.type}`);
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Create an event consumer based on configuration
|
|
1980
|
+
*/
|
|
1981
|
+
static createConsumer(config) {
|
|
1982
|
+
logger.info("[EventQueueFactory] Creating event consumer", {
|
|
1983
|
+
type: config.type,
|
|
1984
|
+
url: config.url,
|
|
1985
|
+
stream: config.stream,
|
|
1986
|
+
consumer: config.consumer
|
|
1987
|
+
});
|
|
1988
|
+
if (config.type === QueueType.NATS) {
|
|
1989
|
+
return new NatsEventConsumer(config);
|
|
1990
|
+
}
|
|
1991
|
+
if (config.type === QueueType.REDIS_STREAMS) {
|
|
1992
|
+
return new RedisStreamsEventConsumer(config);
|
|
1993
|
+
}
|
|
1994
|
+
throw new Error(`Unsupported queue type: ${config.type}`);
|
|
1995
|
+
}
|
|
1996
|
+
/**
|
|
1997
|
+
* Create a publisher from environment variables
|
|
1998
|
+
*/
|
|
1999
|
+
static createPublisherFromEnv() {
|
|
2000
|
+
const type = resolveQueueType();
|
|
2001
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
2002
|
+
return _EventQueueFactory.createPublisher({
|
|
2003
|
+
type,
|
|
2004
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
2005
|
+
password: process.env["REDIS_PASSWORD"],
|
|
2006
|
+
name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? "be-sdk-publisher",
|
|
2007
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
2008
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] ?? "normal"
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
const config = {
|
|
2012
|
+
type,
|
|
2013
|
+
url: process.env["NATS_URL"] || "nats://localhost:4222",
|
|
2014
|
+
user: process.env["NATS_USER"],
|
|
2015
|
+
password: process.env["NATS_PASSWORD"],
|
|
2016
|
+
token: process.env["NATS_TOKEN"],
|
|
2017
|
+
creds: process.env["NATS_CREDS"],
|
|
2018
|
+
credsFile: process.env["NATS_CREDS_FILE"],
|
|
2019
|
+
name: process.env["QUEUE_CLIENT_NAME"] || "workspaces-backend-publisher",
|
|
2020
|
+
maxReconnectAttempts: process.env["NATS_MAX_RECONNECT_ATTEMPTS"] ? parseInt(process.env["NATS_MAX_RECONNECT_ATTEMPTS"], 10) : -1,
|
|
2021
|
+
reconnectTimeWait: process.env["NATS_RECONNECT_TIME_WAIT"] ? parseInt(process.env["NATS_RECONNECT_TIME_WAIT"], 10) : 2e3,
|
|
2022
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
2023
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] || "normal",
|
|
2024
|
+
defaultTTL: process.env["QUEUE_DEFAULT_TTL"] ? parseInt(process.env["QUEUE_DEFAULT_TTL"], 10) : void 0
|
|
2025
|
+
};
|
|
2026
|
+
return _EventQueueFactory.createPublisher(config);
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Create a consumer from environment variables
|
|
2030
|
+
*/
|
|
2031
|
+
static createConsumerFromEnv(stream, consumer) {
|
|
2032
|
+
const type = resolveQueueType();
|
|
2033
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
2034
|
+
return _EventQueueFactory.createConsumer({
|
|
2035
|
+
type,
|
|
2036
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
2037
|
+
password: process.env["REDIS_PASSWORD"],
|
|
2038
|
+
name: process.env["QUEUE_CLIENT_NAME"] ?? process.env["SERVICE_NAME"] ?? consumer,
|
|
2039
|
+
stream,
|
|
2040
|
+
consumer,
|
|
2041
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
2042
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
2043
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
2044
|
+
});
|
|
2045
|
+
}
|
|
2046
|
+
const config = {
|
|
2047
|
+
type,
|
|
2048
|
+
url: process.env["NATS_URL"] || "nats://localhost:4222",
|
|
2049
|
+
user: process.env["NATS_USER"],
|
|
2050
|
+
password: process.env["NATS_PASSWORD"],
|
|
2051
|
+
token: process.env["NATS_TOKEN"],
|
|
2052
|
+
creds: process.env["NATS_CREDS"],
|
|
2053
|
+
credsFile: process.env["NATS_CREDS_FILE"],
|
|
2054
|
+
name: process.env["QUEUE_CLIENT_NAME"] || "workspaces-workers-consumer",
|
|
2055
|
+
maxReconnectAttempts: process.env["NATS_MAX_RECONNECT_ATTEMPTS"] ? parseInt(process.env["NATS_MAX_RECONNECT_ATTEMPTS"], 10) : -1,
|
|
2056
|
+
reconnectTimeWait: process.env["NATS_RECONNECT_TIME_WAIT"] ? parseInt(process.env["NATS_RECONNECT_TIME_WAIT"], 10) : 2e3,
|
|
2057
|
+
stream,
|
|
2058
|
+
consumer,
|
|
2059
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
2060
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
2061
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
2062
|
+
};
|
|
2063
|
+
return _EventQueueFactory.createConsumer(config);
|
|
2064
|
+
}
|
|
2065
|
+
// ========================================================================
|
|
2066
|
+
// Layer-Aware Factory Methods
|
|
2067
|
+
// ========================================================================
|
|
2068
|
+
/**
|
|
2069
|
+
* Create a publisher for a specific layer
|
|
2070
|
+
*
|
|
2071
|
+
* @param layer - Service layer ('wspace' or 'global')
|
|
2072
|
+
* @param serviceName - Service name for identification
|
|
2073
|
+
* @returns Publisher configured for the layer
|
|
2074
|
+
*
|
|
2075
|
+
* @example
|
|
2076
|
+
* ```typescript
|
|
2077
|
+
* const publisher = EventQueueFactory.createPublisherForLayer('wspace', 'wspace-tags-svc');
|
|
2078
|
+
* await publisher.connect();
|
|
2079
|
+
* await publisher.publish({ subject: 'wspace.tags.created', data: { id: '123' } });
|
|
2080
|
+
* ```
|
|
2081
|
+
*/
|
|
2082
|
+
static createPublisherForLayer(layer, serviceName) {
|
|
2083
|
+
const envPrefix = layer.toUpperCase();
|
|
2084
|
+
const defaults = DEFAULT_LAYER_RESOURCES[layer];
|
|
2085
|
+
const type = resolveQueueType();
|
|
2086
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
2087
|
+
return _EventQueueFactory.createPublisher({
|
|
2088
|
+
type,
|
|
2089
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
2090
|
+
password: process.env["REDIS_PASSWORD"],
|
|
2091
|
+
name: serviceName,
|
|
2092
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
2093
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] || "normal"
|
|
2094
|
+
});
|
|
2095
|
+
}
|
|
2096
|
+
const config = {
|
|
2097
|
+
type,
|
|
2098
|
+
url: process.env[`${envPrefix}_NATS_URL`] || process.env["NATS_URL"] || defaults.natsUrl,
|
|
2099
|
+
user: process.env["NATS_USER"],
|
|
2100
|
+
password: process.env["NATS_PASSWORD"],
|
|
2101
|
+
token: process.env["NATS_TOKEN"],
|
|
2102
|
+
creds: process.env["NATS_CREDS"],
|
|
2103
|
+
credsFile: process.env["NATS_CREDS_FILE"],
|
|
2104
|
+
name: serviceName,
|
|
2105
|
+
maxReconnectAttempts: process.env["NATS_MAX_RECONNECT_ATTEMPTS"] ? parseInt(process.env["NATS_MAX_RECONNECT_ATTEMPTS"], 10) : -1,
|
|
2106
|
+
reconnectTimeWait: process.env["NATS_RECONNECT_TIME_WAIT"] ? parseInt(process.env["NATS_RECONNECT_TIME_WAIT"], 10) : 2e3,
|
|
2107
|
+
enableDeduplication: process.env["QUEUE_ENABLE_DEDUPLICATION"] === "true",
|
|
2108
|
+
defaultPriority: process.env["QUEUE_DEFAULT_PRIORITY"] || "normal",
|
|
2109
|
+
defaultTTL: process.env["QUEUE_DEFAULT_TTL"] ? parseInt(process.env["QUEUE_DEFAULT_TTL"], 10) : void 0
|
|
2110
|
+
};
|
|
2111
|
+
logger.info("[EventQueueFactory] Creating layer publisher", {
|
|
2112
|
+
layer,
|
|
2113
|
+
serviceName,
|
|
2114
|
+
url: config.url
|
|
2115
|
+
});
|
|
2116
|
+
return _EventQueueFactory.createPublisher(config);
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Create a consumer for a specific layer
|
|
2120
|
+
*
|
|
2121
|
+
* @param layer - Service layer ('wspace' or 'global')
|
|
2122
|
+
* @param serviceName - Service name for identification
|
|
2123
|
+
* @param stream - NATS stream name
|
|
2124
|
+
* @param consumer - Consumer name
|
|
2125
|
+
* @returns Consumer configured for the layer
|
|
2126
|
+
*
|
|
2127
|
+
* @example
|
|
2128
|
+
* ```typescript
|
|
2129
|
+
* const consumer = EventQueueFactory.createConsumerForLayer(
|
|
2130
|
+
* 'wspace', 'wspace-tags-svc', 'WSPACE_EVENTS', 'tags-consumer'
|
|
2131
|
+
* );
|
|
2132
|
+
* await consumer.connect();
|
|
2133
|
+
* await consumer.consume(async (msg) => { console.log(msg); });
|
|
2134
|
+
* ```
|
|
2135
|
+
*/
|
|
2136
|
+
static createConsumerForLayer(layer, serviceName, stream, consumer) {
|
|
2137
|
+
const envPrefix = layer.toUpperCase();
|
|
2138
|
+
const defaults = DEFAULT_LAYER_RESOURCES[layer];
|
|
2139
|
+
const type = resolveQueueType();
|
|
2140
|
+
if (type === QueueType.REDIS_STREAMS) {
|
|
2141
|
+
return _EventQueueFactory.createConsumer({
|
|
2142
|
+
type,
|
|
2143
|
+
url: process.env["REDIS_URL"] ?? "redis://localhost:6379",
|
|
2144
|
+
password: process.env["REDIS_PASSWORD"],
|
|
2145
|
+
name: serviceName,
|
|
2146
|
+
stream,
|
|
2147
|
+
consumer,
|
|
2148
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
2149
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
2150
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
const config = {
|
|
2154
|
+
type,
|
|
2155
|
+
url: process.env[`${envPrefix}_NATS_URL`] || process.env["NATS_URL"] || defaults.natsUrl,
|
|
2156
|
+
user: process.env["NATS_USER"],
|
|
2157
|
+
password: process.env["NATS_PASSWORD"],
|
|
2158
|
+
token: process.env["NATS_TOKEN"],
|
|
2159
|
+
creds: process.env["NATS_CREDS"],
|
|
2160
|
+
credsFile: process.env["NATS_CREDS_FILE"],
|
|
2161
|
+
name: serviceName,
|
|
2162
|
+
maxReconnectAttempts: process.env["NATS_MAX_RECONNECT_ATTEMPTS"] ? parseInt(process.env["NATS_MAX_RECONNECT_ATTEMPTS"], 10) : -1,
|
|
2163
|
+
reconnectTimeWait: process.env["NATS_RECONNECT_TIME_WAIT"] ? parseInt(process.env["NATS_RECONNECT_TIME_WAIT"], 10) : 2e3,
|
|
2164
|
+
stream,
|
|
2165
|
+
consumer,
|
|
2166
|
+
maxConcurrent: process.env["QUEUE_MAX_CONCURRENT"] ? parseInt(process.env["QUEUE_MAX_CONCURRENT"], 10) : 5,
|
|
2167
|
+
maxDeliver: process.env["QUEUE_MAX_DELIVER"] ? parseInt(process.env["QUEUE_MAX_DELIVER"], 10) : 3,
|
|
2168
|
+
ackWait: process.env["QUEUE_ACK_WAIT"] ? parseInt(process.env["QUEUE_ACK_WAIT"], 10) * 1e9 : 3e10
|
|
2169
|
+
};
|
|
2170
|
+
logger.info("[EventQueueFactory] Creating layer consumer", {
|
|
2171
|
+
layer,
|
|
2172
|
+
serviceName,
|
|
2173
|
+
stream,
|
|
2174
|
+
consumer,
|
|
2175
|
+
url: config.url
|
|
2176
|
+
});
|
|
2177
|
+
return _EventQueueFactory.createConsumer(config);
|
|
2178
|
+
}
|
|
2179
|
+
};
|
|
2180
|
+
|
|
2181
|
+
export { EventQueueFactory, NatsEventConsumer, NatsEventPublisher, RedisStreamsConnection, RedisStreamsEventConsumer, RedisStreamsEventPublisher, dlqKey, streamKey };
|
|
2182
|
+
//# sourceMappingURL=chunk-BQRXB4CW.js.map
|
|
2183
|
+
//# sourceMappingURL=chunk-BQRXB4CW.js.map
|