@mastra/redis-streams 0.3.0-alpha.0 → 0.3.1-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -0
- package/dist/index.cjs +551 -596
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +549 -593
- package/dist/index.js.map +1 -1
- package/package.json +16 -15
package/dist/index.cjs
CHANGED
|
@@ -1,623 +1,578 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
let crypto = require("crypto");
|
|
3
|
+
let _mastra_core_events = require("@mastra/core/events");
|
|
4
|
+
let redis = require("redis");
|
|
5
|
+
//#region src/index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Flatten an error into searchable text. node-redis MULTI failures throw a
|
|
8
|
+
* `MultiErrorReply` whose own message is just "N commands failed…" — the real
|
|
9
|
+
* per-command errors (e.g. BUSYGROUP) live in `err.replies`, so those are
|
|
10
|
+
* folded in too.
|
|
11
|
+
*/
|
|
8
12
|
function errorText(err) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
return parts.join("; ");
|
|
13
|
+
const parts = [err instanceof Error ? err.message : String(err)];
|
|
14
|
+
if (err instanceof Error && "replies" in err && Array.isArray(err.replies)) parts.push(...err.replies.map((r) => String(r)));
|
|
15
|
+
return parts.join("; ");
|
|
14
16
|
}
|
|
15
|
-
var RedisStreamsPubSub = class extends
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
topic,
|
|
314
|
-
err: err instanceof Error ? err.message : err
|
|
315
|
-
});
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
/**
|
|
319
|
-
* Lease key used in Redis. Distinct prefix from streams so leases and
|
|
320
|
-
* streams can't collide on key namespace.
|
|
321
|
-
*/
|
|
322
|
-
#leaseKey(key) {
|
|
323
|
-
return `${this.#keyPrefix}:lease:${key}`;
|
|
324
|
-
}
|
|
325
|
-
/**
|
|
326
|
-
* Atomic claim via SET NX PX. Idempotent for the same owner: if the
|
|
327
|
-
* current value is already this owner, we refresh the TTL instead of
|
|
328
|
-
* failing. Cross-process callers race here; Redis serializes them.
|
|
329
|
-
*/
|
|
330
|
-
async acquireLease(key, owner, ttlMs) {
|
|
331
|
-
if (this.#closed) return { acquired: false };
|
|
332
|
-
await this.#ensureWriterConnected();
|
|
333
|
-
const redisKey = this.#leaseKey(key);
|
|
334
|
-
const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });
|
|
335
|
-
if (result === "OK") return { acquired: true, owner };
|
|
336
|
-
const script = `
|
|
17
|
+
var RedisStreamsPubSub = class extends _mastra_core_events.PubSub {
|
|
18
|
+
get supportedModes() {
|
|
19
|
+
return ["pull"];
|
|
20
|
+
}
|
|
21
|
+
#writeClient;
|
|
22
|
+
#connectOptions;
|
|
23
|
+
#keyPrefix;
|
|
24
|
+
#blockMs;
|
|
25
|
+
#maxStreamLength;
|
|
26
|
+
#streamIdleTtlMs;
|
|
27
|
+
#reclaimIntervalMs;
|
|
28
|
+
#reclaimIdleMs;
|
|
29
|
+
#maxDeliveryAttempts;
|
|
30
|
+
#logger;
|
|
31
|
+
#subscriptions = /* @__PURE__ */ new Map();
|
|
32
|
+
#cbIds = /* @__PURE__ */ new WeakMap();
|
|
33
|
+
#pendingPublishes = /* @__PURE__ */ new Set();
|
|
34
|
+
#localCallbacks = /* @__PURE__ */ new Map();
|
|
35
|
+
#closed = false;
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
super();
|
|
38
|
+
const url = options.url ?? options.redisOptions?.url ?? "redis://localhost:6379";
|
|
39
|
+
this.#connectOptions = {
|
|
40
|
+
...options.redisOptions,
|
|
41
|
+
url
|
|
42
|
+
};
|
|
43
|
+
this.#writeClient = (0, redis.createClient)(this.#connectOptions);
|
|
44
|
+
this.#logger = options.logger;
|
|
45
|
+
this.#attachErrorLogger(this.#writeClient, "write");
|
|
46
|
+
this.#keyPrefix = options.keyPrefix ?? "mastra:topic";
|
|
47
|
+
this.#blockMs = options.blockMs ?? 1e3;
|
|
48
|
+
this.#maxStreamLength = options.maxStreamLength ?? 1e4;
|
|
49
|
+
const ttl = options.streamIdleTtlMs ?? 0;
|
|
50
|
+
if (!Number.isInteger(ttl) || ttl < 0) throw new Error(`redis-streams: streamIdleTtlMs must be a non-negative integer (milliseconds), got ${ttl}`);
|
|
51
|
+
this.#streamIdleTtlMs = ttl;
|
|
52
|
+
this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 3e4;
|
|
53
|
+
this.#reclaimIdleMs = options.reclaimIdleMs ?? 6e4;
|
|
54
|
+
const cap = options.maxDeliveryAttempts ?? 5;
|
|
55
|
+
if (cap === 0) {
|
|
56
|
+
options.logger?.warn?.("redis-streams: maxDeliveryAttempts=0 is treated as Infinity for back-compat; pass Infinity to disable the cap explicitly.");
|
|
57
|
+
this.#maxDeliveryAttempts = Infinity;
|
|
58
|
+
} else if (cap < 0 || Number.isNaN(cap)) throw new Error(`redis-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);
|
|
59
|
+
else this.#maxDeliveryAttempts = cap;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Attach the `'error'` listener node-redis requires on every client. Without
|
|
63
|
+
* one, a mid-life socket close makes the client emit an unhandled `'error'`,
|
|
64
|
+
* which (per EventEmitter semantics) throws from inside RedisSocket's error
|
|
65
|
+
* handler BEFORE the built-in reconnect is scheduled — the process gets an
|
|
66
|
+
* uncaughtException and the client is left permanently un-reconnected
|
|
67
|
+
* (isOpen=true, isReady=false), hanging every subsequent command. Merely
|
|
68
|
+
* having a listener lets node-redis's own reconnect run.
|
|
69
|
+
*
|
|
70
|
+
* Logged at warn so operators can see connection churn, throttled to one line
|
|
71
|
+
* per 30s per client: node-redis re-emits `'error'` roughly every 500ms while
|
|
72
|
+
* a server is unreachable, and with one reader client per subscription an
|
|
73
|
+
* outage would otherwise flood the logs with thousands of identical lines.
|
|
74
|
+
*/
|
|
75
|
+
#attachErrorLogger(client, role, extra = {}) {
|
|
76
|
+
let lastWarnAt = 0;
|
|
77
|
+
client.on("error", (err) => {
|
|
78
|
+
const now = Date.now();
|
|
79
|
+
if (now - lastWarnAt < 3e4) return;
|
|
80
|
+
lastWarnAt = now;
|
|
81
|
+
this.#logger?.warn?.(`redis-streams: ${role} client connection error (node-redis will reconnect)`, {
|
|
82
|
+
...extra,
|
|
83
|
+
err: err instanceof Error ? err.message : err
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
#subKey(topic, cb) {
|
|
88
|
+
let cbId = this.#cbIds.get(cb);
|
|
89
|
+
if (!cbId) {
|
|
90
|
+
cbId = (0, crypto.randomUUID)();
|
|
91
|
+
this.#cbIds.set(cb, cbId);
|
|
92
|
+
}
|
|
93
|
+
return `${topic}::${cbId}`;
|
|
94
|
+
}
|
|
95
|
+
/** Lazily connect the shared writer client. Idempotent. */
|
|
96
|
+
async #ensureWriterConnected() {
|
|
97
|
+
if (this.#writeClient.isOpen) return;
|
|
98
|
+
await this.#writeClient.connect();
|
|
99
|
+
}
|
|
100
|
+
#streamKey(topic) {
|
|
101
|
+
return `${this.#keyPrefix}:${topic}`;
|
|
102
|
+
}
|
|
103
|
+
async publish(topic, event, options) {
|
|
104
|
+
if (this.#closed) throw new Error("RedisStreamsPubSub: cannot publish on closed client");
|
|
105
|
+
if (options?.localOnly) {
|
|
106
|
+
const localEvent = {
|
|
107
|
+
...event,
|
|
108
|
+
id: (0, crypto.randomUUID)(),
|
|
109
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
110
|
+
deliveryAttempt: event.deliveryAttempt ?? 1
|
|
111
|
+
};
|
|
112
|
+
this.#deliverLocal(topic, localEvent);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
await this.#ensureWriterConnected();
|
|
116
|
+
const id = (0, crypto.randomUUID)();
|
|
117
|
+
const createdAt = /* @__PURE__ */ new Date();
|
|
118
|
+
const payload = {
|
|
119
|
+
...event,
|
|
120
|
+
id,
|
|
121
|
+
createdAt,
|
|
122
|
+
deliveryAttempt: event.deliveryAttempt ?? 1
|
|
123
|
+
};
|
|
124
|
+
const xaddOptions = {};
|
|
125
|
+
if (this.#maxStreamLength > 0) xaddOptions.TRIM = {
|
|
126
|
+
strategy: "MAXLEN",
|
|
127
|
+
strategyModifier: "~",
|
|
128
|
+
threshold: this.#maxStreamLength
|
|
129
|
+
};
|
|
130
|
+
const streamKey = this.#streamKey(topic);
|
|
131
|
+
const promise = this.#streamIdleTtlMs > 0 ? this.#writeClient.multi().xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions).pExpire(streamKey, this.#streamIdleTtlMs).exec().then((replies) => {
|
|
132
|
+
if (Number(replies[1]) !== 1) this.#logger?.warn?.("redis-streams: PEXPIRE inside publish MULTI did not apply", {
|
|
133
|
+
streamKey,
|
|
134
|
+
reply: String(replies[1])
|
|
135
|
+
});
|
|
136
|
+
}) : this.#writeClient.xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions);
|
|
137
|
+
this.#pendingPublishes.add(promise);
|
|
138
|
+
try {
|
|
139
|
+
await promise;
|
|
140
|
+
} finally {
|
|
141
|
+
this.#pendingPublishes.delete(promise);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async subscribe(topic, cb, options) {
|
|
145
|
+
if (this.#closed) throw new Error("RedisStreamsPubSub: cannot subscribe on closed client");
|
|
146
|
+
const key = this.#subKey(topic, cb);
|
|
147
|
+
if (this.#subscriptions.has(key)) return;
|
|
148
|
+
let localBucket = this.#localCallbacks.get(topic);
|
|
149
|
+
if (!localBucket) {
|
|
150
|
+
localBucket = /* @__PURE__ */ new Set();
|
|
151
|
+
this.#localCallbacks.set(topic, localBucket);
|
|
152
|
+
}
|
|
153
|
+
localBucket.add(cb);
|
|
154
|
+
await this.#ensureWriterConnected();
|
|
155
|
+
const isGrouped = !!options?.group;
|
|
156
|
+
const group = options?.group ?? `__fanout-${(0, crypto.randomUUID)()}`;
|
|
157
|
+
const consumer = `${group}-${(0, crypto.randomUUID)()}`;
|
|
158
|
+
const streamKey = this.#streamKey(topic);
|
|
159
|
+
try {
|
|
160
|
+
if (this.#streamIdleTtlMs > 0) await this.#writeClient.multi().xGroupCreate(streamKey, group, "0", { MKSTREAM: true }).pExpire(streamKey, this.#streamIdleTtlMs).exec();
|
|
161
|
+
else await this.#writeClient.xGroupCreate(streamKey, group, "0", { MKSTREAM: true });
|
|
162
|
+
} catch (err) {
|
|
163
|
+
if (!errorText(err).includes("BUSYGROUP")) throw err;
|
|
164
|
+
this.#logger?.debug?.("redis-streams: consumer group already exists", {
|
|
165
|
+
topic,
|
|
166
|
+
group
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
const readClient = (0, redis.createClient)(this.#connectOptions);
|
|
170
|
+
this.#attachErrorLogger(readClient, "read", { topic });
|
|
171
|
+
await readClient.connect();
|
|
172
|
+
const sub = {
|
|
173
|
+
cb,
|
|
174
|
+
topic,
|
|
175
|
+
streamKey,
|
|
176
|
+
group,
|
|
177
|
+
consumer,
|
|
178
|
+
isGrouped,
|
|
179
|
+
readClient,
|
|
180
|
+
stopped: false,
|
|
181
|
+
loop: void 0,
|
|
182
|
+
reclaimTimer: void 0
|
|
183
|
+
};
|
|
184
|
+
this.#subscriptions.set(key, sub);
|
|
185
|
+
sub.loop = this.#runReadLoop(sub);
|
|
186
|
+
this.#startReclaimLoop(sub);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Periodically run XAUTOCLAIM against this subscription's group so that
|
|
190
|
+
* messages a crashed/stuck consumer read but never acked get redelivered to
|
|
191
|
+
* a live sibling. Runs only for grouped subscriptions — fan-out groups are
|
|
192
|
+
* private to one consumer, so there's no sibling to claim from.
|
|
193
|
+
*/
|
|
194
|
+
#startReclaimLoop(sub) {
|
|
195
|
+
if (this.#reclaimIntervalMs <= 0) return;
|
|
196
|
+
if (!sub.isGrouped) return;
|
|
197
|
+
const tick = async () => {
|
|
198
|
+
if (sub.stopped || this.#closed) return;
|
|
199
|
+
try {
|
|
200
|
+
const messages = (await this.#writeClient.xAutoClaim(sub.streamKey, sub.group, sub.consumer, this.#reclaimIdleMs, "0-0", { COUNT: 100 }))?.messages ?? [];
|
|
201
|
+
for (const entry of messages) {
|
|
202
|
+
if (sub.stopped || this.#closed) return;
|
|
203
|
+
if (!entry) continue;
|
|
204
|
+
await this.#deliverMessage(sub, entry.id, entry.message);
|
|
205
|
+
}
|
|
206
|
+
} catch (err) {
|
|
207
|
+
this.#logger?.debug?.("redis-streams: XAUTOCLAIM failed", {
|
|
208
|
+
topic: sub.topic,
|
|
209
|
+
group: sub.group,
|
|
210
|
+
err: err instanceof Error ? err.message : err
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
if (sub.stopped || this.#closed) return;
|
|
214
|
+
sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
|
|
215
|
+
};
|
|
216
|
+
sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
|
|
217
|
+
}
|
|
218
|
+
async unsubscribe(topic, cb) {
|
|
219
|
+
const localBucket = this.#localCallbacks.get(topic);
|
|
220
|
+
if (localBucket) {
|
|
221
|
+
localBucket.delete(cb);
|
|
222
|
+
if (localBucket.size === 0) this.#localCallbacks.delete(topic);
|
|
223
|
+
}
|
|
224
|
+
const key = this.#subKey(topic, cb);
|
|
225
|
+
const sub = this.#subscriptions.get(key);
|
|
226
|
+
if (!sub) return;
|
|
227
|
+
this.#subscriptions.delete(key);
|
|
228
|
+
sub.stopped = true;
|
|
229
|
+
if (sub.reclaimTimer) {
|
|
230
|
+
clearTimeout(sub.reclaimTimer);
|
|
231
|
+
sub.reclaimTimer = void 0;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
await sub.readClient.quit();
|
|
235
|
+
} catch (err) {
|
|
236
|
+
this.#logger?.debug?.("redis-streams: reader quit failed", {
|
|
237
|
+
topic: sub.topic,
|
|
238
|
+
err: err instanceof Error ? err.message : err
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (sub.loop) try {
|
|
242
|
+
await sub.loop;
|
|
243
|
+
} catch (err) {
|
|
244
|
+
this.#logger?.debug?.("redis-streams: read loop exited with error", {
|
|
245
|
+
topic: sub.topic,
|
|
246
|
+
err: err instanceof Error ? err.message : err
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (!sub.isGrouped) try {
|
|
250
|
+
await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
this.#logger?.debug?.("redis-streams: xGroupDestroy failed", {
|
|
253
|
+
topic: sub.topic,
|
|
254
|
+
group: sub.group,
|
|
255
|
+
err: err instanceof Error ? err.message : err
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async flush() {
|
|
260
|
+
if (this.#pendingPublishes.size > 0) await Promise.allSettled([...this.#pendingPublishes]);
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Delete a topic's stream, and with it every consumer group on that stream.
|
|
264
|
+
* This is the Redis implementation of the optional `clearTopic` hook that
|
|
265
|
+
* consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is
|
|
266
|
+
* over — without it, a persistent backend accumulates every finished run's
|
|
267
|
+
* stream until Redis exhausts its memory.
|
|
268
|
+
*
|
|
269
|
+
* Because it removes events for ALL consumers and destroys their groups, only
|
|
270
|
+
* call it once nothing will read the topic again. A subscriber still attached
|
|
271
|
+
* when the stream is deleted recovers on its own (the read loop recreates the
|
|
272
|
+
* group on NOGROUP) but will have missed the deleted entries.
|
|
273
|
+
*
|
|
274
|
+
* Best-effort and non-throwing: callers commonly invoke this fire-and-forget
|
|
275
|
+
* (`void clearTopic(...)`), so a rejection would surface as an
|
|
276
|
+
* unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`
|
|
277
|
+
* backstop (when configured) reclaims anything a failed delete leaves behind.
|
|
278
|
+
*/
|
|
279
|
+
async clearTopic(topic) {
|
|
280
|
+
if (this.#closed) return;
|
|
281
|
+
try {
|
|
282
|
+
await this.#ensureWriterConnected();
|
|
283
|
+
await this.#writeClient.del(this.#streamKey(topic));
|
|
284
|
+
} catch (err) {
|
|
285
|
+
this.#logger?.warn?.("redis-streams: clearTopic failed", {
|
|
286
|
+
topic,
|
|
287
|
+
err: err instanceof Error ? err.message : err
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Lease key used in Redis. Distinct prefix from streams so leases and
|
|
293
|
+
* streams can't collide on key namespace.
|
|
294
|
+
*/
|
|
295
|
+
#leaseKey(key) {
|
|
296
|
+
return `${this.#keyPrefix}:lease:${key}`;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Atomic claim via SET NX PX. Idempotent for the same owner: if the
|
|
300
|
+
* current value is already this owner, we refresh the TTL instead of
|
|
301
|
+
* failing. Cross-process callers race here; Redis serializes them.
|
|
302
|
+
*/
|
|
303
|
+
async acquireLease(key, owner, ttlMs) {
|
|
304
|
+
if (this.#closed) return { acquired: false };
|
|
305
|
+
await this.#ensureWriterConnected();
|
|
306
|
+
const redisKey = this.#leaseKey(key);
|
|
307
|
+
if (await this.#writeClient.set(redisKey, owner, {
|
|
308
|
+
NX: true,
|
|
309
|
+
PX: ttlMs
|
|
310
|
+
}) === "OK") return {
|
|
311
|
+
acquired: true,
|
|
312
|
+
owner
|
|
313
|
+
};
|
|
314
|
+
if (await this.#writeClient.eval(`
|
|
337
315
|
local current = redis.call("GET", KEYS[1])
|
|
338
316
|
if current == ARGV[1] then
|
|
339
317
|
redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
|
340
318
|
return 1
|
|
341
319
|
end
|
|
342
320
|
return 0
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
321
|
+
`, {
|
|
322
|
+
keys: [redisKey],
|
|
323
|
+
arguments: [owner, String(ttlMs)]
|
|
324
|
+
}) === 1) return {
|
|
325
|
+
acquired: true,
|
|
326
|
+
owner
|
|
327
|
+
};
|
|
328
|
+
return {
|
|
329
|
+
acquired: false,
|
|
330
|
+
owner: await this.#writeClient.get(redisKey) ?? void 0
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
async getLeaseOwner(key) {
|
|
334
|
+
if (this.#closed) return void 0;
|
|
335
|
+
await this.#ensureWriterConnected();
|
|
336
|
+
return await this.#writeClient.get(this.#leaseKey(key)) ?? void 0;
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Release only if we still own it. Implemented as GET+DEL with a Lua
|
|
340
|
+
* script so the check-and-delete is atomic against concurrent renewals
|
|
341
|
+
* from other processes.
|
|
342
|
+
*/
|
|
343
|
+
async releaseLease(key, owner) {
|
|
344
|
+
if (this.#closed) return;
|
|
345
|
+
await this.#ensureWriterConnected();
|
|
346
|
+
await this.#writeClient.eval(`
|
|
366
347
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
367
348
|
return redis.call("DEL", KEYS[1])
|
|
368
349
|
else
|
|
369
350
|
return 0
|
|
370
351
|
end
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const script = `
|
|
352
|
+
`, {
|
|
353
|
+
keys: [this.#leaseKey(key)],
|
|
354
|
+
arguments: [owner]
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Extend the TTL only if we still own the lease. Returns false if the
|
|
359
|
+
* lease was lost (expired or another owner took it).
|
|
360
|
+
*/
|
|
361
|
+
async renewLease(key, owner, ttlMs) {
|
|
362
|
+
if (this.#closed) return false;
|
|
363
|
+
await this.#ensureWriterConnected();
|
|
364
|
+
return await this.#writeClient.eval(`
|
|
385
365
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
386
366
|
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
|
|
387
367
|
else
|
|
388
368
|
return 0
|
|
389
369
|
end
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
await this.#ensureWriterConnected();
|
|
409
|
-
const script = `
|
|
370
|
+
`, {
|
|
371
|
+
keys: [this.#leaseKey(key)],
|
|
372
|
+
arguments: [owner, String(ttlMs)]
|
|
373
|
+
}) === 1;
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the
|
|
377
|
+
* TTL to the full `ttlMs`, without the key ever going empty.
|
|
378
|
+
*
|
|
379
|
+
* Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)
|
|
380
|
+
* so a racing process cannot win the key between a release and a re-acquire.
|
|
381
|
+
* Returns false if `fromOwner` no longer holds the lease (expired or taken),
|
|
382
|
+
* in which case the caller should fall back to a fresh `acquireLease`.
|
|
383
|
+
*/
|
|
384
|
+
async transferLease(key, fromOwner, toOwner, ttlMs) {
|
|
385
|
+
if (this.#closed) return false;
|
|
386
|
+
await this.#ensureWriterConnected();
|
|
387
|
+
return await this.#writeClient.eval(`
|
|
410
388
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
|
411
389
|
redis.call("SET", KEYS[1], ARGV[2], "PX", ARGV[3])
|
|
412
390
|
return 1
|
|
413
391
|
else
|
|
414
392
|
return 0
|
|
415
393
|
end
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
const nack = async () => {
|
|
597
|
-
};
|
|
598
|
-
try {
|
|
599
|
-
const result = cb(
|
|
600
|
-
event,
|
|
601
|
-
ack,
|
|
602
|
-
nack
|
|
603
|
-
);
|
|
604
|
-
if (result && typeof result.catch === "function") {
|
|
605
|
-
result.catch((err) => {
|
|
606
|
-
this.#logger?.debug?.("redis-streams: local subscriber rejected", {
|
|
607
|
-
topic,
|
|
608
|
-
err: err instanceof Error ? err.message : err
|
|
609
|
-
});
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
} catch (err) {
|
|
613
|
-
this.#logger?.debug?.("redis-streams: local subscriber threw", {
|
|
614
|
-
topic,
|
|
615
|
-
err: err instanceof Error ? err.message : err
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
}
|
|
394
|
+
`, {
|
|
395
|
+
keys: [this.#leaseKey(key)],
|
|
396
|
+
arguments: [
|
|
397
|
+
fromOwner,
|
|
398
|
+
toOwner,
|
|
399
|
+
String(ttlMs)
|
|
400
|
+
]
|
|
401
|
+
}) === 1;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Disconnect all clients and stop all subscription loops.
|
|
405
|
+
*/
|
|
406
|
+
async close() {
|
|
407
|
+
if (this.#closed) return;
|
|
408
|
+
this.#closed = true;
|
|
409
|
+
const subs = [...this.#subscriptions.values()];
|
|
410
|
+
await Promise.all(subs.map((sub) => this.unsubscribe(sub.topic, sub.cb)));
|
|
411
|
+
this.#localCallbacks.clear();
|
|
412
|
+
if (this.#writeClient.isOpen) try {
|
|
413
|
+
await this.#writeClient.quit();
|
|
414
|
+
} catch (err) {
|
|
415
|
+
this.#logger?.debug?.("redis-streams: writer quit failed", { err: err instanceof Error ? err.message : err });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
async #runReadLoop(sub) {
|
|
419
|
+
while (!sub.stopped) {
|
|
420
|
+
let result;
|
|
421
|
+
try {
|
|
422
|
+
result = await sub.readClient.xReadGroup(sub.group, sub.consumer, [{
|
|
423
|
+
key: sub.streamKey,
|
|
424
|
+
id: ">"
|
|
425
|
+
}], {
|
|
426
|
+
COUNT: 10,
|
|
427
|
+
BLOCK: this.#blockMs
|
|
428
|
+
});
|
|
429
|
+
} catch (err) {
|
|
430
|
+
if (sub.stopped) return;
|
|
431
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
432
|
+
if (msg.includes("NOGROUP")) try {
|
|
433
|
+
if (this.#streamIdleTtlMs > 0) await this.#writeClient.multi().xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
|
|
434
|
+
else await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true });
|
|
435
|
+
this.#logger?.debug?.("redis-streams: recreated consumer group after NOGROUP", {
|
|
436
|
+
topic: sub.topic,
|
|
437
|
+
group: sub.group
|
|
438
|
+
});
|
|
439
|
+
} catch (recreateErr) {
|
|
440
|
+
const rmsg = errorText(recreateErr);
|
|
441
|
+
if (!rmsg.includes("BUSYGROUP")) this.#logger?.debug?.("redis-streams: consumer group re-create failed", {
|
|
442
|
+
topic: sub.topic,
|
|
443
|
+
err: rmsg
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
else this.#logger?.debug?.("redis-streams: xReadGroup failed", {
|
|
447
|
+
topic: sub.topic,
|
|
448
|
+
group: sub.group,
|
|
449
|
+
err: msg
|
|
450
|
+
});
|
|
451
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (!result || result.length === 0) continue;
|
|
455
|
+
for (const stream of result) for (const entry of stream.messages) {
|
|
456
|
+
if (sub.stopped) return;
|
|
457
|
+
await this.#deliverMessage(sub, entry.id, entry.message);
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async #deliverMessage(sub, streamId, fields) {
|
|
462
|
+
let event;
|
|
463
|
+
try {
|
|
464
|
+
event = JSON.parse(fields.event ?? "{}");
|
|
465
|
+
if (typeof event.createdAt === "string") event.createdAt = new Date(event.createdAt);
|
|
466
|
+
} catch (err) {
|
|
467
|
+
this.#logger?.debug?.("redis-streams: malformed payload, dropping", {
|
|
468
|
+
topic: sub.topic,
|
|
469
|
+
streamId,
|
|
470
|
+
err: err instanceof Error ? err.message : err
|
|
471
|
+
});
|
|
472
|
+
try {
|
|
473
|
+
await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
|
|
474
|
+
} catch (ackErr) {
|
|
475
|
+
this.#logger?.debug?.("redis-streams: xAck after malformed payload failed", {
|
|
476
|
+
topic: sub.topic,
|
|
477
|
+
err: ackErr instanceof Error ? ackErr.message : ackErr
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
let settled = false;
|
|
483
|
+
const ack = async () => {
|
|
484
|
+
if (settled) return;
|
|
485
|
+
settled = true;
|
|
486
|
+
try {
|
|
487
|
+
await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
|
|
488
|
+
} catch (err) {
|
|
489
|
+
this.#logger?.debug?.("redis-streams: ack cleanup failed", {
|
|
490
|
+
topic: sub.topic,
|
|
491
|
+
err: err instanceof Error ? err.message : err
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const nack = async () => {
|
|
496
|
+
if (settled) return;
|
|
497
|
+
settled = true;
|
|
498
|
+
const attempt = event.deliveryAttempt ?? 1;
|
|
499
|
+
if (attempt >= this.#maxDeliveryAttempts) {
|
|
500
|
+
this.#logger?.warn?.("redis-streams: dropping event after max delivery attempts", {
|
|
501
|
+
topic: sub.topic,
|
|
502
|
+
eventType: event.type,
|
|
503
|
+
eventId: event.id,
|
|
504
|
+
attempt,
|
|
505
|
+
max: this.#maxDeliveryAttempts
|
|
506
|
+
});
|
|
507
|
+
try {
|
|
508
|
+
await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
|
|
509
|
+
} catch (err) {
|
|
510
|
+
this.#logger?.debug?.("redis-streams: ack on dropped poison message failed", {
|
|
511
|
+
topic: sub.topic,
|
|
512
|
+
err: err instanceof Error ? err.message : err
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
const next = {
|
|
518
|
+
...event,
|
|
519
|
+
deliveryAttempt: attempt + 1
|
|
520
|
+
};
|
|
521
|
+
try {
|
|
522
|
+
if (this.#streamIdleTtlMs > 0) await this.#writeClient.multi().xAdd(sub.streamKey, "*", { event: JSON.stringify(next) }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
|
|
523
|
+
else await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
|
|
524
|
+
} catch (err) {
|
|
525
|
+
this.#logger?.warn?.("redis-streams: nack republish failed; leaving original pending for reclaim", {
|
|
526
|
+
topic: sub.topic,
|
|
527
|
+
eventId: event.id,
|
|
528
|
+
err: err instanceof Error ? err.message : err
|
|
529
|
+
});
|
|
530
|
+
settled = false;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
try {
|
|
534
|
+
await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
this.#logger?.debug?.("redis-streams: xAck after nack failed", {
|
|
537
|
+
topic: sub.topic,
|
|
538
|
+
err: err instanceof Error ? err.message : err
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
try {
|
|
543
|
+
const result = sub.cb(event, ack, nack);
|
|
544
|
+
if (result && typeof result.catch === "function") result.catch(async () => {
|
|
545
|
+
await nack();
|
|
546
|
+
});
|
|
547
|
+
} catch {
|
|
548
|
+
await nack();
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
#deliverLocal(topic, event) {
|
|
552
|
+
const callbacks = this.#localCallbacks.get(topic);
|
|
553
|
+
if (!callbacks || callbacks.size === 0) return;
|
|
554
|
+
for (const cb of callbacks) this.#invokeLocalCallback(topic, event, cb);
|
|
555
|
+
}
|
|
556
|
+
#invokeLocalCallback(topic, event, cb) {
|
|
557
|
+
const ack = async () => {};
|
|
558
|
+
const nack = async () => {};
|
|
559
|
+
try {
|
|
560
|
+
const result = cb(event, ack, nack);
|
|
561
|
+
if (result && typeof result.catch === "function") result.catch((err) => {
|
|
562
|
+
this.#logger?.debug?.("redis-streams: local subscriber rejected", {
|
|
563
|
+
topic,
|
|
564
|
+
err: err instanceof Error ? err.message : err
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
} catch (err) {
|
|
568
|
+
this.#logger?.debug?.("redis-streams: local subscriber threw", {
|
|
569
|
+
topic,
|
|
570
|
+
err: err instanceof Error ? err.message : err
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
619
574
|
};
|
|
620
|
-
|
|
575
|
+
//#endregion
|
|
621
576
|
exports.RedisStreamsPubSub = RedisStreamsPubSub;
|
|
622
|
-
|
|
577
|
+
|
|
623
578
|
//# sourceMappingURL=index.cjs.map
|