@mastra/redis-streams 0.6.2-alpha.0 → 0.6.3-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/dist/index.cjs CHANGED
@@ -62,6 +62,26 @@ function compareStreamIds(a, b) {
62
62
  const seq = BigInt(aSeq) - BigInt(bSeq);
63
63
  return seq === 0n ? 0 : seq < 0n ? -1 : 1;
64
64
  }
65
+ /**
66
+ * Disconnect a client, standalone or cluster.
67
+ *
68
+ * node-redis 5 `quit()` never settles on a client that is open but waiting to
69
+ * reconnect: it marks the socket closed, which ends the reconnect loop before
70
+ * QUIT is ever written. Such a client has nothing on the wire to drain, and its
71
+ * offline-queued commands could not be sent after `quit()` either, so destroy
72
+ * it instead. A cluster's `quit()` quits every node client, so one
73
+ * reconnecting node is enough to hang it.
74
+ */
75
+ async function disconnect(client) {
76
+ const reconnecting = (c) => !!c && c.isOpen && c.isReady === false;
77
+ const cluster = client;
78
+ const nodes = [...cluster.masters ?? [], ...cluster.replicas ?? []];
79
+ if (reconnecting(client) || nodes.some((node) => reconnecting(node.client))) {
80
+ client.destroy();
81
+ return;
82
+ }
83
+ await client.quit();
84
+ }
65
85
  var RedisStreamsPubSub = class RedisStreamsPubSub extends _mastra_core_events.PubSub {
66
86
  get supportedModes() {
67
87
  return ["pull"];
@@ -472,7 +492,7 @@ var RedisStreamsPubSub = class RedisStreamsPubSub extends _mastra_core_events.Pu
472
492
  }
473
493
  async #stopSubscription(sub) {
474
494
  try {
475
- await sub.readClient.quit();
495
+ await disconnect(sub.readClient);
476
496
  } catch (err) {
477
497
  this.#logger?.debug?.("redis-streams: reader quit failed", {
478
498
  topic: sub.topic,
@@ -697,7 +717,7 @@ var RedisStreamsPubSub = class RedisStreamsPubSub extends _mastra_core_events.Pu
697
717
  this.#localCallbacks.clear();
698
718
  await this.flush();
699
719
  if (this.#writeClient.isOpen) try {
700
- await this.#writeClient.quit();
720
+ await disconnect(this.#writeClient);
701
721
  } catch (err) {
702
722
  this.#logger?.debug?.("redis-streams: writer quit failed", { err: err instanceof Error ? err.message : err });
703
723
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["PubSub","#logger","#writeClient","#createWriteClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#inFlightTimeoutMs","#cbIds","#ensureWriterConnected","#writerConnecting","#closed","#deliverLocal","#publishRemote","#pendingPublishes","#streamKey","#subKey","#subscriptions","#pendingSubscribes","#doSubscribe","#localCallbacks","#openSubscription","#runReadLoop","#startReclaimLoop","#fanoutReaders","#joinFanoutReader","#pendingFanoutReaders","#deliverDetached","#createReadClient","#expireInFlight","#deliverMessage","#stopSubscription","#leaseKey","#parseEvent","#invokeCallback","#invokeLocalCallback"],"sources":["../src/index.ts"],"sourcesContent":["import { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient, createCluster } from 'redis';\nimport type { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType } from 'redis';\n\n/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */\nconst RECLAIM_PAGE_SIZE = 100;\nconst TRIM_PAGE_SIZE = 500;\n\n/**\n * Heartbeat: reset a pending entry's idle time only if this consumer still\n * owns it, so a late heartbeat never takes an entry back from a sibling.\n * KEYS[1] stream, ARGV[1] group, ARGV[2] consumer, ARGV[3] entry id\n */\nconst EXTEND_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n redis.call(\"XCLAIM\", KEYS[1], ARGV[1], ARGV[2], 0, ARGV[3], \"JUSTID\")\n return 1\n`;\n\n/**\n * Atomically nack a pending entry only if it is still owned by the given\n * consumer. Returns 1 if this consumer owned it and it was settled, 0 if the\n * entry was not pending for this consumer (acked, or claimed by a sibling).\n *\n * KEYS[1] stream key\n * ARGV[1] group, ARGV[2] consumer, ARGV[3] stream entry id,\n * ARGV[4] republish payload ('' = drop without republish),\n * ARGV[5] stream idle TTL in ms (0 = none)\n */\nconst NACK_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n if ARGV[4] ~= \"\" then\n redis.call(\"XADD\", KEYS[1], \"*\", \"event\", ARGV[4])\n if tonumber(ARGV[5]) > 0 then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[5])\n end\n end\n redis.call(\"XACK\", KEYS[1], ARGV[1], ARGV[3])\n return 1\n`;\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/** Order two Redis stream ids (`<ms>-<seq>`). */\nfunction compareStreamIds(a: string, b: string): number {\n const [aMs = '0', aSeq = '0'] = a.split('-');\n const [bMs = '0', bSeq = '0'] = b.split('-');\n const ms = BigInt(aMs) - BigInt(bMs);\n if (ms !== 0n) return ms < 0n ? -1 : 1;\n const seq = BigInt(aSeq) - BigInt(bSeq);\n return seq === 0n ? 0 : seq < 0n ? -1 : 1;\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 get fan-out semantics (every subscriber sees\n * every event). All fan-out subscribers to one topic in this instance share\n * one private consumer group and one reader connection; each extra callback\n * is dispatched locally, so it costs no Redis connection.\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 * Connect to a Redis Cluster instead of a standalone server. Options are\n * passed to `createCluster()` from `redis`. Mutually exclusive with\n * `url`/`redisOptions`/`client`.\n */\n cluster?: RedisClusterOptions;\n /**\n * A pre-configured, UNCONNECTED `redis` client (standalone or cluster) to\n * use as the shared writer. Each subscription's blocking reader is created\n * from it with `client.duplicate()`, so the client's options are reused but\n * every connection is distinct. The pubsub owns the client's lifecycle\n * (connects on first use, quits on `close()`), so do not share it with the\n * rest of your app. Mutually exclusive with `url`/`redisOptions`/`cluster`.\n */\n client?: RedisClientType | RedisClusterType;\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, subscribe/group creation, nack retry). Each write resets it, so\n * an actively-used stream never expires mid-flight; a stream left idle for\n * the full duration is deleted by Redis automatically — entries, consumer\n * groups and their pending lists included.\n *\n * Defaults to 0 (disabled) to preserve existing behavior: streams live\n * until an explicit `clearTopic`, and a stream that never reaches one stays\n * in Redis forever. **Production deployments should set this** — see below.\n *\n * Why a TTL matters: topics fall into two classes.\n * - *Lifecycle-owned* topics (workflow/agent run streams, request/reply\n * topics) are cleaned up eagerly via `clearTopic` when their lifecycle\n * ends. For these the TTL is a backstop covering crashes and missed\n * cleanup.\n * - *Open-ended* topics (per-conversation streams, per-project feeds) have\n * no moment at which any process can safely declare them finished, so\n * nothing ever calls `clearTopic` on them. For these the TTL is the ONLY\n * reclamation mechanism; with it disabled they accumulate for the\n * lifetime of the Redis instance.\n *\n * Sizing: the TTL must exceed the longest window in which an idle stream is\n * still legitimately useful — chiefly, how long a grouped worker may be down\n * and still expect to replay its backlog on restart. If workers can be\n * offline longer than the TTL while publishers stay quiet, raise it.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs a reclaim pass (XPENDING + XCLAIM) 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 * How long (in ms) a handler may hold a message in-flight (neither acked nor\n * nacked) before the reclaim loop gives up on it and nacks it on the\n * handler's behalf. The nack republishes the event with an incremented\n * `deliveryAttempt`, so `maxDeliveryAttempts` still bounds retries, and acks\n * the original entry; the hung handler's own eventual ack/nack becomes a\n * no-op.\n *\n * Without this, an in-flight entry is only ever recovered by a *different*\n * consumer in the group once it has been idle for `reclaimIdleMs`. A\n * single-consumer group therefore holds a hung message until the process\n * restarts. Defaults to 0 (disabled). When set, it should be comfortably\n * larger than the slowest legitimate handler.\n */\n inFlightTimeoutMs?: 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 override get supportsOffsets(): boolean {\n return false;\n }\n\n protected override onUnsupportedOffset(offset: number): void {\n this.#logger?.warn?.(`redis-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n // Standalone and cluster clients share the command surface this class uses.\n // Every MULTI/EVAL here is single-key and XREADGROUP reads one stream, so\n // commands never cross hash slots; keep it that way or Cluster breaks.\n #writeClient: RedisClientType;\n // Dedupes concurrent cold callers onto a single writer connect(); see\n // #ensureWriterConnected.\n #writerConnecting?: Promise<void>;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #streamIdleTtlMs: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #inFlightTimeoutMs: 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 // Fan-out subscriptions to the same topic share one Subscription, so several\n // keys can point at the same record.\n #subscriptions: Map<string, Subscription> = new Map();\n // The shared fan-out reader per topic, and readers still being opened so\n // concurrent first subscribers to a topic don't each open one.\n #fanoutReaders: Map<string, Subscription> = new Map();\n #pendingFanoutReaders: Map<string, Promise<Subscription>> = new Map();\n // Subscribes that are still wiring up Redis (XGROUP CREATE + reader connect).\n // `unsubscribe` and `close` await these so an unsubscribe issued while the\n // subscribe round trip is in flight tears the subscription down instead of\n // no-oping — otherwise the late-registering subscription (reader connection,\n // blocked read loop) would leak with nothing left to stop it.\n #pendingSubscribes: Map<string, Promise<void>> = 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 this.#writeClient = RedisStreamsPubSub.#createWriteClient(options);\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 const inFlightTimeout = options.inFlightTimeoutMs ?? 0;\n if (!Number.isFinite(inFlightTimeout) || inFlightTimeout < 0) {\n throw new Error(\n `redis-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`,\n );\n }\n this.#inFlightTimeoutMs = inFlightTimeout;\n }\n\n /**\n * Resolve the shared writer client from config. Exactly one connection\n * source is allowed: an injected `client`, `cluster` options, or the\n * standalone `url`/`redisOptions` pair (the default).\n */\n static #createWriteClient(options: RedisStreamsPubSubConfig): RedisClientType {\n const sources = [\n options.client && 'client',\n options.cluster && 'cluster',\n (options.url ?? options.redisOptions) && 'url/redisOptions',\n ].filter(Boolean);\n if (sources.length > 1) {\n throw new Error(`redis-streams: ${sources.join(', ')} are mutually exclusive; pass only one connection source`);\n }\n if (options.client) {\n if (options.client.isOpen) {\n throw new Error('redis-streams: `client` must not be connected; the pubsub owns its connection lifecycle');\n }\n return options.client as RedisClientType;\n }\n if (options.cluster) {\n return createCluster(options.cluster) as unknown as RedisClientType;\n }\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n return createClient({ ...options.redisOptions, url }) as RedisClientType;\n }\n\n /**\n * A fresh, unconnected client with the writer's configuration. Each reader\n * (one per fan-out topic, one per grouped subscription) needs its own\n * because XREADGROUP BLOCK holds the connection.\n */\n #createReadClient(): RedisClientType {\n return this.#writeClient.duplicate() as RedisClientType;\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 = globalThis.crypto.randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent and safe under concurrent callers. */\n async #ensureWriterConnected(): Promise<void> {\n // node-redis flips `isOpen` true synchronously inside connect(), BEFORE the\n // socket is ready (standalone) or slot discovery has finished (cluster). So\n // while the initial connect is in flight, `isOpen` alone would let a second\n // caller through to issue commands; on a Cluster client that crashes in\n // slot lookup (\"Cannot read properties of undefined (reading 'master')\").\n // Check the in-flight promise first so every cold caller awaits the same\n // connect(). Outside that window we still gate on `isOpen` (not `isReady`)\n // so node-redis's own automatic mid-life reconnect is never fought with a\n // second connect().\n if (!this.#writerConnecting && this.#writeClient.isOpen) return;\n if (!this.#writerConnecting) {\n this.#writerConnecting = this.#writeClient\n .connect()\n .then(() => undefined)\n .finally(() => {\n // Clear so a failed initial connect can be retried on the next call.\n this.#writerConnecting = undefined;\n });\n }\n return this.#writerConnecting;\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: globalThis.crypto.randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n // Track the whole remote publish, connect included, so close() can drain\n // a publish that is still connecting and not only one already in XADD.\n const promise = this.#publishRemote(topic, event);\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async #publishRemote(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n await this.#ensureWriterConnected();\n\n const id = globalThis.crypto.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 await promise;\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 const pending = this.#pendingSubscribes.get(key);\n if (pending) return pending; // idempotent: same (topic, cb) subscribe already in flight\n\n // Register synchronously so an unsubscribe/close racing this subscribe can\n // find and await it. `#doSubscribe` runs synchronously up to its first\n // await, which keeps the localOnly registration guarantee documented there.\n const promise = this.#doSubscribe(topic, cb, key, options).finally(() => {\n this.#pendingSubscribes.delete(key);\n });\n this.#pendingSubscribes.set(key, promise);\n return promise;\n }\n\n async #doSubscribe(topic: string, cb: EventCallback, key: string, options?: SubscribeOptions): Promise<void> {\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 groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\n if (options?.group) {\n const sub = await this.#openSubscription(topic, cb, options.group, groupAnchor);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n return;\n }\n\n // Fan-out: every subscriber to this topic in this instance shares one\n // consumer group and reader. Join the topic's reader if one is open (or\n // being opened); otherwise open it.\n for (;;) {\n const reader = this.#fanoutReaders.get(topic);\n if (reader) return this.#joinFanoutReader(reader, cb, key, groupAnchor);\n const opening = this.#pendingFanoutReaders.get(topic);\n if (!opening) break;\n await opening.catch(() => {});\n }\n const opening = this.#openSubscription(topic, cb, undefined, groupAnchor);\n this.#pendingFanoutReaders.set(topic, opening);\n try {\n const sub = await opening;\n // Register in the same tick the pending entry is cleared, so a\n // subscriber waiting on `opening` finds the reader and joins it.\n this.#fanoutReaders.set(topic, sub);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n } finally {\n this.#pendingFanoutReaders.delete(topic);\n }\n }\n\n /**\n * Add a callback to a topic's open fan-out reader. The reader may already\n * have delivered entries this subscriber should see (`startFrom: 'earliest'`)\n * or still be catching up on entries it should not (`startFrom: 'latest'`),\n * so the new member buffers live entries while its starting point is\n * resolved, replays what it missed, then switches to live delivery.\n */\n async #joinFanoutReader(sub: Subscription, cb: EventCallback, key: string, anchor: '0' | '$'): Promise<void> {\n const member: FanoutMember = { joining: [] };\n sub.members.set(cb, member);\n this.#subscriptions.set(key, sub);\n const active = () => sub.members.get(cb) === member && !sub.stopped;\n\n try {\n if (anchor === '$') {\n // Skip everything already in the stream, even entries the shared\n // reader has not reached yet.\n const [tail] = await this.#writeClient.xRevRange(sub.streamKey, '+', '-', { COUNT: 1 });\n member.skipThrough = tail?.id;\n } else {\n // Entries up to the group's last-delivered id were read before this\n // member was buffering, so replay them; later ones arrive live.\n let boundary: string | undefined;\n try {\n const groups = await this.#writeClient.xInfoGroups(sub.streamKey);\n const info = groups.find(g => String(g.name) === sub.group);\n boundary = info ? String(info['last-delivered-id']) : undefined;\n } catch (err) {\n // No stream (or group) means nothing has been delivered yet.\n if (!errorText(err).match(/no such key|NOGROUP/i)) throw err;\n }\n if (boundary && boundary !== '0-0') {\n member.skipThrough = boundary;\n let start = '-';\n while (active()) {\n const page = await this.#writeClient.xRange(sub.streamKey, start, boundary, { COUNT: TRIM_PAGE_SIZE });\n for (const entry of page) {\n if (!active()) break;\n this.#deliverDetached(sub, cb, entry.message);\n }\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n }\n }\n } catch (err) {\n sub.members.delete(cb);\n this.#subscriptions.delete(key);\n throw err;\n }\n\n if (!active()) return;\n const buffered = member.joining ?? [];\n for (const entry of buffered) {\n if (member.skipThrough && compareStreamIds(entry.id, member.skipThrough) <= 0) continue;\n this.#deliverDetached(sub, cb, entry.fields);\n }\n // `skipThrough` stays set: the reader may still be dispatching a batch read\n // before this member joined (already replayed), or lagging behind a\n // `latest` member's starting point. clearTopic resets it.\n member.joining = undefined;\n }\n\n async #openSubscription(\n topic: string,\n cb: EventCallback,\n groupName: string | undefined,\n groupAnchor: '0' | '$',\n ): Promise<Subscription> {\n const isGrouped = groupName !== undefined;\n const group = groupName ?? `__fanout-${globalThis.crypto.randomUUID()}`;\n const consumer = `${group}-${globalThis.crypto.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 // Brand-new groups default to '0' (stream start) so a worker which\n // subscribes after a publish still sees the backlog. Callers can opt into\n // '$' with startFrom: 'latest' when they only want future events. Existing\n // groups (BUSYGROUP path) always keep their checkpoint. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM may create an empty stream key, so the TTL must be stamped\n // in the same MULTI — otherwise a topic that is subscribed to but\n // never published to (e.g. a per-run control topic) has no expiry and\n // lingers in Redis forever. When the group already exists (a sibling\n // won the race), exec() throws a MultiErrorReply whose message does\n // NOT contain \"BUSYGROUP\" — it lives in err.replies — while the\n // PEXPIRE in the same transaction still applies, so the TTL is\n // refreshed even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true })\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true });\n }\n } catch (err) {\n const msg = errorText(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each reader (per fan-out topic, per grouped subscription) gets a\n // dedicated connection because XREADGROUP with BLOCK > 0 holds the\n // connection until a message arrives.\n const readClient = this.#createReadClient();\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n return {\n members: new Map([[cb, {}]]),\n settling: new Map(),\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n inFlight: new Map(),\n };\n }\n\n /**\n * Periodically reclaim idle pending entries in this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Fan-out groups are private to one consumer, so there is no\n * sibling to claim from and the XPENDING/XCLAIM scan is skipped for them; the\n * loop still runs so `inFlightTimeoutMs` can recover a hung handler, which is\n * the only recovery path a single-consumer group has.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped && this.#inFlightTimeoutMs <= 0) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n await this.#expireInFlight(sub);\n if (!sub.isGrouped) {\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n return;\n }\n // List idle pending entries first and only claim the ones this\n // subscription is not already processing. Claiming (XAUTOCLAIM/XCLAIM)\n // resets the entry's idle clock even when the claimant already owns it,\n // so a claim-then-skip design would let a handler running longer than\n // reclaimIdleMs re-invoke itself (pre-fix) or keep refreshing the idle\n // timer on every tick and starve a live sibling of a genuinely stalled\n // entry. Filtering before the claim leaves in-flight entries untouched:\n // if the handler settles, ack/nack removes them from the PEL; if it is\n // truly hung, the idle clock keeps running and a sibling claims them.\n //\n // The scan is paginated: locally in-flight entries stay in the PEL\n // (by design, above) and are listed on every tick, so with a single\n // fixed-size page they could fill it and hide a reclaimable entry\n // that sorts after them. Walk pages with an exclusive start id until\n // the page comes back short or the claim batch is full.\n const ids: string[] = [];\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, start, '+', RECLAIM_PAGE_SIZE, {\n IDLE: this.#reclaimIdleMs,\n });\n for (const p of page) {\n const id = String(p.id);\n if (sub.inFlight.has(id)) continue;\n ids.push(id);\n if (ids.length >= RECLAIM_PAGE_SIZE) break;\n }\n if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;\n start = `(${String(page[page.length - 1]!.id)}`;\n }\n // XCLAIM re-checks min-idle atomically, so if a sibling claimed an entry\n // between the XPENDING and here it is simply omitted from the reply.\n const claimed =\n ids.length === 0\n ? []\n : ((await this.#writeClient.xClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n ids,\n )) as Array<{ id: string; message: Record<string, string> } | null>);\n for (const entry of claimed) {\n // Null means the stream entry was trimmed. Redis >= 7.0 drops it\n // from the PEL as part of the claim, so there is nothing left to\n // deliver. (Redis 6 leaves it pending, which is why the package\n // requires 7.0+; otherwise trimmed ids would be re-listed forever.)\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reclaim 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(runTick, this.#reclaimIntervalMs);\n };\n\n const runTick = () => {\n sub.reclaimLoop = tick().finally(() => {\n sub.reclaimLoop = undefined;\n });\n };\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n }\n\n /**\n * Give up on local in-flight entries older than `inFlightTimeoutMs` by\n * nacking them on the handler's behalf. This is the only way a\n * single-consumer group recovers a hung handler: the reclaim loop never\n * claims its own in-flight entries (see the tick above), so without a\n * timeout the entry stays pending until the process restarts.\n */\n async #expireInFlight(sub: Subscription): Promise<void> {\n if (this.#inFlightTimeoutMs <= 0) return;\n const cutoff = Date.now() - this.#inFlightTimeoutMs;\n for (const [streamId, entry] of sub.inFlight) {\n if (entry.since > cutoff) continue;\n this.#logger?.warn?.('redis-streams: handler exceeded inFlightTimeoutMs; nacking on its behalf', {\n topic: sub.topic,\n group: sub.group,\n streamId,\n inFlightMs: Date.now() - entry.since,\n });\n // The local marker is not proof of ownership: once the entry has been\n // idle >= reclaimIdleMs a sibling may have claimed it, or it may have\n // been acked outright. `expire` verifies ownership and settles in one\n // atomic Redis step, honours maxDeliveryAttempts, and removes the entry\n // from `inFlight` either way.\n await entry.expire();\n }\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 // A subscribe for this (topic, cb) may still be mid-flight (XGROUP CREATE +\n // reader connect). Wait for it to register so the teardown below actually\n // finds it — returning early here would orphan the subscription forever.\n const pending = this.#pendingSubscribes.get(key);\n if (pending) await pending.catch(() => {});\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n if (sub.teardown) return sub.teardown;\n\n if (sub.members.size > 1 && sub.members.delete(cb)) {\n // Other members keep the reader. Entries handed to this member no\n // longer wait on it.\n this.#subscriptions.delete(key);\n for (const settle of [...sub.settling.values()]) void settle(cb);\n return;\n }\n\n // Last member: close the reader. The member stays attached so entries an\n // already-issued read returns are still delivered to it. A subscriber\n // arriving from now on opens a fresh reader rather than join this one.\n if (this.#fanoutReaders.get(topic) === sub) this.#fanoutReaders.delete(topic);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n sub.teardown = this.#stopSubscription(sub).finally(() => {\n this.#subscriptions.delete(key);\n });\n return sub.teardown;\n }\n\n async #stopSubscription(sub: Subscription): Promise<void> {\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 // Clearing the timer prevents future claims, but an issued claim still owns work.\n await sub.reclaimLoop;\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 // Redis stream IDs can restart below the last delivered ID after DEL.\n // Attached subscribers therefore recover this known-new stream from its\n // beginning instead of anchoring past newly published entries.\n for (const sub of this.#subscriptions.values()) {\n if (sub.topic === topic) {\n sub.lastId = undefined;\n sub.groupAnchor = '0';\n for (const member of sub.members.values()) member.skipThrough = undefined;\n }\n }\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 * Deletes a run's entries with `XDEL`: pages through the stream with `XRANGE`\n * and matches each entry's `runId`. With `producedBefore`, only unpinned\n * entries produced at or before it are deleted. Entries of other runs,\n * including those published by other processes, are untouched.\n */\n override async trimTopic(\n topic: string,\n { runId, producedBefore }: { runId: string; producedBefore?: number },\n ): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n const streamKey = this.#streamKey(topic);\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xRange(streamKey, start, '+', { COUNT: TRIM_PAGE_SIZE });\n const ids: string[] = [];\n for (const entry of page) {\n if (!entry) continue;\n try {\n const event = JSON.parse(entry.message.event ?? '{}') as {\n runId?: string;\n data?: { producedAt?: unknown; pinned?: unknown };\n };\n if (event.runId !== runId) continue;\n if (producedBefore !== undefined) {\n const producedAt = event.data?.producedAt;\n if (typeof producedAt !== 'number' || producedAt > producedBefore || event.data?.pinned) continue;\n }\n ids.push(entry.id);\n } catch {\n // An unparseable entry can't belong to this run.\n }\n }\n if (ids.length > 0) await this.#writeClient.xDel(streamKey, ids);\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: trimTopic 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 // Let in-flight subscribes finish registering before the snapshot below,\n // so a subscription that lands mid-close is torn down rather than leaked.\n if (this.#pendingSubscribes.size > 0) {\n await Promise.allSettled([...this.#pendingSubscribes.values()]);\n }\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = new Set(this.#subscriptions.values());\n await Promise.all([...subs].flatMap(sub => [...sub.members.keys()].map(cb => this.unsubscribe(sub.topic, cb))));\n this.#localCallbacks.clear();\n\n // Let publishes that were accepted before close() reach the stream. A\n // workflow's terminal event is often the last thing written before\n // shutdown; quitting the writer under it would drop the event and reject\n // the caller with a ClosingError.\n await this.flush();\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 after the last delivered stream entry so events\n // published during recovery remain visible. Before the first delivery,\n // preserve the subscription's original earliest/latest anchor.\n const recoveryAnchor = sub.lastId ?? sub.groupAnchor;\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, recoveryAnchor, { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, recoveryAnchor, { 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 // Entries returned by an issued read belong to us even after stop.\n sub.lastId = entry.id;\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 const event = this.#parseEvent(fields);\n if (!event) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\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 } finally {\n // Clear the in-flight guard only AFTER Redis has settled the entry.\n // Clearing before the xAck resolves would open a window where the\n // entry is neither in-flight nor acked, so a reclaim tick landing in\n // that window (the entry is already idle >= reclaimIdleMs for a\n // long-running handler) would redeliver and re-invoke the handler.\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\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 the reclaim loop (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 reclaim) can ack/nack it again. The entry\n // is intentionally left pending, so clear the in-flight guard now to\n // make it eligible for a future reclaim.\n settled = false;\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\n }\n };\n\n // Timeout-driven nack, used by #expireInFlight. Unlike `nack` above this\n // can run while a sibling may have reclaimed the entry, so the ownership\n // check and the republish+xAck must be one atomic Redis step: a\n // read-then-nack would let a sibling XCLAIM land in between, and we'd\n // republish a duplicate and xAck the sibling's entry out from under it.\n // Either way the local marker is settled afterwards so a late ack/nack\n // from the hung handler is a no-op.\n const expire = async () => {\n if (settled) return;\n settled = true;\n sub.settling.delete(streamId);\n const attempt = event.deliveryAttempt ?? 1;\n const drop = attempt >= this.#maxDeliveryAttempts;\n const payload = drop ? '' : JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event);\n let owned: unknown;\n try {\n owned = await this.#writeClient.eval(NACK_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId, payload, String(this.#streamIdleTtlMs)],\n });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: timeout nack failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Same recovery as a failed nack republish: the entry is still\n // pending, so let a future reclaim (or the next timeout pass, if the\n // handler is still hung) retry it. Fan-out readers have no reclaim\n // scan, so they keep the timeout registration for the next pass.\n settled = false;\n sub.settling.set(streamId, cb => settleMember(cb, false));\n if (sub.isGrouped) sub.inFlight.delete(streamId);\n else sub.inFlight.set(streamId, { since: Date.now(), expire });\n return;\n }\n sub.inFlight.delete(streamId);\n if (owned !== 1) {\n this.#logger?.warn?.(\n 'redis-streams: handler exceeded inFlightTimeoutMs but entry is no longer owned; disowning',\n { topic: sub.topic, group: sub.group, streamId },\n );\n return;\n }\n if (drop) {\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 }\n };\n\n // Mark this entry in-flight for the reclaim loop's benefit for exactly the\n // window between invoking the handler and the delivery settling (ack/nack).\n // Heartbeat for long handlers: reset the entry's PEL idle time so the\n // reclaim loop (ours or a sibling's) does not XCLAIM it mid-execution, and\n // push back the local inFlightTimeoutMs deadline.\n const extend = async () => {\n const entry = sub.inFlight.get(streamId);\n if (!entry || entry.expire !== expire) return;\n entry.since = Date.now();\n if (sub.isGrouped) {\n await this.#writeClient.eval(EXTEND_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId],\n });\n }\n };\n\n // The entry is settled in Redis once, after every member it was handed to\n // has settled: acked if all acked, nacked (one republish) if any nacked.\n // Members joining the topic buffer the entry instead; members whose\n // starting point is past it skip it.\n const waiting = new Set<EventCallback>();\n let anyNack = false;\n const settleMember = async (cb: EventCallback, nacked: boolean) => {\n if (settled || !waiting.delete(cb)) return;\n if (nacked) anyNack = true;\n if (waiting.size > 0) return;\n sub.settling.delete(streamId);\n await (anyNack ? nack() : ack());\n };\n const targets: EventCallback[] = [];\n for (const [cb, member] of sub.members) {\n if (member.joining) {\n member.joining.push({ id: streamId, fields });\n continue;\n }\n if (member.skipThrough) {\n if (compareStreamIds(streamId, member.skipThrough) <= 0) continue;\n member.skipThrough = undefined;\n }\n waiting.add(cb);\n targets.push(cb);\n }\n if (targets.length === 0) {\n await ack();\n return;\n }\n\n sub.inFlight.set(streamId, { since: Date.now(), expire });\n sub.settling.set(streamId, cb => settleMember(cb, false));\n for (const cb of targets) {\n // Each member gets its own Event object so one handler mutating it\n // can't affect another.\n const memberEvent = targets.length === 1 ? event : this.#parseEvent(fields)!;\n this.#invokeCallback(\n cb,\n memberEvent,\n () => settleMember(cb, false),\n () => settleMember(cb, true),\n extend,\n );\n }\n }\n\n /**\n * Invoke a handler without awaiting it. EventCallback is typed `=> void`\n * but handlers commonly return a promise (TS allows Promise<void> to satisfy\n * void); a rejection or synchronous throw routes to nack instead of silently\n * dropping the message. We do NOT await — serializing messages on a\n * subscription would deadlock orchestration callbacks that await their own\n * future events.\n */\n #invokeCallback(\n cb: EventCallback,\n event: Event,\n ack: () => Promise<void>,\n nack: () => Promise<void>,\n extend?: () => Promise<void>,\n ): void {\n try {\n const result: unknown = cb(event, ack, nack, extend);\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 void nack();\n }\n }\n\n #parseEvent(fields: Record<string, string>): Event | undefined {\n try {\n const 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 return event;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Deliver an entry to one fan-out member outside the shared group's PEL:\n * entries the topic's reader handed out before the member joined. Ack is a\n * no-op (the group settles the entry on its own); nack republishes, which\n * redelivers to every member just as a nack on the shared entry would.\n */\n #deliverDetached(sub: Subscription, cb: EventCallback, fields: Record<string, string>): void {\n const event = this.#parseEvent(fields);\n if (!event) return;\n let settled = false;\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\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 return;\n }\n const payload = { event: JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event) };\n try {\n if (this.#streamIdleTtlMs > 0) {\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', payload)\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', payload);\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed for replayed event', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n this.#invokeCallback(\n cb,\n event,\n async () => {\n settled = true;\n },\n 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 FanoutMember {\n // Set while the member is resolving its starting point; live entries are\n // buffered here until then.\n joining?: { id: string; fields: Record<string, string> }[];\n // Entries at or before this id were already delivered to (or are before the\n // start of) this member, so the shared reader skips them for it.\n skipThrough?: string;\n}\n\n/**\n * A reader: one consumer, one connection. A grouped subscription has exactly\n * one member; a fan-out reader is shared by every fan-out subscriber to its\n * topic in this instance.\n */\ninterface Subscription {\n members: Map<EventCallback, FanoutMember>;\n // Per in-flight entry: mark a member as settled-with-ack when it leaves\n // before settling, so the entry is not stranded.\n settling: Map<string, (cb: EventCallback) => Promise<void>>;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n reclaimLoop?: Promise<void>;\n teardown?: Promise<void>;\n // Stream entry IDs currently being processed locally by this subscription.\n // The reclaim loop consults this to avoid re-invoking the handler for a\n // message whose original delivery is still in flight (self-redelivery).\n // `expire` is the timeout path's atomic nack-if-still-owned.\n inFlight: Map<string, { since: number; expire: () => Promise<void> }>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["PubSub","#logger","#writeClient","#createWriteClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#inFlightTimeoutMs","#cbIds","#ensureWriterConnected","#writerConnecting","#closed","#deliverLocal","#publishRemote","#pendingPublishes","#streamKey","#subKey","#subscriptions","#pendingSubscribes","#doSubscribe","#localCallbacks","#openSubscription","#runReadLoop","#startReclaimLoop","#fanoutReaders","#joinFanoutReader","#pendingFanoutReaders","#deliverDetached","#createReadClient","#expireInFlight","#deliverMessage","#stopSubscription","#leaseKey","#parseEvent","#invokeCallback","#invokeLocalCallback"],"sources":["../src/index.ts"],"sourcesContent":["import { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient, createCluster } from 'redis';\nimport type { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType } from 'redis';\n\n/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */\nconst RECLAIM_PAGE_SIZE = 100;\nconst TRIM_PAGE_SIZE = 500;\n\n/**\n * Heartbeat: reset a pending entry's idle time only if this consumer still\n * owns it, so a late heartbeat never takes an entry back from a sibling.\n * KEYS[1] stream, ARGV[1] group, ARGV[2] consumer, ARGV[3] entry id\n */\nconst EXTEND_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n redis.call(\"XCLAIM\", KEYS[1], ARGV[1], ARGV[2], 0, ARGV[3], \"JUSTID\")\n return 1\n`;\n\n/**\n * Atomically nack a pending entry only if it is still owned by the given\n * consumer. Returns 1 if this consumer owned it and it was settled, 0 if the\n * entry was not pending for this consumer (acked, or claimed by a sibling).\n *\n * KEYS[1] stream key\n * ARGV[1] group, ARGV[2] consumer, ARGV[3] stream entry id,\n * ARGV[4] republish payload ('' = drop without republish),\n * ARGV[5] stream idle TTL in ms (0 = none)\n */\nconst NACK_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n if ARGV[4] ~= \"\" then\n redis.call(\"XADD\", KEYS[1], \"*\", \"event\", ARGV[4])\n if tonumber(ARGV[5]) > 0 then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[5])\n end\n end\n redis.call(\"XACK\", KEYS[1], ARGV[1], ARGV[3])\n return 1\n`;\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/** Order two Redis stream ids (`<ms>-<seq>`). */\nfunction compareStreamIds(a: string, b: string): number {\n const [aMs = '0', aSeq = '0'] = a.split('-');\n const [bMs = '0', bSeq = '0'] = b.split('-');\n const ms = BigInt(aMs) - BigInt(bMs);\n if (ms !== 0n) return ms < 0n ? -1 : 1;\n const seq = BigInt(aSeq) - BigInt(bSeq);\n return seq === 0n ? 0 : seq < 0n ? -1 : 1;\n}\n\n/**\n * Disconnect a client, standalone or cluster.\n *\n * node-redis 5 `quit()` never settles on a client that is open but waiting to\n * reconnect: it marks the socket closed, which ends the reconnect loop before\n * QUIT is ever written. Such a client has nothing on the wire to drain, and its\n * offline-queued commands could not be sent after `quit()` either, so destroy\n * it instead. A cluster's `quit()` quits every node client, so one\n * reconnecting node is enough to hang it.\n */\nasync function disconnect(client: RedisClientType): Promise<void> {\n const reconnecting = (c?: { isOpen: boolean; isReady?: boolean }) => !!c && c.isOpen && c.isReady === false;\n const cluster = client as unknown as Partial<Pick<RedisClusterType, 'masters' | 'replicas'>>;\n const nodes = [...(cluster.masters ?? []), ...(cluster.replicas ?? [])];\n if (reconnecting(client) || nodes.some(node => reconnecting(node.client))) {\n client.destroy();\n return;\n }\n await client.quit();\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 get fan-out semantics (every subscriber sees\n * every event). All fan-out subscribers to one topic in this instance share\n * one private consumer group and one reader connection; each extra callback\n * is dispatched locally, so it costs no Redis connection.\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 * Connect to a Redis Cluster instead of a standalone server. Options are\n * passed to `createCluster()` from `redis`. Mutually exclusive with\n * `url`/`redisOptions`/`client`.\n */\n cluster?: RedisClusterOptions;\n /**\n * A pre-configured, UNCONNECTED `redis` client (standalone or cluster) to\n * use as the shared writer. Each subscription's blocking reader is created\n * from it with `client.duplicate()`, so the client's options are reused but\n * every connection is distinct. The pubsub owns the client's lifecycle\n * (connects on first use, quits on `close()`), so do not share it with the\n * rest of your app. Mutually exclusive with `url`/`redisOptions`/`cluster`.\n */\n client?: RedisClientType | RedisClusterType;\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, subscribe/group creation, nack retry). Each write resets it, so\n * an actively-used stream never expires mid-flight; a stream left idle for\n * the full duration is deleted by Redis automatically — entries, consumer\n * groups and their pending lists included.\n *\n * Defaults to 0 (disabled) to preserve existing behavior: streams live\n * until an explicit `clearTopic`, and a stream that never reaches one stays\n * in Redis forever. **Production deployments should set this** — see below.\n *\n * Why a TTL matters: topics fall into two classes.\n * - *Lifecycle-owned* topics (workflow/agent run streams, request/reply\n * topics) are cleaned up eagerly via `clearTopic` when their lifecycle\n * ends. For these the TTL is a backstop covering crashes and missed\n * cleanup.\n * - *Open-ended* topics (per-conversation streams, per-project feeds) have\n * no moment at which any process can safely declare them finished, so\n * nothing ever calls `clearTopic` on them. For these the TTL is the ONLY\n * reclamation mechanism; with it disabled they accumulate for the\n * lifetime of the Redis instance.\n *\n * Sizing: the TTL must exceed the longest window in which an idle stream is\n * still legitimately useful — chiefly, how long a grouped worker may be down\n * and still expect to replay its backlog on restart. If workers can be\n * offline longer than the TTL while publishers stay quiet, raise it.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs a reclaim pass (XPENDING + XCLAIM) 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 * How long (in ms) a handler may hold a message in-flight (neither acked nor\n * nacked) before the reclaim loop gives up on it and nacks it on the\n * handler's behalf. The nack republishes the event with an incremented\n * `deliveryAttempt`, so `maxDeliveryAttempts` still bounds retries, and acks\n * the original entry; the hung handler's own eventual ack/nack becomes a\n * no-op.\n *\n * Without this, an in-flight entry is only ever recovered by a *different*\n * consumer in the group once it has been idle for `reclaimIdleMs`. A\n * single-consumer group therefore holds a hung message until the process\n * restarts. Defaults to 0 (disabled). When set, it should be comfortably\n * larger than the slowest legitimate handler.\n */\n inFlightTimeoutMs?: 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 override get supportsOffsets(): boolean {\n return false;\n }\n\n protected override onUnsupportedOffset(offset: number): void {\n this.#logger?.warn?.(`redis-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n // Standalone and cluster clients share the command surface this class uses.\n // Every MULTI/EVAL here is single-key and XREADGROUP reads one stream, so\n // commands never cross hash slots; keep it that way or Cluster breaks.\n #writeClient: RedisClientType;\n // Dedupes concurrent cold callers onto a single writer connect(); see\n // #ensureWriterConnected.\n #writerConnecting?: Promise<void>;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #streamIdleTtlMs: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #inFlightTimeoutMs: 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 // Fan-out subscriptions to the same topic share one Subscription, so several\n // keys can point at the same record.\n #subscriptions: Map<string, Subscription> = new Map();\n // The shared fan-out reader per topic, and readers still being opened so\n // concurrent first subscribers to a topic don't each open one.\n #fanoutReaders: Map<string, Subscription> = new Map();\n #pendingFanoutReaders: Map<string, Promise<Subscription>> = new Map();\n // Subscribes that are still wiring up Redis (XGROUP CREATE + reader connect).\n // `unsubscribe` and `close` await these so an unsubscribe issued while the\n // subscribe round trip is in flight tears the subscription down instead of\n // no-oping — otherwise the late-registering subscription (reader connection,\n // blocked read loop) would leak with nothing left to stop it.\n #pendingSubscribes: Map<string, Promise<void>> = 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 this.#writeClient = RedisStreamsPubSub.#createWriteClient(options);\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 const inFlightTimeout = options.inFlightTimeoutMs ?? 0;\n if (!Number.isFinite(inFlightTimeout) || inFlightTimeout < 0) {\n throw new Error(\n `redis-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`,\n );\n }\n this.#inFlightTimeoutMs = inFlightTimeout;\n }\n\n /**\n * Resolve the shared writer client from config. Exactly one connection\n * source is allowed: an injected `client`, `cluster` options, or the\n * standalone `url`/`redisOptions` pair (the default).\n */\n static #createWriteClient(options: RedisStreamsPubSubConfig): RedisClientType {\n const sources = [\n options.client && 'client',\n options.cluster && 'cluster',\n (options.url ?? options.redisOptions) && 'url/redisOptions',\n ].filter(Boolean);\n if (sources.length > 1) {\n throw new Error(`redis-streams: ${sources.join(', ')} are mutually exclusive; pass only one connection source`);\n }\n if (options.client) {\n if (options.client.isOpen) {\n throw new Error('redis-streams: `client` must not be connected; the pubsub owns its connection lifecycle');\n }\n return options.client as RedisClientType;\n }\n if (options.cluster) {\n return createCluster(options.cluster) as unknown as RedisClientType;\n }\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n return createClient({ ...options.redisOptions, url }) as RedisClientType;\n }\n\n /**\n * A fresh, unconnected client with the writer's configuration. Each reader\n * (one per fan-out topic, one per grouped subscription) needs its own\n * because XREADGROUP BLOCK holds the connection.\n */\n #createReadClient(): RedisClientType {\n return this.#writeClient.duplicate() as RedisClientType;\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 = globalThis.crypto.randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent and safe under concurrent callers. */\n async #ensureWriterConnected(): Promise<void> {\n // node-redis flips `isOpen` true synchronously inside connect(), BEFORE the\n // socket is ready (standalone) or slot discovery has finished (cluster). So\n // while the initial connect is in flight, `isOpen` alone would let a second\n // caller through to issue commands; on a Cluster client that crashes in\n // slot lookup (\"Cannot read properties of undefined (reading 'master')\").\n // Check the in-flight promise first so every cold caller awaits the same\n // connect(). Outside that window we still gate on `isOpen` (not `isReady`)\n // so node-redis's own automatic mid-life reconnect is never fought with a\n // second connect().\n if (!this.#writerConnecting && this.#writeClient.isOpen) return;\n if (!this.#writerConnecting) {\n this.#writerConnecting = this.#writeClient\n .connect()\n .then(() => undefined)\n .finally(() => {\n // Clear so a failed initial connect can be retried on the next call.\n this.#writerConnecting = undefined;\n });\n }\n return this.#writerConnecting;\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: globalThis.crypto.randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n // Track the whole remote publish, connect included, so close() can drain\n // a publish that is still connecting and not only one already in XADD.\n const promise = this.#publishRemote(topic, event);\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async #publishRemote(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n await this.#ensureWriterConnected();\n\n const id = globalThis.crypto.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 await promise;\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 const pending = this.#pendingSubscribes.get(key);\n if (pending) return pending; // idempotent: same (topic, cb) subscribe already in flight\n\n // Register synchronously so an unsubscribe/close racing this subscribe can\n // find and await it. `#doSubscribe` runs synchronously up to its first\n // await, which keeps the localOnly registration guarantee documented there.\n const promise = this.#doSubscribe(topic, cb, key, options).finally(() => {\n this.#pendingSubscribes.delete(key);\n });\n this.#pendingSubscribes.set(key, promise);\n return promise;\n }\n\n async #doSubscribe(topic: string, cb: EventCallback, key: string, options?: SubscribeOptions): Promise<void> {\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 groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\n if (options?.group) {\n const sub = await this.#openSubscription(topic, cb, options.group, groupAnchor);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n return;\n }\n\n // Fan-out: every subscriber to this topic in this instance shares one\n // consumer group and reader. Join the topic's reader if one is open (or\n // being opened); otherwise open it.\n for (;;) {\n const reader = this.#fanoutReaders.get(topic);\n if (reader) return this.#joinFanoutReader(reader, cb, key, groupAnchor);\n const opening = this.#pendingFanoutReaders.get(topic);\n if (!opening) break;\n await opening.catch(() => {});\n }\n const opening = this.#openSubscription(topic, cb, undefined, groupAnchor);\n this.#pendingFanoutReaders.set(topic, opening);\n try {\n const sub = await opening;\n // Register in the same tick the pending entry is cleared, so a\n // subscriber waiting on `opening` finds the reader and joins it.\n this.#fanoutReaders.set(topic, sub);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n } finally {\n this.#pendingFanoutReaders.delete(topic);\n }\n }\n\n /**\n * Add a callback to a topic's open fan-out reader. The reader may already\n * have delivered entries this subscriber should see (`startFrom: 'earliest'`)\n * or still be catching up on entries it should not (`startFrom: 'latest'`),\n * so the new member buffers live entries while its starting point is\n * resolved, replays what it missed, then switches to live delivery.\n */\n async #joinFanoutReader(sub: Subscription, cb: EventCallback, key: string, anchor: '0' | '$'): Promise<void> {\n const member: FanoutMember = { joining: [] };\n sub.members.set(cb, member);\n this.#subscriptions.set(key, sub);\n const active = () => sub.members.get(cb) === member && !sub.stopped;\n\n try {\n if (anchor === '$') {\n // Skip everything already in the stream, even entries the shared\n // reader has not reached yet.\n const [tail] = await this.#writeClient.xRevRange(sub.streamKey, '+', '-', { COUNT: 1 });\n member.skipThrough = tail?.id;\n } else {\n // Entries up to the group's last-delivered id were read before this\n // member was buffering, so replay them; later ones arrive live.\n let boundary: string | undefined;\n try {\n const groups = await this.#writeClient.xInfoGroups(sub.streamKey);\n const info = groups.find(g => String(g.name) === sub.group);\n boundary = info ? String(info['last-delivered-id']) : undefined;\n } catch (err) {\n // No stream (or group) means nothing has been delivered yet.\n if (!errorText(err).match(/no such key|NOGROUP/i)) throw err;\n }\n if (boundary && boundary !== '0-0') {\n member.skipThrough = boundary;\n let start = '-';\n while (active()) {\n const page = await this.#writeClient.xRange(sub.streamKey, start, boundary, { COUNT: TRIM_PAGE_SIZE });\n for (const entry of page) {\n if (!active()) break;\n this.#deliverDetached(sub, cb, entry.message);\n }\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n }\n }\n } catch (err) {\n sub.members.delete(cb);\n this.#subscriptions.delete(key);\n throw err;\n }\n\n if (!active()) return;\n const buffered = member.joining ?? [];\n for (const entry of buffered) {\n if (member.skipThrough && compareStreamIds(entry.id, member.skipThrough) <= 0) continue;\n this.#deliverDetached(sub, cb, entry.fields);\n }\n // `skipThrough` stays set: the reader may still be dispatching a batch read\n // before this member joined (already replayed), or lagging behind a\n // `latest` member's starting point. clearTopic resets it.\n member.joining = undefined;\n }\n\n async #openSubscription(\n topic: string,\n cb: EventCallback,\n groupName: string | undefined,\n groupAnchor: '0' | '$',\n ): Promise<Subscription> {\n const isGrouped = groupName !== undefined;\n const group = groupName ?? `__fanout-${globalThis.crypto.randomUUID()}`;\n const consumer = `${group}-${globalThis.crypto.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 // Brand-new groups default to '0' (stream start) so a worker which\n // subscribes after a publish still sees the backlog. Callers can opt into\n // '$' with startFrom: 'latest' when they only want future events. Existing\n // groups (BUSYGROUP path) always keep their checkpoint. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM may create an empty stream key, so the TTL must be stamped\n // in the same MULTI — otherwise a topic that is subscribed to but\n // never published to (e.g. a per-run control topic) has no expiry and\n // lingers in Redis forever. When the group already exists (a sibling\n // won the race), exec() throws a MultiErrorReply whose message does\n // NOT contain \"BUSYGROUP\" — it lives in err.replies — while the\n // PEXPIRE in the same transaction still applies, so the TTL is\n // refreshed even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true })\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true });\n }\n } catch (err) {\n const msg = errorText(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each reader (per fan-out topic, per grouped subscription) gets a\n // dedicated connection because XREADGROUP with BLOCK > 0 holds the\n // connection until a message arrives.\n const readClient = this.#createReadClient();\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n return {\n members: new Map([[cb, {}]]),\n settling: new Map(),\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n inFlight: new Map(),\n };\n }\n\n /**\n * Periodically reclaim idle pending entries in this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Fan-out groups are private to one consumer, so there is no\n * sibling to claim from and the XPENDING/XCLAIM scan is skipped for them; the\n * loop still runs so `inFlightTimeoutMs` can recover a hung handler, which is\n * the only recovery path a single-consumer group has.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped && this.#inFlightTimeoutMs <= 0) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n await this.#expireInFlight(sub);\n if (!sub.isGrouped) {\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n return;\n }\n // List idle pending entries first and only claim the ones this\n // subscription is not already processing. Claiming (XAUTOCLAIM/XCLAIM)\n // resets the entry's idle clock even when the claimant already owns it,\n // so a claim-then-skip design would let a handler running longer than\n // reclaimIdleMs re-invoke itself (pre-fix) or keep refreshing the idle\n // timer on every tick and starve a live sibling of a genuinely stalled\n // entry. Filtering before the claim leaves in-flight entries untouched:\n // if the handler settles, ack/nack removes them from the PEL; if it is\n // truly hung, the idle clock keeps running and a sibling claims them.\n //\n // The scan is paginated: locally in-flight entries stay in the PEL\n // (by design, above) and are listed on every tick, so with a single\n // fixed-size page they could fill it and hide a reclaimable entry\n // that sorts after them. Walk pages with an exclusive start id until\n // the page comes back short or the claim batch is full.\n const ids: string[] = [];\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, start, '+', RECLAIM_PAGE_SIZE, {\n IDLE: this.#reclaimIdleMs,\n });\n for (const p of page) {\n const id = String(p.id);\n if (sub.inFlight.has(id)) continue;\n ids.push(id);\n if (ids.length >= RECLAIM_PAGE_SIZE) break;\n }\n if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;\n start = `(${String(page[page.length - 1]!.id)}`;\n }\n // XCLAIM re-checks min-idle atomically, so if a sibling claimed an entry\n // between the XPENDING and here it is simply omitted from the reply.\n const claimed =\n ids.length === 0\n ? []\n : ((await this.#writeClient.xClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n ids,\n )) as Array<{ id: string; message: Record<string, string> } | null>);\n for (const entry of claimed) {\n // Null means the stream entry was trimmed. Redis >= 7.0 drops it\n // from the PEL as part of the claim, so there is nothing left to\n // deliver. (Redis 6 leaves it pending, which is why the package\n // requires 7.0+; otherwise trimmed ids would be re-listed forever.)\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reclaim 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(runTick, this.#reclaimIntervalMs);\n };\n\n const runTick = () => {\n sub.reclaimLoop = tick().finally(() => {\n sub.reclaimLoop = undefined;\n });\n };\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n }\n\n /**\n * Give up on local in-flight entries older than `inFlightTimeoutMs` by\n * nacking them on the handler's behalf. This is the only way a\n * single-consumer group recovers a hung handler: the reclaim loop never\n * claims its own in-flight entries (see the tick above), so without a\n * timeout the entry stays pending until the process restarts.\n */\n async #expireInFlight(sub: Subscription): Promise<void> {\n if (this.#inFlightTimeoutMs <= 0) return;\n const cutoff = Date.now() - this.#inFlightTimeoutMs;\n for (const [streamId, entry] of sub.inFlight) {\n if (entry.since > cutoff) continue;\n this.#logger?.warn?.('redis-streams: handler exceeded inFlightTimeoutMs; nacking on its behalf', {\n topic: sub.topic,\n group: sub.group,\n streamId,\n inFlightMs: Date.now() - entry.since,\n });\n // The local marker is not proof of ownership: once the entry has been\n // idle >= reclaimIdleMs a sibling may have claimed it, or it may have\n // been acked outright. `expire` verifies ownership and settles in one\n // atomic Redis step, honours maxDeliveryAttempts, and removes the entry\n // from `inFlight` either way.\n await entry.expire();\n }\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 // A subscribe for this (topic, cb) may still be mid-flight (XGROUP CREATE +\n // reader connect). Wait for it to register so the teardown below actually\n // finds it — returning early here would orphan the subscription forever.\n const pending = this.#pendingSubscribes.get(key);\n if (pending) await pending.catch(() => {});\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n if (sub.teardown) return sub.teardown;\n\n if (sub.members.size > 1 && sub.members.delete(cb)) {\n // Other members keep the reader. Entries handed to this member no\n // longer wait on it.\n this.#subscriptions.delete(key);\n for (const settle of [...sub.settling.values()]) void settle(cb);\n return;\n }\n\n // Last member: close the reader. The member stays attached so entries an\n // already-issued read returns are still delivered to it. A subscriber\n // arriving from now on opens a fresh reader rather than join this one.\n if (this.#fanoutReaders.get(topic) === sub) this.#fanoutReaders.delete(topic);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n sub.teardown = this.#stopSubscription(sub).finally(() => {\n this.#subscriptions.delete(key);\n });\n return sub.teardown;\n }\n\n async #stopSubscription(sub: Subscription): Promise<void> {\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await disconnect(sub.readClient);\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 // Clearing the timer prevents future claims, but an issued claim still owns work.\n await sub.reclaimLoop;\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 // Redis stream IDs can restart below the last delivered ID after DEL.\n // Attached subscribers therefore recover this known-new stream from its\n // beginning instead of anchoring past newly published entries.\n for (const sub of this.#subscriptions.values()) {\n if (sub.topic === topic) {\n sub.lastId = undefined;\n sub.groupAnchor = '0';\n for (const member of sub.members.values()) member.skipThrough = undefined;\n }\n }\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 * Deletes a run's entries with `XDEL`: pages through the stream with `XRANGE`\n * and matches each entry's `runId`. With `producedBefore`, only unpinned\n * entries produced at or before it are deleted. Entries of other runs,\n * including those published by other processes, are untouched.\n */\n override async trimTopic(\n topic: string,\n { runId, producedBefore }: { runId: string; producedBefore?: number },\n ): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n const streamKey = this.#streamKey(topic);\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xRange(streamKey, start, '+', { COUNT: TRIM_PAGE_SIZE });\n const ids: string[] = [];\n for (const entry of page) {\n if (!entry) continue;\n try {\n const event = JSON.parse(entry.message.event ?? '{}') as {\n runId?: string;\n data?: { producedAt?: unknown; pinned?: unknown };\n };\n if (event.runId !== runId) continue;\n if (producedBefore !== undefined) {\n const producedAt = event.data?.producedAt;\n if (typeof producedAt !== 'number' || producedAt > producedBefore || event.data?.pinned) continue;\n }\n ids.push(entry.id);\n } catch {\n // An unparseable entry can't belong to this run.\n }\n }\n if (ids.length > 0) await this.#writeClient.xDel(streamKey, ids);\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: trimTopic 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 // Let in-flight subscribes finish registering before the snapshot below,\n // so a subscription that lands mid-close is torn down rather than leaked.\n if (this.#pendingSubscribes.size > 0) {\n await Promise.allSettled([...this.#pendingSubscribes.values()]);\n }\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = new Set(this.#subscriptions.values());\n await Promise.all([...subs].flatMap(sub => [...sub.members.keys()].map(cb => this.unsubscribe(sub.topic, cb))));\n this.#localCallbacks.clear();\n\n // Let publishes that were accepted before close() reach the stream. A\n // workflow's terminal event is often the last thing written before\n // shutdown; quitting the writer under it would drop the event and reject\n // the caller with a ClosingError.\n await this.flush();\n\n if (this.#writeClient.isOpen) {\n try {\n await disconnect(this.#writeClient);\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 after the last delivered stream entry so events\n // published during recovery remain visible. Before the first delivery,\n // preserve the subscription's original earliest/latest anchor.\n const recoveryAnchor = sub.lastId ?? sub.groupAnchor;\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, recoveryAnchor, { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, recoveryAnchor, { 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 // Entries returned by an issued read belong to us even after stop.\n sub.lastId = entry.id;\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 const event = this.#parseEvent(fields);\n if (!event) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\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 } finally {\n // Clear the in-flight guard only AFTER Redis has settled the entry.\n // Clearing before the xAck resolves would open a window where the\n // entry is neither in-flight nor acked, so a reclaim tick landing in\n // that window (the entry is already idle >= reclaimIdleMs for a\n // long-running handler) would redeliver and re-invoke the handler.\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\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 the reclaim loop (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 reclaim) can ack/nack it again. The entry\n // is intentionally left pending, so clear the in-flight guard now to\n // make it eligible for a future reclaim.\n settled = false;\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\n }\n };\n\n // Timeout-driven nack, used by #expireInFlight. Unlike `nack` above this\n // can run while a sibling may have reclaimed the entry, so the ownership\n // check and the republish+xAck must be one atomic Redis step: a\n // read-then-nack would let a sibling XCLAIM land in between, and we'd\n // republish a duplicate and xAck the sibling's entry out from under it.\n // Either way the local marker is settled afterwards so a late ack/nack\n // from the hung handler is a no-op.\n const expire = async () => {\n if (settled) return;\n settled = true;\n sub.settling.delete(streamId);\n const attempt = event.deliveryAttempt ?? 1;\n const drop = attempt >= this.#maxDeliveryAttempts;\n const payload = drop ? '' : JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event);\n let owned: unknown;\n try {\n owned = await this.#writeClient.eval(NACK_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId, payload, String(this.#streamIdleTtlMs)],\n });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: timeout nack failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Same recovery as a failed nack republish: the entry is still\n // pending, so let a future reclaim (or the next timeout pass, if the\n // handler is still hung) retry it. Fan-out readers have no reclaim\n // scan, so they keep the timeout registration for the next pass.\n settled = false;\n sub.settling.set(streamId, cb => settleMember(cb, false));\n if (sub.isGrouped) sub.inFlight.delete(streamId);\n else sub.inFlight.set(streamId, { since: Date.now(), expire });\n return;\n }\n sub.inFlight.delete(streamId);\n if (owned !== 1) {\n this.#logger?.warn?.(\n 'redis-streams: handler exceeded inFlightTimeoutMs but entry is no longer owned; disowning',\n { topic: sub.topic, group: sub.group, streamId },\n );\n return;\n }\n if (drop) {\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 }\n };\n\n // Mark this entry in-flight for the reclaim loop's benefit for exactly the\n // window between invoking the handler and the delivery settling (ack/nack).\n // Heartbeat for long handlers: reset the entry's PEL idle time so the\n // reclaim loop (ours or a sibling's) does not XCLAIM it mid-execution, and\n // push back the local inFlightTimeoutMs deadline.\n const extend = async () => {\n const entry = sub.inFlight.get(streamId);\n if (!entry || entry.expire !== expire) return;\n entry.since = Date.now();\n if (sub.isGrouped) {\n await this.#writeClient.eval(EXTEND_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId],\n });\n }\n };\n\n // The entry is settled in Redis once, after every member it was handed to\n // has settled: acked if all acked, nacked (one republish) if any nacked.\n // Members joining the topic buffer the entry instead; members whose\n // starting point is past it skip it.\n const waiting = new Set<EventCallback>();\n let anyNack = false;\n const settleMember = async (cb: EventCallback, nacked: boolean) => {\n if (settled || !waiting.delete(cb)) return;\n if (nacked) anyNack = true;\n if (waiting.size > 0) return;\n sub.settling.delete(streamId);\n await (anyNack ? nack() : ack());\n };\n const targets: EventCallback[] = [];\n for (const [cb, member] of sub.members) {\n if (member.joining) {\n member.joining.push({ id: streamId, fields });\n continue;\n }\n if (member.skipThrough) {\n if (compareStreamIds(streamId, member.skipThrough) <= 0) continue;\n member.skipThrough = undefined;\n }\n waiting.add(cb);\n targets.push(cb);\n }\n if (targets.length === 0) {\n await ack();\n return;\n }\n\n sub.inFlight.set(streamId, { since: Date.now(), expire });\n sub.settling.set(streamId, cb => settleMember(cb, false));\n for (const cb of targets) {\n // Each member gets its own Event object so one handler mutating it\n // can't affect another.\n const memberEvent = targets.length === 1 ? event : this.#parseEvent(fields)!;\n this.#invokeCallback(\n cb,\n memberEvent,\n () => settleMember(cb, false),\n () => settleMember(cb, true),\n extend,\n );\n }\n }\n\n /**\n * Invoke a handler without awaiting it. EventCallback is typed `=> void`\n * but handlers commonly return a promise (TS allows Promise<void> to satisfy\n * void); a rejection or synchronous throw routes to nack instead of silently\n * dropping the message. We do NOT await — serializing messages on a\n * subscription would deadlock orchestration callbacks that await their own\n * future events.\n */\n #invokeCallback(\n cb: EventCallback,\n event: Event,\n ack: () => Promise<void>,\n nack: () => Promise<void>,\n extend?: () => Promise<void>,\n ): void {\n try {\n const result: unknown = cb(event, ack, nack, extend);\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 void nack();\n }\n }\n\n #parseEvent(fields: Record<string, string>): Event | undefined {\n try {\n const 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 return event;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Deliver an entry to one fan-out member outside the shared group's PEL:\n * entries the topic's reader handed out before the member joined. Ack is a\n * no-op (the group settles the entry on its own); nack republishes, which\n * redelivers to every member just as a nack on the shared entry would.\n */\n #deliverDetached(sub: Subscription, cb: EventCallback, fields: Record<string, string>): void {\n const event = this.#parseEvent(fields);\n if (!event) return;\n let settled = false;\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\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 return;\n }\n const payload = { event: JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event) };\n try {\n if (this.#streamIdleTtlMs > 0) {\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', payload)\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', payload);\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed for replayed event', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n this.#invokeCallback(\n cb,\n event,\n async () => {\n settled = true;\n },\n 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 FanoutMember {\n // Set while the member is resolving its starting point; live entries are\n // buffered here until then.\n joining?: { id: string; fields: Record<string, string> }[];\n // Entries at or before this id were already delivered to (or are before the\n // start of) this member, so the shared reader skips them for it.\n skipThrough?: string;\n}\n\n/**\n * A reader: one consumer, one connection. A grouped subscription has exactly\n * one member; a fan-out reader is shared by every fan-out subscriber to its\n * topic in this instance.\n */\ninterface Subscription {\n members: Map<EventCallback, FanoutMember>;\n // Per in-flight entry: mark a member as settled-with-ack when it leaves\n // before settling, so the entry is not stranded.\n settling: Map<string, (cb: EventCallback) => Promise<void>>;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n reclaimLoop?: Promise<void>;\n teardown?: Promise<void>;\n // Stream entry IDs currently being processed locally by this subscription.\n // The reclaim loop consults this to avoid re-invoking the handler for a\n // message whose original delivery is still in flight (self-redelivery).\n // `expire` is the timeout path's atomic nack-if-still-owned.\n inFlight: Map<string, { since: number; expire: () => Promise<void> }>;\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -61,6 +61,26 @@ function compareStreamIds(a, b) {
61
61
  const seq = BigInt(aSeq) - BigInt(bSeq);
62
62
  return seq === 0n ? 0 : seq < 0n ? -1 : 1;
63
63
  }
64
+ /**
65
+ * Disconnect a client, standalone or cluster.
66
+ *
67
+ * node-redis 5 `quit()` never settles on a client that is open but waiting to
68
+ * reconnect: it marks the socket closed, which ends the reconnect loop before
69
+ * QUIT is ever written. Such a client has nothing on the wire to drain, and its
70
+ * offline-queued commands could not be sent after `quit()` either, so destroy
71
+ * it instead. A cluster's `quit()` quits every node client, so one
72
+ * reconnecting node is enough to hang it.
73
+ */
74
+ async function disconnect(client) {
75
+ const reconnecting = (c) => !!c && c.isOpen && c.isReady === false;
76
+ const cluster = client;
77
+ const nodes = [...cluster.masters ?? [], ...cluster.replicas ?? []];
78
+ if (reconnecting(client) || nodes.some((node) => reconnecting(node.client))) {
79
+ client.destroy();
80
+ return;
81
+ }
82
+ await client.quit();
83
+ }
64
84
  var RedisStreamsPubSub = class RedisStreamsPubSub extends PubSub {
65
85
  get supportedModes() {
66
86
  return ["pull"];
@@ -471,7 +491,7 @@ var RedisStreamsPubSub = class RedisStreamsPubSub extends PubSub {
471
491
  }
472
492
  async #stopSubscription(sub) {
473
493
  try {
474
- await sub.readClient.quit();
494
+ await disconnect(sub.readClient);
475
495
  } catch (err) {
476
496
  this.#logger?.debug?.("redis-streams: reader quit failed", {
477
497
  topic: sub.topic,
@@ -696,7 +716,7 @@ var RedisStreamsPubSub = class RedisStreamsPubSub extends PubSub {
696
716
  this.#localCallbacks.clear();
697
717
  await this.flush();
698
718
  if (this.#writeClient.isOpen) try {
699
- await this.#writeClient.quit();
719
+ await disconnect(this.#writeClient);
700
720
  } catch (err) {
701
721
  this.#logger?.debug?.("redis-streams: writer quit failed", { err: err instanceof Error ? err.message : err });
702
722
  }
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["#logger","#writeClient","#createWriteClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#inFlightTimeoutMs","#cbIds","#ensureWriterConnected","#writerConnecting","#closed","#deliverLocal","#publishRemote","#pendingPublishes","#streamKey","#subKey","#subscriptions","#pendingSubscribes","#doSubscribe","#localCallbacks","#openSubscription","#runReadLoop","#startReclaimLoop","#fanoutReaders","#joinFanoutReader","#pendingFanoutReaders","#deliverDetached","#createReadClient","#expireInFlight","#deliverMessage","#stopSubscription","#leaseKey","#parseEvent","#invokeCallback","#invokeLocalCallback"],"sources":["../src/index.ts"],"sourcesContent":["import { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient, createCluster } from 'redis';\nimport type { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType } from 'redis';\n\n/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */\nconst RECLAIM_PAGE_SIZE = 100;\nconst TRIM_PAGE_SIZE = 500;\n\n/**\n * Heartbeat: reset a pending entry's idle time only if this consumer still\n * owns it, so a late heartbeat never takes an entry back from a sibling.\n * KEYS[1] stream, ARGV[1] group, ARGV[2] consumer, ARGV[3] entry id\n */\nconst EXTEND_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n redis.call(\"XCLAIM\", KEYS[1], ARGV[1], ARGV[2], 0, ARGV[3], \"JUSTID\")\n return 1\n`;\n\n/**\n * Atomically nack a pending entry only if it is still owned by the given\n * consumer. Returns 1 if this consumer owned it and it was settled, 0 if the\n * entry was not pending for this consumer (acked, or claimed by a sibling).\n *\n * KEYS[1] stream key\n * ARGV[1] group, ARGV[2] consumer, ARGV[3] stream entry id,\n * ARGV[4] republish payload ('' = drop without republish),\n * ARGV[5] stream idle TTL in ms (0 = none)\n */\nconst NACK_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n if ARGV[4] ~= \"\" then\n redis.call(\"XADD\", KEYS[1], \"*\", \"event\", ARGV[4])\n if tonumber(ARGV[5]) > 0 then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[5])\n end\n end\n redis.call(\"XACK\", KEYS[1], ARGV[1], ARGV[3])\n return 1\n`;\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/** Order two Redis stream ids (`<ms>-<seq>`). */\nfunction compareStreamIds(a: string, b: string): number {\n const [aMs = '0', aSeq = '0'] = a.split('-');\n const [bMs = '0', bSeq = '0'] = b.split('-');\n const ms = BigInt(aMs) - BigInt(bMs);\n if (ms !== 0n) return ms < 0n ? -1 : 1;\n const seq = BigInt(aSeq) - BigInt(bSeq);\n return seq === 0n ? 0 : seq < 0n ? -1 : 1;\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 get fan-out semantics (every subscriber sees\n * every event). All fan-out subscribers to one topic in this instance share\n * one private consumer group and one reader connection; each extra callback\n * is dispatched locally, so it costs no Redis connection.\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 * Connect to a Redis Cluster instead of a standalone server. Options are\n * passed to `createCluster()` from `redis`. Mutually exclusive with\n * `url`/`redisOptions`/`client`.\n */\n cluster?: RedisClusterOptions;\n /**\n * A pre-configured, UNCONNECTED `redis` client (standalone or cluster) to\n * use as the shared writer. Each subscription's blocking reader is created\n * from it with `client.duplicate()`, so the client's options are reused but\n * every connection is distinct. The pubsub owns the client's lifecycle\n * (connects on first use, quits on `close()`), so do not share it with the\n * rest of your app. Mutually exclusive with `url`/`redisOptions`/`cluster`.\n */\n client?: RedisClientType | RedisClusterType;\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, subscribe/group creation, nack retry). Each write resets it, so\n * an actively-used stream never expires mid-flight; a stream left idle for\n * the full duration is deleted by Redis automatically — entries, consumer\n * groups and their pending lists included.\n *\n * Defaults to 0 (disabled) to preserve existing behavior: streams live\n * until an explicit `clearTopic`, and a stream that never reaches one stays\n * in Redis forever. **Production deployments should set this** — see below.\n *\n * Why a TTL matters: topics fall into two classes.\n * - *Lifecycle-owned* topics (workflow/agent run streams, request/reply\n * topics) are cleaned up eagerly via `clearTopic` when their lifecycle\n * ends. For these the TTL is a backstop covering crashes and missed\n * cleanup.\n * - *Open-ended* topics (per-conversation streams, per-project feeds) have\n * no moment at which any process can safely declare them finished, so\n * nothing ever calls `clearTopic` on them. For these the TTL is the ONLY\n * reclamation mechanism; with it disabled they accumulate for the\n * lifetime of the Redis instance.\n *\n * Sizing: the TTL must exceed the longest window in which an idle stream is\n * still legitimately useful — chiefly, how long a grouped worker may be down\n * and still expect to replay its backlog on restart. If workers can be\n * offline longer than the TTL while publishers stay quiet, raise it.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs a reclaim pass (XPENDING + XCLAIM) 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 * How long (in ms) a handler may hold a message in-flight (neither acked nor\n * nacked) before the reclaim loop gives up on it and nacks it on the\n * handler's behalf. The nack republishes the event with an incremented\n * `deliveryAttempt`, so `maxDeliveryAttempts` still bounds retries, and acks\n * the original entry; the hung handler's own eventual ack/nack becomes a\n * no-op.\n *\n * Without this, an in-flight entry is only ever recovered by a *different*\n * consumer in the group once it has been idle for `reclaimIdleMs`. A\n * single-consumer group therefore holds a hung message until the process\n * restarts. Defaults to 0 (disabled). When set, it should be comfortably\n * larger than the slowest legitimate handler.\n */\n inFlightTimeoutMs?: 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 override get supportsOffsets(): boolean {\n return false;\n }\n\n protected override onUnsupportedOffset(offset: number): void {\n this.#logger?.warn?.(`redis-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n // Standalone and cluster clients share the command surface this class uses.\n // Every MULTI/EVAL here is single-key and XREADGROUP reads one stream, so\n // commands never cross hash slots; keep it that way or Cluster breaks.\n #writeClient: RedisClientType;\n // Dedupes concurrent cold callers onto a single writer connect(); see\n // #ensureWriterConnected.\n #writerConnecting?: Promise<void>;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #streamIdleTtlMs: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #inFlightTimeoutMs: 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 // Fan-out subscriptions to the same topic share one Subscription, so several\n // keys can point at the same record.\n #subscriptions: Map<string, Subscription> = new Map();\n // The shared fan-out reader per topic, and readers still being opened so\n // concurrent first subscribers to a topic don't each open one.\n #fanoutReaders: Map<string, Subscription> = new Map();\n #pendingFanoutReaders: Map<string, Promise<Subscription>> = new Map();\n // Subscribes that are still wiring up Redis (XGROUP CREATE + reader connect).\n // `unsubscribe` and `close` await these so an unsubscribe issued while the\n // subscribe round trip is in flight tears the subscription down instead of\n // no-oping — otherwise the late-registering subscription (reader connection,\n // blocked read loop) would leak with nothing left to stop it.\n #pendingSubscribes: Map<string, Promise<void>> = 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 this.#writeClient = RedisStreamsPubSub.#createWriteClient(options);\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 const inFlightTimeout = options.inFlightTimeoutMs ?? 0;\n if (!Number.isFinite(inFlightTimeout) || inFlightTimeout < 0) {\n throw new Error(\n `redis-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`,\n );\n }\n this.#inFlightTimeoutMs = inFlightTimeout;\n }\n\n /**\n * Resolve the shared writer client from config. Exactly one connection\n * source is allowed: an injected `client`, `cluster` options, or the\n * standalone `url`/`redisOptions` pair (the default).\n */\n static #createWriteClient(options: RedisStreamsPubSubConfig): RedisClientType {\n const sources = [\n options.client && 'client',\n options.cluster && 'cluster',\n (options.url ?? options.redisOptions) && 'url/redisOptions',\n ].filter(Boolean);\n if (sources.length > 1) {\n throw new Error(`redis-streams: ${sources.join(', ')} are mutually exclusive; pass only one connection source`);\n }\n if (options.client) {\n if (options.client.isOpen) {\n throw new Error('redis-streams: `client` must not be connected; the pubsub owns its connection lifecycle');\n }\n return options.client as RedisClientType;\n }\n if (options.cluster) {\n return createCluster(options.cluster) as unknown as RedisClientType;\n }\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n return createClient({ ...options.redisOptions, url }) as RedisClientType;\n }\n\n /**\n * A fresh, unconnected client with the writer's configuration. Each reader\n * (one per fan-out topic, one per grouped subscription) needs its own\n * because XREADGROUP BLOCK holds the connection.\n */\n #createReadClient(): RedisClientType {\n return this.#writeClient.duplicate() as RedisClientType;\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 = globalThis.crypto.randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent and safe under concurrent callers. */\n async #ensureWriterConnected(): Promise<void> {\n // node-redis flips `isOpen` true synchronously inside connect(), BEFORE the\n // socket is ready (standalone) or slot discovery has finished (cluster). So\n // while the initial connect is in flight, `isOpen` alone would let a second\n // caller through to issue commands; on a Cluster client that crashes in\n // slot lookup (\"Cannot read properties of undefined (reading 'master')\").\n // Check the in-flight promise first so every cold caller awaits the same\n // connect(). Outside that window we still gate on `isOpen` (not `isReady`)\n // so node-redis's own automatic mid-life reconnect is never fought with a\n // second connect().\n if (!this.#writerConnecting && this.#writeClient.isOpen) return;\n if (!this.#writerConnecting) {\n this.#writerConnecting = this.#writeClient\n .connect()\n .then(() => undefined)\n .finally(() => {\n // Clear so a failed initial connect can be retried on the next call.\n this.#writerConnecting = undefined;\n });\n }\n return this.#writerConnecting;\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: globalThis.crypto.randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n // Track the whole remote publish, connect included, so close() can drain\n // a publish that is still connecting and not only one already in XADD.\n const promise = this.#publishRemote(topic, event);\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async #publishRemote(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n await this.#ensureWriterConnected();\n\n const id = globalThis.crypto.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 await promise;\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 const pending = this.#pendingSubscribes.get(key);\n if (pending) return pending; // idempotent: same (topic, cb) subscribe already in flight\n\n // Register synchronously so an unsubscribe/close racing this subscribe can\n // find and await it. `#doSubscribe` runs synchronously up to its first\n // await, which keeps the localOnly registration guarantee documented there.\n const promise = this.#doSubscribe(topic, cb, key, options).finally(() => {\n this.#pendingSubscribes.delete(key);\n });\n this.#pendingSubscribes.set(key, promise);\n return promise;\n }\n\n async #doSubscribe(topic: string, cb: EventCallback, key: string, options?: SubscribeOptions): Promise<void> {\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 groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\n if (options?.group) {\n const sub = await this.#openSubscription(topic, cb, options.group, groupAnchor);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n return;\n }\n\n // Fan-out: every subscriber to this topic in this instance shares one\n // consumer group and reader. Join the topic's reader if one is open (or\n // being opened); otherwise open it.\n for (;;) {\n const reader = this.#fanoutReaders.get(topic);\n if (reader) return this.#joinFanoutReader(reader, cb, key, groupAnchor);\n const opening = this.#pendingFanoutReaders.get(topic);\n if (!opening) break;\n await opening.catch(() => {});\n }\n const opening = this.#openSubscription(topic, cb, undefined, groupAnchor);\n this.#pendingFanoutReaders.set(topic, opening);\n try {\n const sub = await opening;\n // Register in the same tick the pending entry is cleared, so a\n // subscriber waiting on `opening` finds the reader and joins it.\n this.#fanoutReaders.set(topic, sub);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n } finally {\n this.#pendingFanoutReaders.delete(topic);\n }\n }\n\n /**\n * Add a callback to a topic's open fan-out reader. The reader may already\n * have delivered entries this subscriber should see (`startFrom: 'earliest'`)\n * or still be catching up on entries it should not (`startFrom: 'latest'`),\n * so the new member buffers live entries while its starting point is\n * resolved, replays what it missed, then switches to live delivery.\n */\n async #joinFanoutReader(sub: Subscription, cb: EventCallback, key: string, anchor: '0' | '$'): Promise<void> {\n const member: FanoutMember = { joining: [] };\n sub.members.set(cb, member);\n this.#subscriptions.set(key, sub);\n const active = () => sub.members.get(cb) === member && !sub.stopped;\n\n try {\n if (anchor === '$') {\n // Skip everything already in the stream, even entries the shared\n // reader has not reached yet.\n const [tail] = await this.#writeClient.xRevRange(sub.streamKey, '+', '-', { COUNT: 1 });\n member.skipThrough = tail?.id;\n } else {\n // Entries up to the group's last-delivered id were read before this\n // member was buffering, so replay them; later ones arrive live.\n let boundary: string | undefined;\n try {\n const groups = await this.#writeClient.xInfoGroups(sub.streamKey);\n const info = groups.find(g => String(g.name) === sub.group);\n boundary = info ? String(info['last-delivered-id']) : undefined;\n } catch (err) {\n // No stream (or group) means nothing has been delivered yet.\n if (!errorText(err).match(/no such key|NOGROUP/i)) throw err;\n }\n if (boundary && boundary !== '0-0') {\n member.skipThrough = boundary;\n let start = '-';\n while (active()) {\n const page = await this.#writeClient.xRange(sub.streamKey, start, boundary, { COUNT: TRIM_PAGE_SIZE });\n for (const entry of page) {\n if (!active()) break;\n this.#deliverDetached(sub, cb, entry.message);\n }\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n }\n }\n } catch (err) {\n sub.members.delete(cb);\n this.#subscriptions.delete(key);\n throw err;\n }\n\n if (!active()) return;\n const buffered = member.joining ?? [];\n for (const entry of buffered) {\n if (member.skipThrough && compareStreamIds(entry.id, member.skipThrough) <= 0) continue;\n this.#deliverDetached(sub, cb, entry.fields);\n }\n // `skipThrough` stays set: the reader may still be dispatching a batch read\n // before this member joined (already replayed), or lagging behind a\n // `latest` member's starting point. clearTopic resets it.\n member.joining = undefined;\n }\n\n async #openSubscription(\n topic: string,\n cb: EventCallback,\n groupName: string | undefined,\n groupAnchor: '0' | '$',\n ): Promise<Subscription> {\n const isGrouped = groupName !== undefined;\n const group = groupName ?? `__fanout-${globalThis.crypto.randomUUID()}`;\n const consumer = `${group}-${globalThis.crypto.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 // Brand-new groups default to '0' (stream start) so a worker which\n // subscribes after a publish still sees the backlog. Callers can opt into\n // '$' with startFrom: 'latest' when they only want future events. Existing\n // groups (BUSYGROUP path) always keep their checkpoint. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM may create an empty stream key, so the TTL must be stamped\n // in the same MULTI — otherwise a topic that is subscribed to but\n // never published to (e.g. a per-run control topic) has no expiry and\n // lingers in Redis forever. When the group already exists (a sibling\n // won the race), exec() throws a MultiErrorReply whose message does\n // NOT contain \"BUSYGROUP\" — it lives in err.replies — while the\n // PEXPIRE in the same transaction still applies, so the TTL is\n // refreshed even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true })\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true });\n }\n } catch (err) {\n const msg = errorText(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each reader (per fan-out topic, per grouped subscription) gets a\n // dedicated connection because XREADGROUP with BLOCK > 0 holds the\n // connection until a message arrives.\n const readClient = this.#createReadClient();\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n return {\n members: new Map([[cb, {}]]),\n settling: new Map(),\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n inFlight: new Map(),\n };\n }\n\n /**\n * Periodically reclaim idle pending entries in this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Fan-out groups are private to one consumer, so there is no\n * sibling to claim from and the XPENDING/XCLAIM scan is skipped for them; the\n * loop still runs so `inFlightTimeoutMs` can recover a hung handler, which is\n * the only recovery path a single-consumer group has.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped && this.#inFlightTimeoutMs <= 0) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n await this.#expireInFlight(sub);\n if (!sub.isGrouped) {\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n return;\n }\n // List idle pending entries first and only claim the ones this\n // subscription is not already processing. Claiming (XAUTOCLAIM/XCLAIM)\n // resets the entry's idle clock even when the claimant already owns it,\n // so a claim-then-skip design would let a handler running longer than\n // reclaimIdleMs re-invoke itself (pre-fix) or keep refreshing the idle\n // timer on every tick and starve a live sibling of a genuinely stalled\n // entry. Filtering before the claim leaves in-flight entries untouched:\n // if the handler settles, ack/nack removes them from the PEL; if it is\n // truly hung, the idle clock keeps running and a sibling claims them.\n //\n // The scan is paginated: locally in-flight entries stay in the PEL\n // (by design, above) and are listed on every tick, so with a single\n // fixed-size page they could fill it and hide a reclaimable entry\n // that sorts after them. Walk pages with an exclusive start id until\n // the page comes back short or the claim batch is full.\n const ids: string[] = [];\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, start, '+', RECLAIM_PAGE_SIZE, {\n IDLE: this.#reclaimIdleMs,\n });\n for (const p of page) {\n const id = String(p.id);\n if (sub.inFlight.has(id)) continue;\n ids.push(id);\n if (ids.length >= RECLAIM_PAGE_SIZE) break;\n }\n if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;\n start = `(${String(page[page.length - 1]!.id)}`;\n }\n // XCLAIM re-checks min-idle atomically, so if a sibling claimed an entry\n // between the XPENDING and here it is simply omitted from the reply.\n const claimed =\n ids.length === 0\n ? []\n : ((await this.#writeClient.xClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n ids,\n )) as Array<{ id: string; message: Record<string, string> } | null>);\n for (const entry of claimed) {\n // Null means the stream entry was trimmed. Redis >= 7.0 drops it\n // from the PEL as part of the claim, so there is nothing left to\n // deliver. (Redis 6 leaves it pending, which is why the package\n // requires 7.0+; otherwise trimmed ids would be re-listed forever.)\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reclaim 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(runTick, this.#reclaimIntervalMs);\n };\n\n const runTick = () => {\n sub.reclaimLoop = tick().finally(() => {\n sub.reclaimLoop = undefined;\n });\n };\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n }\n\n /**\n * Give up on local in-flight entries older than `inFlightTimeoutMs` by\n * nacking them on the handler's behalf. This is the only way a\n * single-consumer group recovers a hung handler: the reclaim loop never\n * claims its own in-flight entries (see the tick above), so without a\n * timeout the entry stays pending until the process restarts.\n */\n async #expireInFlight(sub: Subscription): Promise<void> {\n if (this.#inFlightTimeoutMs <= 0) return;\n const cutoff = Date.now() - this.#inFlightTimeoutMs;\n for (const [streamId, entry] of sub.inFlight) {\n if (entry.since > cutoff) continue;\n this.#logger?.warn?.('redis-streams: handler exceeded inFlightTimeoutMs; nacking on its behalf', {\n topic: sub.topic,\n group: sub.group,\n streamId,\n inFlightMs: Date.now() - entry.since,\n });\n // The local marker is not proof of ownership: once the entry has been\n // idle >= reclaimIdleMs a sibling may have claimed it, or it may have\n // been acked outright. `expire` verifies ownership and settles in one\n // atomic Redis step, honours maxDeliveryAttempts, and removes the entry\n // from `inFlight` either way.\n await entry.expire();\n }\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 // A subscribe for this (topic, cb) may still be mid-flight (XGROUP CREATE +\n // reader connect). Wait for it to register so the teardown below actually\n // finds it — returning early here would orphan the subscription forever.\n const pending = this.#pendingSubscribes.get(key);\n if (pending) await pending.catch(() => {});\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n if (sub.teardown) return sub.teardown;\n\n if (sub.members.size > 1 && sub.members.delete(cb)) {\n // Other members keep the reader. Entries handed to this member no\n // longer wait on it.\n this.#subscriptions.delete(key);\n for (const settle of [...sub.settling.values()]) void settle(cb);\n return;\n }\n\n // Last member: close the reader. The member stays attached so entries an\n // already-issued read returns are still delivered to it. A subscriber\n // arriving from now on opens a fresh reader rather than join this one.\n if (this.#fanoutReaders.get(topic) === sub) this.#fanoutReaders.delete(topic);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n sub.teardown = this.#stopSubscription(sub).finally(() => {\n this.#subscriptions.delete(key);\n });\n return sub.teardown;\n }\n\n async #stopSubscription(sub: Subscription): Promise<void> {\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 // Clearing the timer prevents future claims, but an issued claim still owns work.\n await sub.reclaimLoop;\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 // Redis stream IDs can restart below the last delivered ID after DEL.\n // Attached subscribers therefore recover this known-new stream from its\n // beginning instead of anchoring past newly published entries.\n for (const sub of this.#subscriptions.values()) {\n if (sub.topic === topic) {\n sub.lastId = undefined;\n sub.groupAnchor = '0';\n for (const member of sub.members.values()) member.skipThrough = undefined;\n }\n }\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 * Deletes a run's entries with `XDEL`: pages through the stream with `XRANGE`\n * and matches each entry's `runId`. With `producedBefore`, only unpinned\n * entries produced at or before it are deleted. Entries of other runs,\n * including those published by other processes, are untouched.\n */\n override async trimTopic(\n topic: string,\n { runId, producedBefore }: { runId: string; producedBefore?: number },\n ): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n const streamKey = this.#streamKey(topic);\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xRange(streamKey, start, '+', { COUNT: TRIM_PAGE_SIZE });\n const ids: string[] = [];\n for (const entry of page) {\n if (!entry) continue;\n try {\n const event = JSON.parse(entry.message.event ?? '{}') as {\n runId?: string;\n data?: { producedAt?: unknown; pinned?: unknown };\n };\n if (event.runId !== runId) continue;\n if (producedBefore !== undefined) {\n const producedAt = event.data?.producedAt;\n if (typeof producedAt !== 'number' || producedAt > producedBefore || event.data?.pinned) continue;\n }\n ids.push(entry.id);\n } catch {\n // An unparseable entry can't belong to this run.\n }\n }\n if (ids.length > 0) await this.#writeClient.xDel(streamKey, ids);\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: trimTopic 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 // Let in-flight subscribes finish registering before the snapshot below,\n // so a subscription that lands mid-close is torn down rather than leaked.\n if (this.#pendingSubscribes.size > 0) {\n await Promise.allSettled([...this.#pendingSubscribes.values()]);\n }\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = new Set(this.#subscriptions.values());\n await Promise.all([...subs].flatMap(sub => [...sub.members.keys()].map(cb => this.unsubscribe(sub.topic, cb))));\n this.#localCallbacks.clear();\n\n // Let publishes that were accepted before close() reach the stream. A\n // workflow's terminal event is often the last thing written before\n // shutdown; quitting the writer under it would drop the event and reject\n // the caller with a ClosingError.\n await this.flush();\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 after the last delivered stream entry so events\n // published during recovery remain visible. Before the first delivery,\n // preserve the subscription's original earliest/latest anchor.\n const recoveryAnchor = sub.lastId ?? sub.groupAnchor;\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, recoveryAnchor, { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, recoveryAnchor, { 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 // Entries returned by an issued read belong to us even after stop.\n sub.lastId = entry.id;\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 const event = this.#parseEvent(fields);\n if (!event) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\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 } finally {\n // Clear the in-flight guard only AFTER Redis has settled the entry.\n // Clearing before the xAck resolves would open a window where the\n // entry is neither in-flight nor acked, so a reclaim tick landing in\n // that window (the entry is already idle >= reclaimIdleMs for a\n // long-running handler) would redeliver and re-invoke the handler.\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\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 the reclaim loop (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 reclaim) can ack/nack it again. The entry\n // is intentionally left pending, so clear the in-flight guard now to\n // make it eligible for a future reclaim.\n settled = false;\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\n }\n };\n\n // Timeout-driven nack, used by #expireInFlight. Unlike `nack` above this\n // can run while a sibling may have reclaimed the entry, so the ownership\n // check and the republish+xAck must be one atomic Redis step: a\n // read-then-nack would let a sibling XCLAIM land in between, and we'd\n // republish a duplicate and xAck the sibling's entry out from under it.\n // Either way the local marker is settled afterwards so a late ack/nack\n // from the hung handler is a no-op.\n const expire = async () => {\n if (settled) return;\n settled = true;\n sub.settling.delete(streamId);\n const attempt = event.deliveryAttempt ?? 1;\n const drop = attempt >= this.#maxDeliveryAttempts;\n const payload = drop ? '' : JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event);\n let owned: unknown;\n try {\n owned = await this.#writeClient.eval(NACK_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId, payload, String(this.#streamIdleTtlMs)],\n });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: timeout nack failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Same recovery as a failed nack republish: the entry is still\n // pending, so let a future reclaim (or the next timeout pass, if the\n // handler is still hung) retry it. Fan-out readers have no reclaim\n // scan, so they keep the timeout registration for the next pass.\n settled = false;\n sub.settling.set(streamId, cb => settleMember(cb, false));\n if (sub.isGrouped) sub.inFlight.delete(streamId);\n else sub.inFlight.set(streamId, { since: Date.now(), expire });\n return;\n }\n sub.inFlight.delete(streamId);\n if (owned !== 1) {\n this.#logger?.warn?.(\n 'redis-streams: handler exceeded inFlightTimeoutMs but entry is no longer owned; disowning',\n { topic: sub.topic, group: sub.group, streamId },\n );\n return;\n }\n if (drop) {\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 }\n };\n\n // Mark this entry in-flight for the reclaim loop's benefit for exactly the\n // window between invoking the handler and the delivery settling (ack/nack).\n // Heartbeat for long handlers: reset the entry's PEL idle time so the\n // reclaim loop (ours or a sibling's) does not XCLAIM it mid-execution, and\n // push back the local inFlightTimeoutMs deadline.\n const extend = async () => {\n const entry = sub.inFlight.get(streamId);\n if (!entry || entry.expire !== expire) return;\n entry.since = Date.now();\n if (sub.isGrouped) {\n await this.#writeClient.eval(EXTEND_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId],\n });\n }\n };\n\n // The entry is settled in Redis once, after every member it was handed to\n // has settled: acked if all acked, nacked (one republish) if any nacked.\n // Members joining the topic buffer the entry instead; members whose\n // starting point is past it skip it.\n const waiting = new Set<EventCallback>();\n let anyNack = false;\n const settleMember = async (cb: EventCallback, nacked: boolean) => {\n if (settled || !waiting.delete(cb)) return;\n if (nacked) anyNack = true;\n if (waiting.size > 0) return;\n sub.settling.delete(streamId);\n await (anyNack ? nack() : ack());\n };\n const targets: EventCallback[] = [];\n for (const [cb, member] of sub.members) {\n if (member.joining) {\n member.joining.push({ id: streamId, fields });\n continue;\n }\n if (member.skipThrough) {\n if (compareStreamIds(streamId, member.skipThrough) <= 0) continue;\n member.skipThrough = undefined;\n }\n waiting.add(cb);\n targets.push(cb);\n }\n if (targets.length === 0) {\n await ack();\n return;\n }\n\n sub.inFlight.set(streamId, { since: Date.now(), expire });\n sub.settling.set(streamId, cb => settleMember(cb, false));\n for (const cb of targets) {\n // Each member gets its own Event object so one handler mutating it\n // can't affect another.\n const memberEvent = targets.length === 1 ? event : this.#parseEvent(fields)!;\n this.#invokeCallback(\n cb,\n memberEvent,\n () => settleMember(cb, false),\n () => settleMember(cb, true),\n extend,\n );\n }\n }\n\n /**\n * Invoke a handler without awaiting it. EventCallback is typed `=> void`\n * but handlers commonly return a promise (TS allows Promise<void> to satisfy\n * void); a rejection or synchronous throw routes to nack instead of silently\n * dropping the message. We do NOT await — serializing messages on a\n * subscription would deadlock orchestration callbacks that await their own\n * future events.\n */\n #invokeCallback(\n cb: EventCallback,\n event: Event,\n ack: () => Promise<void>,\n nack: () => Promise<void>,\n extend?: () => Promise<void>,\n ): void {\n try {\n const result: unknown = cb(event, ack, nack, extend);\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 void nack();\n }\n }\n\n #parseEvent(fields: Record<string, string>): Event | undefined {\n try {\n const 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 return event;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Deliver an entry to one fan-out member outside the shared group's PEL:\n * entries the topic's reader handed out before the member joined. Ack is a\n * no-op (the group settles the entry on its own); nack republishes, which\n * redelivers to every member just as a nack on the shared entry would.\n */\n #deliverDetached(sub: Subscription, cb: EventCallback, fields: Record<string, string>): void {\n const event = this.#parseEvent(fields);\n if (!event) return;\n let settled = false;\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\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 return;\n }\n const payload = { event: JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event) };\n try {\n if (this.#streamIdleTtlMs > 0) {\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', payload)\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', payload);\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed for replayed event', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n this.#invokeCallback(\n cb,\n event,\n async () => {\n settled = true;\n },\n 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 FanoutMember {\n // Set while the member is resolving its starting point; live entries are\n // buffered here until then.\n joining?: { id: string; fields: Record<string, string> }[];\n // Entries at or before this id were already delivered to (or are before the\n // start of) this member, so the shared reader skips them for it.\n skipThrough?: string;\n}\n\n/**\n * A reader: one consumer, one connection. A grouped subscription has exactly\n * one member; a fan-out reader is shared by every fan-out subscriber to its\n * topic in this instance.\n */\ninterface Subscription {\n members: Map<EventCallback, FanoutMember>;\n // Per in-flight entry: mark a member as settled-with-ack when it leaves\n // before settling, so the entry is not stranded.\n settling: Map<string, (cb: EventCallback) => Promise<void>>;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n reclaimLoop?: Promise<void>;\n teardown?: Promise<void>;\n // Stream entry IDs currently being processed locally by this subscription.\n // The reclaim loop consults this to avoid re-invoking the handler for a\n // message whose original delivery is still in flight (self-redelivery).\n // `expire` is the timeout path's atomic nack-if-still-owned.\n inFlight: Map<string, { since: number; expire: () => Promise<void> }>;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["#logger","#writeClient","#createWriteClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#inFlightTimeoutMs","#cbIds","#ensureWriterConnected","#writerConnecting","#closed","#deliverLocal","#publishRemote","#pendingPublishes","#streamKey","#subKey","#subscriptions","#pendingSubscribes","#doSubscribe","#localCallbacks","#openSubscription","#runReadLoop","#startReclaimLoop","#fanoutReaders","#joinFanoutReader","#pendingFanoutReaders","#deliverDetached","#createReadClient","#expireInFlight","#deliverMessage","#stopSubscription","#leaseKey","#parseEvent","#invokeCallback","#invokeLocalCallback"],"sources":["../src/index.ts"],"sourcesContent":["import { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient, createCluster } from 'redis';\nimport type { RedisClientOptions, RedisClientType, RedisClusterOptions, RedisClusterType } from 'redis';\n\n/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */\nconst RECLAIM_PAGE_SIZE = 100;\nconst TRIM_PAGE_SIZE = 500;\n\n/**\n * Heartbeat: reset a pending entry's idle time only if this consumer still\n * owns it, so a late heartbeat never takes an entry back from a sibling.\n * KEYS[1] stream, ARGV[1] group, ARGV[2] consumer, ARGV[3] entry id\n */\nconst EXTEND_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n redis.call(\"XCLAIM\", KEYS[1], ARGV[1], ARGV[2], 0, ARGV[3], \"JUSTID\")\n return 1\n`;\n\n/**\n * Atomically nack a pending entry only if it is still owned by the given\n * consumer. Returns 1 if this consumer owned it and it was settled, 0 if the\n * entry was not pending for this consumer (acked, or claimed by a sibling).\n *\n * KEYS[1] stream key\n * ARGV[1] group, ARGV[2] consumer, ARGV[3] stream entry id,\n * ARGV[4] republish payload ('' = drop without republish),\n * ARGV[5] stream idle TTL in ms (0 = none)\n */\nconst NACK_IF_OWNED_SCRIPT = `\n local pending = redis.call(\"XPENDING\", KEYS[1], ARGV[1], ARGV[3], ARGV[3], 1)\n if #pending == 0 or pending[1][2] ~= ARGV[2] then\n return 0\n end\n if ARGV[4] ~= \"\" then\n redis.call(\"XADD\", KEYS[1], \"*\", \"event\", ARGV[4])\n if tonumber(ARGV[5]) > 0 then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[5])\n end\n end\n redis.call(\"XACK\", KEYS[1], ARGV[1], ARGV[3])\n return 1\n`;\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/** Order two Redis stream ids (`<ms>-<seq>`). */\nfunction compareStreamIds(a: string, b: string): number {\n const [aMs = '0', aSeq = '0'] = a.split('-');\n const [bMs = '0', bSeq = '0'] = b.split('-');\n const ms = BigInt(aMs) - BigInt(bMs);\n if (ms !== 0n) return ms < 0n ? -1 : 1;\n const seq = BigInt(aSeq) - BigInt(bSeq);\n return seq === 0n ? 0 : seq < 0n ? -1 : 1;\n}\n\n/**\n * Disconnect a client, standalone or cluster.\n *\n * node-redis 5 `quit()` never settles on a client that is open but waiting to\n * reconnect: it marks the socket closed, which ends the reconnect loop before\n * QUIT is ever written. Such a client has nothing on the wire to drain, and its\n * offline-queued commands could not be sent after `quit()` either, so destroy\n * it instead. A cluster's `quit()` quits every node client, so one\n * reconnecting node is enough to hang it.\n */\nasync function disconnect(client: RedisClientType): Promise<void> {\n const reconnecting = (c?: { isOpen: boolean; isReady?: boolean }) => !!c && c.isOpen && c.isReady === false;\n const cluster = client as unknown as Partial<Pick<RedisClusterType, 'masters' | 'replicas'>>;\n const nodes = [...(cluster.masters ?? []), ...(cluster.replicas ?? [])];\n if (reconnecting(client) || nodes.some(node => reconnecting(node.client))) {\n client.destroy();\n return;\n }\n await client.quit();\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 get fan-out semantics (every subscriber sees\n * every event). All fan-out subscribers to one topic in this instance share\n * one private consumer group and one reader connection; each extra callback\n * is dispatched locally, so it costs no Redis connection.\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 * Connect to a Redis Cluster instead of a standalone server. Options are\n * passed to `createCluster()` from `redis`. Mutually exclusive with\n * `url`/`redisOptions`/`client`.\n */\n cluster?: RedisClusterOptions;\n /**\n * A pre-configured, UNCONNECTED `redis` client (standalone or cluster) to\n * use as the shared writer. Each subscription's blocking reader is created\n * from it with `client.duplicate()`, so the client's options are reused but\n * every connection is distinct. The pubsub owns the client's lifecycle\n * (connects on first use, quits on `close()`), so do not share it with the\n * rest of your app. Mutually exclusive with `url`/`redisOptions`/`cluster`.\n */\n client?: RedisClientType | RedisClusterType;\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, subscribe/group creation, nack retry). Each write resets it, so\n * an actively-used stream never expires mid-flight; a stream left idle for\n * the full duration is deleted by Redis automatically — entries, consumer\n * groups and their pending lists included.\n *\n * Defaults to 0 (disabled) to preserve existing behavior: streams live\n * until an explicit `clearTopic`, and a stream that never reaches one stays\n * in Redis forever. **Production deployments should set this** — see below.\n *\n * Why a TTL matters: topics fall into two classes.\n * - *Lifecycle-owned* topics (workflow/agent run streams, request/reply\n * topics) are cleaned up eagerly via `clearTopic` when their lifecycle\n * ends. For these the TTL is a backstop covering crashes and missed\n * cleanup.\n * - *Open-ended* topics (per-conversation streams, per-project feeds) have\n * no moment at which any process can safely declare them finished, so\n * nothing ever calls `clearTopic` on them. For these the TTL is the ONLY\n * reclamation mechanism; with it disabled they accumulate for the\n * lifetime of the Redis instance.\n *\n * Sizing: the TTL must exceed the longest window in which an idle stream is\n * still legitimately useful — chiefly, how long a grouped worker may be down\n * and still expect to replay its backlog on restart. If workers can be\n * offline longer than the TTL while publishers stay quiet, raise it.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs a reclaim pass (XPENDING + XCLAIM) 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 * How long (in ms) a handler may hold a message in-flight (neither acked nor\n * nacked) before the reclaim loop gives up on it and nacks it on the\n * handler's behalf. The nack republishes the event with an incremented\n * `deliveryAttempt`, so `maxDeliveryAttempts` still bounds retries, and acks\n * the original entry; the hung handler's own eventual ack/nack becomes a\n * no-op.\n *\n * Without this, an in-flight entry is only ever recovered by a *different*\n * consumer in the group once it has been idle for `reclaimIdleMs`. A\n * single-consumer group therefore holds a hung message until the process\n * restarts. Defaults to 0 (disabled). When set, it should be comfortably\n * larger than the slowest legitimate handler.\n */\n inFlightTimeoutMs?: 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 override get supportsOffsets(): boolean {\n return false;\n }\n\n protected override onUnsupportedOffset(offset: number): void {\n this.#logger?.warn?.(`redis-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n // Standalone and cluster clients share the command surface this class uses.\n // Every MULTI/EVAL here is single-key and XREADGROUP reads one stream, so\n // commands never cross hash slots; keep it that way or Cluster breaks.\n #writeClient: RedisClientType;\n // Dedupes concurrent cold callers onto a single writer connect(); see\n // #ensureWriterConnected.\n #writerConnecting?: Promise<void>;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #streamIdleTtlMs: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #inFlightTimeoutMs: 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 // Fan-out subscriptions to the same topic share one Subscription, so several\n // keys can point at the same record.\n #subscriptions: Map<string, Subscription> = new Map();\n // The shared fan-out reader per topic, and readers still being opened so\n // concurrent first subscribers to a topic don't each open one.\n #fanoutReaders: Map<string, Subscription> = new Map();\n #pendingFanoutReaders: Map<string, Promise<Subscription>> = new Map();\n // Subscribes that are still wiring up Redis (XGROUP CREATE + reader connect).\n // `unsubscribe` and `close` await these so an unsubscribe issued while the\n // subscribe round trip is in flight tears the subscription down instead of\n // no-oping — otherwise the late-registering subscription (reader connection,\n // blocked read loop) would leak with nothing left to stop it.\n #pendingSubscribes: Map<string, Promise<void>> = 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 this.#writeClient = RedisStreamsPubSub.#createWriteClient(options);\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 const inFlightTimeout = options.inFlightTimeoutMs ?? 0;\n if (!Number.isFinite(inFlightTimeout) || inFlightTimeout < 0) {\n throw new Error(\n `redis-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`,\n );\n }\n this.#inFlightTimeoutMs = inFlightTimeout;\n }\n\n /**\n * Resolve the shared writer client from config. Exactly one connection\n * source is allowed: an injected `client`, `cluster` options, or the\n * standalone `url`/`redisOptions` pair (the default).\n */\n static #createWriteClient(options: RedisStreamsPubSubConfig): RedisClientType {\n const sources = [\n options.client && 'client',\n options.cluster && 'cluster',\n (options.url ?? options.redisOptions) && 'url/redisOptions',\n ].filter(Boolean);\n if (sources.length > 1) {\n throw new Error(`redis-streams: ${sources.join(', ')} are mutually exclusive; pass only one connection source`);\n }\n if (options.client) {\n if (options.client.isOpen) {\n throw new Error('redis-streams: `client` must not be connected; the pubsub owns its connection lifecycle');\n }\n return options.client as RedisClientType;\n }\n if (options.cluster) {\n return createCluster(options.cluster) as unknown as RedisClientType;\n }\n const url = options.url ?? options.redisOptions?.url ?? 'redis://localhost:6379';\n return createClient({ ...options.redisOptions, url }) as RedisClientType;\n }\n\n /**\n * A fresh, unconnected client with the writer's configuration. Each reader\n * (one per fan-out topic, one per grouped subscription) needs its own\n * because XREADGROUP BLOCK holds the connection.\n */\n #createReadClient(): RedisClientType {\n return this.#writeClient.duplicate() as RedisClientType;\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 = globalThis.crypto.randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent and safe under concurrent callers. */\n async #ensureWriterConnected(): Promise<void> {\n // node-redis flips `isOpen` true synchronously inside connect(), BEFORE the\n // socket is ready (standalone) or slot discovery has finished (cluster). So\n // while the initial connect is in flight, `isOpen` alone would let a second\n // caller through to issue commands; on a Cluster client that crashes in\n // slot lookup (\"Cannot read properties of undefined (reading 'master')\").\n // Check the in-flight promise first so every cold caller awaits the same\n // connect(). Outside that window we still gate on `isOpen` (not `isReady`)\n // so node-redis's own automatic mid-life reconnect is never fought with a\n // second connect().\n if (!this.#writerConnecting && this.#writeClient.isOpen) return;\n if (!this.#writerConnecting) {\n this.#writerConnecting = this.#writeClient\n .connect()\n .then(() => undefined)\n .finally(() => {\n // Clear so a failed initial connect can be retried on the next call.\n this.#writerConnecting = undefined;\n });\n }\n return this.#writerConnecting;\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: globalThis.crypto.randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n // Track the whole remote publish, connect included, so close() can drain\n // a publish that is still connecting and not only one already in XADD.\n const promise = this.#publishRemote(topic, event);\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async #publishRemote(topic: string, event: Omit<Event, 'id' | 'createdAt'>): Promise<void> {\n await this.#ensureWriterConnected();\n\n const id = globalThis.crypto.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 await promise;\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 const pending = this.#pendingSubscribes.get(key);\n if (pending) return pending; // idempotent: same (topic, cb) subscribe already in flight\n\n // Register synchronously so an unsubscribe/close racing this subscribe can\n // find and await it. `#doSubscribe` runs synchronously up to its first\n // await, which keeps the localOnly registration guarantee documented there.\n const promise = this.#doSubscribe(topic, cb, key, options).finally(() => {\n this.#pendingSubscribes.delete(key);\n });\n this.#pendingSubscribes.set(key, promise);\n return promise;\n }\n\n async #doSubscribe(topic: string, cb: EventCallback, key: string, options?: SubscribeOptions): Promise<void> {\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 groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\n if (options?.group) {\n const sub = await this.#openSubscription(topic, cb, options.group, groupAnchor);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n return;\n }\n\n // Fan-out: every subscriber to this topic in this instance shares one\n // consumer group and reader. Join the topic's reader if one is open (or\n // being opened); otherwise open it.\n for (;;) {\n const reader = this.#fanoutReaders.get(topic);\n if (reader) return this.#joinFanoutReader(reader, cb, key, groupAnchor);\n const opening = this.#pendingFanoutReaders.get(topic);\n if (!opening) break;\n await opening.catch(() => {});\n }\n const opening = this.#openSubscription(topic, cb, undefined, groupAnchor);\n this.#pendingFanoutReaders.set(topic, opening);\n try {\n const sub = await opening;\n // Register in the same tick the pending entry is cleared, so a\n // subscriber waiting on `opening` finds the reader and joins it.\n this.#fanoutReaders.set(topic, sub);\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n } finally {\n this.#pendingFanoutReaders.delete(topic);\n }\n }\n\n /**\n * Add a callback to a topic's open fan-out reader. The reader may already\n * have delivered entries this subscriber should see (`startFrom: 'earliest'`)\n * or still be catching up on entries it should not (`startFrom: 'latest'`),\n * so the new member buffers live entries while its starting point is\n * resolved, replays what it missed, then switches to live delivery.\n */\n async #joinFanoutReader(sub: Subscription, cb: EventCallback, key: string, anchor: '0' | '$'): Promise<void> {\n const member: FanoutMember = { joining: [] };\n sub.members.set(cb, member);\n this.#subscriptions.set(key, sub);\n const active = () => sub.members.get(cb) === member && !sub.stopped;\n\n try {\n if (anchor === '$') {\n // Skip everything already in the stream, even entries the shared\n // reader has not reached yet.\n const [tail] = await this.#writeClient.xRevRange(sub.streamKey, '+', '-', { COUNT: 1 });\n member.skipThrough = tail?.id;\n } else {\n // Entries up to the group's last-delivered id were read before this\n // member was buffering, so replay them; later ones arrive live.\n let boundary: string | undefined;\n try {\n const groups = await this.#writeClient.xInfoGroups(sub.streamKey);\n const info = groups.find(g => String(g.name) === sub.group);\n boundary = info ? String(info['last-delivered-id']) : undefined;\n } catch (err) {\n // No stream (or group) means nothing has been delivered yet.\n if (!errorText(err).match(/no such key|NOGROUP/i)) throw err;\n }\n if (boundary && boundary !== '0-0') {\n member.skipThrough = boundary;\n let start = '-';\n while (active()) {\n const page = await this.#writeClient.xRange(sub.streamKey, start, boundary, { COUNT: TRIM_PAGE_SIZE });\n for (const entry of page) {\n if (!active()) break;\n this.#deliverDetached(sub, cb, entry.message);\n }\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n }\n }\n } catch (err) {\n sub.members.delete(cb);\n this.#subscriptions.delete(key);\n throw err;\n }\n\n if (!active()) return;\n const buffered = member.joining ?? [];\n for (const entry of buffered) {\n if (member.skipThrough && compareStreamIds(entry.id, member.skipThrough) <= 0) continue;\n this.#deliverDetached(sub, cb, entry.fields);\n }\n // `skipThrough` stays set: the reader may still be dispatching a batch read\n // before this member joined (already replayed), or lagging behind a\n // `latest` member's starting point. clearTopic resets it.\n member.joining = undefined;\n }\n\n async #openSubscription(\n topic: string,\n cb: EventCallback,\n groupName: string | undefined,\n groupAnchor: '0' | '$',\n ): Promise<Subscription> {\n const isGrouped = groupName !== undefined;\n const group = groupName ?? `__fanout-${globalThis.crypto.randomUUID()}`;\n const consumer = `${group}-${globalThis.crypto.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 // Brand-new groups default to '0' (stream start) so a worker which\n // subscribes after a publish still sees the backlog. Callers can opt into\n // '$' with startFrom: 'latest' when they only want future events. Existing\n // groups (BUSYGROUP path) always keep their checkpoint. Stream growth is\n // bounded by the MAXLEN ~ trim applied on every publish.\n try {\n if (this.#streamIdleTtlMs > 0) {\n // MKSTREAM may create an empty stream key, so the TTL must be stamped\n // in the same MULTI — otherwise a topic that is subscribed to but\n // never published to (e.g. a per-run control topic) has no expiry and\n // lingers in Redis forever. When the group already exists (a sibling\n // won the race), exec() throws a MultiErrorReply whose message does\n // NOT contain \"BUSYGROUP\" — it lives in err.replies — while the\n // PEXPIRE in the same transaction still applies, so the TTL is\n // refreshed even on that path.\n await this.#writeClient\n .multi()\n .xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true })\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(streamKey, group, groupAnchor, { MKSTREAM: true });\n }\n } catch (err) {\n const msg = errorText(err);\n if (!msg.includes('BUSYGROUP')) throw err;\n this.#logger?.debug?.('redis-streams: consumer group already exists', { topic, group });\n }\n\n // Each reader (per fan-out topic, per grouped subscription) gets a\n // dedicated connection because XREADGROUP with BLOCK > 0 holds the\n // connection until a message arrives.\n const readClient = this.#createReadClient();\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n return {\n members: new Map([[cb, {}]]),\n settling: new Map(),\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n inFlight: new Map(),\n };\n }\n\n /**\n * Periodically reclaim idle pending entries in this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Fan-out groups are private to one consumer, so there is no\n * sibling to claim from and the XPENDING/XCLAIM scan is skipped for them; the\n * loop still runs so `inFlightTimeoutMs` can recover a hung handler, which is\n * the only recovery path a single-consumer group has.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped && this.#inFlightTimeoutMs <= 0) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n await this.#expireInFlight(sub);\n if (!sub.isGrouped) {\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n return;\n }\n // List idle pending entries first and only claim the ones this\n // subscription is not already processing. Claiming (XAUTOCLAIM/XCLAIM)\n // resets the entry's idle clock even when the claimant already owns it,\n // so a claim-then-skip design would let a handler running longer than\n // reclaimIdleMs re-invoke itself (pre-fix) or keep refreshing the idle\n // timer on every tick and starve a live sibling of a genuinely stalled\n // entry. Filtering before the claim leaves in-flight entries untouched:\n // if the handler settles, ack/nack removes them from the PEL; if it is\n // truly hung, the idle clock keeps running and a sibling claims them.\n //\n // The scan is paginated: locally in-flight entries stay in the PEL\n // (by design, above) and are listed on every tick, so with a single\n // fixed-size page they could fill it and hide a reclaimable entry\n // that sorts after them. Walk pages with an exclusive start id until\n // the page comes back short or the claim batch is full.\n const ids: string[] = [];\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, start, '+', RECLAIM_PAGE_SIZE, {\n IDLE: this.#reclaimIdleMs,\n });\n for (const p of page) {\n const id = String(p.id);\n if (sub.inFlight.has(id)) continue;\n ids.push(id);\n if (ids.length >= RECLAIM_PAGE_SIZE) break;\n }\n if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;\n start = `(${String(page[page.length - 1]!.id)}`;\n }\n // XCLAIM re-checks min-idle atomically, so if a sibling claimed an entry\n // between the XPENDING and here it is simply omitted from the reply.\n const claimed =\n ids.length === 0\n ? []\n : ((await this.#writeClient.xClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n ids,\n )) as Array<{ id: string; message: Record<string, string> } | null>);\n for (const entry of claimed) {\n // Null means the stream entry was trimmed. Redis >= 7.0 drops it\n // from the PEL as part of the claim, so there is nothing left to\n // deliver. (Redis 6 leaves it pending, which is why the package\n // requires 7.0+; otherwise trimmed ids would be re-listed forever.)\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('redis-streams: reclaim 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(runTick, this.#reclaimIntervalMs);\n };\n\n const runTick = () => {\n sub.reclaimLoop = tick().finally(() => {\n sub.reclaimLoop = undefined;\n });\n };\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n }\n\n /**\n * Give up on local in-flight entries older than `inFlightTimeoutMs` by\n * nacking them on the handler's behalf. This is the only way a\n * single-consumer group recovers a hung handler: the reclaim loop never\n * claims its own in-flight entries (see the tick above), so without a\n * timeout the entry stays pending until the process restarts.\n */\n async #expireInFlight(sub: Subscription): Promise<void> {\n if (this.#inFlightTimeoutMs <= 0) return;\n const cutoff = Date.now() - this.#inFlightTimeoutMs;\n for (const [streamId, entry] of sub.inFlight) {\n if (entry.since > cutoff) continue;\n this.#logger?.warn?.('redis-streams: handler exceeded inFlightTimeoutMs; nacking on its behalf', {\n topic: sub.topic,\n group: sub.group,\n streamId,\n inFlightMs: Date.now() - entry.since,\n });\n // The local marker is not proof of ownership: once the entry has been\n // idle >= reclaimIdleMs a sibling may have claimed it, or it may have\n // been acked outright. `expire` verifies ownership and settles in one\n // atomic Redis step, honours maxDeliveryAttempts, and removes the entry\n // from `inFlight` either way.\n await entry.expire();\n }\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 // A subscribe for this (topic, cb) may still be mid-flight (XGROUP CREATE +\n // reader connect). Wait for it to register so the teardown below actually\n // finds it — returning early here would orphan the subscription forever.\n const pending = this.#pendingSubscribes.get(key);\n if (pending) await pending.catch(() => {});\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n if (sub.teardown) return sub.teardown;\n\n if (sub.members.size > 1 && sub.members.delete(cb)) {\n // Other members keep the reader. Entries handed to this member no\n // longer wait on it.\n this.#subscriptions.delete(key);\n for (const settle of [...sub.settling.values()]) void settle(cb);\n return;\n }\n\n // Last member: close the reader. The member stays attached so entries an\n // already-issued read returns are still delivered to it. A subscriber\n // arriving from now on opens a fresh reader rather than join this one.\n if (this.#fanoutReaders.get(topic) === sub) this.#fanoutReaders.delete(topic);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n sub.teardown = this.#stopSubscription(sub).finally(() => {\n this.#subscriptions.delete(key);\n });\n return sub.teardown;\n }\n\n async #stopSubscription(sub: Subscription): Promise<void> {\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await disconnect(sub.readClient);\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 // Clearing the timer prevents future claims, but an issued claim still owns work.\n await sub.reclaimLoop;\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 // Redis stream IDs can restart below the last delivered ID after DEL.\n // Attached subscribers therefore recover this known-new stream from its\n // beginning instead of anchoring past newly published entries.\n for (const sub of this.#subscriptions.values()) {\n if (sub.topic === topic) {\n sub.lastId = undefined;\n sub.groupAnchor = '0';\n for (const member of sub.members.values()) member.skipThrough = undefined;\n }\n }\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 * Deletes a run's entries with `XDEL`: pages through the stream with `XRANGE`\n * and matches each entry's `runId`. With `producedBefore`, only unpinned\n * entries produced at or before it are deleted. Entries of other runs,\n * including those published by other processes, are untouched.\n */\n override async trimTopic(\n topic: string,\n { runId, producedBefore }: { runId: string; producedBefore?: number },\n ): Promise<void> {\n if (this.#closed) return;\n try {\n await this.#ensureWriterConnected();\n const streamKey = this.#streamKey(topic);\n let start = '-';\n for (;;) {\n const page = await this.#writeClient.xRange(streamKey, start, '+', { COUNT: TRIM_PAGE_SIZE });\n const ids: string[] = [];\n for (const entry of page) {\n if (!entry) continue;\n try {\n const event = JSON.parse(entry.message.event ?? '{}') as {\n runId?: string;\n data?: { producedAt?: unknown; pinned?: unknown };\n };\n if (event.runId !== runId) continue;\n if (producedBefore !== undefined) {\n const producedAt = event.data?.producedAt;\n if (typeof producedAt !== 'number' || producedAt > producedBefore || event.data?.pinned) continue;\n }\n ids.push(entry.id);\n } catch {\n // An unparseable entry can't belong to this run.\n }\n }\n if (ids.length > 0) await this.#writeClient.xDel(streamKey, ids);\n if (page.length < TRIM_PAGE_SIZE) break;\n start = `(${page[page.length - 1]!.id}`;\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: trimTopic 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 // Let in-flight subscribes finish registering before the snapshot below,\n // so a subscription that lands mid-close is torn down rather than leaked.\n if (this.#pendingSubscribes.size > 0) {\n await Promise.allSettled([...this.#pendingSubscribes.values()]);\n }\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = new Set(this.#subscriptions.values());\n await Promise.all([...subs].flatMap(sub => [...sub.members.keys()].map(cb => this.unsubscribe(sub.topic, cb))));\n this.#localCallbacks.clear();\n\n // Let publishes that were accepted before close() reach the stream. A\n // workflow's terminal event is often the last thing written before\n // shutdown; quitting the writer under it would drop the event and reject\n // the caller with a ClosingError.\n await this.flush();\n\n if (this.#writeClient.isOpen) {\n try {\n await disconnect(this.#writeClient);\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 after the last delivered stream entry so events\n // published during recovery remain visible. Before the first delivery,\n // preserve the subscription's original earliest/latest anchor.\n const recoveryAnchor = sub.lastId ?? sub.groupAnchor;\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, recoveryAnchor, { MKSTREAM: true })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xGroupCreate(sub.streamKey, sub.group, recoveryAnchor, { 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 // Entries returned by an issued read belong to us even after stop.\n sub.lastId = entry.id;\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 const event = this.#parseEvent(fields);\n if (!event) {\n this.#logger?.debug?.('redis-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\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 } finally {\n // Clear the in-flight guard only AFTER Redis has settled the entry.\n // Clearing before the xAck resolves would open a window where the\n // entry is neither in-flight nor acked, so a reclaim tick landing in\n // that window (the entry is already idle >= reclaimIdleMs for a\n // long-running handler) would redeliver and re-invoke the handler.\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\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 the reclaim loop (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 reclaim) can ack/nack it again. The entry\n // is intentionally left pending, so clear the in-flight guard now to\n // make it eligible for a future reclaim.\n settled = false;\n sub.inFlight.delete(streamId);\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 } finally {\n // Cleared only after settlement — see ack() for the reclaim-window\n // rationale.\n sub.inFlight.delete(streamId);\n }\n };\n\n // Timeout-driven nack, used by #expireInFlight. Unlike `nack` above this\n // can run while a sibling may have reclaimed the entry, so the ownership\n // check and the republish+xAck must be one atomic Redis step: a\n // read-then-nack would let a sibling XCLAIM land in between, and we'd\n // republish a duplicate and xAck the sibling's entry out from under it.\n // Either way the local marker is settled afterwards so a late ack/nack\n // from the hung handler is a no-op.\n const expire = async () => {\n if (settled) return;\n settled = true;\n sub.settling.delete(streamId);\n const attempt = event.deliveryAttempt ?? 1;\n const drop = attempt >= this.#maxDeliveryAttempts;\n const payload = drop ? '' : JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event);\n let owned: unknown;\n try {\n owned = await this.#writeClient.eval(NACK_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId, payload, String(this.#streamIdleTtlMs)],\n });\n } catch (err) {\n this.#logger?.warn?.('redis-streams: timeout nack failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Same recovery as a failed nack republish: the entry is still\n // pending, so let a future reclaim (or the next timeout pass, if the\n // handler is still hung) retry it. Fan-out readers have no reclaim\n // scan, so they keep the timeout registration for the next pass.\n settled = false;\n sub.settling.set(streamId, cb => settleMember(cb, false));\n if (sub.isGrouped) sub.inFlight.delete(streamId);\n else sub.inFlight.set(streamId, { since: Date.now(), expire });\n return;\n }\n sub.inFlight.delete(streamId);\n if (owned !== 1) {\n this.#logger?.warn?.(\n 'redis-streams: handler exceeded inFlightTimeoutMs but entry is no longer owned; disowning',\n { topic: sub.topic, group: sub.group, streamId },\n );\n return;\n }\n if (drop) {\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 }\n };\n\n // Mark this entry in-flight for the reclaim loop's benefit for exactly the\n // window between invoking the handler and the delivery settling (ack/nack).\n // Heartbeat for long handlers: reset the entry's PEL idle time so the\n // reclaim loop (ours or a sibling's) does not XCLAIM it mid-execution, and\n // push back the local inFlightTimeoutMs deadline.\n const extend = async () => {\n const entry = sub.inFlight.get(streamId);\n if (!entry || entry.expire !== expire) return;\n entry.since = Date.now();\n if (sub.isGrouped) {\n await this.#writeClient.eval(EXTEND_IF_OWNED_SCRIPT, {\n keys: [sub.streamKey],\n arguments: [sub.group, sub.consumer, streamId],\n });\n }\n };\n\n // The entry is settled in Redis once, after every member it was handed to\n // has settled: acked if all acked, nacked (one republish) if any nacked.\n // Members joining the topic buffer the entry instead; members whose\n // starting point is past it skip it.\n const waiting = new Set<EventCallback>();\n let anyNack = false;\n const settleMember = async (cb: EventCallback, nacked: boolean) => {\n if (settled || !waiting.delete(cb)) return;\n if (nacked) anyNack = true;\n if (waiting.size > 0) return;\n sub.settling.delete(streamId);\n await (anyNack ? nack() : ack());\n };\n const targets: EventCallback[] = [];\n for (const [cb, member] of sub.members) {\n if (member.joining) {\n member.joining.push({ id: streamId, fields });\n continue;\n }\n if (member.skipThrough) {\n if (compareStreamIds(streamId, member.skipThrough) <= 0) continue;\n member.skipThrough = undefined;\n }\n waiting.add(cb);\n targets.push(cb);\n }\n if (targets.length === 0) {\n await ack();\n return;\n }\n\n sub.inFlight.set(streamId, { since: Date.now(), expire });\n sub.settling.set(streamId, cb => settleMember(cb, false));\n for (const cb of targets) {\n // Each member gets its own Event object so one handler mutating it\n // can't affect another.\n const memberEvent = targets.length === 1 ? event : this.#parseEvent(fields)!;\n this.#invokeCallback(\n cb,\n memberEvent,\n () => settleMember(cb, false),\n () => settleMember(cb, true),\n extend,\n );\n }\n }\n\n /**\n * Invoke a handler without awaiting it. EventCallback is typed `=> void`\n * but handlers commonly return a promise (TS allows Promise<void> to satisfy\n * void); a rejection or synchronous throw routes to nack instead of silently\n * dropping the message. We do NOT await — serializing messages on a\n * subscription would deadlock orchestration callbacks that await their own\n * future events.\n */\n #invokeCallback(\n cb: EventCallback,\n event: Event,\n ack: () => Promise<void>,\n nack: () => Promise<void>,\n extend?: () => Promise<void>,\n ): void {\n try {\n const result: unknown = cb(event, ack, nack, extend);\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 void nack();\n }\n }\n\n #parseEvent(fields: Record<string, string>): Event | undefined {\n try {\n const 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 return event;\n } catch {\n return undefined;\n }\n }\n\n /**\n * Deliver an entry to one fan-out member outside the shared group's PEL:\n * entries the topic's reader handed out before the member joined. Ack is a\n * no-op (the group settles the entry on its own); nack republishes, which\n * redelivers to every member just as a nack on the shared entry would.\n */\n #deliverDetached(sub: Subscription, cb: EventCallback, fields: Record<string, string>): void {\n const event = this.#parseEvent(fields);\n if (!event) return;\n let settled = false;\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\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 return;\n }\n const payload = { event: JSON.stringify({ ...event, deliveryAttempt: attempt + 1 } satisfies Event) };\n try {\n if (this.#streamIdleTtlMs > 0) {\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', payload)\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', payload);\n }\n } catch (err) {\n this.#logger?.warn?.('redis-streams: nack republish failed for replayed event', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n this.#invokeCallback(\n cb,\n event,\n async () => {\n settled = true;\n },\n 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 FanoutMember {\n // Set while the member is resolving its starting point; live entries are\n // buffered here until then.\n joining?: { id: string; fields: Record<string, string> }[];\n // Entries at or before this id were already delivered to (or are before the\n // start of) this member, so the shared reader skips them for it.\n skipThrough?: string;\n}\n\n/**\n * A reader: one consumer, one connection. A grouped subscription has exactly\n * one member; a fan-out reader is shared by every fan-out subscriber to its\n * topic in this instance.\n */\ninterface Subscription {\n members: Map<EventCallback, FanoutMember>;\n // Per in-flight entry: mark a member as settled-with-ack when it leaves\n // before settling, so the entry is not stranded.\n settling: Map<string, (cb: EventCallback) => Promise<void>>;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: RedisClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n reclaimLoop?: Promise<void>;\n teardown?: Promise<void>;\n // Stream entry IDs currently being processed locally by this subscription.\n // The reclaim loop consults this to avoid re-invoking the handler for a\n // message whose original delivery is still in flight (self-redelivery).\n // `expire` is the timeout path's atomic nack-if-still-owned.\n inFlight: Map<string, { since: number; expire: () => Promise<void> }>;\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mastra/redis-streams",
3
- "version": "0.6.2-alpha.0",
3
+ "version": "0.6.3-alpha.0",
4
4
  "description": "Mastra Redis Streams PubSub integration",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -36,12 +36,12 @@
36
36
  "typescript": "^7.0.2",
37
37
  "vitest": "4.1.11",
38
38
  "zod": "^4.6.4",
39
- "@internal/ai-sdk-v5": "0.0.87",
40
- "@internal/lint": "0.0.140",
41
- "@internal/types-builder": "0.0.115",
42
- "@mastra/core": "1.75.0-alpha.8",
43
- "@mastra/deployer": "1.75.0-alpha.8",
44
- "@mastra/libsql": "1.25.1-alpha.0"
39
+ "@internal/ai-sdk-v5": "0.0.88",
40
+ "@internal/lint": "0.0.141",
41
+ "@internal/types-builder": "0.0.116",
42
+ "@mastra/core": "1.76.0-alpha.2",
43
+ "@mastra/deployer": "1.76.0-alpha.2",
44
+ "@mastra/libsql": "1.25.2-alpha.1"
45
45
  },
46
46
  "peerDependencies": {
47
47
  "@mastra/core": ">=1.0.0-0 <2.0.0-0"