diff --git a/src/strongbus.ts b/src/strongbus.ts index 53f7de8..85cb4e8 100644 --- a/src/strongbus.ts +++ b/src/strongbus.ts @@ -528,19 +528,21 @@ export class Bus implements public get listeners(): ReadonlyMap|Events.WILDCARD, ReadonlySet> { if(!this._cachedGetListersValue) { const listenerCache = new Map(this.ownListeners); - this._delegates.forEach((_, delegate) => { - delegate.listeners.forEach((delegateListeners, event) => { + for(const delegate of this._delegates.keys()) { + for(const [event, delegateListeners] of delegate.listeners) { if(!delegateListeners.size) { - return; + continue; } let listeners = listenerCache.get(event); if(!listeners) { listeners = new Set(); listenerCache.set(event, listeners); } - delegateListeners.forEach(d => (listeners as Set).add(d)); - }); - }); + for(const listener of delegateListeners) { + (listeners as Set).add(listener); + } + } + } this._cachedGetListersValue = listenerCache; } return this._cachedGetListersValue; @@ -550,11 +552,11 @@ export class Bus implements public get ownListeners(): ReadonlyMap|Events.WILDCARD, ReadonlySet>> { if(!this._cachedGetOwnListenersValue) { const ownListenerCache = new Map|Events.WILDCARD, Set>>(); - this.bus.forEach((listeners, event) => { + for(const [event, listeners] of this.bus) { if(listeners.size) { ownListenerCache.set(event, new Set(listeners)); } - }); + } this._cachedGetOwnListenersValue = ownListenerCache; } return this._cachedGetOwnListenersValue; @@ -610,7 +612,11 @@ export class Bus implements } private releaseDelegates(): void { - this._delegates.forEach(subs => over(subs)()); + for(const subs of Object.values(this._delegates)) { + for(const sub of subs()) { + sub(); + } + } this._delegates.clear(); } @@ -675,13 +681,19 @@ export class Bus implements private emitEvent(event: EventKeys|Events.WILDCARD, ...args: any[]): boolean { const handlers = this.bus.get(event); if(handlers && handlers.size) { - handlers.forEach(async fn => { + for(const fn of handlers) { try { - await fn(...args); + const execution = fn(...args); + + // Emit errors if fn returns promise that rejects + (execution as Promise)?.catch?.((e) => { + this.emitLifecycleEvent(Lifecycle.error, {error: e, event}); + }); } catch(e) { + // Emit errors if callback fails synchronously this.emitLifecycleEvent(Lifecycle.error, {error: e, event}); } - }); + } return true; } return false; @@ -690,10 +702,26 @@ export class Bus implements private emitLifecycleEvent(event: L, payload: Lifecycle.EventMap[L]): void { const handlers = this.lifecycle.get(event); if(handlers && handlers.size) { - handlers.forEach(async fn => { + for(const fn of handlers) { try { - await fn(payload); + const execution = fn(payload); + + // Emit errors if fn returns promise that rejects + (execution as Promise)?.catch?.((e) => { + if(event === Lifecycle.error) { + const errorPayload = payload as Lifecycle.EventMap['error']; + this.options.logger.error('Error thrown in async error handler', { + errorHandler: fn.name, + errorHandlerError: e, + originalEvent: errorPayload.event, + eventHandlerError: errorPayload.error + }); + } else { + this.emitLifecycleEvent(Lifecycle.error, {error: e, event}); + } + }) } catch(e) { + // Emit errors if callback fails synchronously if(event === Lifecycle.error) { const errorPayload = payload as Lifecycle.EventMap['error']; this.options.logger.error('Error thrown in error handler', { @@ -706,7 +734,7 @@ export class Bus implements this.emitLifecycleEvent(Lifecycle.error, {error: e, event}); } } - }); + } } } diff --git a/src/strongbus_spec.ts b/src/strongbus_spec.ts index 77951e3..176df2d 100644 --- a/src/strongbus_spec.ts +++ b/src/strongbus_spec.ts @@ -786,15 +786,61 @@ describe('Strongbus.Bus', () => { sub2(); }); - it('raises "error" when errors are thrown in the listener', () => { - const error = new Error('Error in callback'); - bus.on('bar', () => { - throw error; + describe('error events', () => { + it('emits "error" when errors are thrown in the listener', () => { + const error = new Error('Error in callback'); + bus.on('bar', () => { + throw error; + }); + bus.emit('bar', true); + expect(onError).toHaveBeenCalledWith({ + error, + event: 'bar' + }); }); - bus.emit('bar', true); - expect(onError).toHaveBeenCalledWith({ - error, - event: 'bar' + + it('emits "error" when the listener returns a promise that rejects', async () => { + const error = new Error('Error in callback'); + bus.on('bar', () => ( + Promise.reject(error) + )); + bus.emit('bar', true); + + // Wait for promises to be processed + await Promise.resolve(); + + expect(onError).toHaveBeenCalledWith({ + error, + event: 'bar' + }); + }); + + it('emits an "error" event if a synchronous error is thrown in a hook', () => { + const error = new Error('error'); + bus.hook('active', () => { + throw error; + }); + bus.on('foo', () => {}); + expect(onError).toHaveBeenCalledWith({ + error, + event: 'active' + }); + }); + + it('emits "error" when a hook returns a promise that rejects', async () => { + const error = new Error('error'); + bus.hook('active', () => ( + Promise.reject(error) + )); + bus.on('foo', () => {}); + + // Wait for promises to be processed + await Promise.resolve(); + + expect(onError).toHaveBeenCalledWith({ + error, + event: 'active' + }); }); });