@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
JavaScript
;
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