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