@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 +30 -0
- package/dist/index.cjs +360 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +358 -0
- package/dist/index.js.map +1 -0
- package/package.json +19 -19
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":"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","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"]}
|
package/dist/index.d.ts
ADDED
|
@@ -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": "
|
|
44
|
-
"@vitest/ui": "
|
|
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": "
|
|
49
|
-
"vitest": "
|
|
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
|
+
}
|