@mastra/redis-streams 0.2.0-alpha.1 → 0.3.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,62 @@
1
1
  # @mastra/redis-streams
2
2
 
3
+ ## 0.3.0-alpha.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Make `RedisStreamsPubSub` survive Redis connection drops, and add topic cleanup to bound memory. ([#19138](https://github.com/mastra-ai/mastra/pull/19138))
8
+
9
+ **Fixed** — a dropped Redis connection (restart, failover, idle reset) no longer wedges the client. The clients were missing the `'error'` listener node-redis requires, so a socket drop threw an uncaughtException and left the client unable to reconnect, hanging every later publish. It now reconnects on its own. The read loop also recovers when a stream's consumer group disappears, instead of retrying forever.
10
+
11
+ **Added** — `clearTopic(topic)` deletes a topic's stream so finished runs release their memory instead of accumulating. The durable-agent runtime calls it during cleanup.
12
+
13
+ **Added** — an opt-in `streamIdleTtlMs` option puts a rolling time-to-live on each stream. Active streams keep refreshing it and never expire mid-flight; abandoned ones (e.g. a crashed run) are removed after they go quiet.
14
+
15
+ ```ts
16
+ const pubsub = new RedisStreamsPubSub({
17
+ url: 'redis://localhost:6379',
18
+ streamIdleTtlMs: 24 * 60 * 60 * 1000, // remove streams idle for 24h
19
+ });
20
+
21
+ // free a finished topic's stream immediately
22
+ await pubsub.clearTopic('workflow.events.run-123');
23
+ ```
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [[`4adc391`](https://github.com/mastra-ai/mastra/commit/4adc3911075249c352bb4832d2471922826344de), [`b486abf`](https://github.com/mastra-ai/mastra/commit/b486abfa2a7528c6f527e4015c819ea9fa54aaad)]:
28
+ - @mastra/core@1.51.0-alpha.10
29
+
30
+ ## 0.2.0
31
+
32
+ ### Minor Changes
33
+
34
+ - Collapsed the result of `agent.sendSignal` / `sendMessage` / `queueMessage` / `sendStateSignal` / `sendNotificationSignal` into a single `accepted` promise that resolves at decision-time to a discriminated union: `{ action: 'wake'; runId; output }` when the signal started a run in this process, `{ action: 'deliver'; runId }` when it was forwarded onto an existing run, or `{ action: 'persist' }` / `{ action: 'discard' }` when nothing ran. `runId` is present only on `wake`/`deliver`; for `persist`/`discard` correlate via `result.signal.id`. `accepted` resolves for routing decisions and rejects only when the signal couldn't be routed at all (e.g. misconfigured agent). This replaces the old `accepted: true` boolean and the best-effort top-level `runId`. `result.persisted` stays top-level. `sendNotificationSignal`'s `accepted` is optional (a notification may be dropped by policy with no signal) — read `result.decision` for the policy verdict. ([#17723](https://github.com/mastra-ai/mastra/pull/17723))
35
+
36
+ On serverless platforms, when this process/Lambda is the one that woke the run (`action === 'wake'`), it can use the platform's `waitUntil` to keep itself alive until the run it started completes — otherwise the runtime may be frozen or torn down mid-run:
37
+
38
+ ```ts
39
+ const result = agent.sendSignal(signal, { resourceId, threadId });
40
+ ctx.waitUntil(
41
+ result.accepted.then(async accepted => {
42
+ if (accepted.action === 'wake') await accepted.output.consumeStream();
43
+ }),
44
+ );
45
+ ```
46
+
47
+ Added a `LeaseProvider` capability (`acquireLease` / `releaseLease` / `renewLease` / `getLeaseOwner` / `transferLease`) — distributed leasing kept separate from event delivery (`PubSub`) — so processes racing to wake the same thread coordinate a single owner. The winner runs the stream; losers forward their signal to it. `EventEmitterPubSub` leases in-memory; `RedisStreamsPubSub` uses `SET NX PX` with owner-verified Lua scripts for release, renew, and transfer; the default `NoopLeaseProvider` always wins, preserving single-process behavior.
48
+
49
+ Fixed a cross-process race where, after a run finished, the thread's lease briefly went free before its queued follow-up work started — letting another process win the lease and start a competing run for the same thread. The owner now keeps the lease until all queued work is drained. The lease TTL and renewal interval are overridable for tests via `MASTRA_AGENT_THREAD_LEASE_TTL_MS` and `MASTRA_AGENT_THREAD_LEASE_RENEW_INTERVAL_MS` (production defaults unchanged).
50
+
51
+ ### Patch Changes
52
+
53
+ - Honor the `localOnly` publish option so in-process subscribers can receive events without round-tripping through the broker. ([#17836](https://github.com/mastra-ai/mastra/pull/17836))
54
+
55
+ This matches the contract already implemented by `UnixSocketPubSub` in `@mastra/core`: when `Mastra` tags an internal workflow event as `localOnly`, the payload is delivered by reference to local subscribers and the broker is skipped entirely. Live runtime values like `MastraModelOutput` instances now keep their prototypes when the evented agent loop runs against a Redis Streams or Google Cloud Pub/Sub broker, fixing `output.consumeStream is not a function` style failures.
56
+
57
+ - Updated dependencies [[`5bd72d2`](https://github.com/mastra-ai/mastra/commit/5bd72d255f45b5ea8ab342643bd463814a980a24), [`1cc9ee1`](https://github.com/mastra-ai/mastra/commit/1cc9ee1ba51db53020a735626d33017a60b4b5b3), [`417baae`](https://github.com/mastra-ai/mastra/commit/417baae40b995db5819c845036947f0c27dc1c00), [`65f255a`](https://github.com/mastra-ai/mastra/commit/65f255a38667beb6ceeadabfa9eb5059bfec8298), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`30ebaf0`](https://github.com/mastra-ai/mastra/commit/30ebaf07bed5f4d30f2f257836c15d1bf7e40aae), [`5704634`](https://github.com/mastra-ai/mastra/commit/5704634b22133167dea337a942a34f57aaa3fa14), [`5c4e9a4`](https://github.com/mastra-ai/mastra/commit/5c4e9a4cfb2216bb3ea7f8988ad3727f3b92bb3a), [`4a88c6e`](https://github.com/mastra-ai/mastra/commit/4a88c6e2bdce316f8d7551b4ec3449b0b06fc71c), [`417baae`](https://github.com/mastra-ai/mastra/commit/417baae40b995db5819c845036947f0c27dc1c00), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`25961e3`](https://github.com/mastra-ai/mastra/commit/25961e3260ff3b1464637af8fcdb36210551c39f), [`6a1428a`](https://github.com/mastra-ai/mastra/commit/6a1428a23133fc070fc6c1caa08d28f3ba4fe5ff), [`87a17ef`](https://github.com/mastra-ai/mastra/commit/87a17efbd725aca6639febdc5e69e2abb3048689), [`e11ff30`](https://github.com/mastra-ai/mastra/commit/e11ff301408bf1731dca2fb7fbfcd8c819500a35), [`7794d71`](https://github.com/mastra-ai/mastra/commit/7794d71872c68733a30e028dfb7b1705daf6c5d2), [`9d2c946`](https://github.com/mastra-ai/mastra/commit/9d2c946d0859e90ae4bcec5beeb1da7398d2ad1e), [`c0eda2b`](https://github.com/mastra-ai/mastra/commit/c0eda2bcd91a228427314b12c91d8b147f3a739f), [`7b29f33`](https://github.com/mastra-ai/mastra/commit/7b29f332a357a83e555f29e718e5f2fab9979943), [`c0eda2b`](https://github.com/mastra-ai/mastra/commit/c0eda2bcd91a228427314b12c91d8b147f3a739f), [`b13925b`](https://github.com/mastra-ai/mastra/commit/b13925bfa91aa8700f56fa54a9ce707ee7e4ba62), [`f1ec385`](https://github.com/mastra-ai/mastra/commit/f1ec385386f62b1a0847ec5353ae2bb169d1c3d9), [`e14986f`](https://github.com/mastra-ai/mastra/commit/e14986f6e5478d6384d04ff9a7f9a79a46a8b529), [`24912b1`](https://github.com/mastra-ai/mastra/commit/24912b1f855d29ec36af4ef4bde1f7417e20cdf5), [`bf94ec6`](https://github.com/mastra-ai/mastra/commit/bf94ec68192d9f16e46ef7e5ac36370aeeddf35d), [`a29f371`](https://github.com/mastra-ai/mastra/commit/a29f371aef629ac8562661524a497127e93b5131), [`7686216`](https://github.com/mastra-ai/mastra/commit/7686216f37e74568feddec17cef3c3d24e10e60a), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`073f910`](https://github.com/mastra-ai/mastra/commit/073f910481e7d94b95ba3830f96531774ae95d33), [`0be490f`](https://github.com/mastra-ai/mastra/commit/0be490fabb538c5a7de796ea0aff7d04a0bea1f3), [`0be490f`](https://github.com/mastra-ai/mastra/commit/0be490fabb538c5a7de796ea0aff7d04a0bea1f3), [`ebbe1d3`](https://github.com/mastra-ai/mastra/commit/ebbe1d31a965a3adb0e728758f326b8122b4b55f), [`974f614`](https://github.com/mastra-ai/mastra/commit/974f614e083bd68278536f94453f7b320b86a3c7), [`3818814`](https://github.com/mastra-ai/mastra/commit/38188149ce454c4403fe9fcbdf73b735c68d36be), [`975c59a`](https://github.com/mastra-ai/mastra/commit/975c59ae363ee275fc55062392e1ffd2cbccbd53), [`1f97ce5`](https://github.com/mastra-ai/mastra/commit/1f97ce5695463bebb4eaacf501da6fb403e20885), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`7f51548`](https://github.com/mastra-ai/mastra/commit/7f515481213780be7047cef00640b9d35f3d545c), [`64f58c0`](https://github.com/mastra-ai/mastra/commit/64f58c04e78b40137497d47f781e897e416f22a5), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`ebbe1d3`](https://github.com/mastra-ai/mastra/commit/ebbe1d31a965a3adb0e728758f326b8122b4b55f), [`d95f394`](https://github.com/mastra-ai/mastra/commit/d95f394fd24c8411886930d727679c4d5252aa26), [`417baae`](https://github.com/mastra-ai/mastra/commit/417baae40b995db5819c845036947f0c27dc1c00), [`8e25a78`](https://github.com/mastra-ai/mastra/commit/8e25a78e0597575f0b0729bae8c5e190c84869b5), [`417baae`](https://github.com/mastra-ai/mastra/commit/417baae40b995db5819c845036947f0c27dc1c00), [`f3f0c9d`](https://github.com/mastra-ai/mastra/commit/f3f0c9d7c878db5a13177871ce3523a14f14b311), [`a5b22d3`](https://github.com/mastra-ai/mastra/commit/a5b22d314d62a68d801886a8d3d0eb6c089473db), [`31be1cf`](https://github.com/mastra-ai/mastra/commit/31be1cf5f2a7b5eef12f6123a40653b4d8115c16), [`417baae`](https://github.com/mastra-ai/mastra/commit/417baae40b995db5819c845036947f0c27dc1c00), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858), [`74955f9`](https://github.com/mastra-ai/mastra/commit/74955f9120cde8b1d8ce4399232b4033236be858)]:
58
+ - @mastra/core@1.46.0
59
+
3
60
  ## 0.2.0-alpha.1
4
61
 
5
62
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -5,6 +5,13 @@ var events = require('@mastra/core/events');
5
5
  var redis = require('redis');
6
6
 
7
7
  // src/index.ts
8
+ function errorText(err) {
9
+ const parts = [err instanceof Error ? err.message : String(err)];
10
+ if (err instanceof Error && "replies" in err && Array.isArray(err.replies)) {
11
+ parts.push(...err.replies.map((r) => String(r)));
12
+ }
13
+ return parts.join("; ");
14
+ }
8
15
  var RedisStreamsPubSub = class extends events.PubSub {
9
16
  // Redis Streams is a pull transport: consumers issue XREADGROUP to read
10
17
  // events. Mastra reads this to know an OrchestrationWorker is required.
@@ -16,6 +23,7 @@ var RedisStreamsPubSub = class extends events.PubSub {
16
23
  #keyPrefix;
17
24
  #blockMs;
18
25
  #maxStreamLength;
26
+ #streamIdleTtlMs;
19
27
  #reclaimIntervalMs;
20
28
  #reclaimIdleMs;
21
29
  #maxDeliveryAttempts;
@@ -38,9 +46,16 @@ var RedisStreamsPubSub = class extends events.PubSub {
38
46
  const url = options.url ?? options.redisOptions?.url ?? "redis://localhost:6379";
39
47
  this.#connectOptions = { ...options.redisOptions, url };
40
48
  this.#writeClient = redis.createClient(this.#connectOptions);
49
+ this.#logger = options.logger;
50
+ this.#attachErrorLogger(this.#writeClient, "write");
41
51
  this.#keyPrefix = options.keyPrefix ?? "mastra:topic";
42
52
  this.#blockMs = options.blockMs ?? 1e3;
43
53
  this.#maxStreamLength = options.maxStreamLength ?? 1e4;
54
+ const ttl = options.streamIdleTtlMs ?? 0;
55
+ if (!Number.isInteger(ttl) || ttl < 0) {
56
+ throw new Error(`redis-streams: streamIdleTtlMs must be a non-negative integer (milliseconds), got ${ttl}`);
57
+ }
58
+ this.#streamIdleTtlMs = ttl;
44
59
  this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 3e4;
45
60
  this.#reclaimIdleMs = options.reclaimIdleMs ?? 6e4;
46
61
  const cap = options.maxDeliveryAttempts ?? 5;
@@ -54,7 +69,32 @@ var RedisStreamsPubSub = class extends events.PubSub {
54
69
  } else {
55
70
  this.#maxDeliveryAttempts = cap;
56
71
  }
57
- this.#logger = options.logger;
72
+ }
73
+ /**
74
+ * Attach the `'error'` listener node-redis requires on every client. Without
75
+ * one, a mid-life socket close makes the client emit an unhandled `'error'`,
76
+ * which (per EventEmitter semantics) throws from inside RedisSocket's error
77
+ * handler BEFORE the built-in reconnect is scheduled — the process gets an
78
+ * uncaughtException and the client is left permanently un-reconnected
79
+ * (isOpen=true, isReady=false), hanging every subsequent command. Merely
80
+ * having a listener lets node-redis's own reconnect run.
81
+ *
82
+ * Logged at warn so operators can see connection churn, throttled to one line
83
+ * per 30s per client: node-redis re-emits `'error'` roughly every 500ms while
84
+ * a server is unreachable, and with one reader client per subscription an
85
+ * outage would otherwise flood the logs with thousands of identical lines.
86
+ */
87
+ #attachErrorLogger(client, role, extra = {}) {
88
+ let lastWarnAt = 0;
89
+ client.on("error", (err) => {
90
+ const now = Date.now();
91
+ if (now - lastWarnAt < 3e4) return;
92
+ lastWarnAt = now;
93
+ this.#logger?.warn?.(`redis-streams: ${role} client connection error (node-redis will reconnect)`, {
94
+ ...extra,
95
+ err: err instanceof Error ? err.message : err
96
+ });
97
+ });
58
98
  }
59
99
  #subKey(topic, cb) {
60
100
  let cbId = this.#cbIds.get(cb);
@@ -101,12 +141,15 @@ var RedisStreamsPubSub = class extends events.PubSub {
101
141
  threshold: this.#maxStreamLength
102
142
  };
103
143
  }
104
- const promise = this.#writeClient.xAdd(
105
- this.#streamKey(topic),
106
- "*",
107
- { event: JSON.stringify(payload) },
108
- xaddOptions
109
- );
144
+ const streamKey = this.#streamKey(topic);
145
+ const promise = this.#streamIdleTtlMs > 0 ? this.#writeClient.multi().xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions).pExpire(streamKey, this.#streamIdleTtlMs).exec().then((replies) => {
146
+ if (Number(replies[1]) !== 1) {
147
+ this.#logger?.warn?.("redis-streams: PEXPIRE inside publish MULTI did not apply", {
148
+ streamKey,
149
+ reply: String(replies[1])
150
+ });
151
+ }
152
+ }) : this.#writeClient.xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions);
110
153
  this.#pendingPublishes.add(promise);
111
154
  try {
112
155
  await promise;
@@ -137,6 +180,7 @@ var RedisStreamsPubSub = class extends events.PubSub {
137
180
  this.#logger?.debug?.("redis-streams: consumer group already exists", { topic, group });
138
181
  }
139
182
  const readClient = redis.createClient(this.#connectOptions);
183
+ this.#attachErrorLogger(readClient, "read", { topic });
140
184
  await readClient.connect();
141
185
  const sub = {
142
186
  cb,
@@ -242,6 +286,35 @@ var RedisStreamsPubSub = class extends events.PubSub {
242
286
  await Promise.allSettled([...this.#pendingPublishes]);
243
287
  }
244
288
  }
289
+ /**
290
+ * Delete a topic's stream, and with it every consumer group on that stream.
291
+ * This is the Redis implementation of the optional `clearTopic` hook that
292
+ * consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is
293
+ * over — without it, a persistent backend accumulates every finished run's
294
+ * stream until Redis exhausts its memory.
295
+ *
296
+ * Because it removes events for ALL consumers and destroys their groups, only
297
+ * call it once nothing will read the topic again. A subscriber still attached
298
+ * when the stream is deleted recovers on its own (the read loop recreates the
299
+ * group on NOGROUP) but will have missed the deleted entries.
300
+ *
301
+ * Best-effort and non-throwing: callers commonly invoke this fire-and-forget
302
+ * (`void clearTopic(...)`), so a rejection would surface as an
303
+ * unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`
304
+ * backstop (when configured) reclaims anything a failed delete leaves behind.
305
+ */
306
+ async clearTopic(topic) {
307
+ if (this.#closed) return;
308
+ try {
309
+ await this.#ensureWriterConnected();
310
+ await this.#writeClient.del(this.#streamKey(topic));
311
+ } catch (err) {
312
+ this.#logger?.warn?.("redis-streams: clearTopic failed", {
313
+ topic,
314
+ err: err instanceof Error ? err.message : err
315
+ });
316
+ }
317
+ }
245
318
  /**
246
319
  * Lease key used in Redis. Distinct prefix from streams so leases and
247
320
  * streams can't collide on key namespace.
@@ -376,11 +449,31 @@ var RedisStreamsPubSub = class extends events.PubSub {
376
449
  });
377
450
  } catch (err) {
378
451
  if (sub.stopped) return;
379
- this.#logger?.debug?.("redis-streams: xReadGroup failed", {
380
- topic: sub.topic,
381
- group: sub.group,
382
- err: err instanceof Error ? err.message : err
383
- });
452
+ const msg = err instanceof Error ? err.message : String(err);
453
+ if (msg.includes("NOGROUP")) {
454
+ try {
455
+ if (this.#streamIdleTtlMs > 0) {
456
+ await this.#writeClient.multi().xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
457
+ } else {
458
+ await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true });
459
+ }
460
+ this.#logger?.debug?.("redis-streams: recreated consumer group after NOGROUP", {
461
+ topic: sub.topic,
462
+ group: sub.group
463
+ });
464
+ } catch (recreateErr) {
465
+ const rmsg = errorText(recreateErr);
466
+ if (!rmsg.includes("BUSYGROUP")) {
467
+ this.#logger?.debug?.("redis-streams: consumer group re-create failed", { topic: sub.topic, err: rmsg });
468
+ }
469
+ }
470
+ } else {
471
+ this.#logger?.debug?.("redis-streams: xReadGroup failed", {
472
+ topic: sub.topic,
473
+ group: sub.group,
474
+ err: msg
475
+ });
476
+ }
384
477
  await new Promise((r) => setTimeout(r, 100));
385
478
  continue;
386
479
  }
@@ -456,7 +549,11 @@ var RedisStreamsPubSub = class extends events.PubSub {
456
549
  deliveryAttempt: attempt + 1
457
550
  };
458
551
  try {
459
- await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
552
+ if (this.#streamIdleTtlMs > 0) {
553
+ await this.#writeClient.multi().xAdd(sub.streamKey, "*", { event: JSON.stringify(next) }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
554
+ } else {
555
+ await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
556
+ }
460
557
  } catch (err) {
461
558
  this.#logger?.warn?.("redis-streams: nack republish failed; leaving original pending for reclaim", {
462
559
  topic: sub.topic,
@@ -1 +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, LeaseProvider, 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 implements LeaseProvider {\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 // `localOnly` publishes bypass Redis entirely so values carrying live\n // methods (e.g. `MastraModelOutput` returned from an evented agent run via\n // `workflows-finish`) survive intact. Mirrors the same contract honored by\n // `UnixSocketPubSub` so consumers like `Mastra.__registerInternalWorkflow`\n // get consistent semantics across transports.\n #localCallbacks: Map<string, Set<EventCallback>> = new Map();\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(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Redis, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\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 // Register for `localOnly` delivery before wiring up the Redis reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\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 localBucket = this.#localCallbacks.get(topic);\n if (localBucket) {\n localBucket.delete(cb);\n if (localBucket.size === 0) this.#localCallbacks.delete(topic);\n }\n\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 * Lease key used in Redis. Distinct prefix from streams so leases and\n * streams can't collide on key namespace.\n */\n #leaseKey(key: string): string {\n return `${this.#keyPrefix}:lease:${key}`;\n }\n\n /**\n * Atomic claim via SET NX PX. Idempotent for the same owner: if the\n * current value is already this owner, we refresh the TTL instead of\n * failing. Cross-process callers race here; Redis serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\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 this.#localCallbacks.clear();\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 #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('redis-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\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"]}
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, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from 'redis';\nimport type { RedisClientOptions, RedisClientType } from 'redis';\n\n/**\n * Flatten an error into searchable text. node-redis MULTI failures throw a\n * `MultiErrorReply` whose own message is just \"N commands failed…\" — the real\n * per-command errors (e.g. BUSYGROUP) live in `err.replies`, so those are\n * folded in too.\n */\nfunction errorText(err: unknown): string {\n const parts: string[] = [err instanceof Error ? err.message : String(err)];\n if (err instanceof Error && 'replies' in err && Array.isArray((err as { replies?: unknown[] }).replies)) {\n parts.push(...(err as { replies: unknown[] }).replies.map(r => String(r)));\n }\n return parts.join('; ');\n}\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 * Idle expiry (in ms): a sliding TTL refreshed on every write to the stream\n * (publish, nack retry, group re-creation). Each write resets it, so an\n * actively-used stream never expires mid-flight; a stream left idle for the\n * full duration is deleted by Redis automatically.\n *\n * This is a BACKSTOP, not the primary cleanup. Normal cleanup is explicit:\n * `clearTopic` deletes a topic's stream the moment its lifecycle ends. This\n * option only bounds memory for streams that never reach a `clearTopic` call\n * — e.g. a run that crashed before cleanup — so they don't linger forever.\n *\n * Defaults to 0 (disabled) to preserve existing behavior.\n */\n streamIdleTtlMs?: 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 implements LeaseProvider {\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 #streamIdleTtlMs: 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 // `localOnly` publishes bypass Redis entirely so values carrying live\n // methods (e.g. `MastraModelOutput` returned from an evented agent run via\n // `workflows-finish`) survive intact. Mirrors the same contract honored by\n // `UnixSocketPubSub` so consumers like `Mastra.__registerInternalWorkflow`\n // get consistent semantics across transports.\n #localCallbacks: Map<string, Set<EventCallback>> = new Map();\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.#logger = options.logger;\n this.#attachErrorLogger(this.#writeClient, 'write');\n this.#keyPrefix = options.keyPrefix ?? 'mastra:topic';\n this.#blockMs = options.blockMs ?? 1000;\n this.#maxStreamLength = options.maxStreamLength ?? 10_000;\n const ttl = options.streamIdleTtlMs ?? 0;\n // Must be a non-negative integer: node-redis serializes the PEXPIRE arg with\n // toString(), and Redis rejects a non-integer/Infinity ('value is not an\n // integer or out of range'). Without this guard a bad value would poison\n // every publish MULTI and be silently swallowed on the nack/recovery\n // paths, so fail fast at construction instead.\n if (!Number.isInteger(ttl) || ttl < 0) {\n throw new Error(`redis-streams: streamIdleTtlMs must be a non-negative integer (milliseconds), got ${ttl}`);\n }\n this.#streamIdleTtlMs = ttl;\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 }\n\n /**\n * Attach the `'error'` listener node-redis requires on every client. Without\n * one, a mid-life socket close makes the client emit an unhandled `'error'`,\n * which (per EventEmitter semantics) throws from inside RedisSocket's error\n * handler BEFORE the built-in reconnect is scheduled — the process gets an\n * uncaughtException and the client is left permanently un-reconnected\n * (isOpen=true, isReady=false), hanging every subsequent command. Merely\n * having a listener lets node-redis's own reconnect run.\n *\n * Logged at warn so operators can see connection churn, throttled to one line\n * per 30s per client: node-redis re-emits `'error'` roughly every 500ms while\n * a server is unreachable, and with one reader client per subscription an\n * outage would otherwise flood the logs with thousands of identical lines.\n */\n #attachErrorLogger(client: RedisClientType, role: 'write' | 'read', extra: Record<string, unknown> = {}): void {\n let lastWarnAt = 0;\n client.on('error', err => {\n const now = Date.now();\n if (now - lastWarnAt < 30_000) return;\n lastWarnAt = now;\n this.#logger?.warn?.(`redis-streams: ${role} client connection error (node-redis will reconnect)`, {\n ...extra,\n err: err instanceof Error ? err.message : err,\n });\n });\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(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Redis, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\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 streamKey = this.#streamKey(topic);\n // When a TTL is configured the write and its PEXPIRE run in one MULTI\n // transaction. A detached PEXPIRE could fail or be skipped (process exit)\n // after a successful XADD — and on the *last* write before a topic is\n // abandoned there is no \"next write\" to self-heal, leaving an immortal\n // stream despite the TTL. Atomicity closes that window.\n const promise =\n this.#streamIdleTtlMs > 0\n ? this.#writeClient\n .multi()\n .xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions)\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec()\n .then(replies => {\n // XADD in the same transaction guarantees the key exists, so\n // PEXPIRE must reply 1; anything else means the TTL backstop is\n // not actually armed for this stream.\n if (Number(replies[1]) !== 1) {\n this.#logger?.warn?.('redis-streams: PEXPIRE inside publish MULTI did not apply', {\n streamKey,\n reply: String(replies[1]),\n });\n }\n })\n : this.#writeClient.xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions);\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 // Register for `localOnly` delivery before wiring up the Redis reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\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 this.#attachErrorLogger(readClient, 'read', { topic });\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 localBucket = this.#localCallbacks.get(topic);\n if (localBucket) {\n localBucket.delete(cb);\n if (localBucket.size === 0) this.#localCallbacks.delete(topic);\n }\n\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 * Delete a topic's stream, and with it every consumer group on that stream.\n * This is the Redis implementation of the optional `clearTopic` hook that\n * consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is\n * over — without it, a persistent backend accumulates every finished run's\n * stream until Redis exhausts its memory.\n *\n * Because it removes events for ALL consumers and destroys their groups, only\n * call it once nothing will read the topic again. A subscriber still attached\n * when the stream is deleted recovers on its own (the read loop recreates the\n * group on NOGROUP) but will have missed the deleted entries.\n *\n * Best-effort and non-throwing: callers commonly invoke this fire-and-forget\n * (`void clearTopic(...)`), so a rejection would surface as an\n * unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`\n * backstop (when configured) reclaims anything a failed delete leaves behind.\n */\n async clearTopic(topic: string): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n await this.#writeClient.del(this.#streamKey(topic));\n } catch (err) {\n // warn, not debug: a failed delete means the memory leak clearTopic\n // exists to prevent is silently recurring for this topic.\n this.#logger?.warn?.('redis-streams: clearTopic failed', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n /**\n * Lease key used in Redis. Distinct prefix from streams so leases and\n * streams can't collide on key namespace.\n */\n #leaseKey(key: string): string {\n return `${this.#keyPrefix}:lease:${key}`;\n }\n\n /**\n * Atomic claim via SET NX PX. Idempotent for the same owner: if the\n * current value is already this owner, we refresh the TTL instead of\n * failing. Cross-process callers race here; Redis serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\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 this.#localCallbacks.clear();\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 const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('NOGROUP')) {\n // The stream or its consumer group was deleted out from under us\n // (clearTopic, a streamIdleTtlMs expiry, or an external FLUSH). XREADGROUP\n // returns NOGROUP immediately, ignoring BLOCK, so without recovery\n // this loop busy-retries forever and the subscriber goes permanently\n // deaf — a later publish recreates the stream but not the group.\n // Recreate the group (anchored at '0', matching subscribe()) so\n // delivery resumes.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM recreates an (empty) stream key, so the TTL must be\n // stamped in the same MULTI — a detached PEXPIRE that fails or is\n // skipped would leave an abandoned-but-recreated topic lingering\n // forever with no further write to self-heal it. Note that when\n // the group already exists (a sibling won the race), exec()\n // throws a MultiErrorReply whose message does NOT contain\n // \"BUSYGROUP\" — it lives in err.replies — while the PEXPIRE in\n // the same transaction still applies, so the TTL is refreshed\n // even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(sub.streamKey, sub.group, '0', { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, '0', { MKSTREAM: true });\n }\n this.#logger?.debug?.('redis-streams: recreated consumer group after NOGROUP', {\n topic: sub.topic,\n group: sub.group,\n });\n } catch (recreateErr) {\n const rmsg = errorText(recreateErr);\n if (!rmsg.includes('BUSYGROUP')) {\n this.#logger?.debug?.('redis-streams: consumer group re-create failed', { topic: sub.topic, err: rmsg });\n }\n }\n } else {\n this.#logger?.debug?.('redis-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: msg,\n });\n }\n // Pause briefly then retry (with the group recreated, if it was NOGROUP).\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 if (this.#streamIdleTtlMs > 0) {\n // A nack republish is a write that keeps this stream in use, so it\n // must refresh the TTL too — otherwise a retry near the end of the\n // original window inherits the old expiry and the stream can be\n // deleted while the retry is still being processed. Same MULTI as\n // the write for the same reason as publish(): the republish may be\n // the stream's final write.\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', { event: JSON.stringify(next) })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', { event: JSON.stringify(next) });\n }\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 #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('redis-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\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 CHANGED
@@ -24,6 +24,20 @@ export interface RedisStreamsPubSubConfig {
24
24
  * 10_000 — set to 0 to disable trimming.
25
25
  */
26
26
  maxStreamLength?: number;
27
+ /**
28
+ * Idle expiry (in ms): a sliding TTL refreshed on every write to the stream
29
+ * (publish, nack retry, group re-creation). Each write resets it, so an
30
+ * actively-used stream never expires mid-flight; a stream left idle for the
31
+ * full duration is deleted by Redis automatically.
32
+ *
33
+ * This is a BACKSTOP, not the primary cleanup. Normal cleanup is explicit:
34
+ * `clearTopic` deletes a topic's stream the moment its lifecycle ends. This
35
+ * option only bounds memory for streams that never reach a `clearTopic` call
36
+ * — e.g. a run that crashed before cleanup — so they don't linger forever.
37
+ *
38
+ * Defaults to 0 (disabled) to preserve existing behavior.
39
+ */
40
+ streamIdleTtlMs?: number;
27
41
  /**
28
42
  * How often (in ms) each subscription runs XAUTOCLAIM to recover messages
29
43
  * that an earlier consumer in the group read but never acked. Defaults to
@@ -66,6 +80,24 @@ export declare class RedisStreamsPubSub extends PubSub implements LeaseProvider
66
80
  subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void>;
67
81
  unsubscribe(topic: string, cb: EventCallback): Promise<void>;
68
82
  flush(): Promise<void>;
83
+ /**
84
+ * Delete a topic's stream, and with it every consumer group on that stream.
85
+ * This is the Redis implementation of the optional `clearTopic` hook that
86
+ * consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is
87
+ * over — without it, a persistent backend accumulates every finished run's
88
+ * stream until Redis exhausts its memory.
89
+ *
90
+ * Because it removes events for ALL consumers and destroys their groups, only
91
+ * call it once nothing will read the topic again. A subscriber still attached
92
+ * when the stream is deleted recovers on its own (the read loop recreates the
93
+ * group on NOGROUP) but will have missed the deleted entries.
94
+ *
95
+ * Best-effort and non-throwing: callers commonly invoke this fire-and-forget
96
+ * (`void clearTopic(...)`), so a rejection would surface as an
97
+ * unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`
98
+ * backstop (when configured) reclaims anything a failed delete leaves behind.
99
+ */
100
+ clearTopic(topic: string): Promise<void>;
69
101
  /**
70
102
  * Atomic claim via SET NX PX. Idempotent for the same owner: if the
71
103
  * current value is already this owner, we refresh the TTL instead of
@@ -1 +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,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAErH,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,MAAO,YAAW,aAAa;;IAGrE,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;gBAyBW,OAAO,GAAE,wBAA6B;IA2C5C,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAkDV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsGtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqD5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAe5B;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAyBvG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAO7D;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB7D;;;OAGG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB7E;;;;;;;;OAQG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBrG;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CA4M7B"}
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,aAAa,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAErH,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,OAAO,CAAC;AAgBjE;;;;;;;;;;;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;;;;;;;;;;;;OAYG;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,MAAO,YAAW,aAAa;;IAGrE,IAAa,cAAc,IAAI,aAAa,CAAC,kBAAkB,CAAC,CAE/D;gBA0BW,OAAO,GAAE,wBAA6B;IAiF5C,OAAO,CACX,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,GAAG,WAAW,CAAC,EACtC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAChC,OAAO,CAAC,IAAI,CAAC;IAqEV,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuGtF,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAqD5D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B;;;;;;;;;;;;;;;;OAgBG;IACG,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB9C;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAyBvG,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAO7D;;;;OAIG;IACG,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAgB7D;;;OAGG;IACG,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAiB7E;;;;;;;;OAQG;IACG,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAkBrG;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAkQ7B"}
package/dist/index.js CHANGED
@@ -3,6 +3,13 @@ import { PubSub } from '@mastra/core/events';
3
3
  import { createClient } from 'redis';
4
4
 
5
5
  // src/index.ts
6
+ function errorText(err) {
7
+ const parts = [err instanceof Error ? err.message : String(err)];
8
+ if (err instanceof Error && "replies" in err && Array.isArray(err.replies)) {
9
+ parts.push(...err.replies.map((r) => String(r)));
10
+ }
11
+ return parts.join("; ");
12
+ }
6
13
  var RedisStreamsPubSub = class extends PubSub {
7
14
  // Redis Streams is a pull transport: consumers issue XREADGROUP to read
8
15
  // events. Mastra reads this to know an OrchestrationWorker is required.
@@ -14,6 +21,7 @@ var RedisStreamsPubSub = class extends PubSub {
14
21
  #keyPrefix;
15
22
  #blockMs;
16
23
  #maxStreamLength;
24
+ #streamIdleTtlMs;
17
25
  #reclaimIntervalMs;
18
26
  #reclaimIdleMs;
19
27
  #maxDeliveryAttempts;
@@ -36,9 +44,16 @@ var RedisStreamsPubSub = class extends PubSub {
36
44
  const url = options.url ?? options.redisOptions?.url ?? "redis://localhost:6379";
37
45
  this.#connectOptions = { ...options.redisOptions, url };
38
46
  this.#writeClient = createClient(this.#connectOptions);
47
+ this.#logger = options.logger;
48
+ this.#attachErrorLogger(this.#writeClient, "write");
39
49
  this.#keyPrefix = options.keyPrefix ?? "mastra:topic";
40
50
  this.#blockMs = options.blockMs ?? 1e3;
41
51
  this.#maxStreamLength = options.maxStreamLength ?? 1e4;
52
+ const ttl = options.streamIdleTtlMs ?? 0;
53
+ if (!Number.isInteger(ttl) || ttl < 0) {
54
+ throw new Error(`redis-streams: streamIdleTtlMs must be a non-negative integer (milliseconds), got ${ttl}`);
55
+ }
56
+ this.#streamIdleTtlMs = ttl;
42
57
  this.#reclaimIntervalMs = options.reclaimIntervalMs ?? 3e4;
43
58
  this.#reclaimIdleMs = options.reclaimIdleMs ?? 6e4;
44
59
  const cap = options.maxDeliveryAttempts ?? 5;
@@ -52,7 +67,32 @@ var RedisStreamsPubSub = class extends PubSub {
52
67
  } else {
53
68
  this.#maxDeliveryAttempts = cap;
54
69
  }
55
- this.#logger = options.logger;
70
+ }
71
+ /**
72
+ * Attach the `'error'` listener node-redis requires on every client. Without
73
+ * one, a mid-life socket close makes the client emit an unhandled `'error'`,
74
+ * which (per EventEmitter semantics) throws from inside RedisSocket's error
75
+ * handler BEFORE the built-in reconnect is scheduled — the process gets an
76
+ * uncaughtException and the client is left permanently un-reconnected
77
+ * (isOpen=true, isReady=false), hanging every subsequent command. Merely
78
+ * having a listener lets node-redis's own reconnect run.
79
+ *
80
+ * Logged at warn so operators can see connection churn, throttled to one line
81
+ * per 30s per client: node-redis re-emits `'error'` roughly every 500ms while
82
+ * a server is unreachable, and with one reader client per subscription an
83
+ * outage would otherwise flood the logs with thousands of identical lines.
84
+ */
85
+ #attachErrorLogger(client, role, extra = {}) {
86
+ let lastWarnAt = 0;
87
+ client.on("error", (err) => {
88
+ const now = Date.now();
89
+ if (now - lastWarnAt < 3e4) return;
90
+ lastWarnAt = now;
91
+ this.#logger?.warn?.(`redis-streams: ${role} client connection error (node-redis will reconnect)`, {
92
+ ...extra,
93
+ err: err instanceof Error ? err.message : err
94
+ });
95
+ });
56
96
  }
57
97
  #subKey(topic, cb) {
58
98
  let cbId = this.#cbIds.get(cb);
@@ -99,12 +139,15 @@ var RedisStreamsPubSub = class extends PubSub {
99
139
  threshold: this.#maxStreamLength
100
140
  };
101
141
  }
102
- const promise = this.#writeClient.xAdd(
103
- this.#streamKey(topic),
104
- "*",
105
- { event: JSON.stringify(payload) },
106
- xaddOptions
107
- );
142
+ const streamKey = this.#streamKey(topic);
143
+ const promise = this.#streamIdleTtlMs > 0 ? this.#writeClient.multi().xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions).pExpire(streamKey, this.#streamIdleTtlMs).exec().then((replies) => {
144
+ if (Number(replies[1]) !== 1) {
145
+ this.#logger?.warn?.("redis-streams: PEXPIRE inside publish MULTI did not apply", {
146
+ streamKey,
147
+ reply: String(replies[1])
148
+ });
149
+ }
150
+ }) : this.#writeClient.xAdd(streamKey, "*", { event: JSON.stringify(payload) }, xaddOptions);
108
151
  this.#pendingPublishes.add(promise);
