UNPKG

@sailboat-computer/event-bus

Version:

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

272 lines (233 loc) 7 kB
/** * Dead letter queue implementation */ import { v4 as uuidv4 } from 'uuid'; import { DeadLetterEvent, EventEnvelope } from './types'; import { logger } from './utils'; /** * Dead letter queue manager */ export class DeadLetterQueueManager { /** * Dead letter queue events */ private events: Map<string, DeadLetterEvent> = new Map(); /** * Maximum number of events to store in the dead letter queue */ private maxSize: number; /** * Maximum number of processing attempts before sending to dead letter queue */ private maxAttempts: number; /** * Number of events sent to the dead letter queue */ private totalEvents: number = 0; /** * Last time an event was sent to the dead letter queue */ private lastEventTime: Date | null = null; /** * Create a new dead letter queue manager * * @param maxSize - Maximum number of events to store in the dead letter queue * @param maxAttempts - Maximum number of processing attempts before sending to dead letter queue */ constructor(maxSize: number = 1000, maxAttempts: number = 3) { this.maxSize = maxSize; this.maxAttempts = maxAttempts; } /** * Add an event to the dead letter queue * * @param event - Event envelope * @param error - Error that caused the event to be sent to the dead letter queue * @param attempts - Number of processing attempts * @returns Event ID */ addEvent(event: EventEnvelope, error: Error, attempts: number): string { // Generate a new ID for the dead letter event const eventId = uuidv4(); // Create dead letter event const deadLetterEvent: DeadLetterEvent = { eventType: event.type, data: event.data, originalEventId: event.id, timestamp: new Date(), attempts, error: { message: error.message, ...(error.stack ? { stack: error.stack } : {}), ...(((error as any).code !== undefined) ? { code: (error as any).code } : {}) } }; // Add event to the queue this.events.set(eventId, deadLetterEvent); // Update metrics this.totalEvents++; this.lastEventTime = deadLetterEvent.timestamp; // Prune if necessary if (this.events.size > this.maxSize) { this.pruneOldestEvent(); } logger.warn(`Event ${event.id} of type ${event.type} sent to dead letter queue after ${attempts} attempts: ${error.message}`); return eventId; } /** * Get events from the dead letter queue * * @param eventType - Optional event type to filter by * @param limit - Maximum number of events to return * @returns Dead letter queue events */ getEvents(eventType?: string, limit?: number): DeadLetterEvent[] { // Convert map to array let events = Array.from(this.events.entries()).map(([id, event]) => ({ ...event, id })); // Filter by event type if specified if (eventType) { events = events.filter(event => event.eventType === eventType); } // Sort by timestamp (newest first) events.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); // Limit if specified if (limit && limit > 0) { events = events.slice(0, limit); } return events; } /** * Get an event from the dead letter queue * * @param eventId - Event ID * @returns Dead letter event or null if not found */ getEvent(eventId: string): DeadLetterEvent | null { return this.events.get(eventId) || null; } /** * Remove an event from the dead letter queue * * @param eventId - Event ID * @returns True if the event was removed, false if it wasn't found */ removeEvent(eventId: string): boolean { return this.events.delete(eventId); } /** * Clear the dead letter queue * * @param eventType - Optional event type to filter by * @returns Number of events removed */ clearEvents(eventType?: string): number { if (!eventType) { // Clear all events const count = this.events.size; this.events.clear(); return count; } // Clear events of a specific type let count = 0; for (const [id, event] of this.events.entries()) { if (event.eventType === eventType) { this.events.delete(id); count++; } } return count; } /** * Get the number of events in the dead letter queue * * @returns Number of events */ getSize(): number { return this.events.size; } /** * Get the total number of events sent to the dead letter queue * * @returns Total number of events */ getTotalEvents(): number { return this.totalEvents; } /** * Get the last time an event was sent to the dead letter queue * * @returns Last event time or null if no events have been sent */ getLastEventTime(): Date | null { return this.lastEventTime; } /** * Get the maximum number of processing attempts before sending to dead letter queue * * @returns Maximum number of attempts */ getMaxAttempts(): number { return this.maxAttempts; } /** * Set the maximum number of processing attempts before sending to dead letter queue * * @param maxAttempts - Maximum number of attempts */ setMaxAttempts(maxAttempts: number): void { this.maxAttempts = maxAttempts; } /** * Get the maximum number of events to store in the dead letter queue * * @returns Maximum number of events */ getMaxSize(): number { return this.maxSize; } /** * Set the maximum number of events to store in the dead letter queue * * @param maxSize - Maximum number of events */ setMaxSize(maxSize: number): void { this.maxSize = maxSize; // Prune if necessary while (this.events.size > this.maxSize) { this.pruneOldestEvent(); } } /** * Prune the oldest event from the dead letter queue */ private pruneOldestEvent(): void { // Find the oldest event let oldestId: string | null = null; let oldestTime: number = Date.now(); for (const [id, event] of this.events.entries()) { const eventTime = event.timestamp.getTime(); if (eventTime < oldestTime) { oldestId = id; oldestTime = eventTime; } } // Remove the oldest event if (oldestId) { this.events.delete(oldestId); logger.debug(`Pruned oldest event ${oldestId} from dead letter queue`); } } } /** * Create a new dead letter queue manager * * @param maxSize - Maximum number of events to store in the dead letter queue * @param maxAttempts - Maximum number of processing attempts before sending to dead letter queue * @returns Dead letter queue manager */ export function createDeadLetterQueueManager(maxSize?: number, maxAttempts?: number): DeadLetterQueueManager { return new DeadLetterQueueManager(maxSize, maxAttempts); }