@mastra/redis-streams 0.0.0 → 0.0.2-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/LICENSE.md ADDED
@@ -0,0 +1,30 @@
1
+ Portions of this software are licensed as follows:
2
+
3
+ - All content that resides under any directory named "ee/" within this
4
+ repository, including but not limited to:
5
+ - `packages/core/src/auth/ee/`
6
+ - `packages/server/src/server/auth/ee/`
7
+ is licensed under the license defined in `ee/LICENSE`.
8
+
9
+ - All third-party components incorporated into the Mastra Software are
10
+ licensed under the original license provided by the owner of the
11
+ applicable component.
12
+
13
+ - Content outside of the above-mentioned directories or restrictions is
14
+ available under the "Apache License 2.0" as defined below.
15
+
16
+ # Apache License 2.0
17
+
18
+ Copyright (c) 2025 Kepler Software, Inc.
19
+
20
+ Licensed under the Apache License, Version 2.0 (the "License");
21
+ you may not use this file except in compliance with the License.
22
+ You may obtain a copy of the License at
23
+
24
+ http://www.apache.org/licenses/LICENSE-2.0
25
+
26
+ Unless required by applicable law or agreed to in writing, software
27
+ distributed under the License is distributed on an "AS IS" BASIS,
28
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29
+ See the License for the specific language governing permissions and
30
+ limitations under the License.
package/dist/index.cjs ADDED
@@ -0,0 +1,360 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var events = require('@mastra/core/events');
5
+ var redis = require('redis');
6
+
7
+ // src/index.ts
8
+ var RedisStreamsPubSub = class extends events.PubSub {
9
+ // Redis Streams is a pull transport: consumers issue XREADGROUP to read
10
+ // events. Mastra reads this to know an OrchestrationWorker is required.
11
+ get supportedModes() {
12
+ return ["pull"];
13
+ }
14
+ #writeClient;
15
+ #connectOptions;
16
+ #keyPrefix;
17
+ #blockMs;
18
+ #maxStreamLength;
19
+ #reclaimIntervalMs;
20
+ #reclaimIdleMs;
21
+ #maxDeliveryAttempts;
22
+ #logger;
23
+ // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to
24
+ // multiple topics independently. Without the topic in the key,
25
+ // unsubscribe(otherTopic, cb) would tear down the wrong subscription.
26
+ #subscriptions = /* @__PURE__ */ new Map();
27
+ #cbIds = /* @__PURE__ */ new WeakMap();
28
+ #pendingPublishes = /* @__PURE__ */ new Set();
29
+ #closed = false;
30
+ constructor(options = {}) {
31
+ super();
32
+ const url = options.url ?? options.redisOptions?.url ?? "redis://localhost:6379";
33
+ this.#connectOptions = { ...options.redisOptions, url };
34
+ this.#writeClient = redis.createClient(this.#connectOptions);
35
+ this.#keyPrefix = options.keyPrefix ?? "mastra:topic";
36
+ this.#blockMs = options.blockMs ?? 1e3;
37
+ this.#maxStreamLength = options.maxStreamLength ?? 1e4;
38
+ this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 3e4;
39
+ this.#reclaimIdleMs = options.reclaimIdleMs ?? 6e4;
40
+ const cap = options.maxDeliveryAttempts ?? 5;
41
+ if (cap === 0) {
42
+ options.logger?.warn?.(
43
+ "redis-streams: maxDeliveryAttempts=0 is treated as Infinity for back-compat; pass Infinity to disable the cap explicitly."
44
+ );
45
+ this.#maxDeliveryAttempts = Infinity;
46
+ } else if (cap < 0 || Number.isNaN(cap)) {
47
+ throw new Error(`redis-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);
48
+ } else {
49
+ this.#maxDeliveryAttempts = cap;
50
+ }
51
+ this.#logger = options.logger;
52
+ }
53
+ #subKey(topic, cb) {
54
+ let cbId = this.#cbIds.get(cb);
55
+ if (!cbId) {
56
+ cbId = crypto.randomUUID();
57
+ this.#cbIds.set(cb, cbId);
58
+ }
59
+ return `${topic}::${cbId}`;
60
+ }
61
+ /** Lazily connect the shared writer client. Idempotent. */
62
+ async #ensureWriterConnected() {
63
+ if (this.#writeClient.isOpen) return;
64
+ await this.#writeClient.connect();
65
+ }
66
+ #streamKey(topic) {
67
+ return `${this.#keyPrefix}:${topic}`;
68
+ }
69
+ async publish(topic, event) {
70
+ if (this.#closed) throw new Error("RedisStreamsPubSub: cannot publish on closed client");
71
+ await this.#ensureWriterConnected();
72
+ const id = crypto.randomUUID();
73
+ const createdAt = /* @__PURE__ */ new Date();
74
+ const payload = {
75
+ ...event,
76
+ id,
77
+ createdAt,
78
+ deliveryAttempt: event.deliveryAttempt ?? 1
79
+ };
80
+ const xaddOptions = {};
81
+ if (this.#maxStreamLength > 0) {
82
+ xaddOptions.TRIM = {
83
+ strategy: "MAXLEN",
84
+ strategyModifier: "~",
85
+ threshold: this.#maxStreamLength
86
+ };
87
+ }
88
+ const promise = this.#writeClient.xAdd(
89
+ this.#streamKey(topic),
90
+ "*",
91
+ { event: JSON.stringify(payload) },
92
+ xaddOptions
93
+ );
94
+ this.#pendingPublishes.add(promise);
95
+ try {
96
+ await promise;
97
+ } finally {
98
+ this.#pendingPublishes.delete(promise);
99
+ }
100
+ }
101
+ async subscribe(topic, cb, options) {
102
+ if (this.#closed) throw new Error("RedisStreamsPubSub: cannot subscribe on closed client");
103
+ const key = this.#subKey(topic, cb);
104
+ if (this.#subscriptions.has(key)) return;
105
+ await this.#ensureWriterConnected();
106
+ const isGrouped = !!options?.group;
107
+ const group = options?.group ?? `__fanout-${crypto.randomUUID()}`;
108
+ const consumer = `${group}-${crypto.randomUUID()}`;
109
+ const streamKey = this.#streamKey(topic);
110
+ try {
111
+ await this.#writeClient.xGroupCreate(streamKey, group, "0", { MKSTREAM: true });
112
+ } catch (err) {
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ if (!msg.includes("BUSYGROUP")) throw err;
115
+ this.#logger?.debug?.("redis-streams: consumer group already exists", { topic, group });
116
+ }
117
+ const readClient = redis.createClient(this.#connectOptions);
118
+ await readClient.connect();
119
+ const sub = {
120
+ cb,
121
+ topic,
122
+ streamKey,
123
+ group,
124
+ consumer,
125
+ isGrouped,
126
+ readClient,
127
+ stopped: false,
128
+ loop: void 0,
129
+ reclaimTimer: void 0
130
+ };
131
+ this.#subscriptions.set(key, sub);
132
+ sub.loop = this.#runReadLoop(sub);
133
+ this.#startReclaimLoop(sub);
134
+ }
135
+ /**
136
+ * Periodically run XAUTOCLAIM against this subscription's group so that
137
+ * messages a crashed/stuck consumer read but never acked get redelivered to
138
+ * a live sibling. Runs only for grouped subscriptions — fan-out groups are
139
+ * private to one consumer, so there's no sibling to claim from.
140
+ */
141
+ #startReclaimLoop(sub) {
142
+ if (this.#reclaimIntervalMs <= 0) return;
143
+ if (!sub.isGrouped) return;
144
+ const tick = async () => {
145
+ if (sub.stopped || this.#closed) return;
146
+ try {
147
+ const reply = await this.#writeClient.xAutoClaim(
148
+ sub.streamKey,
149
+ sub.group,
150
+ sub.consumer,
151
+ this.#reclaimIdleMs,
152
+ "0-0",
153
+ { COUNT: 100 }
154
+ );
155
+ const messages = reply?.messages ?? [];
156
+ for (const entry of messages) {
157
+ if (sub.stopped || this.#closed) return;
158
+ if (!entry) continue;
159
+ await this.#deliverMessage(sub, entry.id, entry.message);
160
+ }
161
+ } catch (err) {
162
+ this.#logger?.debug?.("redis-streams: XAUTOCLAIM failed", {
163
+ topic: sub.topic,
164
+ group: sub.group,
165
+ err: err instanceof Error ? err.message : err
166
+ });
167
+ }
168
+ if (sub.stopped || this.#closed) return;
169
+ sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
170
+ };
171
+ sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
172
+ }
173
+ async unsubscribe(topic, cb) {
174
+ const key = this.#subKey(topic, cb);
175
+ const sub = this.#subscriptions.get(key);
176
+ if (!sub) return;
177
+ this.#subscriptions.delete(key);
178
+ sub.stopped = true;
179
+ if (sub.reclaimTimer) {
180
+ clearTimeout(sub.reclaimTimer);
181
+ sub.reclaimTimer = void 0;
182
+ }
183
+ try {
184
+ await sub.readClient.quit();
185
+ } catch (err) {
186
+ this.#logger?.debug?.("redis-streams: reader quit failed", {
187
+ topic: sub.topic,
188
+ err: err instanceof Error ? err.message : err
189
+ });
190
+ }
191
+ if (sub.loop) {
192
+ try {
193
+ await sub.loop;
194
+ } catch (err) {
195
+ this.#logger?.debug?.("redis-streams: read loop exited with error", {
196
+ topic: sub.topic,
197
+ err: err instanceof Error ? err.message : err
198
+ });
199
+ }
200
+ }
201
+ if (!sub.isGrouped) {
202
+ try {
203
+ await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);
204
+ } catch (err) {
205
+ this.#logger?.debug?.("redis-streams: xGroupDestroy failed", {
206
+ topic: sub.topic,
207
+ group: sub.group,
208
+ err: err instanceof Error ? err.message : err
209
+ });
210
+ }
211
+ }
212
+ }
213
+ async flush() {
214
+ if (this.#pendingPublishes.size > 0) {
215
+ await Promise.allSettled([...this.#pendingPublishes]);
216
+ }
217
+ }
218
+ /**
219
+ * Disconnect all clients and stop all subscription loops.
220
+ */
221
+ async close() {
222
+ if (this.#closed) return;
223
+ this.#closed = true;
224
+ const subs = [...this.#subscriptions.values()];
225
+ await Promise.all(subs.map((sub) => this.unsubscribe(sub.topic, sub.cb)));
226
+ if (this.#writeClient.isOpen) {
227
+ try {
228
+ await this.#writeClient.quit();
229
+ } catch (err) {
230
+ this.#logger?.debug?.("redis-streams: writer quit failed", {
231
+ err: err instanceof Error ? err.message : err
232
+ });
233
+ }
234
+ }
235
+ }
236
+ async #runReadLoop(sub) {
237
+ while (!sub.stopped) {
238
+ let result;
239
+ try {
240
+ result = await sub.readClient.xReadGroup(sub.group, sub.consumer, [{ key: sub.streamKey, id: ">" }], {
241
+ COUNT: 10,
242
+ BLOCK: this.#blockMs
243
+ });
244
+ } catch (err) {
245
+ if (sub.stopped) return;
246
+ this.#logger?.debug?.("redis-streams: xReadGroup failed", {
247
+ topic: sub.topic,
248
+ group: sub.group,
249
+ err: err instanceof Error ? err.message : err
250
+ });
251
+ await new Promise((r) => setTimeout(r, 100));
252
+ continue;
253
+ }
254
+ if (!result || result.length === 0) continue;
255
+ for (const stream of result) {
256
+ for (const entry of stream.messages) {
257
+ if (sub.stopped) return;
258
+ await this.#deliverMessage(sub, entry.id, entry.message);
259
+ }
260
+ }
261
+ }
262
+ }
263
+ async #deliverMessage(sub, streamId, fields) {
264
+ let event;
265
+ try {
266
+ event = JSON.parse(fields.event ?? "{}");
267
+ if (typeof event.createdAt === "string") {
268
+ event.createdAt = new Date(event.createdAt);
269
+ }
270
+ } catch (err) {
271
+ this.#logger?.debug?.("redis-streams: malformed payload, dropping", {
272
+ topic: sub.topic,
273
+ streamId,
274
+ err: err instanceof Error ? err.message : err
275
+ });
276
+ try {
277
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
278
+ } catch (ackErr) {
279
+ this.#logger?.debug?.("redis-streams: xAck after malformed payload failed", {
280
+ topic: sub.topic,
281
+ err: ackErr instanceof Error ? ackErr.message : ackErr
282
+ });
283
+ }
284
+ return;
285
+ }
286
+ let settled = false;
287
+ const ack = async () => {
288
+ if (settled) return;
289
+ settled = true;
290
+ try {
291
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
292
+ } catch (err) {
293
+ this.#logger?.debug?.("redis-streams: ack cleanup failed", {
294
+ topic: sub.topic,
295
+ err: err instanceof Error ? err.message : err
296
+ });
297
+ }
298
+ };
299
+ const nack = async () => {
300
+ if (settled) return;
301
+ settled = true;
302
+ const attempt = event.deliveryAttempt ?? 1;
303
+ if (attempt >= this.#maxDeliveryAttempts) {
304
+ this.#logger?.warn?.("redis-streams: dropping event after max delivery attempts", {
305
+ topic: sub.topic,
306
+ eventType: event.type,
307
+ eventId: event.id,
308
+ attempt,
309
+ max: this.#maxDeliveryAttempts
310
+ });
311
+ try {
312
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
313
+ } catch (err) {
314
+ this.#logger?.debug?.("redis-streams: ack on dropped poison message failed", {
315
+ topic: sub.topic,
316
+ err: err instanceof Error ? err.message : err
317
+ });
318
+ }
319
+ return;
320
+ }
321
+ const next = {
322
+ ...event,
323
+ deliveryAttempt: attempt + 1
324
+ };
325
+ try {
326
+ await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
327
+ } catch (err) {
328
+ this.#logger?.warn?.("redis-streams: nack republish failed; leaving original pending for reclaim", {
329
+ topic: sub.topic,
330
+ eventId: event.id,
331
+ err: err instanceof Error ? err.message : err
332
+ });
333
+ settled = false;
334
+ return;
335
+ }
336
+ try {
337
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
338
+ } catch (err) {
339
+ this.#logger?.debug?.("redis-streams: xAck after nack failed", {
340
+ topic: sub.topic,
341
+ err: err instanceof Error ? err.message : err
342
+ });
343
+ }
344
+ };
345
+ try {
346
+ const result = sub.cb(event, ack, nack);
347
+ if (result && typeof result.catch === "function") {
348
+ result.catch(async () => {
349
+ await nack();
350
+ });
351
+ }
352
+ } catch {
353
+ await nack();
354
+ }
355
+ }
356
+ };
357
+
358
+ exports.RedisStreamsPubSub = RedisStreamsPubSub;
359
+ //# sourceMappingURL=index.cjs.map
360
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["PubSub","createClient","randomUUID"],"mappings":";;;;;;;AA2DO,IAAM,kBAAA,GAAN,cAAiCA,aAAA,CAAO;AAAA;AAAA;AAAA,EAG7C,IAAa,cAAA,GAAoD;AAC/D,IAAA,OAAO,CAAC,MAAM,CAAA;AAAA,EAChB;AAAA,EAEA,YAAA;AAAA,EACA,eAAA;AAAA,EACA,UAAA;AAAA,EACA,QAAA;AAAA,EACA,gBAAA;AAAA,EACA,kBAAA;AAAA,EACA,cAAA;AAAA,EACA,oBAAA;AAAA,EACA,OAAA;AAAA;AAAA;AAAA;AAAA,EAIA,cAAA,uBAAgD,GAAA,EAAI;AAAA,EACpD,MAAA,uBAA6C,OAAA,EAAQ;AAAA,EACrD,iBAAA,uBAA+C,GAAA,EAAI;AAAA,EACnD,OAAA,GAAU,KAAA;AAAA,EAEV,WAAA,CAAY,OAAA,GAAoC,EAAC,EAAG;AAClD,IAAA,KAAA,EAAM;AACN,IAAA,MAAM,GAAA,GAAM,OAAA,CAAQ,GAAA,IAAO,OAAA,CAAQ,cAAc,GAAA,IAAO,wBAAA;AACxD,IAAA,IAAA,CAAK,eAAA,GAAkB,EAAE,GAAG,OAAA,CAAQ,cAAc,GAAA,EAAI;AACtD,IAAA,IAAA,CAAK,YAAA,GAAeC,kBAAA,CAAa,IAAA,CAAK,eAAe,CAAA;AACrD,IAAA,IAAA,CAAK,UAAA,GAAa,QAAQ,SAAA,IAAa,cAAA;AACvC,IAAA,IAAA,CAAK,QAAA,GAAW,QAAQ,OAAA,IAAW,GAAA;AACnC,IAAA,IAAA,CAAK,gBAAA,GAAmB,QAAQ,eAAA,IAAmB,GAAA;AACnD,IAAA,IAAA,CAAK,kBAAA,GAAqB,QAAQ,iBAAA,IAAqB,GAAA;AACvD,IAAA,IAAA,CAAK,cAAA,GAAiB,QAAQ,aAAA,IAAiB,GAAA;AAC/C,IAAA,MAAM,GAAA,GAAM,QAAQ,mBAAA,IAAuB,CAAA;AAC3C,IAAA,IAAI,QAAQ,CAAA,EAAG;AACb,MAAA,OAAA,CAAQ,MAAA,EAAQ,IAAA;AAAA,QACd;AAAA,OACF;AACA,MAAA,IAAA,CAAK,oBAAA,GAAuB,QAAA;AAAA,IAC9B,WAAW,GAAA,GAAM,CAAA,IAAK,MAAA,CAAO,KAAA,CAAM,GAAG,CAAA,EAAG;AACvC,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,iEAAA,EAAoE,GAAG,CAAA,CAAE,CAAA;AAAA,IAC3F,CAAA,MAAO;AACL,MAAA,IAAA,CAAK,oBAAA,GAAuB,GAAA;AAAA,IAC9B;AACA,IAAA,IAAA,CAAK,UAAU,OAAA,CAAQ,MAAA;AAAA,EACzB;AAAA,EAEA,OAAA,CAAQ,OAAe,EAAA,EAA2B;AAChD,IAAA,IAAI,IAAA,GAAO,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,EAAE,CAAA;AAC7B,IAAA,IAAI,CAAC,IAAA,EAAM;AACT,MAAA,IAAA,GAAOC,iBAAA,EAAW;AAClB,MAAA,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,EAAA,EAAI,IAAI,CAAA;AAAA,IAC1B;AACA,IAAA,OAAO,CAAA,EAAG,KAAK,CAAA,EAAA,EAAK,IAAI,CAAA,CAAA;AAAA,EAC1B;AAAA;AAAA,EAGA,MAAM,sBAAA,GAAwC;AAC5C,IAAA,IAAI,IAAA,CAAK,aAAa,MAAA,EAAQ;AAC9B,IAAA,MAAM,IAAA,CAAK,aAAa,OAAA,EAAQ;AAAA,EAClC;AAAA,EAEA,WAAW,KAAA,EAAuB;AAChC,IAAA,OAAO,CAAA,EAAG,IAAA,CAAK,UAAU,CAAA,CAAA,EAAI,KAAK,CAAA,CAAA;AAAA,EACpC;AAAA,EAEA,MAAM,OAAA,CAAQ,KAAA,EAAe,KAAA,EAAuD;AAClF,IAAA,IAAI,IAAA,CAAK,OAAA,EAAS,MAAM,IAAI,MAAM,qDAAqD,CAAA;AACvF,IAAA,MAAM,KAAK,sBAAA,EAAuB;AAElC,IAAA,MAAM,KAAKA,iBAAA,EAAW;AACtB,IAAA,MAAM,SAAA,uBAAgB,IAAA,EAAK;AAC3B,IAAA,MAAM,OAAA,GAAiB;AAAA,MACrB,GAAG,KAAA;AAAA,MACH,EAAA;AAAA,MACA,SAAA;AAAA,MACA,eAAA,EAAiB,MAAM,eAAA,IAAmB;AAAA,KAC5C;AACA,IAAA,MAAM,cAA2F,EAAC;AAClG,IAAA,IAAI,IAAA,CAAK,mBAAmB,CAAA,EAAG;AAC7B,MAAA,WAAA,CAAY,IAAA,GAAO;AAAA,QACjB,QAAA,EAAU,QAAA;AAAA,QACV,gBAAA,EAAkB,GAAA;AAAA,QAClB,WAAW,IAAA,CAAK;AAAA,OAClB;AAAA,IACF;AACA,IAAA,MAAM,OAAA,GAAU,KAAK,YAAA,CAAa,IAAA;AAAA,MAChC,IAAA,CAAK,WAAW,KAAK,CAAA;AAAA,MACrB,GAAA;AAAA,MACA,EAAE,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,OAAO,CAAA,EAAE;AAAA,MACjC;AAAA,KACF;AACA,IAAA,IAAA,CAAK,iBAAA,CAAkB,IAAI,OAAO,CAAA;AAClC,IAAA,IAAI;AACF,MAAA,MAAM,OAAA;AAAA,IACR,CAAA,SAAE;AACA,MAAA,IAAA,CAAK,iBAAA,CAAkB,OAAO,OAAO,CAAA;AAAA,IACvC;AAAA,EACF;AAAA,EAEA,MAAM,SAAA,CAAU,KAAA,EAAe,EAAA,EAAmB,OAAA,EAA2C;AAC3F,IAAA,IAAI,IAAA,CAAK,OAAA,EAAS,MAAM,IAAI,MAAM,uDAAuD,CAAA;AACzF,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAClC,IAAA,IAAI,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA,EAAG;AAElC,IAAA,MAAM,KAAK,sBAAA,EAAuB;AAElC,IAAA,MAAM,SAAA,GAAY,CAAC,CAAC,OAAA,EAAS,KAAA;AAC7B,IAAA,MAAM,KAAA,GAAQ,OAAA,EAAS,KAAA,IAAS,CAAA,SAAA,EAAYA,mBAAY,CAAA,CAAA;AACxD,IAAA,MAAM,QAAA,GAAW,CAAA,EAAG,KAAK,CAAA,CAAA,EAAIA,mBAAY,CAAA,CAAA;AACzC,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,UAAA,CAAW,KAAK,CAAA;AAYvC,IAAA,IAAI;AACF,MAAA,MAAM,IAAA,CAAK,aAAa,YAAA,CAAa,SAAA,EAAW,OAAO,GAAA,EAAK,EAAE,QAAA,EAAU,IAAA,EAAM,CAAA;AAAA,IAChF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,MAAM,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,OAAO,GAAG,CAAA;AAC3D,MAAA,IAAI,CAAC,GAAA,CAAI,QAAA,CAAS,WAAW,GAAG,MAAM,GAAA;AACtC,MAAA,IAAA,CAAK,SAAS,KAAA,GAAQ,8CAAA,EAAgD,EAAE,KAAA,EAAO,OAAO,CAAA;AAAA,IACxF;AAIA,IAAA,MAAM,UAAA,GAAaD,kBAAA,CAAa,IAAA,CAAK,eAAe,CAAA;AACpD,IAAA,MAAM,WAAW,OAAA,EAAQ;AAEzB,IAAA,MAAM,GAAA,GAAoB;AAAA,MACxB,EAAA;AAAA,MACA,KAAA;AAAA,MACA,SAAA;AAAA,MACA,KAAA;AAAA,MACA,QAAA;AAAA,MACA,SAAA;AAAA,MACA,UAAA;AAAA,MACA,OAAA,EAAS,KAAA;AAAA,MACT,IAAA,EAAM,MAAA;AAAA,MACN,YAAA,EAAc;AAAA,KAChB;AACA,IAAA,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,GAAA,EAAK,GAAG,CAAA;AAChC,IAAA,GAAA,CAAI,IAAA,GAAO,IAAA,CAAK,YAAA,CAAa,GAAG,CAAA;AAChC,IAAA,IAAA,CAAK,kBAAkB,GAAG,CAAA;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,kBAAkB,GAAA,EAAyB;AACzC,IAAA,IAAI,IAAA,CAAK,sBAAsB,CAAA,EAAG;AAClC,IAAA,IAAI,CAAC,IAAI,SAAA,EAAW;AAEpB,IAAA,MAAM,OAAO,YAAY;AACvB,MAAA,IAAI,GAAA,CAAI,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACjC,MAAA,IAAI;AACF,QAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,YAAA,CAAa,UAAA;AAAA,UACpC,GAAA,CAAI,SAAA;AAAA,UACJ,GAAA,CAAI,KAAA;AAAA,UACJ,GAAA,CAAI,QAAA;AAAA,UACJ,IAAA,CAAK,cAAA;AAAA,UACL,KAAA;AAAA,UACA,EAAE,OAAO,GAAA;AAAI,SACf;AACA,QAAA,MAAM,QAAA,GAAY,KAAA,EAAO,QAAA,IAAY,EAAC;AACtC,QAAA,KAAA,MAAW,SAAS,QAAA,EAAU;AAC5B,UAAA,IAAI,GAAA,CAAI,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACjC,UAAA,IAAI,CAAC,KAAA,EAAO;AACZ,UAAA,MAAM,KAAK,eAAA,CAAgB,GAAA,EAAK,KAAA,CAAM,EAAA,EAAI,MAAM,OAAO,CAAA;AAAA,QACzD;AAAA,MACF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,kCAAA,EAAoC;AAAA,UACxD,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AACA,MAAA,IAAI,GAAA,CAAI,OAAA,IAAW,IAAA,CAAK,OAAA,EAAS;AACjC,MAAA,GAAA,CAAI,YAAA,GAAe,UAAA,CAAW,IAAA,EAAM,IAAA,CAAK,kBAAkB,CAAA;AAAA,IAC7D,CAAA;AAEA,IAAA,GAAA,CAAI,YAAA,GAAe,UAAA,CAAW,IAAA,EAAM,IAAA,CAAK,kBAAkB,CAAA;AAAA,EAC7D;AAAA,EAEA,MAAM,WAAA,CAAY,KAAA,EAAe,EAAA,EAAkC;AACjE,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAA;AAClC,IAAA,MAAM,GAAA,GAAM,IAAA,CAAK,cAAA,CAAe,GAAA,CAAI,GAAG,CAAA;AACvC,IAAA,IAAI,CAAC,GAAA,EAAK;AACV,IAAA,IAAA,CAAK,cAAA,CAAe,OAAO,GAAG,CAAA;AAC9B,IAAA,GAAA,CAAI,OAAA,GAAU,IAAA;AACd,IAAA,IAAI,IAAI,YAAA,EAAc;AACpB,MAAA,YAAA,CAAa,IAAI,YAAY,CAAA;AAC7B,MAAA,GAAA,CAAI,YAAA,GAAe,MAAA;AAAA,IACrB;AAGA,IAAA,IAAI;AACF,MAAA,MAAM,GAAA,CAAI,WAAW,IAAA,EAAK;AAAA,IAC5B,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,mCAAA,EAAqC;AAAA,QACzD,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,OAC3C,CAAA;AAAA,IACH;AAEA,IAAA,IAAI,IAAI,IAAA,EAAM;AACZ,MAAA,IAAI;AACF,QAAA,MAAM,GAAA,CAAI,IAAA;AAAA,MACZ,SAAS,GAAA,EAAK;AAEZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,4CAAA,EAA8C;AAAA,UAClE,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AAAA,IACF;AAGA,IAAA,IAAI,CAAC,IAAI,SAAA,EAAW;AAClB,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,YAAA,CAAa,aAAA,CAAc,GAAA,CAAI,SAAA,EAAW,IAAI,KAAK,CAAA;AAAA,MAChE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,qCAAA,EAAuC;AAAA,UAC3D,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,KAAA,GAAuB;AAE3B,IAAA,IAAI,IAAA,CAAK,iBAAA,CAAkB,IAAA,GAAO,CAAA,EAAG;AACnC,MAAA,MAAM,QAAQ,UAAA,CAAW,CAAC,GAAG,IAAA,CAAK,iBAAiB,CAAC,CAAA;AAAA,IACtD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,KAAA,GAAuB;AAC3B,IAAA,IAAI,KAAK,OAAA,EAAS;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAIf,IAAA,MAAM,OAAO,CAAC,GAAG,IAAA,CAAK,cAAA,CAAe,QAAQ,CAAA;AAC7C,IAAA,MAAM,OAAA,CAAQ,GAAA,CAAI,IAAA,CAAK,GAAA,CAAI,CAAA,GAAA,KAAO,IAAA,CAAK,WAAA,CAAY,GAAA,CAAI,KAAA,EAAO,GAAA,CAAI,EAAE,CAAC,CAAC,CAAA;AAEtE,IAAA,IAAI,IAAA,CAAK,aAAa,MAAA,EAAQ;AAC5B,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,aAAa,IAAA,EAAK;AAAA,MAC/B,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,mCAAA,EAAqC;AAAA,UACzD,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,aAAa,GAAA,EAAkC;AACnD,IAAA,OAAO,CAAC,IAAI,OAAA,EAAS;AACnB,MAAA,IAAI,MAAA;AACJ,MAAA,IAAI;AACF,QAAA,MAAA,GAAS,MAAM,GAAA,CAAI,UAAA,CAAW,UAAA,CAAW,GAAA,CAAI,OAAO,GAAA,CAAI,QAAA,EAAU,CAAC,EAAE,KAAK,GAAA,CAAI,SAAA,EAAW,EAAA,EAAI,GAAA,EAAK,CAAA,EAAG;AAAA,UACnG,KAAA,EAAO,EAAA;AAAA,UACP,OAAO,IAAA,CAAK;AAAA,SACb,CAAA;AAAA,MACH,SAAS,GAAA,EAAK;AACZ,QAAA,IAAI,IAAI,OAAA,EAAS;AACjB,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,kCAAA,EAAoC;AAAA,UACxD,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAED,QAAA,MAAM,IAAI,OAAA,CAAQ,CAAA,CAAA,KAAK,UAAA,CAAW,CAAA,EAAG,GAAG,CAAC,CAAA;AACzC,QAAA;AAAA,MACF;AAEA,MAAA,IAAI,CAAC,MAAA,IAAU,MAAA,CAAO,MAAA,KAAW,CAAA,EAAG;AAEpC,MAAA,KAAA,MAAW,UAAU,MAAA,EAAQ;AAC3B,QAAA,KAAA,MAAW,KAAA,IAAS,OAAO,QAAA,EAAU;AACnC,UAAA,IAAI,IAAI,OAAA,EAAS;AACjB,UAAA,MAAM,KAAK,eAAA,CAAgB,GAAA,EAAK,KAAA,CAAM,EAAA,EAAI,MAAM,OAAO,CAAA;AAAA,QACzD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,eAAA,CAAgB,GAAA,EAAmB,QAAA,EAAkB,MAAA,EAA+C;AACxG,IAAA,IAAI,KAAA;AACJ,IAAA,IAAI;AACF,MAAA,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAM,MAAA,CAAO,KAAA,IAAS,IAAI,CAAA;AAEvC,MAAA,IAAI,OAAO,KAAA,CAAM,SAAA,KAAc,QAAA,EAAU;AACvC,QAAA,KAAA,CAAM,SAAA,GAAY,IAAI,IAAA,CAAK,KAAA,CAAM,SAAS,CAAA;AAAA,MAC5C;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,4CAAA,EAA8C;AAAA,QAClE,OAAO,GAAA,CAAI,KAAA;AAAA,QACX,QAAA;AAAA,QACA,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,OAC3C,CAAA;AACD,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,YAAA,CAAa,IAAA,CAAK,IAAI,SAAA,EAAW,GAAA,CAAI,OAAO,QAAQ,CAAA;AAAA,MACjE,SAAS,MAAA,EAAQ;AACf,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,oDAAA,EAAsD;AAAA,UAC1E,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,MAAA,YAAkB,KAAA,GAAQ,MAAA,CAAO,OAAA,GAAU;AAAA,SACjD,CAAA;AAAA,MACH;AACA,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,OAAA,GAAU,KAAA;AACd,IAAA,MAAM,MAAM,YAAY;AACtB,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,IAAI;AAIF,QAAA,MAAM,KAAK,YAAA,CAAa,IAAA,CAAK,IAAI,SAAA,EAAW,GAAA,CAAI,OAAO,QAAQ,CAAA;AAAA,MACjE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,mCAAA,EAAqC;AAAA,UACzD,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AACA,IAAA,MAAM,OAAO,YAAY;AACvB,MAAA,IAAI,OAAA,EAAS;AACb,MAAA,OAAA,GAAU,IAAA;AACV,MAAA,MAAM,OAAA,GAAU,MAAM,eAAA,IAAmB,CAAA;AAIzC,MAAA,IAAI,OAAA,IAAW,KAAK,oBAAA,EAAsB;AACxC,QAAA,IAAA,CAAK,OAAA,EAAS,OAAO,2DAAA,EAA6D;AAAA,UAChF,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,WAAW,KAAA,CAAM,IAAA;AAAA,UACjB,SAAS,KAAA,CAAM,EAAA;AAAA,UACf,OAAA;AAAA,UACA,KAAK,IAAA,CAAK;AAAA,SACX,CAAA;AACD,QAAA,IAAI;AAEF,UAAA,MAAM,KAAK,YAAA,CAAa,IAAA,CAAK,IAAI,SAAA,EAAW,GAAA,CAAI,OAAO,QAAQ,CAAA;AAAA,QACjE,SAAS,GAAA,EAAK;AACZ,UAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,qDAAA,EAAuD;AAAA,YAC3E,OAAO,GAAA,CAAI,KAAA;AAAA,YACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,WAC3C,CAAA;AAAA,QACH;AACA,QAAA;AAAA,MACF;AAKA,MAAA,MAAM,IAAA,GAAc;AAAA,QAClB,GAAG,KAAA;AAAA,QACH,iBAAiB,OAAA,GAAU;AAAA,OAC7B;AACA,MAAA,IAAI;AACF,QAAA,MAAM,IAAA,CAAK,YAAA,CAAa,IAAA,CAAK,GAAA,CAAI,SAAA,EAAW,GAAA,EAAK,EAAE,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG,CAAA;AAAA,MAClF,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,OAAO,4EAAA,EAA8E;AAAA,UACjG,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,SAAS,KAAA,CAAM,EAAA;AAAA,UACf,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAGD,QAAA,OAAA,GAAU,KAAA;AACV,QAAA;AAAA,MACF;AACA,MAAA,IAAI;AACF,QAAA,MAAM,KAAK,YAAA,CAAa,IAAA,CAAK,IAAI,SAAA,EAAW,GAAA,CAAI,OAAO,QAAQ,CAAA;AAAA,MACjE,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,OAAA,EAAS,QAAQ,uCAAA,EAAyC;AAAA,UAC7D,OAAO,GAAA,CAAI,KAAA;AAAA,UACX,GAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU;AAAA,SAC3C,CAAA;AAAA,MACH;AAAA,IACF,CAAA;AAEA,IAAA,IAAI;AAOF,MAAA,MAAM,MAAA,GAAkB,GAAA,CAAI,EAAA,CAAG,KAAA,EAAO,KAAK,IAAI,CAAA;AAC/C,MAAA,IAAI,MAAA,IAAU,OAAQ,MAAA,CAA+C,KAAA,KAAU,UAAA,EAAY;AACzF,QAAC,MAAA,CAA4B,MAAM,YAAY;AAC7C,UAAA,MAAM,IAAA,EAAK;AAAA,QACb,CAAC,CAAA;AAAA,MACH;AAAA,IACF,CAAA,CAAA,MAAQ;AAEN,MAAA,MAAM,IAAA,EAAK;AAAA,IACb;AAAA,EACF;AACF","file":"index.cjs","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from 'redis';\nimport type { RedisClientOptions, RedisClientType } from 'redis';\n\n/**\n * Mastra PubSub backed by Redis Streams.\n *\n * - Each topic maps to a Redis stream key `<prefix>:<topic>`.\n * - Subscriptions with `options.group` use a real Redis consumer group, so\n * competing subscribers in the same group share the work (round-robin).\n * - Subscriptions without a group create a private per-subscriber consumer\n * group, so they get fan-out semantics (every subscriber sees every event).\n * - Nack triggers redelivery by re-publishing the event with an incremented\n * `deliveryAttempt` field, then XACK-ing the original. This trades strict\n * FIFO ordering on retry for a simple, reliable redelivery path.\n */\nexport interface RedisStreamsPubSubConfig {\n url?: string;\n keyPrefix?: string;\n blockMs?: number;\n redisOptions?: RedisClientOptions;\n /**\n * Approximate maximum number of entries kept per stream. On every publish we\n * issue MAXLEN ~ N which lets Redis trim opportunistically. Defaults to\n * 10_000 — set to 0 to disable trimming.\n */\n maxStreamLength?: number;\n /**\n * How often (in ms) each subscription runs XAUTOCLAIM to recover messages\n * that an earlier consumer in the group read but never acked. Defaults to\n * 30_000 ms. Set to 0 to disable.\n */\n reclaimIntervalMs?: number;\n /**\n * Minimum idle time (in ms) before a pending message is eligible for\n * reclaim. Should be much larger than typical in-flight processing time to\n * avoid double-delivery. Defaults to 60_000 ms.\n */\n reclaimIdleMs?: number;\n /**\n * Maximum number of times a single event will be redelivered via nack\n * before it is dropped (acked without republish). Defaults to 5. Set to\n * `Infinity` to disable the cap (events redeliver forever on every nack).\n *\n * `0` is treated as `Infinity` with a one-time warn for back-compat;\n * prefer `Infinity` to disable the cap explicitly.\n */\n maxDeliveryAttempts?: number;\n /**\n * Optional logger for diagnostics. When omitted, suppressed errors\n * (BUSYGROUP, malformed payloads, connection-close races) are swallowed\n * silently. When provided, those paths emit `debug`/`warn` entries so\n * operators can see what's happening without noise on the happy path.\n */\n logger?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };\n}\n\nexport class RedisStreamsPubSub extends PubSub {\n // Redis Streams is a pull transport: consumers issue XREADGROUP to read\n // events. Mastra reads this to know an OrchestrationWorker is required.\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['pull'];\n }\n\n #writeClient: RedisClientType;\n #connectOptions: RedisClientOptions;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #logger?: RedisStreamsPubSubConfig['logger'];\n // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to\n // multiple topics independently. Without the topic in the key,\n // unsubscribe(otherTopic, cb) would tear down the wrong subscription.\n #subscriptions: Map<string, Subscription> = new Map();\n #cbIds: WeakMap<EventCallback, string> = new WeakMap();\n #pendingPublishes: Set<Promise<unknown>> = new Set();\n #closed = false;\n\n constructor(options: RedisStreamsPubSubConfig = {}) {\n super();\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n this.#connectOptions = { ...options.redisOptions, url };\n this.#writeClient = createClient(this.#connectOptions) as RedisClientType;\n this.#keyPrefix = options.keyPrefix ?? 'mastra:topic';\n this.#blockMs = options.blockMs ?? 1000;\n this.#maxStreamLength = options.maxStreamLength ?? 10_000;\n this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 30_000;\n this.#reclaimIdleMs = options.reclaimIdleMs ?? 60_000;\n const cap = options.maxDeliveryAttempts ?? 5;\n if (cap === 0) {\n options.logger?.warn?.(\n 'redis-streams: maxDeliveryAttempts=0 is treated as Infinity for back-compat; pass Infinity to disable the cap explicitly.',\n );\n this.#maxDeliveryAttempts = Infinity;\n } else if (cap < 0 || Number.isNaN(cap)) {\n throw new Error(`redis-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);\n } else {\n this.#maxDeliveryAttempts = cap;\n }\n this.#logger = options.logger;\n }\n\n #subKey(topic: string, cb: EventCallback): string {\n let cbId = this.#cbIds.get(cb);\n if (!cbId) {\n cbId = randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent. */\n async #ensureWriterConnected(): Promise<void> {\n if (this.#writeClient.isOpen) return;\n await this.#writeClient.connect();\n }\n\n #streamKey(topic: string): string {\n return `${this.#keyPrefix}:${topic}`;\n }\n\n async publish(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n await this.#ensureWriterConnected();\n\n const id = randomUUID();\n const createdAt = new Date();\n const payload: Event = {\n ...event,\n id,\n createdAt,\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n const xaddOptions: { TRIM?: { strategy: 'MAXLEN'; strategyModifier: '~'; threshold: number } } = {};\n if (this.#maxStreamLength > 0) {\n xaddOptions.TRIM = {\n strategy: 'MAXLEN',\n strategyModifier: '~',\n threshold: this.#maxStreamLength,\n };\n }\n const promise = this.#writeClient.xAdd(\n this.#streamKey(topic),\n '*',\n { event: JSON.stringify(payload) },\n xaddOptions,\n );\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot subscribe on closed client');\n const key = this.#subKey(topic, cb);\n if (this.#subscriptions.has(key)) return; // idempotent: same (topic, cb) already subscribed\n\n await this.#ensureWriterConnected();\n\n const isGrouped = !!options?.group;\n const group = options?.group ?? `__fanout-${randomUUID()}`;\n const consumer = `${group}-${randomUUID()}`;\n const streamKey = this.#streamKey(topic);\n\n // Create the consumer group if it doesn't exist. MKSTREAM creates the\n // stream if needed. BUSYGROUP means another subscriber raced us — fine.\n //\n // We anchor brand-new groups at '0' (stream start) instead of '$' so that\n // a worker which subscribes after a publish still sees the backlog. This\n // is the \"late join\" case: a server may publish workflow.start before any\n // orchestrator process exists. Without this, that work is silently lost.\n // Existing groups (BUSYGROUP path) keep their own checkpoint, so this\n // doesn't change semantics for already-running clusters. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n await this.#writeClient.xGroupCreate(streamKey, group, '0', { MKSTREAM: true });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each subscription gets a dedicated reader connection because XREADGROUP\n // with BLOCK > 0 holds the connection until a message arrives.\n const readClient = createClient(this.#connectOptions) as RedisClientType;\n await readClient.connect();\n\n const sub: Subscription = {\n cb,\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n };\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n }\n\n /**\n * Periodically run XAUTOCLAIM against this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Runs only for grouped subscriptions — fan-out groups are\n * private to one consumer, so there's no sibling to claim from.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n const reply = await this.#writeClient.xAutoClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n '0-0',\n { COUNT: 100 },\n );\n const messages = (reply?.messages ?? []) as Array<{ id: string; message: Record<string, string> } | null>;\n for (const entry of messages) {\n if (sub.stopped || this.#closed) return;\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: XAUTOCLAIM failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n }\n if (sub.stopped || this.#closed) return;\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n };\n\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const key = this.#subKey(topic, cb);\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n this.#subscriptions.delete(key);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await sub.readClient.quit();\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reader quit failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n\n if (sub.loop) {\n try {\n await sub.loop;\n } catch (err) {\n // loop exits naturally when readClient closes; surface only at debug.\n this.#logger?.debug?.('redis-streams: read loop exited with error', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n // For fan-out, drop the private group entirely so the stream can be reclaimed.\n if (!sub.isGrouped) {\n try {\n await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: xGroupDestroy failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n }\n\n async flush(): Promise<void> {\n // Wait for any in-flight publishes to settle.\n if (this.#pendingPublishes.size > 0) {\n await Promise.allSettled([...this.#pendingPublishes]);\n }\n }\n\n /**\n * Disconnect all clients and stop all subscription loops.\n */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = [...this.#subscriptions.values()];\n await Promise.all(subs.map(sub => this.unsubscribe(sub.topic, sub.cb)));\n\n if (this.#writeClient.isOpen) {\n try {\n await this.#writeClient.quit();\n } catch (err) {\n this.#logger?.debug?.('redis-streams: writer quit failed', {\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n }\n\n async #runReadLoop(sub: Subscription): Promise<void> {\n while (!sub.stopped) {\n let result;\n try {\n result = await sub.readClient.xReadGroup(sub.group, sub.consumer, [{ key: sub.streamKey, id: '>' }], {\n COUNT: 10,\n BLOCK: this.#blockMs,\n });\n } catch (err) {\n if (sub.stopped) return;\n this.#logger?.debug?.('redis-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n // Connection error or similar — pause briefly then retry.\n await new Promise(r => setTimeout(r, 100));\n continue;\n }\n\n if (!result || result.length === 0) continue;\n\n for (const stream of result) {\n for (const entry of stream.messages) {\n if (sub.stopped) return;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n }\n }\n }\n\n async #deliverMessage(sub: Subscription, streamId: string, fields: Record<string, string>): Promise<void> {\n let event: Event;\n try {\n event = JSON.parse(fields.event ?? '{}') as Event;\n // createdAt is serialized as a string; rehydrate.\n if (typeof event.createdAt === 'string') {\n event.createdAt = new Date(event.createdAt);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\n err: err instanceof Error ? err.message : err,\n });\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (ackErr) {\n this.#logger?.debug?.('redis-streams: xAck after malformed payload failed', {\n topic: sub.topic,\n err: ackErr instanceof Error ? ackErr.message : ackErr,\n });\n }\n return;\n }\n\n let settled = false;\n const ack = async () => {\n if (settled) return;\n settled = true;\n try {\n // Only ack against this consumer group. Do NOT xDel: the stream may\n // be consumed by other groups, and xDel removes the entry for all of\n // them. Stream growth is bounded elsewhere via MAXLEN-style trimming.\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: ack cleanup failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\n // Cap redelivery to avoid an infinite poison-pill loop. When the cap\n // is hit we drop the event (xAck without republish) and warn so an\n // operator can find it in logs.\n if (attempt >= this.#maxDeliveryAttempts) {\n this.#logger?.warn?.('redis-streams: dropping event after max delivery attempts', {\n topic: sub.topic,\n eventType: event.type,\n eventId: event.id,\n attempt,\n max: this.#maxDeliveryAttempts,\n });\n try {\n // Group-scoped ack only — see ack() above for why we never xDel.\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: ack on dropped poison message failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n return;\n }\n // Republish with incremented deliveryAttempt FIRST, then ack the\n // original entry. If the republish fails we deliberately leave the\n // original message pending so XAUTOCLAIM (or another consumer) can\n // pick it up on a future tick — acking first would silently drop it.\n const next: Event = {\n ...event,\n deliveryAttempt: attempt + 1,\n };\n try {\n await this.#writeClient.xAdd(sub.streamKey, '*', { event: JSON.stringify(next) });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Allow this entry to be redelivered: reset settled so the next\n // delivery attempt (via XAUTOCLAIM) can ack/nack it again.\n settled = false;\n return;\n }\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: xAck after nack failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n\n try {\n // EventCallback is typed `=> void` but handlers commonly return a\n // promise (TS allows Promise<void> to satisfy void). If we get one\n // back, attach a catch handler so async rejections route to nack\n // instead of silently dropping the message. We do NOT await here —\n // serializing messages on a subscription would deadlock orchestration\n // callbacks that await their own future events.\n const result: unknown = sub.cb(event, ack, nack);\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(async () => {\n await nack();\n });\n }\n } catch {\n // Caller threw synchronously — treat as nack.\n await nack();\n }\n }\n}\n\ninterface Subscription {\n cb: EventCallback;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n}\n"]}
@@ -0,0 +1,72 @@
1
+ import { PubSub } from '@mastra/core/events';
2
+ import type { Event, EventCallback, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';
3
+ import type { RedisClientOptions } from 'redis';
4
+ /**
5
+ * Mastra PubSub backed by Redis Streams.
6
+ *
7
+ * - Each topic maps to a Redis stream key `<prefix>:<topic>`.
8
+ * - Subscriptions with `options.group` use a real Redis consumer group, so
9
+ * competing subscribers in the same group share the work (round-robin).
10
+ * - Subscriptions without a group create a private per-subscriber consumer
11
+ * group, so they get fan-out semantics (every subscriber sees every event).
12
+ * - Nack triggers redelivery by re-publishing the event with an incremented
13
+ * `deliveryAttempt` field, then XACK-ing the original. This trades strict
14
+ * FIFO ordering on retry for a simple, reliable redelivery path.
15
+ */
16
+ export interface RedisStreamsPubSubConfig {
17
+ url?: string;
18
+ keyPrefix?: string;
19
+ blockMs?: number;
20
+ redisOptions?: RedisClientOptions;
21
+ /**
22
+ * Approximate maximum number of entries kept per stream. On every publish we
23
+ * issue MAXLEN ~ N which lets Redis trim opportunistically. Defaults to
24
+ * 10_000 — set to 0 to disable trimming.
25
+ */
26
+ maxStreamLength?: number;
27
+ /**
28
+ * How often (in ms) each subscription runs XAUTOCLAIM to recover messages
29
+ * that an earlier consumer in the group read but never acked. Defaults to
30
+ * 30_000 ms. Set to 0 to disable.
31
+ */
32
+ reclaimIntervalMs?: number;
33
+ /**
34
+ * Minimum idle time (in ms) before a pending message is eligible for
35
+ * reclaim. Should be much larger than typical in-flight processing time to
36
+ * avoid double-delivery. Defaults to 60_000 ms.
37
+ */
38
+ reclaimIdleMs?: number;
39
+ /**
40
+ * Maximum number of times a single event will be redelivered via nack
41
+ * before it is dropped (acked without republish). Defaults to 5. Set to
42
+ * `Infinity` to disable the cap (events redeliver forever on every nack).
43
+ *
44
+ * `0` is treated as `Infinity` with a one-time warn for back-compat;
45
+ * prefer `Infinity` to disable the cap explicitly.
46
+ */
47
+ maxDeliveryAttempts?: number;
48
+ /**
49
+ * Optional logger for diagnostics. When omitted, suppressed errors
50
+ * (BUSYGROUP, malformed payloads, connection-close races) are swallowed
51
+ * silently. When provided, those paths emit `debug`/`warn` entries so
52
+ * operators can see what's happening without noise on the happy path.
53
+ */
54
+ logger?: {
55
+ debug?: (...args: unknown[]) => void;
56
+ warn?: (...args: unknown[]) => void;
57
+ };
58
+ }
59
+ export declare class RedisStreamsPubSub extends PubSub {
60
+ #private;
61
+ get supportedModes(): ReadonlyArray<PubSubDeliveryMode>;
62
+ constructor(options?: RedisStreamsPubSubConfig);
63
+ publish(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void>;
64
+ subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void>;
65
+ unsubscribe(topic: string, cb: EventCallback): Promise<void>;
66
+ flush(): Promise<void>;
67
+ /**
68
+ * Disconnect all clients and stop all subscription loops.
69
+ */
70
+ close(): Promise<void>;
71
+ }
72
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC7C,OAAO,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAEtG,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,OAAO,CAAC;AAEjE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,wBAAwB;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,kBAAkB,CAAC;IAClC;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;;OAKG;IACH,MAAM,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAA;KAAE,CAAC;CACxF;AAED,qBAAa,kBAAmB,SAAQ,MAAM;;IAG5C,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;gBAmBW,OAAO,GAAE,wBAA6B;IA2C5C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAkC7E,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA6FtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IA+C5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAuK7B"}
package/dist/index.js ADDED
@@ -0,0 +1,358 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { PubSub } from '@mastra/core/events';
3
+ import { createClient } from 'redis';
4
+
5
+ // src/index.ts
6
+ var RedisStreamsPubSub = class extends PubSub {
7
+ // Redis Streams is a pull transport: consumers issue XREADGROUP to read
8
+ // events. Mastra reads this to know an OrchestrationWorker is required.
9
+ get supportedModes() {
10
+ return ["pull"];
11
+ }
12
+ #writeClient;
13
+ #connectOptions;
14
+ #keyPrefix;
15
+ #blockMs;
16
+ #maxStreamLength;
17
+ #reclaimIntervalMs;
18
+ #reclaimIdleMs;
19
+ #maxDeliveryAttempts;
20
+ #logger;
21
+ // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to
22
+ // multiple topics independently. Without the topic in the key,
23
+ // unsubscribe(otherTopic, cb) would tear down the wrong subscription.
24
+ #subscriptions = /* @__PURE__ */ new Map();
25
+ #cbIds = /* @__PURE__ */ new WeakMap();
26
+ #pendingPublishes = /* @__PURE__ */ new Set();
27
+ #closed = false;
28
+ constructor(options = {}) {
29
+ super();
30
+ const url = options.url ?? options.redisOptions?.url ?? "redis://localhost:6379";
31
+ this.#connectOptions = { ...options.redisOptions, url };
32
+ this.#writeClient = createClient(this.#connectOptions);
33
+ this.#keyPrefix = options.keyPrefix ?? "mastra:topic";
34
+ this.#blockMs = options.blockMs ?? 1e3;
35
+ this.#maxStreamLength = options.maxStreamLength ?? 1e4;
36
+ this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 3e4;
37
+ this.#reclaimIdleMs = options.reclaimIdleMs ?? 6e4;
38
+ const cap = options.maxDeliveryAttempts ?? 5;
39
+ if (cap === 0) {
40
+ options.logger?.warn?.(
41
+ "redis-streams: maxDeliveryAttempts=0 is treated as Infinity for back-compat; pass Infinity to disable the cap explicitly."
42
+ );
43
+ this.#maxDeliveryAttempts = Infinity;
44
+ } else if (cap < 0 || Number.isNaN(cap)) {
45
+ throw new Error(`redis-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);
46
+ } else {
47
+ this.#maxDeliveryAttempts = cap;
48
+ }
49
+ this.#logger = options.logger;
50
+ }
51
+ #subKey(topic, cb) {
52
+ let cbId = this.#cbIds.get(cb);
53
+ if (!cbId) {
54
+ cbId = randomUUID();
55
+ this.#cbIds.set(cb, cbId);
56
+ }
57
+ return `${topic}::${cbId}`;
58
+ }
59
+ /** Lazily connect the shared writer client. Idempotent. */
60
+ async #ensureWriterConnected() {
61
+ if (this.#writeClient.isOpen) return;
62
+ await this.#writeClient.connect();
63
+ }
64
+ #streamKey(topic) {
65
+ return `${this.#keyPrefix}:${topic}`;
66
+ }
67
+ async publish(topic, event) {
68
+ if (this.#closed) throw new Error("RedisStreamsPubSub: cannot publish on closed client");
69
+ await this.#ensureWriterConnected();
70
+ const id = randomUUID();
71
+ const createdAt = /* @__PURE__ */ new Date();
72
+ const payload = {
73
+ ...event,
74
+ id,
75
+ createdAt,
76
+ deliveryAttempt: event.deliveryAttempt ?? 1
77
+ };
78
+ const xaddOptions = {};
79
+ if (this.#maxStreamLength > 0) {
80
+ xaddOptions.TRIM = {
81
+ strategy: "MAXLEN",
82
+ strategyModifier: "~",
83
+ threshold: this.#maxStreamLength
84
+ };
85
+ }
86
+ const promise = this.#writeClient.xAdd(
87
+ this.#streamKey(topic),
88
+ "*",
89
+ { event: JSON.stringify(payload) },
90
+ xaddOptions
91
+ );
92
+ this.#pendingPublishes.add(promise);
93
+ try {
94
+ await promise;
95
+ } finally {
96
+ this.#pendingPublishes.delete(promise);
97
+ }
98
+ }
99
+ async subscribe(topic, cb, options) {
100
+ if (this.#closed) throw new Error("RedisStreamsPubSub: cannot subscribe on closed client");
101
+ const key = this.#subKey(topic, cb);
102
+ if (this.#subscriptions.has(key)) return;
103
+ await this.#ensureWriterConnected();
104
+ const isGrouped = !!options?.group;
105
+ const group = options?.group ?? `__fanout-${randomUUID()}`;
106
+ const consumer = `${group}-${randomUUID()}`;
107
+ const streamKey = this.#streamKey(topic);
108
+ try {
109
+ await this.#writeClient.xGroupCreate(streamKey, group, "0", { MKSTREAM: true });
110
+ } catch (err) {
111
+ const msg = err instanceof Error ? err.message : String(err);
112
+ if (!msg.includes("BUSYGROUP")) throw err;
113
+ this.#logger?.debug?.("redis-streams: consumer group already exists", { topic, group });
114
+ }
115
+ const readClient = createClient(this.#connectOptions);
116
+ await readClient.connect();
117
+ const sub = {
118
+ cb,
119
+ topic,
120
+ streamKey,
121
+ group,
122
+ consumer,
123
+ isGrouped,
124
+ readClient,
125
+ stopped: false,
126
+ loop: void 0,
127
+ reclaimTimer: void 0
128
+ };
129
+ this.#subscriptions.set(key, sub);
130
+ sub.loop = this.#runReadLoop(sub);
131
+ this.#startReclaimLoop(sub);
132
+ }
133
+ /**
134
+ * Periodically run XAUTOCLAIM against this subscription's group so that
135
+ * messages a crashed/stuck consumer read but never acked get redelivered to
136
+ * a live sibling. Runs only for grouped subscriptions — fan-out groups are
137
+ * private to one consumer, so there's no sibling to claim from.
138
+ */
139
+ #startReclaimLoop(sub) {
140
+ if (this.#reclaimIntervalMs <= 0) return;
141
+ if (!sub.isGrouped) return;
142
+ const tick = async () => {
143
+ if (sub.stopped || this.#closed) return;
144
+ try {
145
+ const reply = await this.#writeClient.xAutoClaim(
146
+ sub.streamKey,
147
+ sub.group,
148
+ sub.consumer,
149
+ this.#reclaimIdleMs,
150
+ "0-0",
151
+ { COUNT: 100 }
152
+ );
153
+ const messages = reply?.messages ?? [];
154
+ for (const entry of messages) {
155
+ if (sub.stopped || this.#closed) return;
156
+ if (!entry) continue;
157
+ await this.#deliverMessage(sub, entry.id, entry.message);
158
+ }
159
+ } catch (err) {
160
+ this.#logger?.debug?.("redis-streams: XAUTOCLAIM failed", {
161
+ topic: sub.topic,
162
+ group: sub.group,
163
+ err: err instanceof Error ? err.message : err
164
+ });
165
+ }
166
+ if (sub.stopped || this.#closed) return;
167
+ sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
168
+ };
169
+ sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);
170
+ }
171
+ async unsubscribe(topic, cb) {
172
+ const key = this.#subKey(topic, cb);
173
+ const sub = this.#subscriptions.get(key);
174
+ if (!sub) return;
175
+ this.#subscriptions.delete(key);
176
+ sub.stopped = true;
177
+ if (sub.reclaimTimer) {
178
+ clearTimeout(sub.reclaimTimer);
179
+ sub.reclaimTimer = void 0;
180
+ }
181
+ try {
182
+ await sub.readClient.quit();
183
+ } catch (err) {
184
+ this.#logger?.debug?.("redis-streams: reader quit failed", {
185
+ topic: sub.topic,
186
+ err: err instanceof Error ? err.message : err
187
+ });
188
+ }
189
+ if (sub.loop) {
190
+ try {
191
+ await sub.loop;
192
+ } catch (err) {
193
+ this.#logger?.debug?.("redis-streams: read loop exited with error", {
194
+ topic: sub.topic,
195
+ err: err instanceof Error ? err.message : err
196
+ });
197
+ }
198
+ }
199
+ if (!sub.isGrouped) {
200
+ try {
201
+ await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);
202
+ } catch (err) {
203
+ this.#logger?.debug?.("redis-streams: xGroupDestroy failed", {
204
+ topic: sub.topic,
205
+ group: sub.group,
206
+ err: err instanceof Error ? err.message : err
207
+ });
208
+ }
209
+ }
210
+ }
211
+ async flush() {
212
+ if (this.#pendingPublishes.size > 0) {
213
+ await Promise.allSettled([...this.#pendingPublishes]);
214
+ }
215
+ }
216
+ /**
217
+ * Disconnect all clients and stop all subscription loops.
218
+ */
219
+ async close() {
220
+ if (this.#closed) return;
221
+ this.#closed = true;
222
+ const subs = [...this.#subscriptions.values()];
223
+ await Promise.all(subs.map((sub) => this.unsubscribe(sub.topic, sub.cb)));
224
+ if (this.#writeClient.isOpen) {
225
+ try {
226
+ await this.#writeClient.quit();
227
+ } catch (err) {
228
+ this.#logger?.debug?.("redis-streams: writer quit failed", {
229
+ err: err instanceof Error ? err.message : err
230
+ });
231
+ }
232
+ }
233
+ }
234
+ async #runReadLoop(sub) {
235
+ while (!sub.stopped) {
236
+ let result;
237
+ try {
238
+ result = await sub.readClient.xReadGroup(sub.group, sub.consumer, [{ key: sub.streamKey, id: ">" }], {
239
+ COUNT: 10,
240
+ BLOCK: this.#blockMs
241
+ });
242
+ } catch (err) {
243
+ if (sub.stopped) return;
244
+ this.#logger?.debug?.("redis-streams: xReadGroup failed", {
245
+ topic: sub.topic,
246
+ group: sub.group,
247
+ err: err instanceof Error ? err.message : err
248
+ });
249
+ await new Promise((r) => setTimeout(r, 100));
250
+ continue;
251
+ }
252
+ if (!result || result.length === 0) continue;
253
+ for (const stream of result) {
254
+ for (const entry of stream.messages) {
255
+ if (sub.stopped) return;
256
+ await this.#deliverMessage(sub, entry.id, entry.message);
257
+ }
258
+ }
259
+ }
260
+ }
261
+ async #deliverMessage(sub, streamId, fields) {
262
+ let event;
263
+ try {
264
+ event = JSON.parse(fields.event ?? "{}");
265
+ if (typeof event.createdAt === "string") {
266
+ event.createdAt = new Date(event.createdAt);
267
+ }
268
+ } catch (err) {
269
+ this.#logger?.debug?.("redis-streams: malformed payload, dropping", {
270
+ topic: sub.topic,
271
+ streamId,
272
+ err: err instanceof Error ? err.message : err
273
+ });
274
+ try {
275
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
276
+ } catch (ackErr) {
277
+ this.#logger?.debug?.("redis-streams: xAck after malformed payload failed", {
278
+ topic: sub.topic,
279
+ err: ackErr instanceof Error ? ackErr.message : ackErr
280
+ });
281
+ }
282
+ return;
283
+ }
284
+ let settled = false;
285
+ const ack = async () => {
286
+ if (settled) return;
287
+ settled = true;
288
+ try {
289
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
290
+ } catch (err) {
291
+ this.#logger?.debug?.("redis-streams: ack cleanup failed", {
292
+ topic: sub.topic,
293
+ err: err instanceof Error ? err.message : err
294
+ });
295
+ }
296
+ };
297
+ const nack = async () => {
298
+ if (settled) return;
299
+ settled = true;
300
+ const attempt = event.deliveryAttempt ?? 1;
301
+ if (attempt >= this.#maxDeliveryAttempts) {
302
+ this.#logger?.warn?.("redis-streams: dropping event after max delivery attempts", {
303
+ topic: sub.topic,
304
+ eventType: event.type,
305
+ eventId: event.id,
306
+ attempt,
307
+ max: this.#maxDeliveryAttempts
308
+ });
309
+ try {
310
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
311
+ } catch (err) {
312
+ this.#logger?.debug?.("redis-streams: ack on dropped poison message failed", {
313
+ topic: sub.topic,
314
+ err: err instanceof Error ? err.message : err
315
+ });
316
+ }
317
+ return;
318
+ }
319
+ const next = {
320
+ ...event,
321
+ deliveryAttempt: attempt + 1
322
+ };
323
+ try {
324
+ await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
325
+ } catch (err) {
326
+ this.#logger?.warn?.("redis-streams: nack republish failed; leaving original pending for reclaim", {
327
+ topic: sub.topic,
328
+ eventId: event.id,
329
+ err: err instanceof Error ? err.message : err
330
+ });
331
+ settled = false;
332
+ return;
333
+ }
334
+ try {
335
+ await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);
336
+ } catch (err) {
337
+ this.#logger?.debug?.("redis-streams: xAck after nack failed", {
338
+ topic: sub.topic,
339
+ err: err instanceof Error ? err.message : err
340
+ });
341
+ }
342
+ };
343
+ try {
344
+ const result = sub.cb(event, ack, nack);
345
+ if (result && typeof result.catch === "function") {
346
+ result.catch(async () => {
347
+ await nack();
348
+ });
349
+ }
350
+ } catch {
351
+ await nack();
352
+ }
353
+ }
354
+ };
355
+
356
+ export { RedisStreamsPubSub };
357
+ //# sourceMappingURL=index.js.map
358
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from 'redis';\nimport type { RedisClientOptions, RedisClientType } from 'redis';\n\n/**\n * Mastra PubSub backed by Redis Streams.\n *\n * - Each topic maps to a Redis stream key `<prefix>:<topic>`.\n * - Subscriptions with `options.group` use a real Redis consumer group, so\n * competing subscribers in the same group share the work (round-robin).\n * - Subscriptions without a group create a private per-subscriber consumer\n * group, so they get fan-out semantics (every subscriber sees every event).\n * - Nack triggers redelivery by re-publishing the event with an incremented\n * `deliveryAttempt` field, then XACK-ing the original. This trades strict\n * FIFO ordering on retry for a simple, reliable redelivery path.\n */\nexport interface RedisStreamsPubSubConfig {\n url?: string;\n keyPrefix?: string;\n blockMs?: number;\n redisOptions?: RedisClientOptions;\n /**\n * Approximate maximum number of entries kept per stream. On every publish we\n * issue MAXLEN ~ N which lets Redis trim opportunistically. Defaults to\n * 10_000 — set to 0 to disable trimming.\n */\n maxStreamLength?: number;\n /**\n * How often (in ms) each subscription runs XAUTOCLAIM to recover messages\n * that an earlier consumer in the group read but never acked. Defaults to\n * 30_000 ms. Set to 0 to disable.\n */\n reclaimIntervalMs?: number;\n /**\n * Minimum idle time (in ms) before a pending message is eligible for\n * reclaim. Should be much larger than typical in-flight processing time to\n * avoid double-delivery. Defaults to 60_000 ms.\n */\n reclaimIdleMs?: number;\n /**\n * Maximum number of times a single event will be redelivered via nack\n * before it is dropped (acked without republish). Defaults to 5. Set to\n * `Infinity` to disable the cap (events redeliver forever on every nack).\n *\n * `0` is treated as `Infinity` with a one-time warn for back-compat;\n * prefer `Infinity` to disable the cap explicitly.\n */\n maxDeliveryAttempts?: number;\n /**\n * Optional logger for diagnostics. When omitted, suppressed errors\n * (BUSYGROUP, malformed payloads, connection-close races) are swallowed\n * silently. When provided, those paths emit `debug`/`warn` entries so\n * operators can see what's happening without noise on the happy path.\n */\n logger?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };\n}\n\nexport class RedisStreamsPubSub extends PubSub {\n // Redis Streams is a pull transport: consumers issue XREADGROUP to read\n // events. Mastra reads this to know an OrchestrationWorker is required.\n override get supportedModes(): ReadonlyArray<PubSubDeliveryMode> {\n return ['pull'];\n }\n\n #writeClient: RedisClientType;\n #connectOptions: RedisClientOptions;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #logger?: RedisStreamsPubSubConfig['logger'];\n // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to\n // multiple topics independently. Without the topic in the key,\n // unsubscribe(otherTopic, cb) would tear down the wrong subscription.\n #subscriptions: Map<string, Subscription> = new Map();\n #cbIds: WeakMap<EventCallback, string> = new WeakMap();\n #pendingPublishes: Set<Promise<unknown>> = new Set();\n #closed = false;\n\n constructor(options: RedisStreamsPubSubConfig = {}) {\n super();\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n this.#connectOptions = { ...options.redisOptions, url };\n this.#writeClient = createClient(this.#connectOptions) as RedisClientType;\n this.#keyPrefix = options.keyPrefix ?? 'mastra:topic';\n this.#blockMs = options.blockMs ?? 1000;\n this.#maxStreamLength = options.maxStreamLength ?? 10_000;\n this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 30_000;\n this.#reclaimIdleMs = options.reclaimIdleMs ?? 60_000;\n const cap = options.maxDeliveryAttempts ?? 5;\n if (cap === 0) {\n options.logger?.warn?.(\n 'redis-streams: maxDeliveryAttempts=0 is treated as Infinity for back-compat; pass Infinity to disable the cap explicitly.',\n );\n this.#maxDeliveryAttempts = Infinity;\n } else if (cap < 0 || Number.isNaN(cap)) {\n throw new Error(`redis-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);\n } else {\n this.#maxDeliveryAttempts = cap;\n }\n this.#logger = options.logger;\n }\n\n #subKey(topic: string, cb: EventCallback): string {\n let cbId = this.#cbIds.get(cb);\n if (!cbId) {\n cbId = randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent. */\n async #ensureWriterConnected(): Promise<void> {\n if (this.#writeClient.isOpen) return;\n await this.#writeClient.connect();\n }\n\n #streamKey(topic: string): string {\n return `${this.#keyPrefix}:${topic}`;\n }\n\n async publish(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n await this.#ensureWriterConnected();\n\n const id = randomUUID();\n const createdAt = new Date();\n const payload: Event = {\n ...event,\n id,\n createdAt,\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n const xaddOptions: { TRIM?: { strategy: 'MAXLEN'; strategyModifier: '~'; threshold: number } } = {};\n if (this.#maxStreamLength > 0) {\n xaddOptions.TRIM = {\n strategy: 'MAXLEN',\n strategyModifier: '~',\n threshold: this.#maxStreamLength,\n };\n }\n const promise = this.#writeClient.xAdd(\n this.#streamKey(topic),\n '*',\n { event: JSON.stringify(payload) },\n xaddOptions,\n );\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot subscribe on closed client');\n const key = this.#subKey(topic, cb);\n if (this.#subscriptions.has(key)) return; // idempotent: same (topic, cb) already subscribed\n\n await this.#ensureWriterConnected();\n\n const isGrouped = !!options?.group;\n const group = options?.group ?? `__fanout-${randomUUID()}`;\n const consumer = `${group}-${randomUUID()}`;\n const streamKey = this.#streamKey(topic);\n\n // Create the consumer group if it doesn't exist. MKSTREAM creates the\n // stream if needed. BUSYGROUP means another subscriber raced us — fine.\n //\n // We anchor brand-new groups at '0' (stream start) instead of '$' so that\n // a worker which subscribes after a publish still sees the backlog. This\n // is the \"late join\" case: a server may publish workflow.start before any\n // orchestrator process exists. Without this, that work is silently lost.\n // Existing groups (BUSYGROUP path) keep their own checkpoint, so this\n // doesn't change semantics for already-running clusters. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n await this.#writeClient.xGroupCreate(streamKey, group, '0', { MKSTREAM: true });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each subscription gets a dedicated reader connection because XREADGROUP\n // with BLOCK > 0 holds the connection until a message arrives.\n const readClient = createClient(this.#connectOptions) as RedisClientType;\n await readClient.connect();\n\n const sub: Subscription = {\n cb,\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n };\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n }\n\n /**\n * Periodically run XAUTOCLAIM against this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Runs only for grouped subscriptions — fan-out groups are\n * private to one consumer, so there's no sibling to claim from.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n const reply = await this.#writeClient.xAutoClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n '0-0',\n { COUNT: 100 },\n );\n const messages = (reply?.messages ?? []) as Array<{ id: string; message: Record<string, string> } | null>;\n for (const entry of messages) {\n if (sub.stopped || this.#closed) return;\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: XAUTOCLAIM failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n }\n if (sub.stopped || this.#closed) return;\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n };\n\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const key = this.#subKey(topic, cb);\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n this.#subscriptions.delete(key);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await sub.readClient.quit();\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reader quit failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n\n if (sub.loop) {\n try {\n await sub.loop;\n } catch (err) {\n // loop exits naturally when readClient closes; surface only at debug.\n this.#logger?.debug?.('redis-streams: read loop exited with error', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n // For fan-out, drop the private group entirely so the stream can be reclaimed.\n if (!sub.isGrouped) {\n try {\n await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: xGroupDestroy failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n }\n\n async flush(): Promise<void> {\n // Wait for any in-flight publishes to settle.\n if (this.#pendingPublishes.size > 0) {\n await Promise.allSettled([...this.#pendingPublishes]);\n }\n }\n\n /**\n * Disconnect all clients and stop all subscription loops.\n */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = [...this.#subscriptions.values()];\n await Promise.all(subs.map(sub => this.unsubscribe(sub.topic, sub.cb)));\n\n if (this.#writeClient.isOpen) {\n try {\n await this.#writeClient.quit();\n } catch (err) {\n this.#logger?.debug?.('redis-streams: writer quit failed', {\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n }\n\n async #runReadLoop(sub: Subscription): Promise<void> {\n while (!sub.stopped) {\n let result;\n try {\n result = await sub.readClient.xReadGroup(sub.group, sub.consumer, [{ key: sub.streamKey, id: '>' }], {\n COUNT: 10,\n BLOCK: this.#blockMs,\n });\n } catch (err) {\n if (sub.stopped) return;\n this.#logger?.debug?.('redis-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n // Connection error or similar — pause briefly then retry.\n await new Promise(r => setTimeout(r, 100));\n continue;\n }\n\n if (!result || result.length === 0) continue;\n\n for (const stream of result) {\n for (const entry of stream.messages) {\n if (sub.stopped) return;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n }\n }\n }\n\n async #deliverMessage(sub: Subscription, streamId: string, fields: Record<string, string>): Promise<void> {\n let event: Event;\n try {\n event = JSON.parse(fields.event ?? '{}') as Event;\n // createdAt is serialized as a string; rehydrate.\n if (typeof event.createdAt === 'string') {\n event.createdAt = new Date(event.createdAt);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\n err: err instanceof Error ? err.message : err,\n });\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (ackErr) {\n this.#logger?.debug?.('redis-streams: xAck after malformed payload failed', {\n topic: sub.topic,\n err: ackErr instanceof Error ? ackErr.message : ackErr,\n });\n }\n return;\n }\n\n let settled = false;\n const ack = async () => {\n if (settled) return;\n settled = true;\n try {\n // Only ack against this consumer group. Do NOT xDel: the stream may\n // be consumed by other groups, and xDel removes the entry for all of\n // them. Stream growth is bounded elsewhere via MAXLEN-style trimming.\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: ack cleanup failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\n // Cap redelivery to avoid an infinite poison-pill loop. When the cap\n // is hit we drop the event (xAck without republish) and warn so an\n // operator can find it in logs.\n if (attempt >= this.#maxDeliveryAttempts) {\n this.#logger?.warn?.('redis-streams: dropping event after max delivery attempts', {\n topic: sub.topic,\n eventType: event.type,\n eventId: event.id,\n attempt,\n max: this.#maxDeliveryAttempts,\n });\n try {\n // Group-scoped ack only — see ack() above for why we never xDel.\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: ack on dropped poison message failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n return;\n }\n // Republish with incremented deliveryAttempt FIRST, then ack the\n // original entry. If the republish fails we deliberately leave the\n // original message pending so XAUTOCLAIM (or another consumer) can\n // pick it up on a future tick — acking first would silently drop it.\n const next: Event = {\n ...event,\n deliveryAttempt: attempt + 1,\n };\n try {\n await this.#writeClient.xAdd(sub.streamKey, '*', { event: JSON.stringify(next) });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Allow this entry to be redelivered: reset settled so the next\n // delivery attempt (via XAUTOCLAIM) can ack/nack it again.\n settled = false;\n return;\n }\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('redis-streams: xAck after nack failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n\n try {\n // EventCallback is typed `=> void` but handlers commonly return a\n // promise (TS allows Promise<void> to satisfy void). If we get one\n // back, attach a catch handler so async rejections route to nack\n // instead of silently dropping the message. We do NOT await here —\n // serializing messages on a subscription would deadlock orchestration\n // callbacks that await their own future events.\n const result: unknown = sub.cb(event, ack, nack);\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(async () => {\n await nack();\n });\n }\n } catch {\n // Caller threw synchronously — treat as nack.\n await nack();\n }\n }\n}\n\ninterface Subscription {\n cb: EventCallback;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/redis-streams",
3
- "version": "0.0.0",
3
+ "version": "0.0.2-alpha.0",
4
4
  "description": "Mastra Redis Streams PubSub integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,31 +22,23 @@
22
22
  },
