backpackflow
Version:
A config-driven LLM framework built on top of PocketFlow
414 lines • 13.1 kB
JavaScript
"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