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:
saravanakumardb1 2026-04-13 11:42:14 -07:00
parent fbac905e9c
commit 82428d7bf9
2 changed files with 52 additions and 1 deletions

View 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('timer.created', handler);
await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' });
expect(handler).toHaveBeenCalledOnce();
});
it('unsubscribe stops delivery', async () => {
const bus = getEventBus();
const handler = vi.fn();
const unsub = bus.on('timer.created', handler);
unsub();
await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' });
expect(handler).not.toHaveBeenCalled();
});
it('failing handler does not block others', async () => {
const bus = getEventBus();
const good = vi.fn();
bus.on('timer.created', () => { throw new Error('boom'); });
bus.on('timer.created', good);
await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: '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('timer.created', handler);
bus.removeAll();
await bus.emit('timer.created', { timerId: 't1', userId: 'u1', label: 'Test' });
expect(handler).not.toHaveBeenCalled();
});
});

View File

@ -35,7 +35,7 @@ class DomainEventBus {
async emit<K extends keyof ChronoMindEventMap>(event: K, payload: ChronoMindEventMap[K]): Promise<void> {
const fns = this.handlers.get(event);
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 { this.handlers.clear(); }