crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
407 lines (406 loc) • 15.7 kB
JavaScript
/**
* FlowEventBus
*
* Optimized event system for flows with minimal overhead,
* efficient event filtering, and performance monitoring.
*/
import EventEmitter from 'events';
/**
* Event priority levels
*/
export var EventPriority;
(function (EventPriority) {
EventPriority[EventPriority["LOW"] = 0] = "LOW";
EventPriority[EventPriority["NORMAL"] = 1] = "NORMAL";
EventPriority[EventPriority["HIGH"] = 2] = "HIGH";
EventPriority[EventPriority["CRITICAL"] = 3] = "CRITICAL";
})(EventPriority || (EventPriority = {}));
/**
* Manages event handling for flow system with optimized event routing
* and efficient subscription management.
*/
export class FlowEventBus {
// Underlying event emitter with modifications for performance
eventEmitter = new EventEmitter();
// Event queue for batching and prioritization
eventQueue = new Map();
processing = false;
// Batched event handling
batchedEvents = new Map();
batchTimers = new Map();
// Options
options;
// Handler storage for optimized lookups
handlerSets = new Map();
handlerFilters = new Map();
handlerPriorities = new Map();
// Performance metrics - memory-optimized implementation with all required fields
metrics = {
eventsPublished: 0,
eventsDelivered: 0,
eventTypeCounts: {},
averageProcessingTimeMs: 0,
totalProcessingTimeMs: 0,
maxProcessingTimeMs: 0,
queueOverflowCount: 0,
errorCount: 0,
activeSubscriptions: 0,
// Initialize newly added properties for type-safety
eventsProcessed: 0,
batchedEventCount: 0
};
constructor(options = {}) {
// Set default options
this.options = {
enableProfiling: true,
maxQueueSize: 1000,
defaultBatchSize: 10,
warningThresholdMs: 50,
...options
};
// Initialize queue for each priority level
for (const priority of Object.values(EventPriority)) {
if (typeof priority === 'number') {
this.eventQueue.set(priority, []);
}
}
// Increase max listeners to avoid Node.js warnings
this.eventEmitter.setMaxListeners(0);
}
/**
* Subscribe to flow events with optional filtering
* @param eventType Event type to subscribe to (can include wildcards)
* @param handler Event handler function
* @param options Subscription options
* @returns Subscription ID that can be used to unsubscribe
*/
subscribe(eventType, handler, options = {}) {
// Generate unique handler ID
const handlerId = `${eventType}:${Date.now()}:${Math.random().toString(36).substring(2, 9)}`;
// Store handler for this event type
if (!this.handlerSets.has(eventType)) {
this.handlerSets.set(eventType, new Set());
}
this.handlerSets.get(eventType).add(handlerId);
// Store filter if provided
if (options.filter) {
this.handlerFilters.set(handlerId, options.filter);
}
// Store priority threshold
this.handlerPriorities.set(handlerId, options.priorityThreshold ?? EventPriority.LOW);
// Create actual handler function with optimization features
const wrappedHandler = async (event) => {
// Skip if below priority threshold
if (event.priority < (options.priorityThreshold ?? EventPriority.LOW)) {
return;
}
// Apply filter if provided
if (options.filter && !options.filter(event)) {
return;
}
// Track metrics if profiling enabled
const startTime = this.options.enableProfiling ? performance.now() : 0;
try {
// Execute handler
await handler(event);
// Update metrics
if (this.options.enableProfiling) {
this.metrics.eventsDelivered++;
}
}
catch (error) {
// Handle errors
this.metrics.errorCount++;
if (this.options.errorHandler) {
this.options.errorHandler(error, event);
}
else {
console.error('Error in event handler:', error);
}
}
finally {
// Update processing time metrics
if (this.options.enableProfiling) {
const processingTime = performance.now() - startTime;
this.metrics.totalProcessingTimeMs += processingTime;
this.metrics.averageProcessingTimeMs =
this.metrics.totalProcessingTimeMs / this.metrics.eventsDelivered;
if (processingTime > this.metrics.maxProcessingTimeMs) {
this.metrics.maxProcessingTimeMs = processingTime;
}
// Log warning if processing took too long
if (processingTime > this.options.warningThresholdMs) {
console.warn(`Slow event handler for ${event.type}: ${processingTime.toFixed(2)}ms ` +
`(threshold: ${this.options.warningThresholdMs}ms)`);
}
}
}
};
// Add listener
this.eventEmitter.on(eventType, wrappedHandler);
this.metrics.activeSubscriptions++;
return handlerId;
}
/**
* Subscribe to multiple event types at once
* @param eventTypes Array of event types to subscribe to
* @param handler Event handler function
* @param options Subscription options
* @returns Array of subscription IDs
*/
subscribeToMany(eventTypes, handler, options = {}) {
return eventTypes.map(type => this.subscribe(type, handler, options));
}
/**
* Unsubscribe from events
* @param subscriptionId Subscription ID returned from subscribe()
*/
unsubscribe(subscriptionId) {
// Parse subscription ID to get event type
// Memory-optimized string safety implementation
// Ensure subscriptionId is always a valid string before operating on it
if (typeof subscriptionId !== 'string' || !subscriptionId) {
return false; // Invalid subscription ID
}
// Split with string safety validation
const parts = subscriptionId.split(':');
// Only proceed if we have valid parts after splitting
if (!parts || parts.length === 0) {
return false; // Cannot extract event type
}
// Extract event type with null safety
const eventType = parts[0] || '';
// Skip processing if eventType is empty
if (!eventType) {
return false;
}
// Remove from handler sets with type safety
const handlerSet = this.handlerSets.get(eventType);
if (handlerSet) {
handlerSet.delete(subscriptionId);
if (handlerSet.size === 0) {
this.handlerSets.delete(eventType);
}
}
// Remove filter and priority
this.handlerFilters.delete(subscriptionId);
this.handlerPriorities.delete(subscriptionId);
// Remove actual listener
this.eventEmitter.removeAllListeners(subscriptionId);
this.metrics.activeSubscriptions--;
return true;
}
/**
* Publish an event to the event bus
* @param event Event to publish
* @param immediate Whether to process immediately or queue
*/
async publish(event, immediate = false) {
// Set timestamp if not provided
if (!event.timestamp) {
event.timestamp = Date.now();
}
// Track metrics with memory-optimized implementation
if (this.options.enableProfiling && this.metrics) {
// Safely increment event count with null checks
this.metrics.eventsPublished = (this.metrics.eventsPublished || 0) + 1;
// Initialize event type counts if needed
if (!this.metrics.eventTypeCounts) {
this.metrics.eventTypeCounts = {};
}
// Get event type with string safety
const eventType = event.type || 'unknown';
// Initialize counter for this event type if needed
if (typeof this.metrics.eventTypeCounts[eventType] !== 'number') {
this.metrics.eventTypeCounts[eventType] = 0;
}
// Increment with type safety
this.metrics.eventTypeCounts[eventType]++;
}
if (immediate) {
// Emit immediately
this.eventEmitter.emit(event.type, event);
}
else {
// Add to priority queue with memory-optimized implementation
const priority = event.priority || EventPriority.NORMAL; // Default priority for type safety
let queue = this.eventQueue.get(priority);
// Create queue if it doesn't exist (lazy initialization for memory efficiency)
if (!queue) {
queue = [];
this.eventQueue.set(priority, queue);
}
// Add event to queue with type checking for memory safety
queue.push(event);
// Check for queue overflow with null safety
const maxSize = this.options.maxQueueSize || 1000; // Provide default if undefined
if (queue.length > maxSize && this.metrics) {
// Safely increment with null check
this.metrics.queueOverflowCount = (this.metrics.queueOverflowCount || 0) + 1;
queue.splice(0, queue.length - maxSize);
}
// Process queue if not already processing and method exists
if (!this.processing) {
this.processEventQueue();
}
}
}
/**
* Publish multiple events at once
* @param events Events to publish
* @param options Publishing options
*/
async publishMany(events, options = {}) {
for (const event of events) {
await this.publish(event, options.immediate);
}
}
/**
* Publish events in batched mode for better performance
* @param eventType Event type for batching
* @param event Event to add to batch
* @param batchId Batch identifier (default is event type)
* @param flushTimeMs Time in milliseconds before automatically flushing batch
*/
publishBatched(eventType, event, batchId = eventType, flushTimeMs = 100) {
// Initialize batch for this type if needed
if (!this.batchedEvents.has(eventType)) {
this.batchedEvents.set(eventType, new Map());
}
// Initialize batch for this ID if needed
const typeBatches = this.batchedEvents.get(eventType);
if (!typeBatches.has(batchId)) {
typeBatches.set(batchId, []);
}
// Add event to batch
typeBatches.get(batchId).push(event);
// Set or reset timer for this batch
if (this.batchTimers.has(batchId)) {
clearTimeout(this.batchTimers.get(batchId));
}
this.batchTimers.set(batchId, setTimeout(() => {
this.flushBatch(eventType, batchId);
}, flushTimeMs));
}
/**
* Flush a batched event immediately
* @param eventType Event type to flush
* @param batchId Batch identifier (default is event type)
*/
flushBatch(eventType, batchId = eventType) {
// Get batches for this type
const typeBatches = this.batchedEvents.get(eventType);
if (!typeBatches)
return;
// Get batch for this ID
const batch = typeBatches.get(batchId);
if (!batch || batch.length === 0)
return;
// Clear batch
typeBatches.delete(batchId);
// Clear timer
if (this.batchTimers.has(batchId)) {
clearTimeout(this.batchTimers.get(batchId));
this.batchTimers.delete(batchId);
}
// Create batch event with all events in the batch
const batchEvent = {
type: `${eventType}:batch`,
timestamp: Date.now(),
priority: EventPriority.NORMAL,
metadata: {
batchId,
eventCount: batch.length,
events: batch
}
};
// Publish batch event
this.publish(batchEvent, true);
}
/**
* Process events in the queue based on priority
*/
async processEventQueue() {
this.processing = true;
try {
// Process events in priority order
for (let priority = EventPriority.CRITICAL; priority >= EventPriority.LOW; priority--) {
const queue = this.eventQueue.get(priority);
if (!queue || queue.length === 0)
continue;
// Process a batch of events (default 10)
const batchSize = Math.min(queue.length, this.options.defaultBatchSize);
const batch = queue.splice(0, batchSize);
// Process each event in the batch
for (const event of batch) {
this.eventEmitter.emit(event.type, event);
}
}
// Check if more events to process
const moreEvents = Array.from(this.eventQueue.values()).some(q => q.length > 0);
if (moreEvents) {
// Continue processing in next tick
setImmediate(() => this.processEventQueue());
}
else {
this.processing = false;
}
}
catch (error) {
console.error('Error processing event queue:', error);
this.processing = false;
}
}
/**
* Get performance metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Reset all metrics
*/
resetMetrics() {
// Create memory-optimized metrics object with all required fields initialized
this.metrics = {
eventsPublished: 0,
eventsDelivered: 0,
eventTypeCounts: {},
averageProcessingTimeMs: 0,
totalProcessingTimeMs: 0,
maxProcessingTimeMs: 0,
queueOverflowCount: 0,
errorCount: 0,
// Preserve active subscriptions count when resetting other metrics
activeSubscriptions: this.metrics?.activeSubscriptions || 0,
// Initialize newly added properties
eventsProcessed: 0,
batchedEventCount: 0
};
}
/**
* Remove all event listeners
*/
removeAllListeners() {
this.eventEmitter.removeAllListeners();
this.handlerSets.clear();
this.handlerFilters.clear();
this.handlerPriorities.clear();
// Clear batched events
this.batchedEvents.clear();
for (const timer of this.batchTimers.values()) {
clearTimeout(timer);
}
this.batchTimers.clear();
// Reset metrics with memory optimization
if (this.metrics) {
// Initialize metrics object if needed
this.metrics.activeSubscriptions = 0;
this.metrics.eventsPublished = 0;
this.metrics.eventsProcessed = 0;
this.metrics.queueOverflowCount = 0;
this.metrics.eventTypeCounts = {};
this.metrics.batchedEventCount = 0;
}
}
}