learning_ai_notes/backend/src/lib/event-bus.ts
saravanakumardb1 18b680ad53 fix(backend): fix sync throw isolation in event bus + add 6 tests
Promise.allSettled only catches rejected promises, not synchronous throws.
Wrap handler calls in Promise.resolve().then() to isolate sync errors.
Add 6 unit tests covering delivery, unsubscribe, error isolation,
singleton, reset, and removeAll.
2026-04-13 11:42:52 -07:00

85 lines
2.0 KiB
TypeScript

/**
* Domain event bus singleton for NoteLett backend.
*
* Lightweight typed pub/sub for domain events. Handlers run via
* Promise.allSettled — a failing handler never blocks others.
*/
export interface NoteCreatedEvent {
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface NoteUpdatedEvent {
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface NoteDeletedEvent {
noteId: string;
workspaceId: string;
userId: string;
}
export interface TaskCreatedEvent {
taskId: string;
noteId: string;
workspaceId: string;
userId: string;
title: string;
}
export interface WorkspaceCreatedEvent {
workspaceId: string;
userId: string;
name: string;
}
export type NoteLettEventMap = {
'note.created': NoteCreatedEvent;
'note.updated': NoteUpdatedEvent;
'note.deleted': NoteDeletedEvent;
'task.created': TaskCreatedEvent;
'workspace.created': WorkspaceCreatedEvent;
};
type Handler<T> = (payload: T) => void | Promise<void>;
class DomainEventBus {
private handlers = new Map<string, Set<Handler<unknown>>>();
on<K extends keyof NoteLettEventMap>(event: K, handler: Handler<NoteLettEventMap[K]>): () => void {
if (!this.handlers.has(event)) this.handlers.set(event, new Set());
this.handlers.get(event)!.add(handler as Handler<unknown>);
return () => { this.handlers.get(event)?.delete(handler as Handler<unknown>); };
}
async emit<K extends keyof NoteLettEventMap>(event: K, payload: NoteLettEventMap[K]): Promise<void> {
const fns = this.handlers.get(event);
if (!fns || fns.size === 0) return;
await Promise.allSettled([...fns].map(fn => Promise.resolve().then(() => fn(payload))));
}
removeAll(): void {
this.handlers.clear();
}
}
let _bus: DomainEventBus | null = null;
export function getEventBus(): DomainEventBus {
if (!_bus) {
_bus = new DomainEventBus();
}
return _bus;
}
/** @internal — for testing only. */
export function _resetEventBus(): void {
_bus = null;
}