UNPKG

backpackflow

Version:

A config-driven LLM framework built on top of PocketFlow

414 lines 13.1 kB
"use strict"; /** * Core EventStream implementation for BackpackFlow * * Provides hierarchical event streaming with namespace support. * Events follow the pattern: {namespace}:{category}:{action} */ Object.defineProperty(exports, "__esModule", { value: true }); exports.eventStreamManager = exports.EventStreamManager = exports.EventStream = void 0; exports.createEventStream = createEventStream; exports.createNamespacedStream = createNamespacedStream; const events_1 = require("events"); /** * Core EventStream class implementing hierarchical event streaming */ class EventStream { constructor(config = {}) { this.subscriptions = new Map(); this.emitter = new events_1.EventEmitter(); this.namespace = config.namespace; this.globalEvents = config.globalEvents ?? true; this.enableDebugLogs = config.enableDebugLogs ?? false; this.enableMetrics = config.enableMetrics ?? true; if (config.maxListeners) { this.emitter.setMaxListeners(config.maxListeners); } // Initialize metrics this.metrics = { totalEvents: 0, eventsByType: new Map(), eventsByNamespace: new Map(), averageListeners: 0, peakListeners: 0, uptime: 0, startTime: Date.now() }; // Setup internal listeners for metrics if (this.enableMetrics) { this.setupMetricsCollection(); } } /** * Emit an event with automatic namespace handling */ emit(eventType, data) { const baseEvent = eventType; const timestamp = Date.now(); // Ensure timestamp is set const eventData = { ...data, timestamp }; if (this.enableDebugLogs) { console.log(`🎯 Event: ${this.formatEventName(baseEvent)}`, eventData); } // Update metrics if (this.enableMetrics) { this.updateMetrics(baseEvent); } let emitted = false; // Emit namespaced event if (this.namespace) { const namespacedEvent = `${this.namespace}:${baseEvent}`; emitted = this.emitter.emit(namespacedEvent, eventData) || emitted; } // Emit global event if enabled if (this.globalEvents) { emitted = this.emitter.emit(baseEvent, eventData) || emitted; } return emitted; } /** * Subscribe to events with optional filtering */ on(eventType, listener, options) { const eventName = eventType; const wrappedListener = this.wrapListener(listener, options); this.emitter.on(eventName, wrappedListener); // Track subscription for management if (!this.subscriptions.has(eventName)) { this.subscriptions.set(eventName, new Set()); } this.subscriptions.get(eventName).add({ listener, options }); return this; } /** * Subscribe to namespaced events */ onNamespaced(eventType, listener, options) { if (!this.namespace) { throw new Error('Cannot use onNamespaced without a namespace'); } const namespacedEvent = `${this.namespace}:${eventType}`; return this.on(namespacedEvent, listener, options); } /** * Subscribe to events matching a pattern * Supports wildcards: 'tool:*', '*:error', 'namespace:*:*' */ onPattern(pattern, listener, options) { const patternRegex = this.createPatternRegex(pattern); // Listen to newListener events to catch matching patterns const patternListener = (eventName, eventListener) => { if (patternRegex.test(eventName)) { this.emitter.on(eventName, (data) => { listener(eventName, data); }); } }; this.emitter.on('newListener', patternListener); // Also check existing listeners for (const existingEvent of this.emitter.eventNames()) { const eventName = existingEvent.toString(); if (patternRegex.test(eventName)) { this.emitter.on(eventName, (data) => { listener(eventName, data); }); } } return this; } /** * One-time event listener */ once(eventType, listener) { return this.on(eventType, listener, { once: true }); } /** * Remove event listener */ off(eventType, listener) { const eventName = eventType; // Find and remove from our tracking const subscriptionSet = this.subscriptions.get(eventName); if (subscriptionSet) { for (const sub of subscriptionSet) { if (sub.listener === listener) { subscriptionSet.delete(sub); break; } } } this.emitter.off(eventName, listener); return this; } /** * Get all listeners for an event */ listeners(eventType) { return this.emitter.listeners(eventType); } /** * Remove all listeners for an event or all events */ removeAllListeners(eventType) { if (eventType) { this.subscriptions.delete(eventType); } else { this.subscriptions.clear(); } this.emitter.removeAllListeners(eventType); return this; } /** * Create a child stream with a sub-namespace */ createChildStream(childNamespace, config) { const fullNamespace = this.namespace ? `${this.namespace}:${childNamespace}` : childNamespace; return new EventStream({ namespace: fullNamespace, globalEvents: this.globalEvents, enableDebugLogs: this.enableDebugLogs, enableMetrics: this.enableMetrics, ...config }); } /** * Get event stream statistics */ getStats() { const currentTime = Date.now(); this.metrics.uptime = (currentTime - this.metrics.startTime) / 1000; const eventNames = this.emitter.eventNames(); const listenerCount = eventNames.reduce((total, event) => { return total + this.emitter.listenerCount(event); }, 0); return { listenerCount, eventNames, namespace: this.namespace, metrics: this.enableMetrics ? { ...this.metrics } : undefined }; } /** * Get current metrics */ getMetrics() { if (!this.enableMetrics) { throw new Error('Metrics are disabled. Enable with enableMetrics: true'); } const currentTime = Date.now(); return { ...this.metrics, uptime: (currentTime - this.metrics.startTime) / 1000 }; } /** * Reset metrics */ resetMetrics() { this.metrics = { totalEvents: 0, eventsByType: new Map(), eventsByNamespace: new Map(), averageListeners: 0, peakListeners: 0, uptime: 0, startTime: Date.now() }; } /** * Get or set the namespace */ getNamespace() { return this.namespace; } setNamespace(namespace) { this.namespace = namespace; } /** * Enable or disable debug logging */ setDebugLogging(enabled) { this.enableDebugLogs = enabled; } /** * Check if the stream has any listeners for an event */ hasListeners(eventType) { return this.emitter.listenerCount(eventType) > 0; } /** * Get the underlying EventEmitter (for advanced usage) */ getEmitter() { return this.emitter; } // Private helper methods setupMetricsCollection() { // Track listener changes this.emitter.on('newListener', () => { const currentListeners = this.getCurrentListenerCount(); this.metrics.peakListeners = Math.max(this.metrics.peakListeners, currentListeners); }); // Periodically update average listeners setInterval(() => { const currentListeners = this.getCurrentListenerCount(); this.metrics.averageListeners = (this.metrics.averageListeners + currentListeners) / 2; }, 10000); // Every 10 seconds } updateMetrics(eventName) { this.metrics.totalEvents++; // Update by type const count = this.metrics.eventsByType.get(eventName) || 0; this.metrics.eventsByType.set(eventName, count + 1); // Update by namespace if (this.namespace) { const nsCount = this.metrics.eventsByNamespace.get(this.namespace) || 0; this.metrics.eventsByNamespace.set(this.namespace, nsCount + 1); } } wrapListener(listener, options) { let callCount = 0; return function wrappedListener(...args) { // Check maxEvents limit if (options?.maxEvents && callCount >= options.maxEvents) { return; } callCount++; // Apply filtering if specified if (options?.filter && !this.matchesFilter(args[0], options.filter)) { return; } // Call the original listener const result = listener(args[0]); // Handle once option if (options?.once) { // Remove listener after first call // Note: This would need the original event name to properly remove } return result; }; } matchesFilter(eventData, filter) { if (filter.nodeId && eventData.nodeId !== filter.nodeId) { return false; } // Additional filter logic can be added here return true; } createPatternRegex(pattern) { // Convert glob-style pattern to regex const escapedPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '[^:]*') .replace(/\*\*/g, '.*'); return new RegExp(`^${escapedPattern}$`); } formatEventName(eventType) { return this.namespace ? `${this.namespace}:${eventType}` : eventType; } getCurrentListenerCount() { return this.emitter.eventNames().reduce((total, event) => { return total + this.emitter.listenerCount(event); }, 0); } } exports.EventStream = EventStream; /** * Factory function to create a new EventStream */ function createEventStream(config) { return new EventStream(config); } /** * Factory function to create a namespaced EventStream */ function createNamespacedStream(namespace, config) { return new EventStream({ namespace, ...config }); } /** * Utility class for managing multiple event streams */ class EventStreamManager { constructor() { this.streams = new Map(); this.globalStream = new EventStream({ globalEvents: true, enableMetrics: true }); } /** * Create or get a namespaced stream */ getStream(namespace, config) { if (!this.streams.has(namespace)) { const stream = createNamespacedStream(namespace, config); this.streams.set(namespace, stream); } return this.streams.get(namespace); } /** * Get the global stream */ getGlobalStream() { return this.globalStream; } /** * Get all managed streams */ getAllStreams() { return new Map(this.streams); } /** * Remove a stream */ removeStream(namespace) { const stream = this.streams.get(namespace); if (stream) { stream.removeAllListeners(); return this.streams.delete(namespace); } return false; } /** * Get aggregated statistics from all streams */ getAggregatedStats() { const streamStats = []; let totalListeners = 0; let totalEvents = 0; for (const [namespace, stream] of this.streams) { const stats = stream.getStats(); streamStats.push({ namespace, stats }); totalListeners += stats.listenerCount; if (stats.metrics) { totalEvents += stats.metrics.totalEvents; } } return { totalStreams: this.streams.size, totalListeners, totalEvents, streamStats }; } /** * Cleanup all streams */ cleanup() { for (const stream of this.streams.values()) { stream.removeAllListeners(); } this.streams.clear(); this.globalStream.removeAllListeners(); } } exports.EventStreamManager = EventStreamManager; // Export a singleton manager for convenience exports.eventStreamManager = new EventStreamManager(); //# sourceMappingURL=event-stream.js.map