@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
text/typescript
/**
* 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');
}
}
}