UNPKG

@mastra/core

Version:

Mastra is a framework for building AI-powered applications and agents with a modern TypeScript stack.

193 lines (190 loc) • 5.53 kB
'use strict'; var chunkCVF4W47C_cjs = require('./chunk-CVF4W47C.cjs'); // src/events/caching-pubsub.ts var CachingPubSub = class extends chunkCVF4W47C_cjs.PubSub { constructor(inner, cache, options = {}) { super(); this.inner = inner; this.cache = cache; this.keyPrefix = options.keyPrefix ?? "pubsub:"; this.logger = options.logger; } inner; cache; keyPrefix; logger; /** Maps original callbacks to their wrapped versions for proper unsubscribe */ callbackMap = /* @__PURE__ */ new Map(); /** * Log an error message using the configured logger or console.error. */ logError(message, error) { if (this.logger) { this.logger.error(message, error); } else { console.error(message, error); } } /** * Get the cache key for a topic's event list */ getCacheKey(topic) { return `${this.keyPrefix}${topic}`; } /** * Get the cache key for a topic's index counter */ getCounterKey(topic) { return `${this.keyPrefix}${topic}:counter`; } /** * Publish an event to a topic. * The event is cached with a sequential index before being published to the inner PubSub. * * Uses atomic increment for index assignment to prevent race conditions * when multiple events are published concurrently. */ async publish(topic, event) { const cacheKey = this.getCacheKey(topic); const counterKey = this.getCounterKey(topic); let index = 0; let indexFailed = false; try { index = await this.cache.increment(counterKey) - 1; } catch (error) { this.logError(`[CachingPubSub] Failed to increment counter for ${topic}`, error); indexFailed = true; } const fullEvent = { ...event, id: crypto.randomUUID(), createdAt: /* @__PURE__ */ new Date(), index }; if (!indexFailed) { try { await this.cache.listPush(cacheKey, fullEvent); } catch (error) { this.logError(`[CachingPubSub] Failed to cache event for ${topic}`, error); } } await this.inner.publish(topic, fullEvent); } /** * Subscribe to live events on a topic (no replay). */ async subscribe(topic, cb, options) { await this.inner.subscribe(topic, cb, options); } /** * Subscribe to a topic with automatic replay of cached events. * * Order of operations: * 1. Subscribe to live events FIRST (to avoid missing events during replay) * 2. Fetch and replay cached history * 3. Deduplicate events at the boundary using event IDs * * Each subscriber gets its own deduplication set to ensure * multiple subscribers can independently receive all events. */ async subscribeWithReplay(topic, cb) { let seen = /* @__PURE__ */ new Set(); const wrappedCb = (event, ack) => { if (seen) { if (!seen.has(event.id)) { seen.add(event.id); cb(event, ack); } } else { cb(event, ack); } }; this.callbackMap.set(cb, wrappedCb); await this.inner.subscribe(topic, wrappedCb); const history = await this.getHistory(topic); for (const event of history) { if (!seen.has(event.id)) { seen.add(event.id); cb(event); } } seen = null; } /** * Subscribe to a topic with replay starting from a specific index. * More efficient than full replay when the client knows their last position. * * @param topic - The topic to subscribe to * @param offset - Start replaying from this index (0-based) * @param cb - Callback invoked for each event */ async subscribeFromOffset(topic, offset, cb) { let seen = /* @__PURE__ */ new Set(); const wrappedCb = (event, ack) => { if (seen) { if (!seen.has(event.id)) { seen.add(event.id); cb(event, ack); } } else { cb(event, ack); } }; this.callbackMap.set(cb, wrappedCb); await this.inner.subscribe(topic, wrappedCb); const history = await this.getHistory(topic, offset); for (const event of history) { if (!seen.has(event.id)) { seen.add(event.id); cb(event); } } seen = null; } /** * Unsubscribe from a topic. */ async unsubscribe(topic, cb) { const wrappedCb = this.callbackMap.get(cb) ?? cb; this.callbackMap.delete(cb); await this.inner.unsubscribe(topic, wrappedCb); } /** * Get historical events for a topic from cache. */ async getHistory(topic, offset = 0) { const cacheKey = this.getCacheKey(topic); const events = await this.cache.listFromTo(cacheKey, offset); return events; } /** * Flush any pending operations. */ async flush() { await this.inner.flush(); } /** * Clear cached events for a specific topic. * Call this when a stream completes to free memory. * Also clears the index counter. */ async clearTopic(topic) { const cacheKey = this.getCacheKey(topic); const counterKey = this.getCounterKey(topic); await Promise.all([this.cache.delete(cacheKey), this.cache.delete(counterKey)]); } /** * Get the inner PubSub instance. * Useful for accessing implementation-specific methods like close(). */ getInner() { return this.inner; } }; function withCaching(pubsub, cache, options) { return new CachingPubSub(pubsub, cache, options); } exports.CachingPubSub = CachingPubSub; exports.withCaching = withCaching; //# sourceMappingURL=chunk-VEYVZLLD.cjs.map //# sourceMappingURL=chunk-VEYVZLLD.cjs.map