109
152
  try {
110
153
  await promise;
@@ -135,6 +178,7 @@ var RedisStreamsPubSub = class extends PubSub {
135
178
  this.#logger?.debug?.("redis-streams: consumer group already exists", { topic, group });
136
179
  }
137
180
  const readClient = createClient(this.#connectOptions);
181
+ this.#attachErrorLogger(readClient, "read", { topic });
138
182
  await readClient.connect();
139
183
  const sub = {
140
184
  cb,
@@ -240,6 +284,35 @@ var RedisStreamsPubSub = class extends PubSub {
240
284
  await Promise.allSettled([...this.#pendingPublishes]);
241
285
  }
242
286
  }
287
+ /**
288
+ * Delete a topic's stream, and with it every consumer group on that stream.
289
+ * This is the Redis implementation of the optional `clearTopic` hook that
290
+ * consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is
291
+ * over — without it, a persistent backend accumulates every finished run's
292
+ * stream until Redis exhausts its memory.
293
+ *
294
+ * Because it removes events for ALL consumers and destroys their groups, only
295
+ * call it once nothing will read the topic again. A subscriber still attached
296
+ * when the stream is deleted recovers on its own (the read loop recreates the
297
+ * group on NOGROUP) but will have missed the deleted entries.
298
+ *
299
+ * Best-effort and non-throwing: callers commonly invoke this fire-and-forget
300
+ * (`void clearTopic(...)`), so a rejection would surface as an
301
+ * unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`
302
+ * backstop (when configured) reclaims anything a failed delete leaves behind.
303
+ */
304
+ async clearTopic(topic) {
305
+ if (this.#closed) return;
306
+ try {
307
+ await this.#ensureWriterConnected();
308
+ await this.#writeClient.del(this.#streamKey(topic));
309
+ } catch (err) {
310
+ this.#logger?.warn?.("redis-streams: clearTopic failed", {
311
+ topic,
312
+ err: err instanceof Error ? err.message : err
313
+ });
314
+ }
315
+ }
243
316
  /**
244
317
  * Lease key used in Redis. Distinct prefix from streams so leases and
245
318
  * streams can't collide on key namespace.
@@ -374,11 +447,31 @@ var RedisStreamsPubSub = class extends PubSub {
374
447
  });
375
448
  } catch (err) {
376
449
  if (sub.stopped) return;
377
- this.#logger?.debug?.("redis-streams: xReadGroup failed", {
378
- topic: sub.topic,
379
- group: sub.group,
380
- err: err instanceof Error ? err.message : err
381
- });
450
+ const msg = err instanceof Error ? err.message : String(err);
451
+ if (msg.includes("NOGROUP")) {
452
+ try {
453
+ if (this.#streamIdleTtlMs > 0) {
454
+ await this.#writeClient.multi().xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
455
+ } else {
456
+ await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, "0", { MKSTREAM: true });
457
+ }
458
+ this.#logger?.debug?.("redis-streams: recreated consumer group after NOGROUP", {
459
+ topic: sub.topic,
460
+ group: sub.group
461
+ });
462
+ } catch (recreateErr) {
463
+ const rmsg = errorText(recreateErr);
464
+ if (!rmsg.includes("BUSYGROUP")) {
465
+ this.#logger?.debug?.("redis-streams: consumer group re-create failed", { topic: sub.topic, err: rmsg });
466
+ }
467
+ }
468
+ } else {
469
+ this.#logger?.debug?.("redis-streams: xReadGroup failed", {
470
+ topic: sub.topic,
471
+ group: sub.group,
472
+ err: msg
473
+ });
474
+ }
382
475
  await new Promise((r) => setTimeout(r, 100));
383
476
  continue;
384
477
  }
@@ -454,7 +547,11 @@ var RedisStreamsPubSub = class extends PubSub {
454
547
  deliveryAttempt: attempt + 1
455
548
  };
456
549
  try {
457
- await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
550
+ if (this.#streamIdleTtlMs > 0) {
551
+ await this.#writeClient.multi().xAdd(sub.streamKey, "*", { event: JSON.stringify(next) }).pExpire(sub.streamKey, this.#streamIdleTtlMs).exec();
552
+ } else {
553
+ await this.#writeClient.xAdd(sub.streamKey, "*", { event: JSON.stringify(next) });
554
+ }
458
555
  } catch (err) {
459
556
  this.#logger?.warn?.("redis-streams: nack republish failed; leaving original pending for reclaim", {
460
557
  topic: sub.topic,
package/dist/index.js.map CHANGED
@@ -1 +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, LeaseProvider, 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 implements LeaseProvider {\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 // `localOnly` publishes bypass Redis entirely so values carrying live\n // methods (e.g. `MastraModelOutput` returned from an evented agent run via\n // `workflows-finish`) survive intact. Mirrors the same contract honored by\n // `UnixSocketPubSub` so consumers like `Mastra.__registerInternalWorkflow`\n // get consistent semantics across transports.\n #localCallbacks: Map<string, Set<EventCallback>> = new Map();\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(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Redis, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\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 // Register for `localOnly` delivery before wiring up the Redis reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\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 localBucket = this.#localCallbacks.get(topic);\n if (localBucket) {\n localBucket.delete(cb);\n if (localBucket.size === 0) this.#localCallbacks.delete(topic);\n }\n\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 * Lease key used in Redis. Distinct prefix from streams so leases and\n * streams can't collide on key namespace.\n */\n #leaseKey(key: string): string {\n return `${this.#keyPrefix}:lease:${key}`;\n }\n\n /**\n * Atomic claim via SET NX PX. Idempotent for the same owner: if the\n * current value is already this owner, we refresh the TTL instead of\n * failing. Cross-process callers race here; Redis serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\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 this.#localCallbacks.clear();\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 #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('redis-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\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"]}
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, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from 'redis';\nimport type { RedisClientOptions, RedisClientType } from 'redis';\n\n/**\n * Flatten an error into searchable text. node-redis MULTI failures throw a\n * `MultiErrorReply` whose own message is just \"N commands failed…\" — the real\n * per-command errors (e.g. BUSYGROUP) live in `err.replies`, so those are\n * folded in too.\n */\nfunction errorText(err: unknown): string {\n const parts: string[] = [err instanceof Error ? err.message : String(err)];\n if (err instanceof Error && 'replies' in err && Array.isArray((err as { replies?: unknown[] }).replies)) {\n parts.push(...(err as { replies: unknown[] }).replies.map(r => String(r)));\n }\n return parts.join('; ');\n}\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 * Idle expiry (in ms): a sliding TTL refreshed on every write to the stream\n * (publish, nack retry, group re-creation). Each write resets it, so an\n * actively-used stream never expires mid-flight; a stream left idle for the\n * full duration is deleted by Redis automatically.\n *\n * This is a BACKSTOP, not the primary cleanup. Normal cleanup is explicit:\n * `clearTopic` deletes a topic's stream the moment its lifecycle ends. This\n * option only bounds memory for streams that never reach a `clearTopic` call\n * — e.g. a run that crashed before cleanup — so they don't linger forever.\n *\n * Defaults to 0 (disabled) to preserve existing behavior.\n */\n streamIdleTtlMs?: 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 implements LeaseProvider {\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 #streamIdleTtlMs: 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 // `localOnly` publishes bypass Redis entirely so values carrying live\n // methods (e.g. `MastraModelOutput` returned from an evented agent run via\n // `workflows-finish`) survive intact. Mirrors the same contract honored by\n // `UnixSocketPubSub` so consumers like `Mastra.__registerInternalWorkflow`\n // get consistent semantics across transports.\n #localCallbacks: Map<string, Set<EventCallback>> = new Map();\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.#logger = options.logger;\n this.#attachErrorLogger(this.#writeClient, 'write');\n this.#keyPrefix = options.keyPrefix ?? 'mastra:topic';\n this.#blockMs = options.blockMs ?? 1000;\n this.#maxStreamLength = options.maxStreamLength ?? 10_000;\n const ttl = options.streamIdleTtlMs ?? 0;\n // Must be a non-negative integer: node-redis serializes the PEXPIRE arg with\n // toString(), and Redis rejects a non-integer/Infinity ('value is not an\n // integer or out of range'). Without this guard a bad value would poison\n // every publish MULTI and be silently swallowed on the nack/recovery\n // paths, so fail fast at construction instead.\n if (!Number.isInteger(ttl) || ttl < 0) {\n throw new Error(`redis-streams: streamIdleTtlMs must be a non-negative integer (milliseconds), got ${ttl}`);\n }\n this.#streamIdleTtlMs = ttl;\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 }\n\n /**\n * Attach the `'error'` listener node-redis requires on every client. Without\n * one, a mid-life socket close makes the client emit an unhandled `'error'`,\n * which (per EventEmitter semantics) throws from inside RedisSocket's error\n * handler BEFORE the built-in reconnect is scheduled — the process gets an\n * uncaughtException and the client is left permanently un-reconnected\n * (isOpen=true, isReady=false), hanging every subsequent command. Merely\n * having a listener lets node-redis's own reconnect run.\n *\n * Logged at warn so operators can see connection churn, throttled to one line\n * per 30s per client: node-redis re-emits `'error'` roughly every 500ms while\n * a server is unreachable, and with one reader client per subscription an\n * outage would otherwise flood the logs with thousands of identical lines.\n */\n #attachErrorLogger(client: RedisClientType, role: 'write' | 'read', extra: Record<string, unknown> = {}): void {\n let lastWarnAt = 0;\n client.on('error', err => {\n const now = Date.now();\n if (now - lastWarnAt < 30_000) return;\n lastWarnAt = now;\n this.#logger?.warn?.(`redis-streams: ${role} client connection error (node-redis will reconnect)`, {\n ...extra,\n err: err instanceof Error ? err.message : err,\n });\n });\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(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('RedisStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Redis, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\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 streamKey = this.#streamKey(topic);\n // When a TTL is configured the write and its PEXPIRE run in one MULTI\n // transaction. A detached PEXPIRE could fail or be skipped (process exit)\n // after a successful XADD — and on the *last* write before a topic is\n // abandoned there is no \"next write\" to self-heal, leaving an immortal\n // stream despite the TTL. Atomicity closes that window.\n const promise =\n this.#streamIdleTtlMs > 0\n ? this.#writeClient\n .multi()\n .xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions)\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec()\n .then(replies => {\n // XADD in the same transaction guarantees the key exists, so\n // PEXPIRE must reply 1; anything else means the TTL backstop is\n // not actually armed for this stream.\n if (Number(replies[1]) !== 1) {\n this.#logger?.warn?.('redis-streams: PEXPIRE inside publish MULTI did not apply', {\n streamKey,\n reply: String(replies[1]),\n });\n }\n })\n : this.#writeClient.xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions);\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 // Register for `localOnly` delivery before wiring up the Redis reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\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 this.#attachErrorLogger(readClient, 'read', { topic });\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 localBucket = this.#localCallbacks.get(topic);\n if (localBucket) {\n localBucket.delete(cb);\n if (localBucket.size === 0) this.#localCallbacks.delete(topic);\n }\n\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 * Delete a topic's stream, and with it every consumer group on that stream.\n * This is the Redis implementation of the optional `clearTopic` hook that\n * consumers (e.g. `DurableAgent`) call to free a topic once its lifecycle is\n * over — without it, a persistent backend accumulates every finished run's\n * stream until Redis exhausts its memory.\n *\n * Because it removes events for ALL consumers and destroys their groups, only\n * call it once nothing will read the topic again. A subscriber still attached\n * when the stream is deleted recovers on its own (the read loop recreates the\n * group on NOGROUP) but will have missed the deleted entries.\n *\n * Best-effort and non-throwing: callers commonly invoke this fire-and-forget\n * (`void clearTopic(...)`), so a rejection would surface as an\n * unhandledRejection. Failures are swallowed and logged; the `streamIdleTtlMs`\n * backstop (when configured) reclaims anything a failed delete leaves behind.\n */\n async clearTopic(topic: string): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n await this.#writeClient.del(this.#streamKey(topic));\n } catch (err) {\n // warn, not debug: a failed delete means the memory leak clearTopic\n // exists to prevent is silently recurring for this topic.\n this.#logger?.warn?.('redis-streams: clearTopic failed', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n /**\n * Lease key used in Redis. Distinct prefix from streams so leases and\n * streams can't collide on key namespace.\n */\n #leaseKey(key: string): string {\n return `${this.#keyPrefix}:lease:${key}`;\n }\n\n /**\n * Atomic claim via SET NX PX. Idempotent for the same owner: if the\n * current value is already this owner, we refresh the TTL instead of\n * failing. Cross-process callers race here; Redis serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\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 this.#localCallbacks.clear();\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 const msg = err instanceof Error ? err.message : String(err);\n if (msg.includes('NOGROUP')) {\n // The stream or its consumer group was deleted out from under us\n // (clearTopic, a streamIdleTtlMs expiry, or an external FLUSH). XREADGROUP\n // returns NOGROUP immediately, ignoring BLOCK, so without recovery\n // this loop busy-retries forever and the subscriber goes permanently\n // deaf — a later publish recreates the stream but not the group.\n // Recreate the group (anchored at '0', matching subscribe()) so\n // delivery resumes.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM recreates an (empty) stream key, so the TTL must be\n // stamped in the same MULTI — a detached PEXPIRE that fails or is\n // skipped would leave an abandoned-but-recreated topic lingering\n // forever with no further write to self-heal it. Note that when\n // the group already exists (a sibling won the race), exec()\n // throws a MultiErrorReply whose message does NOT contain\n // \"BUSYGROUP\" — it lives in err.replies — while the PEXPIRE in\n // the same transaction still applies, so the TTL is refreshed\n // even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(sub.streamKey, sub.group, '0', { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, '0', { MKSTREAM: true });\n }\n this.#logger?.debug?.('redis-streams: recreated consumer group after NOGROUP', {\n topic: sub.topic,\n group: sub.group,\n });\n } catch (recreateErr) {\n const rmsg = errorText(recreateErr);\n if (!rmsg.includes('BUSYGROUP')) {\n this.#logger?.debug?.('redis-streams: consumer group re-create failed', { topic: sub.topic, err: rmsg });\n }\n }\n } else {\n this.#logger?.debug?.('redis-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: msg,\n });\n }\n // Pause briefly then retry (with the group recreated, if it was NOGROUP).\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 if (this.#streamIdleTtlMs > 0) {\n // A nack republish is a write that keeps this stream in use, so it\n // must refresh the TTL too — otherwise a retry near the end of the\n // original window inherits the old expiry and the stream can be\n // deleted while the retry is still being processed. Same MULTI as\n // the write for the same reason as publish(): the republish may be\n // the stream's final write.\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', { event: JSON.stringify(next) })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', { event: JSON.stringify(next) });\n }\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 #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('redis-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\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.2.0-alpha.1",
3
+ "version": "0.3.0-alpha.0",
4
4
  "description": "Mastra Redis Streams PubSub integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -26,23 +26,23 @@
26
26
  "redis": "5.12.1"
27
27
  },
28
28
  "devDependencies": {
29
- "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.106",
29
+ "@ai-sdk/openai-v5": "npm:@ai-sdk/openai@2.0.110",
30
30
  "@copilotkit/aimock": "^1.29.0",
31
31
  "@types/node": "22.19.21",
32
- "@vitest/coverage-v8": "4.1.8",
33
- "@vitest/ui": "4.1.8",
32
+ "@vitest/coverage-v8": "4.1.10",
33
+ "@vitest/ui": "4.1.10",
34
34
  "eslint": "^10.4.1",
35
35
  "tsup": "^8.5.1",
36
36
  "tsx": "^4.22.4",
37
37
  "typescript": "^6.0.3",
38
- "vitest": "4.1.8",
38
+ "vitest": "4.1.10",
39
39
  "zod": "^4.4.3",
40
- "@internal/ai-sdk-v5": "0.0.54",
41
- "@internal/types-builder": "0.0.82",
42
- "@mastra/core": "1.46.0-alpha.4",
43
- "@internal/lint": "0.0.107",
44
- "@mastra/deployer": "1.46.0-alpha.4",
45
- "@mastra/libsql": "1.14.1-alpha.1"
40
+ "@internal/ai-sdk-v5": "0.0.60",
41
+ "@internal/lint": "0.0.113",
42
+ "@mastra/deployer": "1.51.0-alpha.10",
43
+ "@internal/types-builder": "0.0.88",
44
+ "@mastra/core": "1.51.0-alpha.10",
45
+ "@mastra/libsql": "1.16.0-alpha.0"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@mastra/core": ">=1.0.0-0 <2.0.0-0"