UNPKG

@mastra/core

Version:
672 lines (671 loc) 21.1 kB
const require_rolldown_runtime = require("./rolldown-runtime-uwYp4b74.cjs"); let events = require("events"); events = require_rolldown_runtime.__toESM(events, 1); //#region src/events/pubsub.ts var PubSub = class { /** * Delete all retained state for a topic (cached history, persistent stream * entries, consumer groups) once no more events will be published to it. * * Called by run lifecycles (durable agents, the evented workflow engine) * when a run reaches a terminal state, so per-run topics don't accumulate * forever on transports that retain messages (e.g. Redis Streams). * * Default implementation is a no-op: transports that don't retain anything * per topic (e.g. plain EventEmitter delivery) have nothing to clear. * * Best-effort contract: implementations should not throw — callers invoke * this fire-and-forget at cleanup boundaries, so failures should be logged * by the implementation rather than rejected. * * @param topic - The topic whose retained state should be deleted */ clearTopic(_topic) { return Promise.resolve(); } /** * 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"]; } /** * Whether this implementation honors `options.batch` on `subscribe()` * natively. Defaults to `false`. * * Implementations that integrate batching internally (e.g. against their * own broker retention or via an `AckHandleBuffer`) override this getter * and return `true`. */ get supportsNativeBatching() { return false; } /** * 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); } }; /** * Duck-typed check for whether a value implements {@link LeaseProvider}. * * Uses structural detection rather than `instanceof` so it works across * package boundaries (e.g. a separately-published pubsub backend resolving * a different copy of `@mastra/core`). */ function isLeaseProvider(value) { if (!value || typeof value !== "object" && typeof value !== "function") return false; const candidate = value; return typeof candidate.acquireLease === "function" && typeof candidate.getLeaseOwner === "function" && typeof candidate.releaseLease === "function" && typeof candidate.renewLease === "function" && typeof candidate.transferLease === "function"; } /** * Always-win / no-op {@link LeaseProvider}. Used by the signals runtime * when the configured pubsub does not implement `LeaseProvider` — this * preserves single-process behavior where every caller "wins" its own * lease race and release/renew are inert. */ const NoopLeaseProvider = { acquireLease(_key, owner, _ttlMs) { return Promise.resolve({ acquired: true, owner }); }, getLeaseOwner(_key) { return Promise.resolve(void 0); }, releaseLease(_key, _owner) { return Promise.resolve(); }, renewLease(_key, _owner, _ttlMs) { return Promise.resolve(true); }, transferLease(_key, _fromOwner, _toOwner, _ttlMs) { return Promise.resolve(true); } }; //#endregion //#region src/events/event-emitter/batch-policy.ts const defaultDeps = { now: () => Date.now(), setTimeout: (cb, ms) => setTimeout(cb, ms), clearTimeout: (handle) => clearTimeout(handle) }; /** * Internal to `EventEmitterPubSub`. Embedded by `AckHandleBuffer` to decide * when a batched subscription should flush (size, time, coalesce, overflow). * * Not part of the public API — users configure batching via * `SubscribeBatchOptions` on `subscribe`. */ var BatchPolicy = class { opts; deps; maxBufferSize; overflow; firstQueuedAt = null; lastDeliveredAt = -Infinity; size = 0; timer = null; flushHandler = null; constructor(opts, deps = defaultDeps) { this.opts = opts; this.deps = deps; this.maxBufferSize = opts.maxBufferSize ?? 256; this.overflow = opts.overflow ?? "coalesce-or-drop-oldest"; } /** Bind the function invoked when the deadline timer fires. */ bindFlushHandler(fn) { this.flushHandler = fn; } /** * Called by the integrator each time an event is enqueued. * Returns whether the integrator should flush immediately. */ onEnqueue(event) { this.size += 1; if (this.firstQueuedAt === null) this.firstQueuedAt = this.deps.now(); const now = this.deps.now(); const intervalFloor = this.lastDeliveredAt + (this.opts.minIntervalMs ?? 0); if (this.opts.isImmediate?.(event)) { if (now >= intervalFloor) return "flush-now"; this.scheduleAt(intervalFloor); return "wait"; } if (this.size >= this.maxBufferSize) return "flush-now"; if (this.opts.maxSize !== void 0 && this.size >= this.opts.maxSize) { if (now >= intervalFloor) return "flush-now"; this.scheduleAt(intervalFloor); return "wait"; } this.scheduleDeadline(); return "wait"; } /** * Called by the integrator after a successful flush has delivered * `deliveredCount` events. Resets timer + firstQueuedAt. */ onFlushed(deliveredCount) { this.lastDeliveredAt = this.deps.now(); this.size = Math.max(0, this.size - deliveredCount); this.firstQueuedAt = null; this.cancelTimer(); } /** * Pure helper. Given the caller-owned queue contents, applies `coalesce` * and `overflow` to decide what to deliver and what to drop. * Order-preserving for kept events. */ prepareBatch(events) { let working = events; if (this.opts.coalesce) { const coalesced = this.opts.coalesce(working); const inputRefs = new Set(working); working = coalesced.every((e) => inputRefs.has(e)) ? coalesced : []; } const keptRefs = new Set(working); const computeDropped = () => events.filter((e) => !keptRefs.has(e)); if (working.length <= this.maxBufferSize) return { delivered: working, dropped: computeDropped() }; const overBy = working.length - this.maxBufferSize; const isImmediate = this.opts.isImmediate; let kept; let droppedFromOverflow; switch (this.overflow) { case "drop-newest": { const splitFromEnd = this.takeWithoutDropping(working, overBy, isImmediate, true); kept = splitFromEnd.kept; droppedFromOverflow = splitFromEnd.dropped; break; } default: { const splitFromStart = this.takeWithoutDropping(working, overBy, isImmediate, false); kept = splitFromStart.kept; droppedFromOverflow = splitFromStart.dropped; break; } } return { delivered: kept, dropped: [...computeDropped(), ...droppedFromOverflow] }; } /** Stop the timer and clear policy state. */ dispose() { this.cancelTimer(); this.flushHandler = null; this.firstQueuedAt = null; this.size = 0; } scheduleDeadline() { if (this.opts.maxWaitMs === void 0 && this.opts.minIntervalMs === void 0) return; const firstQueuedAt = this.firstQueuedAt ?? this.deps.now(); const deadline = this.opts.maxWaitMs !== void 0 ? firstQueuedAt + this.opts.maxWaitMs : Number.POSITIVE_INFINITY; const floor = this.lastDeliveredAt + (this.opts.minIntervalMs ?? 0); const at = Math.max(deadline, floor); this.scheduleAt(at); } scheduleAt(at) { if (!isFinite(at)) return; this.cancelTimer(); const delay = Math.max(0, at - this.deps.now()); this.timer = this.deps.setTimeout(() => { this.timer = null; const handler = this.flushHandler; if (handler) handler(); }, delay); } cancelTimer() { if (this.timer !== null) { this.deps.clearTimeout(this.timer); this.timer = null; } } /** * Drop `count` non-immediate items from the start (or end) of `items`. * Immediate items are never dropped — if every candidate is immediate, * fewer than `count` items are dropped. */ takeWithoutDropping(items, count, isImmediate, fromEnd) { const dropped = []; const result = [...items]; let remaining = count; const order = fromEnd ? [...result.keys()].reverse() : [...result.keys()]; for (const idx of order) { if (remaining === 0) break; const ev = result[idx]; if (isImmediate?.(ev)) continue; dropped.push(ev); result[idx] = void 0; remaining -= 1; } return { kept: result.filter((e) => e !== void 0), dropped }; } }; //#endregion //#region src/events/event-emitter/ack-handle-buffer.ts /** * In-process queue used by `EventEmitterPubSub` to turn its * one-event-per-emit stream into batched callback invocations. * Owns a `BatchPolicy` that decides when to flush (size, time, * quiet-period) and holds (event, ack, nack) triples in publish * order until that decision fires. * * Extracted from `EventEmitterPubSub` only so the batching state * machine can be tested in isolation. Not a public extension point. * State is per-process; the queue dies with the process. */ var AckHandleBuffer = class { cb; onError; policy; queue = []; flushing = false; reflush = false; disposed = false; constructor(cb, opts, deps, onError) { this.cb = cb; this.onError = onError; this.policy = new BatchPolicy(opts, deps); this.policy.bindFlushHandler(() => this.flush().catch((err) => { this.onError?.(err, { phase: "cb" }); })); } /** * Called by the adapter for each event arriving from the underlying transport. */ async push(event, ack, nack) { if (this.disposed) return; this.queue.push({ event, ack, nack }); if (this.policy.onEnqueue(event) === "flush-now") await this.flush(); } /** * Drain the current queue regardless of policy state. Safe to call from * adapter `flush()` or external code that wants to force delivery. */ async flush() { if (this.flushing) { this.reflush = true; return; } if (this.queue.length === 0) return; this.flushing = true; try { do { this.reflush = false; if (this.queue.length === 0) break; const snapshot = this.queue; this.queue = []; const events = snapshot.map((e) => e.event); const byEvent = /* @__PURE__ */ new Map(); for (const e of snapshot) byEvent.set(e.event, e); const { delivered, dropped } = this.policy.prepareBatch(events); for (const ev of dropped) { const entry = byEvent.get(ev); if (entry?.ack) try { await entry.ack(); } catch (err) { this.onError?.(err, { phase: "ack-dropped" }); } } for (const ev of delivered) { if (this.disposed) break; const entry = byEvent.get(ev); try { await this.cb(ev, entry?.ack, entry?.nack); } catch (err) { this.onError?.(err, { phase: "cb" }); } } this.policy.onFlushed(delivered.length + dropped.length); } while (this.reflush && !this.disposed); } finally { this.flushing = false; } } dispose() { this.disposed = true; this.queue = []; this.policy.dispose(); } }; //#endregion //#region src/events/event-emitter/index.ts const NOOP_ACK = async () => {}; var EventEmitterPubSub = class extends PubSub { get supportedModes() { return ["pull", "push"]; } /** * `EventEmitterPubSub` is strictly in-process, so the `AckHandleBuffer` * queue it uses for batching shares the same lifetime as everything * else here. Nothing more durable is promised, and nothing less is * needed. */ get supportsNativeBatching() { return true; } emitter; groups = /* @__PURE__ */ new Map(); groupCounters = /* @__PURE__ */ new Map(); groupListeners = /* @__PURE__ */ new Map(); pendingNacks = /* @__PURE__ */ new Set(); deliveryAttempts = /* @__PURE__ */ new Map(); fanoutWrappers = /* @__PURE__ */ new Map(); batchBuffers = /* @__PURE__ */ new Map(); logger; constructor(existingEmitter, options = {}) { super(); this.emitter = existingEmitter ?? new events.default(); this.logger = options.logger; } /** * Debug-hostile silent failures are the default for emitter listeners. * Surface buffer-side errors on a single channel so they're at least visible. */ logBufferError(topic, err, ctx) { const message = `[EventEmitterPubSub] batched ${ctx.phase} failed for ${topic}`; if (this.logger) this.logger.error(message, err); else console.error(message, err); } async publish(topic, event, _options) { 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?.batch) { const buffer = new AckHandleBuffer(cb, options.batch, void 0, (err, ctx) => { this.logBufferError(topic, err, ctx); }); let byCb = this.batchBuffers.get(topic); if (!byCb) { byCb = /* @__PURE__ */ new Map(); this.batchBuffers.set(topic, byCb); } byCb.set(cb, buffer); if (options.group) this.subscribeWithGroup(topic, cb, options.group); else { const wrapper = (event) => { buffer.push(event, NOOP_ACK, NOOP_ACK).catch((err) => { this.logBufferError(topic, err, { phase: "cb" }); }); }; let byCbFanout = this.fanoutWrappers.get(topic); if (!byCbFanout) { byCbFanout = /* @__PURE__ */ new Map(); this.fanoutWrappers.set(topic, byCbFanout); } byCbFanout.set(cb, wrapper); this.emitter.on(topic, wrapper); } return; } if (options?.group) this.subscribeWithGroup(topic, cb, options.group); else { const wrapper = (event) => { cb(event, NOOP_ACK, NOOP_ACK); }; 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) { const byCbBuffers = this.batchBuffers.get(topic); const buffer = byCbBuffers?.get(cb); if (buffer && byCbBuffers) { buffer.dispose(); byCbBuffers.delete(cb); if (byCbBuffers.size === 0) this.batchBuffers.delete(topic); } 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() { while (true) { const drains = []; for (const [topic, byCb] of this.batchBuffers.entries()) for (const buffer of byCb.values()) drains.push({ topic, promise: buffer.flush() }); if (drains.length > 0) { const results = await Promise.allSettled(drains.map((d) => d.promise)); for (let i = 0; i < results.length; i++) { const result = results[i]; if (result.status === "rejected") this.logBufferError(drains[i].topic, result.reason, { phase: "cb" }); } } if (this.pendingNacks.size === 0) return; 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(); for (const byCb of this.batchBuffers.values()) for (const buffer of byCb.values()) buffer.dispose(); this.batchBuffers.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); }; const member = currentMembers[idx]; const buffer = this.batchBuffers.get(topic)?.get(member); if (buffer) buffer.push(eventWithAttempt, ack, nack).catch((err) => { this.logBufferError(topic, err, { phase: "cb" }); }); else member(eventWithAttempt, ack, nack); } leases = /* @__PURE__ */ new Map(); acquireLease(key, owner, ttlMs) { const now = Date.now(); const existing = this.leases.get(key); if (existing && existing.expiresAt > now && existing.owner !== owner) return Promise.resolve({ acquired: false, owner: existing.owner }); this.leases.set(key, { owner, expiresAt: now + ttlMs }); return Promise.resolve({ acquired: true, owner }); } getLeaseOwner(key) { const existing = this.leases.get(key); if (!existing) return Promise.resolve(void 0); if (existing.expiresAt <= Date.now()) { this.leases.delete(key); return Promise.resolve(void 0); } return Promise.resolve(existing.owner); } releaseLease(key, owner) { const existing = this.leases.get(key); if (existing && existing.owner === owner) this.leases.delete(key); return Promise.resolve(); } renewLease(key, owner, ttlMs) { const existing = this.leases.get(key); if (!existing || existing.owner !== owner || existing.expiresAt <= Date.now()) return Promise.resolve(false); existing.expiresAt = Date.now() + ttlMs; return Promise.resolve(true); } transferLease(key, fromOwner, toOwner, ttlMs) { const existing = this.leases.get(key); if (!existing || existing.owner !== fromOwner || existing.expiresAt <= Date.now()) return Promise.resolve(false); this.leases.set(key, { owner: toOwner, expiresAt: Date.now() + ttlMs }); return Promise.resolve(true); } }; //#endregion Object.defineProperty(exports, "EventEmitterPubSub", { enumerable: true, get: function() { return EventEmitterPubSub; } }); Object.defineProperty(exports, "NoopLeaseProvider", { enumerable: true, get: function() { return NoopLeaseProvider; } }); Object.defineProperty(exports, "PubSub", { enumerable: true, get: function() { return PubSub; } }); Object.defineProperty(exports, "isLeaseProvider", { enumerable: true, get: function() { return isLeaseProvider; } }); //# sourceMappingURL=event-emitter-80LrFIBS.cjs.map