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.
85 lines
2.0 KiB
TypeScript
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;
|
|
}
|