UNPKG

@sailboat-computer/event-bus

Version:

Standardized event bus for sailboat computer v3 with resilience features and offline capabilities

301 lines (246 loc) 7.61 kB
/** * In-memory adapter for the event bus */ import { v4 as uuidv4 } from 'uuid'; import { EventEnvelope, MemoryAdapterConfig, EventHandler } from '../types'; import { BaseAdapter } from './base-adapter'; import { logger } from '../utils'; /** * In-memory event storage */ interface EventStorage { /** * Event data */ event: EventEnvelope; /** * Timestamp when the event was created */ timestamp: number; /** * Whether the event has been acknowledged */ acknowledged: boolean; } /** * In-memory adapter for the event bus */ export class MemoryAdapter extends BaseAdapter { /** * Events by type */ private events = new Map<string, Map<string, EventStorage>>(); /** * Event TTL in milliseconds */ private eventTtl: number = 3600000; // Default: 1 hour /** * Whether to simulate latency */ private simulateLatency: boolean = false; /** * Latency range in milliseconds [min, max] */ private latencyRange: [number, number] = [10, 50]; /** * Cleanup interval */ private cleanupInterval: NodeJS.Timeout | null = null; /** * Initialize the adapter * * @param config - Adapter configuration */ override async initialize(config: MemoryAdapterConfig): Promise<void> { await super.initialize(config); this.eventTtl = config.eventTtl ?? 3600000; // Default: 1 hour this.simulateLatency = config.simulateLatency ?? false; this.latencyRange = config.latencyRange ?? [10, 50]; // Start cleanup interval if (this.eventTtl > 0) { this.cleanupInterval = setInterval(() => this.cleanup(), Math.min(this.eventTtl / 2, 60000)); } this.connected = true; logger.info(`Memory adapter initialized for service ${this.config.serviceName}`); } /** * Shutdown the adapter */ override async shutdown(): Promise<void> { await super.shutdown(); // Clear cleanup interval if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } // Clear events this.events.clear(); this.connected = false; logger.info(`Memory adapter shut down for service ${this.config.serviceName}`); } /** * Publish an event * * @param event - Event envelope * @returns Event ID */ override async publish(event: EventEnvelope): Promise<string> { this.ensureInitialized(); // Generate event ID if not provided const eventId = event.id || uuidv4(); // Create event with ID const eventWithId = { ...event, id: eventId }; // Create events map for this event type if it doesn't exist if (!this.events.has(event.type)) { this.events.set(event.type, new Map()); } // Store event this.events.get(event.type)!.set(eventId, { event: eventWithId, timestamp: Date.now(), acknowledged: false }); // Process event asynchronously this.processEvent(eventWithId); logger.debug(`Published event ${eventId} of type ${event.type}`); // Simulate latency if enabled if (this.simulateLatency) { await this.simulateNetworkLatency(); } return eventId; } /** * Acknowledge an event * * @param eventId - Event ID * @param eventType - Event type */ override async acknowledgeEvent(eventId: string, eventType: string): Promise<void> { this.ensureInitialized(); // Get events map for this event type const eventTypeMap = this.events.get(eventType); if (!eventTypeMap) { return; } // Get event const eventStorage = eventTypeMap.get(eventId); if (!eventStorage) { return; } // Mark as acknowledged eventStorage.acknowledged = true; logger.debug(`Acknowledged event ${eventId} of type ${eventType}`); // Simulate latency if enabled if (this.simulateLatency) { await this.simulateNetworkLatency(); } } /** * Process an event * * @param event - Event envelope */ private async processEvent(event: EventEnvelope): Promise<void> { // Get subscriptions for this event type const eventSubscriptions = this.subscriptions.get(event.type); if (!eventSubscriptions || eventSubscriptions.size === 0) { return; } // Call all handlers const promises: Promise<void>[] = []; for (const handler of eventSubscriptions.values()) { promises.push(this.processEventWithHandler(event, handler)); } // Wait for all promises to resolve if (promises.length > 0) { await Promise.all(promises); } } /** * Process an event with a handler * * @param event - Event envelope * @param handler - Event handler */ private async processEventWithHandler(event: EventEnvelope, handler: EventHandler): Promise<void> { try { // Record start time for metrics const startTime = Date.now(); try { // Call handler const result = handler(event); // Wait for result if it's a promise if (result instanceof Promise) { await result; } // Record processing time for metrics if ((this.config as MemoryAdapterConfig).simulateLatency) { // Add simulated latency for testing const [min, max] = (this.config as MemoryAdapterConfig).latencyRange || [10, 50]; const latency = Math.random() * (max - min) + min; await new Promise(resolve => setTimeout(resolve, latency)); } // Acknowledge event await this.acknowledgeEvent(event.id, event.type); } catch (handlerError) { // Log the error but don't re-throw it // The error will be handled by the event bus wrapper logger.error(`Error in handler for event ${event.id} of type ${event.type}: ${handlerError instanceof Error ? handlerError.message : String(handlerError)}`, handlerError as Error); } } catch (error) { logger.error(`Error processing event ${event.id} of type ${event.type}: ${error instanceof Error ? error.message : String(error)}`, error as Error); } } /** * Clean up expired events */ private cleanup(): void { if (!this.initialized || this.eventTtl <= 0) { return; } const now = Date.now(); const expiredBefore = now - this.eventTtl; let removedCount = 0; // Check all event types for (const [eventType, eventTypeMap] of this.events.entries()) { // Check all events of this type for (const [eventId, eventStorage] of eventTypeMap.entries()) { // Remove if expired and acknowledged if (eventStorage.timestamp < expiredBefore && eventStorage.acknowledged) { eventTypeMap.delete(eventId); removedCount++; } } // Remove event type if empty if (eventTypeMap.size === 0) { this.events.delete(eventType); } } if (removedCount > 0) { logger.debug(`Cleaned up ${removedCount} expired events`); } } /** * Simulate network latency */ private async simulateNetworkLatency(): Promise<void> { const [min, max] = this.latencyRange; const latency = Math.random() * (max - min) + min; await new Promise(resolve => setTimeout(resolve, latency)); } } /** * Create a new memory adapter * * @returns Memory adapter */ export function createMemoryAdapter(): MemoryAdapter { return new MemoryAdapter(); }