23
23
  "./package.json": "./package.json"
24
24
  },
25
- "scripts": {
26
- "build": "tsup --silent --config tsup.config.ts",
27
- "build:watch": "tsup --watch --silent --config tsup.config.ts",
28
- "pretest": "docker compose up -d --wait",
29
- "test": "vitest run",
30
- "posttest": "docker compose down",
31
- "lint": "eslint ."
32
- },
33
25
  "dependencies": {
34
26
  "redis": "5.10.0"
35
27
  },
36
28
  "devDependencies": {
37
- "@internal/lint": "workspace:*",
38
- "@internal/types-builder": "workspace:*",
39
- "@mastra/core": "workspace:*",
40
- "@mastra/deployer": "workspace:*",
41
- "@mastra/libsql": "workspace:*",
42
29
  "@types/node": "22.19.15",
43
- "@vitest/coverage-v8": "catalog:",
44
- "@vitest/ui": "catalog:",
30
+ "@vitest/coverage-v8": "4.1.5",
31
+ "@vitest/ui": "4.1.5",
45
32
  "eslint": "^10.2.1",
46
33
  "tsup": "^8.5.1",
47
34
  "tsx": "^4.21.0",
48
- "typescript": "catalog:",
49
- "vitest": "catalog:"
35
+ "typescript": "^6.0.3",
36
+ "vitest": "4.1.5",
37
+ "@internal/lint": "0.0.92",
38
+ "@mastra/core": "1.33.0-alpha.9",
39
+ "@internal/types-builder": "0.0.67",
40
+ "@mastra/libsql": "1.10.1-alpha.2",
41
+ "@mastra/deployer": "1.33.0-alpha.9"
50
42
  },
51
43
  "peerDependencies": {
52
44
  "@mastra/core": ">=1.0.0-0 <2.0.0-0"
@@ -62,5 +54,13 @@
62
54
  },
63
55
  "engines": {
64
56
  "node": ">=22.13.0"
57
+ },
58
+ "scripts": {
59
+ "build": "tsup --silent --config tsup.config.ts",
60
+ "build:watch": "tsup --watch --silent --config tsup.config.ts",
61
+ "pretest": "docker compose up -d --wait",
62
+ "test": "vitest run",
63
+ "posttest": "docker compose down",
64
+ "lint": "eslint ."
65
65
  }
66
- }
66
+ }