UNPKG

@sailboat-computer/event-bus

Version:

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

218 lines (180 loc) 5.29 kB
/** * Base adapter for event bus implementations */ import { v4 as uuidv4 } from 'uuid'; import { EventAdapter, AdapterConfig, EventHandler, EventEnvelope, Subscription } from '../types'; import { ConfigurationError, NotInitializedError } from '../errors'; import { logger } from '../utils'; /** * Base adapter implementation */ export abstract class BaseAdapter implements EventAdapter { /** * Adapter configuration */ protected config: AdapterConfig; /** * Whether the adapter is initialized */ protected initialized = false; /** * Whether the adapter is connected */ protected connected = false; /** * Subscriptions by event type */ protected subscriptions = new Map<string, Map<string, EventHandler>>(); /** * Create a new base adapter */ constructor() { // Configuration will be set in initialize() this.config = { serviceName: '' }; } /** * Initialize the adapter * * @param config - Adapter configuration */ async initialize(config: AdapterConfig): Promise<void> { if (this.initialized) { throw new Error('Adapter already initialized'); } this.validateConfig(config); this.config = { ...config }; this.initialized = true; logger.info(`Initialized ${this.constructor.name} for service ${this.config.serviceName}`); } /** * Shutdown the adapter */ async shutdown(): Promise<void> { if (!this.initialized) { return; } this.initialized = false; this.connected = false; this.subscriptions.clear(); logger.info(`Shut down ${this.constructor.name} for service ${this.config.serviceName}`); } /** * Publish an event * * @param event - Event envelope * @returns Event ID */ abstract publish<T>(event: EventEnvelope): Promise<string>; /** * Subscribe to an event * * @param eventType - Event type * @param handler - Event handler * @returns Subscription */ async subscribe<T>(eventType: string, handler: EventHandler<T>): Promise<Subscription> { this.ensureInitialized(); // Create subscription ID const subscriptionId = uuidv4(); // Get or create subscriptions map for this event type if (!this.subscriptions.has(eventType)) { this.subscriptions.set(eventType, new Map()); } // Add handler to subscriptions this.subscriptions.get(eventType)!.set(subscriptionId, handler as EventHandler); logger.debug(`Subscribed to ${eventType} with ID ${subscriptionId}`); // Return subscription object return { id: subscriptionId, eventType, unsubscribe: async () => { await this.unsubscribeById(subscriptionId, eventType); } }; } /** * Unsubscribe from a specific subscription * * @param subscriptionId - Subscription ID * @param eventType - Event type */ protected async unsubscribeById(subscriptionId: string, eventType: string): Promise<void> { // Get subscriptions map for this event type const eventSubscriptions = this.subscriptions.get(eventType); if (!eventSubscriptions) { return; } // Remove handler from subscriptions eventSubscriptions.delete(subscriptionId); // Remove event type if no more subscriptions if (eventSubscriptions.size === 0) { this.subscriptions.delete(eventType); } logger.debug(`Unsubscribed from ${eventType} with ID ${subscriptionId}`); } /** * Unsubscribe from all handlers for an event type * * @param eventType - Event type to unsubscribe from */ async unsubscribe(eventType: string): Promise<void> { this.ensureInitialized(); // Get subscriptions map for this event type const eventSubscriptions = this.subscriptions.get(eventType); if (!eventSubscriptions) { return; } // Get all subscription IDs for this event type const subscriptionIds = Array.from(eventSubscriptions.keys()); // Unsubscribe from each subscription for (const subscriptionId of subscriptionIds) { await this.unsubscribeById(subscriptionId, eventType); } logger.debug(`Unsubscribed from all handlers for event type ${eventType}`); } /** * Acknowledge an event * * @param eventId - Event ID * @param eventType - Event type */ abstract acknowledgeEvent(eventId: string, eventType: string): Promise<void>; /** * Check if the adapter is connected * * @returns True if connected */ isConnected(): boolean { return this.connected; } /** * Ensure the adapter is initialized * * @throws NotInitializedError if not initialized */ protected ensureInitialized(): void { if (!this.initialized) { throw new NotInitializedError(); } } /** * Validate adapter configuration * * @param config - Adapter configuration * @throws ConfigurationError if configuration is invalid */ protected validateConfig(config: AdapterConfig): void { if (!config) { throw new ConfigurationError('Adapter configuration is required'); } if (!config.serviceName) { throw new ConfigurationError('Service name is required'); } } }