@mastra/valkey-streams 0.5.1 → 0.5.2
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/client.d.ts +18 -10
- package/dist/client.d.ts.map +1 -1
- package/dist/index.cjs +191 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -34
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["#batch","#client","#logger","#connectOptions","#writeClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#cbIds","#ensureWriterConnected","#closed","#deliverLocal","#streamKey","#pendingPublishes","#subKey","#subscriptions","#localCallbacks","#runReadLoop","#startReclaimLoop","#deliverMessage","#leaseKey","#invokeLocalCallback"],"sources":["../src/client.ts","../src/index.ts"],"sourcesContent":["import { Batch, Decoder, GlideClient, type GlideClientConfiguration, type GlideString } from '@valkey/valkey-glide';\n\nexport type ValkeyClientOptions = Omit<Partial<GlideClientConfiguration>, 'addresses'> & {\n url?: string;\n addresses?: GlideClientConfiguration['addresses'];\n};\n\ntype StreamMessage = { id: string; message: Record<string, string> };\ntype StreamReply = { name: string; messages: StreamMessage[] };\n\nconst text = (value: unknown): string => (Buffer.isBuffer(value) ? value.toString() : String(value));\nconst messages = (value: unknown): StreamMessage[] =>\n ((value as unknown[] | null) ?? []).map(entry => {\n const [id, fields] = entry as [unknown, unknown[]];\n const message: Record<string, string> = {};\n for (let index = 0; index < fields.length; index += 2) message[text(fields[index])] = text(fields[index + 1]);\n return { id: text(id), message };\n });\n\nclass ValkeyMulti {\n readonly #batch = new Batch(true);\n constructor(private readonly getClient: () => Promise<GlideClient>) {}\n xAdd(key: string, id: string, fields: Record<string, string>, options: { TRIM?: { threshold: number } } = {}) {\n const args: GlideString[] = ['XADD', key];\n if (options.TRIM) args.push('MAXLEN', '~', String(options.TRIM.threshold));\n args.push(id, ...Object.entries(fields).flat());\n this.#batch.customCommand(args);\n return this;\n }\n pExpire(key: string, ttl: number) {\n this.#batch.customCommand(['PEXPIRE', key, String(ttl)]);\n return this;\n }\n xGroupCreate(key: string, group: string, id: string, options: { MKSTREAM?: boolean } = {}) {\n this.#batch.xgroupCreate(key, group, id, { mkStream: options.MKSTREAM });\n return this;\n }\n async exec(): Promise<unknown[]> {\n return (await (await this.getClient()).exec(this.#batch, true, { decoder: Decoder.String })) ?? [];\n }\n}\n\nexport class ValkeyStreamsClient {\n #client?: Promise<GlideClient>;\n constructor(private readonly options: ValkeyClientOptions) {}\n get isOpen() {\n return this.#client !== undefined;\n }\n on(_event: 'error', _listener: (error: unknown) => void) {\n return this;\n }\n async connect() {\n await this.getClient();\n }\n async quit() {\n (await this.getClient()).close();\n }\n multi() {\n return new ValkeyMulti(() => this.getClient());\n }\n async command(args: GlideString[]) {\n return (await this.getClient()).customCommand(args, { decoder: Decoder.String });\n }\n async xAdd(key: string, id: string, fields: Record<string, string>, options: { TRIM?: { threshold: number } } = {}) {\n const args: GlideString[] = ['XADD', key];\n if (options.TRIM) args.push('MAXLEN', '~', String(options.TRIM.threshold));\n args.push(id, ...Object.entries(fields).flat());\n return text(await this.command(args));\n }\n async pExpire(key: string, ttl: number) {\n return Number(await this.command(['PEXPIRE', key, String(ttl)]));\n }\n async xGroupCreate(key: string, group: string, id: string, options: { MKSTREAM?: boolean } = {}) {\n return (await this.getClient()).xgroupCreate(key, group, id, { mkStream: options.MKSTREAM });\n }\n async xGroupDestroy(key: string, group: string) {\n return Number(await this.command(['XGROUP', 'DESTROY', key, group]));\n }\n async xReadGroup(\n group: string,\n consumer: string,\n streams: Array<{ key: string; id: string }>,\n options: { COUNT?: number; BLOCK?: number } = {},\n ): Promise<StreamReply[] | null> {\n const result = await (\n await this.getClient()\n ).xreadgroup(group, consumer, Object.fromEntries(streams.map(stream => [stream.key, stream.id])), {\n count: options.COUNT,\n block: options.BLOCK,\n decoder: Decoder.String,\n });\n return (\n result?.map(stream => ({\n name: text(stream.key),\n messages: Object.entries(stream.value ?? {}).map(([id, fields]) => ({\n id,\n message: Object.fromEntries(fields?.map(([field, value]) => [text(field), text(value)]) ?? []),\n })),\n })) ?? null\n );\n }\n async xAutoClaim(\n key: string,\n group: string,\n consumer: string,\n minIdle: number,\n start: string,\n options: { COUNT?: number } = {},\n ) {\n const result = await (\n await this.getClient()\n ).xautoclaim(key, group, consumer, minIdle, start, { count: options.COUNT, decoder: Decoder.String });\n return {\n nextId: text(result[0]),\n messages: Object.entries(result[1] ?? {}).map(([id, fields]) => ({\n id,\n message: Object.fromEntries(fields.map(([field, value]) => [text(field), text(value)])),\n })),\n };\n }\n async xAck(key: string, group: string, id: string) {\n return (await this.getClient()).xack(key, group, [id]);\n }\n async del(key: string) {\n return Number(await this.command(['DEL', key]));\n }\n async set(key: string, value: string, options: { NX?: boolean; PX?: number } = {}) {\n const args: GlideString[] = ['SET', key, value];\n if (options.NX) args.push('NX');\n if (options.PX) args.push('PX', String(options.PX));\n const result = await this.command(args);\n return result === null ? null : text(result);\n }\n async get(key: string) {\n const result = await this.command(['GET', key]);\n return result === null ? null : text(result);\n }\n async eval(script: string, options: { keys: string[]; arguments: string[] }) {\n return Number(\n await this.command(['EVAL', script, String(options.keys.length), ...options.keys, ...options.arguments]),\n );\n }\n private getClient() {\n if (!this.#client) {\n const { url, ...configuration } = this.options;\n const parsed = url ? new URL(url) : undefined;\n this.#client = GlideClient.createClient({\n ...configuration,\n addresses: configuration.addresses ?? [\n { host: parsed?.hostname ?? 'localhost', port: Number(parsed?.port || 6379) },\n ],\n credentials:\n configuration.credentials ??\n (parsed?.password ? { username: parsed.username || 'default', password: parsed.password } : undefined),\n databaseId: configuration.databaseId ?? (parsed?.pathname ? Number(parsed.pathname.slice(1) || 0) : undefined),\n });\n }\n return this.#client;\n }\n}\n\nexport const createClient = (options: ValkeyClientOptions) => new ValkeyStreamsClient(options);\nexport type ValkeyClientType = ValkeyStreamsClient;\n","import { randomUUID } from 'node:crypto';\nimport { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from './client.js';\nimport type { ValkeyClientOptions, ValkeyClientType } from './client.js';\n\n/**\n * Flatten an error into searchable text. node-redis MULTI failures throw a\n * `MultiErrorReply` whose own message is just \"N commands failed…\" — the real\n * per-command errors (e.g. BUSYGROUP) live in `err.replies`, so those are\n * folded in too.\n */\nfunction errorText(err: unknown): string {\n const parts: string[] = [err instanceof Error ? err.message : String(err)];\n if (err instanceof Error && 'replies' in err && Array.isArray((err as { replies?: unknown[] }).replies)) {\n parts.push(...(err as { replies: unknown[] }).replies.map(r => String(r)));\n }\n return parts.join('; ');\n}\n\n/**\n * Mastra PubSub backed by Valkey Streams.\n *\n * - Each topic maps to a Valkey stream key `<prefix>:<topic>`.\n * - Subscriptions with `options.group` use a real Valkey consumer group, so\n * competing subscribers in the same group share the work (round-robin).\n * - Subscriptions without a group create a private per-subscriber consumer\n * group, so they get fan-out semantics (every subscriber sees every event).\n * - Nack triggers redelivery by re-publishing the event with an incremented\n * `deliveryAttempt` field, then XACK-ing the original. This trades strict\n * FIFO ordering on retry for a simple, reliable redelivery path.\n */\nexport interface ValkeyStreamsPubSubConfig {\n url?: string;\n keyPrefix?: string;\n blockMs?: number;\n valkeyOptions?: ValkeyClientOptions;\n /**\n * Approximate maximum number of entries kept per stream. On every publish we\n * issue MAXLEN ~ N which lets Valkey trim opportunistically. Defaults to\n * 10_000 — set to 0 to disable trimming.\n */\n maxStreamLength?: number;\n /**\n * Idle expiry (in ms): a sliding TTL refreshed on every write to the stream\n * (publish, nack retry, group re-creation). Each write resets it, so an\n * actively-used stream never expires mid-flight; a stream left idle for the\n * full duration is deleted by Valkey automatically.\n *\n * This is a BACKSTOP, not the primary cleanup. Normal cleanup is explicit:\n * `clearTopic` deletes a topic's stream the moment its lifecycle ends. This\n * option only bounds memory for streams that never reach a `clearTopic` call\n * — e.g. a run that crashed before cleanup — so they don't linger forever.\n *\n * Defaults to 0 (disabled) to preserve existing behavior.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs XAUTOCLAIM to recover messages\n * that an earlier consumer in the group read but never acked. Defaults to\n * 30_000 ms. Set to 0 to disable.\n */\n reclaimIntervalMs?: number;\n /**\n * Minimum idle time (in ms) before a pending message is eligible for\n * reclaim. Should be much larger than typical in-flight processing time to\n * avoid double-delivery. Defaults to 60_000 ms.\n */\n reclaimIdleMs?: number;\n /**\n * Maximum number of times a single event will be redelivered via nack\n * before it is dropped (acked without republish). Defaults to 5. Set to\n * `Infinity` to disable the cap (events redeliver forever on every nack).\n *\n * `0` is treated as `Infinity` with a one-time warn for back-compat;\n * prefer `Infinity` to disable the cap explicitly.\n */\n maxDeliveryAttempts?: number;\n /**\n * Optional logger for diagnostics. When omitted, suppressed errors\n * (BUSYGROUP, malformed payloads, connection-close races) are swallowed\n * silently. When provided, those paths emit `debug`/`warn` entries so\n * operators can see what's happening without noise on the happy path.\n */\n logger?: { debug?: (...args: unknown[]) => void; warn?: (...args: unknown[]) => void };\n}\n\nexport class ValkeyStreamsPubSub extends PubSub implements LeaseProvider {\n // Valkey 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?.(`valkey-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n #writeClient: ValkeyClientType;\n #connectOptions: ValkeyClientOptions;\n #keyPrefix: string;\n #blockMs: number;\n #maxStreamLength: number;\n #streamIdleTtlMs: number;\n #reclaimIntervalMs: number;\n #reclaimIdleMs: number;\n #maxDeliveryAttempts: number;\n #logger?: ValkeyStreamsPubSubConfig['logger'];\n // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to\n // multiple topics independently. Without the topic in the key,\n // unsubscribe(otherTopic, cb) would tear down the wrong subscription.\n #subscriptions: Map<string, Subscription> = new Map();\n #cbIds: WeakMap<EventCallback, string> = new WeakMap();\n #pendingPublishes: Set<Promise<unknown>> = new Set();\n // `localOnly` publishes bypass Valkey 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: ValkeyStreamsPubSubConfig = {}) {\n super();\n const url = options.url ?? options.valkeyOptions?.url ?? 'valkey://localhost:6379';\n this.#connectOptions = { ...options.valkeyOptions, url };\n this.#writeClient = createClient(this.#connectOptions) as ValkeyClientType;\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 Valkey 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(`valkey-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 'valkey-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(`valkey-streams: maxDeliveryAttempts must be >= 1 or Infinity, got ${cap}`);\n } else {\n this.#maxDeliveryAttempts = cap;\n }\n }\n\n /**\n * Attach the `'error'` listener node-redis requires on every client. Without\n * one, a mid-life socket close makes the client emit an unhandled `'error'`,\n * which (per EventEmitter semantics) throws from inside ValkeySocket'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: ValkeyClientType, 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?.(`valkey-streams: ${role} client connection error (node-redis will reconnect)`, {\n ...extra,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n\n #subKey(topic: string, cb: EventCallback): string {\n let cbId = this.#cbIds.get(cb);\n if (!cbId) {\n cbId = randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent. */\n async #ensureWriterConnected(): Promise<void> {\n if (this.#writeClient.isOpen) return;\n await this.#writeClient.connect();\n }\n\n #streamKey(topic: string): string {\n return `${this.#keyPrefix}:${topic}`;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('ValkeyStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Valkey, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n await this.#ensureWriterConnected();\n\n const id = randomUUID();\n const createdAt = new Date();\n const payload: Event = {\n ...event,\n id,\n createdAt,\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n const xaddOptions: { TRIM?: { strategy: 'MAXLEN'; strategyModifier: '~'; threshold: number } } = {};\n if (this.#maxStreamLength > 0) {\n xaddOptions.TRIM = {\n strategy: 'MAXLEN',\n strategyModifier: '~',\n threshold: this.#maxStreamLength,\n };\n }\n const streamKey = this.#streamKey(topic);\n // When a TTL is configured the write and its PEXPIRE run in one MULTI\n // transaction. A detached PEXPIRE could fail or be skipped (process exit)\n // after a successful XADD — and on the *last* write before a topic is\n // abandoned there is no \"next write\" to self-heal, leaving an immortal\n // stream despite the TTL. Atomicity closes that window.\n const promise =\n this.#streamIdleTtlMs > 0\n ? this.#writeClient\n .multi()\n .xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions)\n .pExpire(streamKey, this.#streamIdleTtlMs)\n .exec()\n .then(replies => {\n // XADD in the same transaction guarantees the key exists, so\n // PEXPIRE must reply 1; anything else means the TTL backstop is\n // not actually armed for this stream.\n if (Number(replies[1]) !== 1) {\n this.#logger?.warn?.('valkey-streams: PEXPIRE inside publish MULTI did not apply', {\n streamKey,\n reply: String(replies[1]),\n });\n }\n })\n : this.#writeClient.xAdd(streamKey, '*', { event: JSON.stringify(payload) }, xaddOptions);\n this.#pendingPublishes.add(promise);\n try {\n await promise;\n } finally {\n this.#pendingPublishes.delete(promise);\n }\n }\n\n async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n if (this.#closed) throw new Error('ValkeyStreamsPubSub: cannot subscribe on closed client');\n const key = this.#subKey(topic, cb);\n if (this.#subscriptions.has(key)) return; // idempotent: same (topic, cb) already subscribed\n\n // Register for `localOnly` delivery before wiring up the Valkey reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\n\n await this.#ensureWriterConnected();\n\n const isGrouped = !!options?.group;\n const group = options?.group ?? `__fanout-${randomUUID()}`;\n const consumer = `${group}-${randomUUID()}`;\n const streamKey = this.#streamKey(topic);\n const groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\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 Valkey 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?.('valkey-streams: consumer group already exists', { topic, group });\n }\n\n // Each subscription gets a dedicated reader connection because XREADGROUP\n // with BLOCK > 0 holds the connection until a message arrives.\n const readClient = createClient(this.#connectOptions) as ValkeyClientType;\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n const sub: Subscription = {\n cb,\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n };\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n }\n\n /**\n * Periodically run XAUTOCLAIM against this subscription's group so that\n * messages a crashed/stuck consumer read but never acked get redelivered to\n * a live sibling. Runs only for grouped subscriptions — fan-out groups are\n * private to one consumer, so there's no sibling to claim from.\n */\n #startReclaimLoop(sub: Subscription): void {\n if (this.#reclaimIntervalMs <= 0) return;\n if (!sub.isGrouped) return;\n\n const tick = async () => {\n if (sub.stopped || this.#closed) return;\n try {\n const reply = await this.#writeClient.xAutoClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n '0-0',\n { COUNT: 100 },\n );\n const messages = (reply?.messages ?? []) as Array<{ id: string; message: Record<string, string> } | null>;\n for (const entry of messages) {\n if (sub.stopped || this.#closed) return;\n if (!entry) continue;\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: XAUTOCLAIM failed', {\n topic: sub.topic,\n group: sub.group,\n err: err instanceof Error ? err.message : err,\n });\n }\n if (sub.stopped || this.#closed) return;\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n };\n\n sub.reclaimTimer = setTimeout(tick, this.#reclaimIntervalMs);\n }\n\n async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n const localBucket = this.#localCallbacks.get(topic);\n if (localBucket) {\n localBucket.delete(cb);\n if (localBucket.size === 0) this.#localCallbacks.delete(topic);\n }\n\n const key = this.#subKey(topic, cb);\n const sub = this.#subscriptions.get(key);\n if (!sub) return;\n this.#subscriptions.delete(key);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await sub.readClient.quit();\n } catch (err) {\n this.#logger?.debug?.('valkey-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?.('valkey-streams: read loop exited with error', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n // For fan-out, drop the private group entirely so the stream can be reclaimed.\n if (!sub.isGrouped) {\n try {\n await this.#writeClient.xGroupDestroy(sub.streamKey, sub.group);\n } catch (err) {\n this.#logger?.debug?.('valkey-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 Valkey 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 Valkey 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 // Valkey 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 }\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?.('valkey-streams: clearTopic failed', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n /**\n * Lease key used in Valkey. 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; Valkey serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Disconnect all clients and stop all subscription loops.\n */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = [...this.#subscriptions.values()];\n await Promise.all(subs.map(sub => this.unsubscribe(sub.topic, sub.cb)));\n this.#localCallbacks.clear();\n\n if (this.#writeClient.isOpen) {\n try {\n await this.#writeClient.quit();\n } catch (err) {\n this.#logger?.debug?.('valkey-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?.('valkey-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?.('valkey-streams: consumer group re-create failed', { topic: sub.topic, err: rmsg });\n }\n }\n } else {\n this.#logger?.debug?.('valkey-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: msg,\n });\n }\n // Pause briefly then retry (with the group recreated, if it was NOGROUP).\n await new Promise(r => setTimeout(r, 100));\n continue;\n }\n\n if (!result || result.length === 0) continue;\n\n for (const stream of result) {\n for (const entry of stream.messages) {\n if (sub.stopped) return;\n 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 let event: Event;\n try {\n event = JSON.parse(fields.event ?? '{}') as Event;\n // createdAt is serialized as a string; rehydrate.\n if (typeof event.createdAt === 'string') {\n event.createdAt = new Date(event.createdAt);\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\n err: err instanceof Error ? err.message : err,\n });\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (ackErr) {\n this.#logger?.debug?.('valkey-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?.('valkey-streams: ack cleanup failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n const nack = async () => {\n if (settled) return;\n settled = true;\n const attempt = event.deliveryAttempt ?? 1;\n // Cap redelivery to avoid an infinite poison-pill loop. When the cap\n // is hit we drop the event (xAck without republish) and warn so an\n // operator can find it in logs.\n if (attempt >= this.#maxDeliveryAttempts) {\n this.#logger?.warn?.('valkey-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?.('valkey-streams: ack on dropped poison message failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n return;\n }\n // Republish with incremented deliveryAttempt FIRST, then ack the\n // original entry. If the republish fails we deliberately leave the\n // original message pending so XAUTOCLAIM (or another consumer) can\n // pick it up on a future tick — acking first would silently drop it.\n const next: Event = {\n ...event,\n deliveryAttempt: attempt + 1,\n };\n try {\n if (this.#streamIdleTtlMs > 0) {\n // A nack republish is a write that keeps this stream in use, so it\n // must refresh the TTL too — otherwise a retry near the end of the\n // original window inherits the old expiry and the stream can be\n // deleted while the retry is still being processed. Same MULTI as\n // the write for the same reason as publish(): the republish may be\n // the stream's final write.\n await this.#writeClient\n .multi()\n .xAdd(sub.streamKey, '*', { event: JSON.stringify(next) })\n .pExpire(sub.streamKey, this.#streamIdleTtlMs)\n .exec();\n } else {\n await this.#writeClient.xAdd(sub.streamKey, '*', { event: JSON.stringify(next) });\n }\n } catch (err) {\n this.#logger?.warn?.('valkey-streams: nack republish failed; leaving original pending for reclaim', {\n topic: sub.topic,\n eventId: event.id,\n err: err instanceof Error ? err.message : err,\n });\n // Allow this entry to be redelivered: reset settled so the next\n // delivery attempt (via XAUTOCLAIM) can ack/nack it again.\n settled = false;\n return;\n }\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: xAck after nack failed', {\n topic: sub.topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n };\n\n try {\n // EventCallback is typed `=> void` but handlers commonly return a\n // promise (TS allows Promise<void> to satisfy void). If we get one\n // back, attach a catch handler so async rejections route to nack\n // instead of silently dropping the message. We do NOT await here —\n // serializing messages on a subscription would deadlock orchestration\n // callbacks that await their own future events.\n const result: unknown = sub.cb(event, ack, nack);\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(async () => {\n await nack();\n });\n }\n } catch {\n // Caller threw synchronously — treat as nack.\n await nack();\n }\n }\n\n #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('valkey-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n}\n\ninterface Subscription {\n cb: EventCallback;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: ValkeyClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["#batch","#client","#logger","#connectOptions","#writeClient","#attachErrorLogger","#keyPrefix","#blockMs","#maxStreamLength","#streamIdleTtlMs","#reclaimIntervalMs","#reclaimIdleMs","#maxDeliveryAttempts","#inFlightTimeoutMs","#cbIds","#ensureWriterConnected","#closed","#deliverLocal","#publishRemote","#pendingPublishes","#streamKey","#subKey","#subscriptions","#localCallbacks","#runReadLoop","#startReclaimLoop","#expireInFlight","#deliverMessage","#leaseKey","#invokeLocalCallback"],"sources":["../src/client.ts","../src/index.ts"],"sourcesContent":["import {\n Batch,\n Decoder,\n GlideClient,\n InfBoundary,\n type GlideClientConfiguration,\n type GlideString,\n} from '@valkey/valkey-glide';\n\nexport type ValkeyClientOptions = Omit<Partial<GlideClientConfiguration>, 'addresses'> & {\n url?: string;\n addresses?: GlideClientConfiguration['addresses'];\n};\n\ntype StreamMessage = { id: string; message: Record<string, string> };\ntype StreamReply = { name: string; messages: StreamMessage[] };\n\nconst text = (value: unknown): string => (Buffer.isBuffer(value) ? value.toString() : String(value));\nconst messages = (value: unknown): StreamMessage[] =>\n ((value as unknown[] | null) ?? []).map(entry => {\n const [id, fields] = entry as [unknown, unknown[]];\n const message: Record<string, string> = {};\n for (let index = 0; index < fields.length; index += 2) message[text(fields[index])] = text(fields[index + 1]);\n return { id: text(id), message };\n });\n\nclass ValkeyMulti {\n readonly #batch = new Batch(true);\n constructor(private readonly getClient: () => Promise<GlideClient>) {}\n xAdd(key: string, id: string, fields: Record<string, string>, options: { TRIM?: { threshold: number } } = {}) {\n const args: GlideString[] = ['XADD', key];\n if (options.TRIM) args.push('MAXLEN', '~', String(options.TRIM.threshold));\n args.push(id, ...Object.entries(fields).flat());\n this.#batch.customCommand(args);\n return this;\n }\n pExpire(key: string, ttl: number) {\n this.#batch.customCommand(['PEXPIRE', key, String(ttl)]);\n return this;\n }\n xGroupCreate(key: string, group: string, id: string, options: { MKSTREAM?: boolean } = {}) {\n this.#batch.xgroupCreate(key, group, id, { mkStream: options.MKSTREAM });\n return this;\n }\n async exec(): Promise<unknown[]> {\n return (await (await this.getClient()).exec(this.#batch, true, { decoder: Decoder.String })) ?? [];\n }\n}\n\nexport class ValkeyStreamsClient {\n #client?: Promise<GlideClient>;\n constructor(private readonly options: ValkeyClientOptions) {}\n get isOpen() {\n return this.#client !== undefined;\n }\n on(_event: 'error', _listener: (error: unknown) => void) {\n return this;\n }\n async connect() {\n await this.getClient();\n }\n async quit() {\n (await this.getClient()).close();\n }\n multi() {\n return new ValkeyMulti(() => this.getClient());\n }\n async command(args: GlideString[]) {\n return (await this.getClient()).customCommand(args, { decoder: Decoder.String });\n }\n async xAdd(key: string, id: string, fields: Record<string, string>, options: { TRIM?: { threshold: number } } = {}) {\n const args: GlideString[] = ['XADD', key];\n if (options.TRIM) args.push('MAXLEN', '~', String(options.TRIM.threshold));\n args.push(id, ...Object.entries(fields).flat());\n return text(await this.command(args));\n }\n async pExpire(key: string, ttl: number) {\n return Number(await this.command(['PEXPIRE', key, String(ttl)]));\n }\n async xGroupCreate(key: string, group: string, id: string, options: { MKSTREAM?: boolean } = {}) {\n return (await this.getClient()).xgroupCreate(key, group, id, { mkStream: options.MKSTREAM });\n }\n async xGroupDestroy(key: string, group: string) {\n return Number(await this.command(['XGROUP', 'DESTROY', key, group]));\n }\n async xReadGroup(\n group: string,\n consumer: string,\n streams: Array<{ key: string; id: string }>,\n options: { COUNT?: number; BLOCK?: number } = {},\n ): Promise<StreamReply[] | null> {\n const result = await (\n await this.getClient()\n ).xreadgroup(group, consumer, Object.fromEntries(streams.map(stream => [stream.key, stream.id])), {\n count: options.COUNT,\n block: options.BLOCK,\n decoder: Decoder.String,\n });\n return (\n result?.map(stream => ({\n name: text(stream.key),\n messages: Object.entries(stream.value ?? {}).map(([id, fields]) => ({\n id,\n message: Object.fromEntries(fields?.map(([field, value]) => [text(field), text(value)]) ?? []),\n })),\n })) ?? null\n );\n }\n /**\n * XPENDING <key> <group> [IDLE ms] <start> + <count>. `afterId` makes the\n * range exclusive so callers can page past entries they've already seen.\n */\n async xPendingRange(key: string, group: string, count: number, options: { IDLE?: number; afterId?: string } = {}) {\n const result = await (\n await this.getClient()\n ).xpendingWithOptions(key, group, {\n start: options.afterId ? { value: options.afterId, isInclusive: false } : InfBoundary.NegativeInfinity,\n end: InfBoundary.PositiveInfinity,\n count,\n minIdleTime: options.IDLE,\n });\n return result.map(([id, consumer, millisecondsSinceLastDelivery, deliveriesCounter]) => ({\n id: text(id),\n consumer: text(consumer),\n millisecondsSinceLastDelivery,\n deliveriesCounter,\n }));\n }\n async xClaim(key: string, group: string, consumer: string, minIdle: number, ids: string[]) {\n const result = await (\n await this.getClient()\n ).xclaim(key, group, consumer, minIdle, ids, { decoder: Decoder.String });\n return Object.entries(result ?? {}).map(([id, fields]) => ({\n id,\n message: Object.fromEntries(fields.map(([field, value]) => [text(field), text(value)])),\n }));\n }\n async xAck(key: string, group: string, id: string) {\n return (await this.getClient()).xack(key, group, [id]);\n }\n async del(key: string) {\n return Number(await this.command(['DEL', key]));\n }\n async set(key: string, value: string, options: { NX?: boolean; PX?: number } = {}) {\n const args: GlideString[] = ['SET', key, value];\n if (options.NX) args.push('NX');\n if (options.PX) args.push('PX', String(options.PX));\n const result = await this.command(args);\n return result === null ? null : text(result);\n }\n async get(key: string) {\n const result = await this.command(['GET', key]);\n return result === null ? null : text(result);\n }\n async eval(script: string, options: { keys: string[]; arguments: string[] }) {\n return Number(\n await this.command(['EVAL', script, String(options.keys.length), ...options.keys, ...options.arguments]),\n );\n }\n private getClient() {\n if (!this.#client) {\n const { url, ...configuration } = this.options;\n const parsed = url ? new URL(url) : undefined;\n this.#client = GlideClient.createClient({\n ...configuration,\n addresses: configuration.addresses ?? [\n { host: parsed?.hostname ?? 'localhost', port: Number(parsed?.port || 6379) },\n ],\n credentials:\n configuration.credentials ??\n (parsed?.password ? { username: parsed.username || 'default', password: parsed.password } : undefined),\n databaseId: configuration.databaseId ?? (parsed?.pathname ? Number(parsed.pathname.slice(1) || 0) : undefined),\n });\n }\n return this.#client;\n }\n}\n\nexport const createClient = (options: ValkeyClientOptions) => new ValkeyStreamsClient(options);\nexport type ValkeyClientType = ValkeyStreamsClient;\n","import { randomUUID } from 'node:crypto';\nimport { PubSub } from '@mastra/core/events';\nimport type { Event, EventCallback, LeaseProvider, PubSubDeliveryMode, SubscribeOptions } from '@mastra/core/events';\nimport { createClient } from './client.js';\nimport type { ValkeyClientOptions, ValkeyClientType } from './client.js';\n\n/** Page size for the reclaim loop's XPENDING scan and the max entries claimed per tick. */\nconst RECLAIM_PAGE_SIZE = 100;\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/**\n * Mastra PubSub backed by Valkey Streams.\n *\n * - Each topic maps to a Valkey stream key `<prefix>:<topic>`.\n * - Subscriptions with `options.group` use a real Valkey consumer group, so\n * competing subscribers in the same group share the work (round-robin).\n * - Subscriptions without a group create a private per-subscriber consumer\n * group, so they get fan-out semantics (every subscriber sees every event).\n * - Nack triggers redelivery by re-publishing the event with an incremented\n * `deliveryAttempt` field, then XACK-ing the original. This trades strict\n * FIFO ordering on retry for a simple, reliable redelivery path.\n */\nexport interface ValkeyStreamsPubSubConfig {\n url?: string;\n keyPrefix?: string;\n blockMs?: number;\n valkeyOptions?: ValkeyClientOptions;\n /**\n * Approximate maximum number of entries kept per stream. On every publish we\n * issue MAXLEN ~ N which lets Valkey trim opportunistically. Defaults to\n * 10_000 — set to 0 to disable trimming.\n */\n maxStreamLength?: number;\n /**\n * Idle expiry (in ms): a sliding TTL refreshed on every write to the stream\n * (publish, nack retry, group re-creation). Each write resets it, so an\n * actively-used stream never expires mid-flight; a stream left idle for the\n * full duration is deleted by Valkey automatically.\n *\n * This is a BACKSTOP, not the primary cleanup. Normal cleanup is explicit:\n * `clearTopic` deletes a topic's stream the moment its lifecycle ends. This\n * option only bounds memory for streams that never reach a `clearTopic` call\n * — e.g. a run that crashed before cleanup — so they don't linger forever.\n *\n * Defaults to 0 (disabled) to preserve existing behavior.\n */\n streamIdleTtlMs?: number;\n /**\n * How often (in ms) each subscription runs 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 ValkeyStreamsPubSub extends PubSub implements LeaseProvider {\n // Valkey 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?.(`valkey-streams: numeric replay offset ${offset} is unsupported; falling back to full replay`);\n }\n\n #writeClient: ValkeyClientType;\n #connectOptions: ValkeyClientOptions;\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?: ValkeyStreamsPubSubConfig['logger'];\n // Keyed by `${topic}::${cbId}` so the same callback can be subscribed to\n // multiple topics independently. Without the topic in the key,\n // unsubscribe(otherTopic, cb) would tear down the wrong subscription.\n #subscriptions: Map<string, Subscription> = new Map();\n #cbIds: WeakMap<EventCallback, string> = new WeakMap();\n #pendingPublishes: Set<Promise<unknown>> = new Set();\n // `localOnly` publishes bypass Valkey 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: ValkeyStreamsPubSubConfig = {}) {\n super();\n const url = options.url ?? options.valkeyOptions?.url ?? 'valkey://localhost:6379';\n this.#connectOptions = { ...options.valkeyOptions, url };\n this.#writeClient = createClient(this.#connectOptions) as ValkeyClientType;\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 Valkey 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(`valkey-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 'valkey-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(`valkey-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 `valkey-streams: inFlightTimeoutMs must be a non-negative number (milliseconds), got ${inFlightTimeout}`,\n );\n }\n this.#inFlightTimeoutMs = inFlightTimeout;\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 ValkeySocket'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: ValkeyClientType, 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?.(`valkey-streams: ${role} client connection error (node-redis will reconnect)`, {\n ...extra,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n\n #subKey(topic: string, cb: EventCallback): string {\n let cbId = this.#cbIds.get(cb);\n if (!cbId) {\n cbId = randomUUID();\n this.#cbIds.set(cb, cbId);\n }\n return `${topic}::${cbId}`;\n }\n\n /** Lazily connect the shared writer client. Idempotent. */\n async #ensureWriterConnected(): Promise<void> {\n if (this.#writeClient.isOpen) return;\n await this.#writeClient.connect();\n }\n\n #streamKey(topic: string): string {\n return `${this.#keyPrefix}:${topic}`;\n }\n\n async publish(\n topic: string,\n event: Omit<Event, 'id' | 'createdAt'>,\n options?: { localOnly?: boolean },\n ): Promise<void> {\n if (this.#closed) throw new Error('ValkeyStreamsPubSub: cannot publish on closed client');\n\n // `localOnly` events stay entirely within the publishing process. They are\n // never serialized through Valkey, so live values on the payload (e.g.\n // `MastraModelOutput` returned via `workflows-finish` for an evented\n // agent run) keep their prototype and methods intact.\n if (options?.localOnly) {\n const localEvent: Event = {\n ...event,\n id: randomUUID(),\n createdAt: new Date(),\n deliveryAttempt: event.deliveryAttempt ?? 1,\n };\n this.#deliverLocal(topic, localEvent);\n return;\n }\n\n // 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 = 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?.('valkey-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('ValkeyStreamsPubSub: cannot subscribe on closed client');\n const key = this.#subKey(topic, cb);\n if (this.#subscriptions.has(key)) return; // idempotent: same (topic, cb) already subscribed\n\n // Register for `localOnly` delivery before wiring up the Valkey reader so a\n // racing publisher in the same process never misses this subscriber.\n let localBucket = this.#localCallbacks.get(topic);\n if (!localBucket) {\n localBucket = new Set();\n this.#localCallbacks.set(topic, localBucket);\n }\n localBucket.add(cb);\n\n await this.#ensureWriterConnected();\n\n const isGrouped = !!options?.group;\n const group = options?.group ?? `__fanout-${randomUUID()}`;\n const consumer = `${group}-${randomUUID()}`;\n const streamKey = this.#streamKey(topic);\n const groupAnchor = options?.startFrom === 'latest' ? '$' : '0';\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 Valkey 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?.('valkey-streams: consumer group already exists', { topic, group });\n }\n\n // Each subscription gets a dedicated reader connection because XREADGROUP\n // with BLOCK > 0 holds the connection until a message arrives.\n const readClient = createClient(this.#connectOptions) as ValkeyClientType;\n this.#attachErrorLogger(readClient, 'read', { topic });\n await readClient.connect();\n\n const sub: Subscription = {\n cb,\n topic,\n streamKey,\n group,\n consumer,\n isGrouped,\n groupAnchor,\n lastId: undefined,\n readClient,\n stopped: false,\n loop: undefined,\n reclaimTimer: undefined,\n inFlight: new Map(),\n };\n this.#subscriptions.set(key, sub);\n sub.loop = this.#runReadLoop(sub);\n this.#startReclaimLoop(sub);\n }\n\n /**\n * Periodically reclaim idle pending entries in this subscription's group so\n * that messages a crashed/stuck consumer read but never acked get redelivered\n * to a live sibling. Fan-out groups are private to one consumer, so there is\n * no sibling to claim from and the XPENDING/XCLAIM scan is skipped for them;\n * the loop still runs so `inFlightTimeoutMs` can recover a hung handler,\n * which is 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 afterId: string | undefined;\n for (;;) {\n const page = await this.#writeClient.xPendingRange(sub.streamKey, sub.group, RECLAIM_PAGE_SIZE, {\n IDLE: this.#reclaimIdleMs,\n afterId,\n });\n for (const p of page) {\n if (sub.inFlight.has(p.id)) continue;\n ids.push(p.id);\n if (ids.length >= RECLAIM_PAGE_SIZE) break;\n }\n if (page.length < RECLAIM_PAGE_SIZE || ids.length >= RECLAIM_PAGE_SIZE) break;\n afterId = page[page.length - 1]!.id;\n }\n // Stop may have started while XPENDING was in flight; claiming now would\n // move entries onto a consumer that is about to go away.\n if (ids.length === 0 || sub.stopped || this.#closed) {\n if (sub.stopped || this.#closed) return;\n sub.reclaimTimer = setTimeout(runTick, this.#reclaimIntervalMs);\n return;\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 = await this.#writeClient.xClaim(\n sub.streamKey,\n sub.group,\n sub.consumer,\n this.#reclaimIdleMs,\n ids,\n );\n for (const entry of claimed) {\n // A successful claim already transferred ownership; drain its full batch\n // even if stop started meanwhile, otherwise the entries sit pending under\n // this consumer until a sibling's idle window elapses again.\n await this.#deliverMessage(sub, entry.id, entry.message);\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-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?.('valkey-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 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 const sub = this.#subscriptions.get(key);\n if (!sub) return;\n this.#subscriptions.delete(key);\n sub.stopped = true;\n if (sub.reclaimTimer) {\n clearTimeout(sub.reclaimTimer);\n sub.reclaimTimer = undefined;\n }\n\n // Cancel the in-flight blocking XREADGROUP by closing the reader.\n try {\n await sub.readClient.quit();\n } catch (err) {\n this.#logger?.debug?.('valkey-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?.('valkey-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?.('valkey-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 Valkey 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 Valkey 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 // Valkey 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 }\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?.('valkey-streams: clearTopic failed', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n\n /**\n * Lease key used in Valkey. 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; Valkey serializes them.\n */\n async acquireLease(key: string, owner: string, ttlMs: number): Promise<{ acquired: boolean; owner?: string }> {\n if (this.#closed) return { acquired: false };\n await this.#ensureWriterConnected();\n const redisKey = this.#leaseKey(key);\n const result = await this.#writeClient.set(redisKey, owner, { NX: true, PX: ttlMs });\n if (result === 'OK') return { acquired: true, owner };\n // Someone holds the key. Re-claim only if we still own it, refreshing the\n // TTL atomically: a bare GET+PEXPIRE would let us extend a *new* owner's\n // lease if the key expired and was re-acquired between the two calls.\n const script = `\n local current = redis.call(\"GET\", KEYS[1])\n if current == ARGV[1] then\n redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n return 1\n end\n return 0\n `;\n const refreshed = await this.#writeClient.eval(script, {\n keys: [redisKey],\n arguments: [owner, String(ttlMs)],\n });\n if (refreshed === 1) return { acquired: true, owner };\n return { acquired: false, owner: (await this.#writeClient.get(redisKey)) ?? undefined };\n }\n\n async getLeaseOwner(key: string): Promise<string | undefined> {\n if (this.#closed) return undefined;\n await this.#ensureWriterConnected();\n const current = await this.#writeClient.get(this.#leaseKey(key));\n return current ?? undefined;\n }\n\n /**\n * Release only if we still own it. Implemented as GET+DEL with a Lua\n * script so the check-and-delete is atomic against concurrent renewals\n * from other processes.\n */\n async releaseLease(key: string, owner: string): Promise<void> {\n if (this.#closed) return;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"DEL\", KEYS[1])\n else\n return 0\n end\n `;\n await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner],\n });\n }\n\n /**\n * Extend the TTL only if we still own the lease. Returns false if the\n * lease was lost (expired or another owner took it).\n */\n async renewLease(key: string, owner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n return redis.call(\"PEXPIRE\", KEYS[1], ARGV[2])\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [owner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Atomically hand the lease from `fromOwner` to `toOwner`, refreshing the\n * TTL to the full `ttlMs`, without the key ever going empty.\n *\n * Implemented as a single Lua script (GET == fromOwner -> SET toOwner PX)\n * so a racing process cannot win the key between a release and a re-acquire.\n * Returns false if `fromOwner` no longer holds the lease (expired or taken),\n * in which case the caller should fall back to a fresh `acquireLease`.\n */\n async transferLease(key: string, fromOwner: string, toOwner: string, ttlMs: number): Promise<boolean> {\n if (this.#closed) return false;\n await this.#ensureWriterConnected();\n const script = `\n if redis.call(\"GET\", KEYS[1]) == ARGV[1] then\n redis.call(\"SET\", KEYS[1], ARGV[2], \"PX\", ARGV[3])\n return 1\n else\n return 0\n end\n `;\n const result = await this.#writeClient.eval(script, {\n keys: [this.#leaseKey(key)],\n arguments: [fromOwner, toOwner, String(ttlMs)],\n });\n return result === 1;\n }\n\n /**\n * Disconnect all clients and stop all subscription loops.\n */\n async close(): Promise<void> {\n if (this.#closed) return;\n this.#closed = true;\n\n // Walk the actual subscriptions and pass the original topic through so\n // unsubscribe's key lookup works.\n const subs = [...this.#subscriptions.values()];\n await Promise.all(subs.map(sub => this.unsubscribe(sub.topic, sub.cb)));\n this.#localCallbacks.clear();\n\n // 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?.('valkey-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?.('valkey-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?.('valkey-streams: consumer group re-create failed', { topic: sub.topic, err: rmsg });\n }\n }\n } else {\n this.#logger?.debug?.('valkey-streams: xReadGroup failed', {\n topic: sub.topic,\n group: sub.group,\n err: msg,\n });\n }\n // Pause briefly then retry (with the group recreated, if it was NOGROUP).\n await new Promise(r => setTimeout(r, 100));\n continue;\n }\n\n if (!result || result.length === 0) continue;\n\n for (const stream of result) {\n for (const entry of stream.messages) {\n if (sub.stopped) return;\n 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 let event: Event;\n try {\n event = JSON.parse(fields.event ?? '{}') as Event;\n // createdAt is serialized as a string; rehydrate.\n if (typeof event.createdAt === 'string') {\n event.createdAt = new Date(event.createdAt);\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: malformed payload, dropping', {\n topic: sub.topic,\n streamId,\n err: err instanceof Error ? err.message : err,\n });\n try {\n await this.#writeClient.xAck(sub.streamKey, sub.group, streamId);\n } catch (ackErr) {\n this.#logger?.debug?.('valkey-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?.('valkey-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 the entry has been settled.\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?.('valkey-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?.('valkey-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?.('valkey-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?.('valkey-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 server-side 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 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?.('valkey-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.\n settled = false;\n sub.inFlight.delete(streamId);\n return;\n }\n sub.inFlight.delete(streamId);\n if (owned !== 1) {\n this.#logger?.warn?.(\n 'valkey-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?.('valkey-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 sub.inFlight.set(streamId, { since: Date.now(), expire });\n try {\n // EventCallback is typed `=> void` but handlers commonly return a\n // promise (TS allows Promise<void> to satisfy void). If we get one\n // back, attach a catch handler so async rejections route to nack\n // instead of silently dropping the message. We do NOT await here —\n // serializing messages on a subscription would deadlock orchestration\n // callbacks that await their own future events.\n const result: unknown = sub.cb(event, ack, nack);\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(async () => {\n await nack();\n });\n }\n } catch {\n // Caller threw synchronously — treat as nack.\n await nack();\n }\n }\n\n #deliverLocal(topic: string, event: Event): void {\n const callbacks = this.#localCallbacks.get(topic);\n if (!callbacks || callbacks.size === 0) return;\n for (const cb of callbacks) {\n this.#invokeLocalCallback(topic, event, cb);\n }\n }\n\n #invokeLocalCallback(topic: string, event: Event, cb: EventCallback): void {\n // `localOnly` deliveries don't have an external broker to redeliver from,\n // so ack/nack are no-ops here. The caller still gets a real Event object\n // and can branch on `deliveryAttempt` if it cares.\n const ack = async () => {};\n const nack = async () => {};\n try {\n const result: unknown = (cb as (event: Event, ack: () => Promise<void>, nack: () => Promise<void>) => unknown)(\n event,\n ack,\n nack,\n );\n if (result && typeof (result as { then?: unknown; catch?: unknown }).catch === 'function') {\n (result as Promise<unknown>).catch(err => {\n this.#logger?.debug?.('valkey-streams: local subscriber rejected', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n });\n }\n } catch (err) {\n this.#logger?.debug?.('valkey-streams: local subscriber threw', {\n topic,\n err: err instanceof Error ? err.message : err,\n });\n }\n }\n}\n\ninterface Subscription {\n cb: EventCallback;\n topic: string;\n streamKey: string;\n group: string;\n consumer: string;\n isGrouped: boolean;\n groupAnchor: '0' | '$';\n lastId: string | undefined;\n readClient: ValkeyClientType;\n stopped: boolean;\n loop: Promise<void> | undefined;\n reclaimTimer: ReturnType<typeof setTimeout> | undefined;\n reclaimLoop?: 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/valkey-streams",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Mastra Valkey 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.10",
|
|
38
38
|
"zod": "^4.4.3",
|
|
39
|
-
"@internal/ai-sdk-v5": "0.0.
|
|
40
|
-
"@internal/
|
|
41
|
-
"@
|
|
42
|
-
"@mastra/
|
|
43
|
-
"@mastra/core": "1.
|
|
44
|
-
"@
|
|
39
|
+
"@internal/ai-sdk-v5": "0.0.80",
|
|
40
|
+
"@internal/lint": "0.0.133",
|
|
41
|
+
"@internal/types-builder": "0.0.108",
|
|
42
|
+
"@mastra/deployer": "1.67.0",
|
|
43
|
+
"@mastra/core": "1.67.0",
|
|
44
|
+
"@mastra/libsql": "1.23.0"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
47
|
"@mastra/core": ">=1.0.0-0 <2.0.0-0"
|