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.
This commit is contained in:
parent
cce4ca610f
commit
18b680ad53
51
backend/src/lib/event-bus.test.ts
Normal file
51
backend/src/lib/event-bus.test.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { getEventBus, _resetEventBus } from './event-bus.js';
|
||||||
|
|
||||||
|
describe('DomainEventBus', () => {
|
||||||
|
beforeEach(() => { _resetEventBus(); });
|
||||||
|
|
||||||
|
it('delivers event to subscriber', async () => {
|
||||||
|
const bus = getEventBus();
|
||||||
|
const handler = vi.fn();
|
||||||
|
bus.on('note.created', handler);
|
||||||
|
await bus.emit('note.created', { noteId: 'n1', workspaceId: 'w1', userId: 'u1', title: 'Test' });
|
||||||
|
expect(handler).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unsubscribe stops delivery', async () => {
|
||||||
|
const bus = getEventBus();
|
||||||
|
const handler = vi.fn();
|
||||||
|
const unsub = bus.on('note.created', handler);
|
||||||
|
unsub();
|
||||||
|
await bus.emit('note.created', { noteId: 'n1', workspaceId: 'w1', userId: 'u1', title: 'Test' });
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('failing handler does not block others', async () => {
|
||||||
|
const bus = getEventBus();
|
||||||
|
const good = vi.fn();
|
||||||
|
bus.on('note.created', () => { throw new Error('boom'); });
|
||||||
|
bus.on('note.created', good);
|
||||||
|
await bus.emit('note.created', { noteId: 'n1', workspaceId: 'w1', userId: 'u1', title: 'Test' });
|
||||||
|
expect(good).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('singleton returns same instance', () => {
|
||||||
|
expect(getEventBus()).toBe(getEventBus());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('_resetEventBus creates fresh instance', () => {
|
||||||
|
const a = getEventBus();
|
||||||
|
_resetEventBus();
|
||||||
|
expect(getEventBus()).not.toBe(a);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeAll clears handlers', async () => {
|
||||||
|
const bus = getEventBus();
|
||||||
|
const handler = vi.fn();
|
||||||
|
bus.on('note.created', handler);
|
||||||
|
bus.removeAll();
|
||||||
|
await bus.emit('note.created', { noteId: 'n1', workspaceId: 'w1', userId: 'u1', title: 'Test' });
|
||||||
|
expect(handler).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -61,7 +61,7 @@ class DomainEventBus {
|
|||||||
async emit<K extends keyof NoteLettEventMap>(event: K, payload: NoteLettEventMap[K]): Promise<void> {
|
async emit<K extends keyof NoteLettEventMap>(event: K, payload: NoteLettEventMap[K]): Promise<void> {
|
||||||
const fns = this.handlers.get(event);
|
const fns = this.handlers.get(event);
|
||||||
if (!fns || fns.size === 0) return;
|
if (!fns || fns.size === 0) return;
|
||||||
await Promise.allSettled([...fns].map(fn => fn(payload)));
|
await Promise.allSettled([...fns].map(fn => Promise.resolve().then(() => fn(payload))));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAll(): void {
|
removeAll(): void {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user