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