@sailboat-computer/event-bus
Version:
Standardized event bus for sailboat computer v3 with resilience features and offline capabilities
272 lines (233 loc) • 7 kB
text/typescript
/**
* 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);
}