UNPKG

@mastra/core

Version:

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

235 lines (230 loc) 8.02 kB
'use strict'; var EventEmitter = require('events'); function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; } var EventEmitter__default = /*#__PURE__*/_interopDefault(EventEmitter); // src/events/pubsub.ts var PubSub = class { /** * Delivery modes this PubSub implementation supports. * * Defaults to `['pull']` for backward compatibility — third-party * implementations that don't override this property are treated as * pull-mode, which preserves today's behavior. * * Implementations that deliver events without an active read loop (e.g. * EventEmitter, GCP Pub/Sub push subscriptions) should declare `'push'`. * Implementations that support both modes should declare both. */ get supportedModes() { return ["pull"]; } /** * Get historical events for a topic. * Default implementation returns empty array (no history support). * Override in implementations that support event caching. * * @param topic - The topic to get history for * @param offset - Starting index (0-based), defaults to 0 * @returns Array of events from the specified index */ getHistory(_topic, _offset) { return Promise.resolve([]); } /** * Subscribe to a topic with automatic replay of cached events. * First replays any cached history, then subscribes to live events. * Default implementation falls back to regular subscribe (no replay). * Override in implementations that support event caching. * * @param topic - The topic to subscribe to * @param cb - Callback invoked for each event (both cached and live) */ subscribeWithReplay(topic, cb) { return this.subscribe(topic, cb); } /** * Subscribe to a topic with replay starting from a specific index. * This is more efficient than full replay when the client knows their last position. * Default implementation falls back to subscribeWithReplay (full replay). * Override in implementations that support indexed event caching. * * @param topic - The topic to subscribe to * @param offset - Start replaying from this index (0-based) * @param cb - Callback invoked for each event */ subscribeFromOffset(topic, _offset, cb) { return this.subscribeWithReplay(topic, cb); } }; var EventEmitterPubSub = class extends PubSub { // EventEmitter dispatches synchronously to listeners, so it can serve both // a push consumer (no worker) and a pull-style worker that simply calls // `subscribe()` to register a listener. Both modes are advertised so the // default in-process setup keeps using OrchestrationWorker, while // genuinely push-only transports (GCP Pub/Sub push, SNS, EventBridge) // declare `['push']` only and skip the worker. get supportedModes() { return ["pull", "push"]; } emitter; // group → topic → callbacks[] groups = /* @__PURE__ */ new Map(); // "topic:group" → round-robin counter groupCounters = /* @__PURE__ */ new Map(); // "topic:group" → the single listener registered on the emitter for this group groupListeners = /* @__PURE__ */ new Map(); // Track pending nack redeliveries so flush() can wait and close() can cancel them pendingNacks = /* @__PURE__ */ new Set(); // Track delivery attempts per message id deliveryAttempts = /* @__PURE__ */ new Map(); // topic → (original callback → wrapped listener) for fan-out (non-group) subscribers. // Nested keying so the same callback registered on multiple topics keeps // a distinct wrapper per topic. fanoutWrappers = /* @__PURE__ */ new Map(); constructor(existingEmitter) { super(); this.emitter = existingEmitter ?? new EventEmitter__default.default(); } async publish(topic, event) { const id = crypto.randomUUID(); const createdAt = /* @__PURE__ */ new Date(); this.emitter.emit(topic, { ...event, id, createdAt, deliveryAttempt: 1 }); } async subscribe(topic, cb, options) { if (options?.group) { this.subscribeWithGroup(topic, cb, options.group); } else { const wrapper = (event) => { cb( event, async () => { }, async () => { } ); }; let byCb = this.fanoutWrappers.get(topic); if (!byCb) { byCb = /* @__PURE__ */ new Map(); this.fanoutWrappers.set(topic, byCb); } byCb.set(cb, wrapper); this.emitter.on(topic, wrapper); } } async unsubscribe(topic, cb) { for (const [group, topicMap] of this.groups) { const members = topicMap.get(topic); if (members) { const idx = members.indexOf(cb); if (idx !== -1) { members.splice(idx, 1); if (members.length === 0) { topicMap.delete(topic); const listenerKey = `${topic}:${group}`; const listener = this.groupListeners.get(listenerKey); if (listener) { this.emitter.off(topic, listener); this.groupListeners.delete(listenerKey); this.groupCounters.delete(listenerKey); } } if (topicMap.size === 0) { this.groups.delete(group); } return; } } } const byCb = this.fanoutWrappers.get(topic); const wrapper = byCb?.get(cb); if (wrapper && byCb) { this.emitter.off(topic, wrapper); byCb.delete(cb); if (byCb.size === 0) this.fanoutWrappers.delete(topic); } else { this.emitter.off(topic, cb); } } async flush() { if (this.pendingNacks.size > 0) { await new Promise((resolve) => { const check = () => { if (this.pendingNacks.size === 0) { resolve(); } else { setTimeout(check, 10); } }; check(); }); } } /** * Clean up all listeners during graceful shutdown. */ async close() { for (const handle of this.pendingNacks) { clearTimeout(handle); } this.pendingNacks.clear(); this.deliveryAttempts.clear(); this.emitter.removeAllListeners(); this.groups.clear(); this.groupCounters.clear(); this.groupListeners.clear(); this.fanoutWrappers.clear(); } subscribeWithGroup(topic, cb, group) { let topicMap = this.groups.get(group); if (!topicMap) { topicMap = /* @__PURE__ */ new Map(); this.groups.set(group, topicMap); } let members = topicMap.get(topic); if (!members) { members = []; topicMap.set(topic, members); } members.push(cb); const listenerKey = `${topic}:${group}`; if (!this.groupListeners.has(listenerKey)) { const listener = (event) => { this.deliverToGroup(topic, group, listenerKey, event); }; this.groupListeners.set(listenerKey, listener); this.emitter.on(topic, listener); } } deliverToGroup(topic, group, listenerKey, event) { const currentMembers = this.groups.get(group)?.get(topic); if (!currentMembers || currentMembers.length === 0) return; const counter = this.groupCounters.get(listenerKey) ?? 0; const idx = counter % currentMembers.length; this.groupCounters.set(listenerKey, counter + 1); const attemptKey = `${listenerKey}:${event.id}`; const attempt = this.deliveryAttempts.get(attemptKey) ?? 1; const eventWithAttempt = { ...event, deliveryAttempt: attempt }; const ack = async () => { this.deliveryAttempts.delete(attemptKey); }; const nack = async () => { this.deliveryAttempts.set(attemptKey, attempt + 1); const handle = setTimeout(() => { this.pendingNacks.delete(handle); this.deliverToGroup(topic, group, listenerKey, event); }, 0); this.pendingNacks.add(handle); }; currentMembers[idx](eventWithAttempt, ack, nack); } }; exports.EventEmitterPubSub = EventEmitterPubSub; exports.PubSub = PubSub; //# sourceMappingURL=chunk-CVF4W47C.cjs.map //# sourceMappingURL=chunk-CVF4W47C.cjs.map