{"version":3,"file":"caching-pubsub-DBB7kAgu.cjs","names":["PubSub","isLeaseProvider"],"sources":["../src/events/caching-pubsub.ts"],"sourcesContent":["import type { MastraServerCache } from '../cache/base';\nimport type { IMastraLogger } from '../logger';\nimport { isLeaseProvider, PubSub } from './pubsub';\nimport type { LeaseProvider } from './pubsub';\nimport type { Event, EventCallback, SubscribeOptions } from './types';\n\n/**\n * Options for CachingPubSub\n */\nexport interface CachingPubSubOptions {\n  /**\n   * Optional prefix for cache keys to namespace events.\n   * Defaults to 'pubsub:'.\n   */\n  keyPrefix?: string;\n  /**\n   * Optional logger for structured logging.\n   * Falls back to console.error if not provided.\n   */\n  logger?: IMastraLogger;\n}\n\n/**\n * A PubSub decorator that adds event caching and replay capabilities.\n *\n * Wraps any PubSub implementation and uses MastraServerCache to:\n * - Cache all published events per topic\n * - Enable replay of cached events for late subscribers\n *\n * This enables resumable streams - clients can disconnect and reconnect\n * without missing events.\n *\n * ## Batching\n *\n * `CachingPubSub` is transparent to `options.batch`: `subscribe()` forwards\n * the option to the inner PubSub, and `supportsNativeBatching` mirrors the\n * inner's value. Wrapping a non-native inner with `{ batch: {...} }` results\n * in unbatched delivery — use an inner that returns\n * `supportsNativeBatching === true` (e.g. `EventEmitterPubSub`) if you need\n * batched delivery.\n *\n * @example\n * ```typescript\n * import { EventEmitterPubSub, CachingPubSub } from '@mastra/core/events';\n * import { InMemoryServerCache } from '@mastra/core/cache';\n *\n * const cache = new InMemoryServerCache();\n * const pubsub = new CachingPubSub(new EventEmitterPubSub(), cache);\n *\n * // Subscribe with replay - receives cached events first, then live\n * await pubsub.subscribeWithReplay('my-topic', (event) => {\n *   console.log(event);\n * });\n * ```\n */\nexport class CachingPubSub extends PubSub {\n  private readonly keyPrefix: string;\n  private readonly logger?: IMastraLogger;\n  /** Maps original callbacks to their wrapped versions for proper unsubscribe */\n  private callbackMap = new Map<EventCallback, EventCallback>();\n\n  constructor(\n    private readonly inner: PubSub,\n    private readonly cache: MastraServerCache,\n    options: CachingPubSubOptions = {},\n  ) {\n    super();\n    this.keyPrefix = options.keyPrefix ?? 'pubsub:';\n    this.logger = options.logger;\n  }\n\n  get supportsNativeBatching(): boolean {\n    return this.inner.supportsNativeBatching;\n  }\n\n  /**\n   * Log an error message using the configured logger or console.error.\n   */\n  private logError(message: string, error: unknown): void {\n    if (this.logger) {\n      this.logger.error(message, error);\n    } else {\n      console.error(message, error);\n    }\n  }\n\n  /**\n   * Stable key used to deduplicate an event across the cache-replay and\n   * live-delivery paths.\n   *\n   * We cannot dedup on `event.id`: `CachingPubSub.publish` assigns the id and\n   * caches the event with it, but inner PubSub implementations\n   * (EventEmitterPubSub, UnixSocketPubSub, …) regenerate `id` inside their own\n   * `publish`, so the cached copy and the live copy of the SAME publish carry\n   * different ids. The sequential `index` is assigned here and is preserved by\n   * every inner implementation, so it matches across both paths. Events without\n   * an index are never cached (see `publish`), so they can't be replay/live\n   * duplicated — falling back to `id` for them is safe.\n   */\n  private dedupKey(event: Event): string {\n    return event.index !== undefined ? `i:${event.index}` : `id:${event.id}`;\n  }\n\n  /**\n   * Get the cache key for a topic's event list\n   */\n  private getCacheKey(topic: string): string {\n    return `${this.keyPrefix}${topic}`;\n  }\n\n  /**\n   * Get the cache key for a topic's index counter\n   */\n  private getCounterKey(topic: string): string {\n    return `${this.keyPrefix}${topic}:counter`;\n  }\n\n  /**\n   * Publish an event to a topic.\n   * The event is cached with a sequential index before being published to the inner PubSub.\n   *\n   * Uses atomic increment for index assignment to prevent race conditions\n   * when multiple events are published concurrently.\n   */\n  async publish(\n    topic: string,\n    event: Omit<Event, 'id' | 'createdAt' | 'index'>,\n    options?: { localOnly?: boolean },\n  ): Promise<void> {\n    const cacheKey = this.getCacheKey(topic);\n    const counterKey = this.getCounterKey(topic);\n\n    let index: number | undefined;\n    let indexFailed = false;\n    try {\n      // Atomically get next index (increment returns value after incrementing, so subtract 1 for 0-based index)\n      index = (await this.cache.increment(counterKey)) - 1;\n    } catch (error) {\n      this.logError(`[CachingPubSub] Failed to increment counter for ${topic}`, error);\n      indexFailed = true;\n    }\n\n    // On counter failure leave `index` undefined rather than defaulting to 0:\n    // downstream consumers that key off `index` (e.g. replay-from-offset)\n    // would otherwise see colliding indices across failed publishes.\n    const fullEvent: Event = {\n      ...event,\n      id: crypto.randomUUID(),\n      createdAt: new Date(),\n      ...(index !== undefined ? { index } : {}),\n    };\n\n    if (!indexFailed) {\n      try {\n        // Cache BEFORE live publish so late-joining observers never miss events\n        await this.cache.listPush(cacheKey, fullEvent);\n      } catch (error) {\n        this.logError(`[CachingPubSub] Failed to cache event for ${topic}`, error);\n      }\n    }\n\n    // Always publish to inner PubSub — cache failure must not block live delivery\n    await this.inner.publish(topic, fullEvent, options);\n  }\n\n  /**\n   * Subscribe to live events on a topic (no replay).\n   */\n  async subscribe(topic: string, cb: EventCallback, options?: SubscribeOptions): Promise<void> {\n    await this.inner.subscribe(topic, cb, options);\n  }\n\n  /**\n   * Subscribe to a topic with automatic replay of cached events.\n   * Delegates to {@link subscribeFromOffset} with offset 0.\n   */\n  async subscribeWithReplay(topic: string, cb: EventCallback): Promise<void> {\n    return this.subscribeFromOffset(topic, 0, cb);\n  }\n\n  /**\n   * Subscribe to a topic with replay starting from a specific index.\n   * More efficient than full replay when the client knows their last position.\n   *\n   * Order of operations:\n   * 1. Subscribe to live events FIRST — buffer deliveries during bootstrap\n   * 2. Fetch and deliver cached history in order\n   * 3. Drain the buffer, skipping events already delivered via history\n   * 4. Switch to passthrough with an index watermark for steady-state dedup\n   *\n   * @param topic - The topic to subscribe to\n   * @param offset - Start replaying from this index (0-based)\n   * @param cb - Callback invoked for each event\n   */\n  async subscribeFromOffset(topic: string, offset: number, cb: EventCallback): Promise<void> {\n    // --- Phase 1: subscribe live, buffer everything during bootstrap ---\n    let bootstrapping = true;\n    const buffer: Array<{\n      event: Event;\n      ack?: Parameters<EventCallback>[1];\n      nack?: Parameters<EventCallback>[2];\n    }> = [];\n    let lastDelivered = -1;\n\n    const wrappedCb: EventCallback = (event, ack, nack) => {\n      // Drop events strictly before the requested offset on the live path.\n      if (typeof event.index === 'number' && event.index < offset) {\n        return;\n      }\n\n      if (bootstrapping) {\n        buffer.push({ event, ack, nack });\n        return;\n      }\n\n      // Steady-state: skip events we already delivered via history or buffer drain.\n      // Allow nack-redelivered messages through — they carry the same index but\n      // deliveryAttempt > 1, and the consumer must see them to retry processing.\n      const isRetry = typeof event.deliveryAttempt === 'number' && event.deliveryAttempt > 1;\n      if (typeof event.index === 'number' && event.index <= lastDelivered && !isRetry) {\n        return;\n      }\n\n      if (typeof event.index === 'number' && event.index > lastDelivered) {\n        lastDelivered = event.index;\n      }\n      cb(event, ack, nack);\n    };\n\n    this.callbackMap.set(cb, wrappedCb);\n    await this.inner.subscribe(topic, wrappedCb);\n\n    try {\n      // --- Phase 2: fetch and deliver cached history ---\n      const seen = new Set<string>();\n      const history = await this.getHistory(topic, offset);\n      for (const event of history) {\n        const key = this.dedupKey(event);\n        seen.add(key);\n        if (typeof event.index === 'number') {\n          lastDelivered = event.index;\n        }\n        cb(event);\n      }\n\n      // --- Phase 3: drain buffer, suppressing duplicates history already covered ---\n      for (const { event, ack, nack } of buffer) {\n        const key = this.dedupKey(event);\n        if (seen.has(key)) {\n          continue;\n        }\n        seen.add(key);\n        if (typeof event.index === 'number') {\n          lastDelivered = event.index;\n        }\n        cb(event, ack, nack);\n      }\n\n      // --- Phase 4: flip to passthrough ---\n      bootstrapping = false;\n      buffer.length = 0;\n    } catch (error) {\n      // Rollback: unsubscribe wrappedCb so it doesn't strand in bootstrap mode\n      this.callbackMap.delete(cb);\n      await this.inner.unsubscribe(topic, wrappedCb).catch(() => {});\n      throw error;\n    }\n  }\n\n  /**\n   * Unsubscribe from a topic.\n   */\n  async unsubscribe(topic: string, cb: EventCallback): Promise<void> {\n    const wrappedCb = this.callbackMap.get(cb) ?? cb;\n    this.callbackMap.delete(cb);\n    await this.inner.unsubscribe(topic, wrappedCb);\n  }\n\n  /**\n   * Get historical events for a topic from cache.\n   */\n  async getHistory(topic: string, offset: number = 0): Promise<Event[]> {\n    const cacheKey = this.getCacheKey(topic);\n    const events = await this.cache.listFromTo(cacheKey, offset);\n    return events as Event[];\n  }\n\n  /**\n   * Flush any pending operations on the inner PubSub.\n   */\n  async flush(): Promise<void> {\n    await this.inner.flush();\n  }\n\n  /**\n   * Expose the inner's {@link LeaseProvider} when it has one, otherwise\n   * `undefined`. Leasing is a capability of the underlying backend\n   * (e.g. Redis), not of the caching decorator itself — so rather than\n   * unconditionally declaring lease methods (which would make\n   * {@link isLeaseProvider} report `true` even when the inner can't\n   * coordinate a lock), we surface the inner's capability directly. The\n   * signals runtime unwraps this so wrapping with caching preserves real\n   * distributed lease semantics without faking them.\n   */\n  getLeaseProvider(): LeaseProvider | undefined {\n    return isLeaseProvider(this.inner) ? this.inner : undefined;\n  }\n\n  /**\n   * Clear cached events for a specific topic (and the index counter), and\n   * forward the clear to the inner transport.\n   *\n   * Call this when a stream completes to free memory. The forward matters for\n   * persistent inner transports (e.g. Redis Streams): without it, wrapping a\n   * pubsub in `CachingPubSub` silently turns `clearTopic` into a cache-only\n   * no-op and the inner stream leaks forever.\n   */\n  override async clearTopic(topic: string): Promise<void> {\n    const cacheKey = this.getCacheKey(topic);\n    const counterKey = this.getCounterKey(topic);\n    try {\n      await Promise.all([this.cache.delete(cacheKey), this.cache.delete(counterKey), this.inner.clearTopic(topic)]);\n    } catch (error) {\n      // Honor the base-class contract: clearTopic is best-effort and callers\n      // invoke it fire-and-forget, so a cache failure must not become an\n      // unhandled rejection. A failed delete means retained state may leak\n      // until the transport-level TTL backstop, so make it visible.\n      this.logError(`[CachingPubSub] Failed to clear topic ${topic}`, error);\n    }\n  }\n\n  /**\n   * Get the inner PubSub instance.\n   * Useful for accessing implementation-specific methods like close().\n   */\n  getInner(): PubSub {\n    return this.inner;\n  }\n}\n\n/**\n * Factory function to wrap a PubSub with caching capabilities.\n *\n * @example\n * ```typescript\n * import { withCaching, EventEmitterPubSub } from '@mastra/core/events';\n * import { InMemoryServerCache } from '@mastra/core/cache';\n *\n * const cache = new InMemoryServerCache();\n * const pubsub = withCaching(new EventEmitterPubSub(), cache);\n * ```\n */\nexport function withCaching(pubsub: PubSub, cache: MastraServerCache, options?: CachingPubSubOptions): CachingPubSub {\n  return new CachingPubSub(pubsub, cache, options);\n}\n"],"mappings":"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"}