diff --git a/src/strongbus.ts b/src/strongbus.ts index 65fac2f..5635166 100644 --- a/src/strongbus.ts +++ b/src/strongbus.ts @@ -69,6 +69,8 @@ export class Bus implements private _active = false; private _delegates = new Map, Events.Subscription[]>(); + private _delegateListenerTotalCount: number = 0; + private _delegateListenerCountsByEvent = new Map|Events.WILDCARD, number>(); private readonly subscriptionCache = new Map(); private readonly options: Required & {thresholds: Required}; @@ -447,9 +449,9 @@ export class Bus implements if(!this._delegates.has(delegate)) { this._delegates.set(delegate, [ delegate.hook(Lifecycle.willAddListener, this.willAddListener), - delegate.hook(Lifecycle.didAddListener, this.didAddDelegateListener), + delegate.hook(Lifecycle.didAddListener, event => this.didAddListener(event, delegate)), delegate.hook(Lifecycle.willRemoveListener, this.willRemoveListener), - delegate.hook(Lifecycle.didRemoveListener, this.didRemoveDelegateListener) + delegate.hook(Lifecycle.didRemoveListener, event => this.didRemoveListener(event, delegate)) ]); } } @@ -514,12 +516,7 @@ export class Bus implements * @getter `boolean` */ public get hasDelegateListeners(): boolean { - for(const delegate of this._delegates.keys()) { - if(delegate.hasListeners) { - return true; - } - } - return false; + return this._delegateListenerTotalCount > 0; } private _cachedGetListersValue: Map|Events.WILDCARD, ReadonlySet>; @@ -559,21 +556,31 @@ export class Bus implements } public hasListenersFor(event: EventKeys|Events.WILDCARD): boolean { - return this.hasOwnListenersFor(event) || this.hasDelegateListenersFor(event); + return this.getListenerCountFor(event) > 0; } public hasOwnListenersFor(event: EventKeys|Events.WILDCARD): boolean { - const handlers = this.bus.get(event); - return handlers?.size > 0; + return this.getOwnListenerCountFor(event) > 0; } public hasDelegateListenersFor(event: EventKeys|Events.WILDCARD): boolean { - for(const delegate of this._delegates.keys()) { - if(delegate.hasListenersFor(event)) { - return true; - } - } - return false; + return this.getDelegateListenerCountFor(event) > 0; + } + + public get listenerCount(): number { + return this.bus.size + this._delegateListenerTotalCount; + } + + public getListenerCountFor(event: EventKeys|Events.WILDCARD): number { + return this.getOwnListenerCountFor(event) + this.getDelegateListenerCountFor(event); + } + + public getOwnListenerCountFor(event: EventKeys|Events.WILDCARD): number { + return this.bus.get(event)?.size ?? 0; + } + + public getDelegateListenerCountFor(event: EventKeys|Events.WILDCARD): number { + return (this._delegateListenerCountsByEvent.get(event) ?? 0); } /** @@ -710,52 +717,57 @@ export class Bus implements private willAddListener(event: EventKeys|Events.WILDCARD) { this.emitLifecycleEvent(Lifecycle.willAddListener, event); - if(!this.active) { + if(!this._active) { this.emitLifecycleEvent(Lifecycle.willActivate, null); } } - private didAddListener(event: EventKeys|Events.WILDCARD, invokedByDelegate: boolean = false) { + private didAddListener(event: EventKeys|Events.WILDCARD, bus: Bus = this) { + this._cachedGetListersValue = null; - if(!invokedByDelegate) { + if(bus === this) { this._cachedGetOwnListenersValue = null; + } else { + const currCount = this._delegateListenerCountsByEvent.get(event) ?? 0; + this._delegateListenerCountsByEvent.set(event, Math.max(currCount + 1, 0)); + this._delegateListenerTotalCount = Math.max(this._delegateListenerTotalCount + 1, 0); } + this.emitLifecycleEvent(Lifecycle.didAddListener, event); - if(!this.active && this.hasListeners) { + if(!this._active && this.hasListeners) { this._active = true; this.emitLifecycleEvent(Lifecycle.active, null); } } - private didAddDelegateListener(event: EventKeys|Events.WILDCARD): void { - this.didAddListener(event, true); - } private willRemoveListener(event: EventKeys|Events.WILDCARD): void { - const eventHandlerCount = this.listeners.get(event)?.size || 0; + const eventHandlerCount = this.getListenerCountFor(event); if(eventHandlerCount) { this.emitLifecycleEvent(Lifecycle.willRemoveListener, event); - if(this.active && this.listeners.size === 1 && eventHandlerCount === 1) { + if(this._active && this.listenerCount === 1 && eventHandlerCount === 1) { this.emitLifecycleEvent(Lifecycle.willIdle, null); } } } - private didRemoveListener(event: EventKeys|Events.WILDCARD, invokedByDelegate: boolean = false) { + private didRemoveListener(event: EventKeys|Events.WILDCARD, bus: Bus = this) { + this._cachedGetListersValue = null; - if(!invokedByDelegate) { + if(bus === this) { this._cachedGetOwnListenersValue = null; + } else { + const currCount = this._delegateListenerCountsByEvent.get(event) ?? 0; + this._delegateListenerCountsByEvent.set(event, Math.max(currCount - 1, 0)); + this._delegateListenerTotalCount = Math.max(this._delegateListenerTotalCount - 1, 0); } + this.emitLifecycleEvent(Lifecycle.didRemoveListener, event); - if(this.active && !this.hasListeners) { + if(this._active && !this.hasListeners) { this._active = false; this.emitLifecycleEvent(Lifecycle.idle, null); } } - - private didRemoveDelegateListener(event: EventKeys|Events.WILDCARD): void { - this.didRemoveListener(event, true); - } } diff --git a/src/strongbus_spec.ts b/src/strongbus_spec.ts index 0bc4aae..f1fd7b8 100644 --- a/src/strongbus_spec.ts +++ b/src/strongbus_spec.ts @@ -1250,6 +1250,94 @@ describe('Strongbus.Bus', () => { }); }); + describe('#listenerCount', () => { + describe('given an instance has no delegates', () => { + it('counts listeners for the instance', () => { + bus.on('foo', onTestEvent); + bus.on('bar', () => ({})); + + expect(bus.listenerCount).toEqual(2); + }); + }); + + describe('given an instance has delegates', () => { + let bus2: DelegateTestBus; + beforeEach(() => { + bus2 = new DelegateTestBus({emulateListenerCount: true}); + bus.pipe(bus2); + }); + + describe('and a delegate has listeners', () => { + it('counts listeners for the instance and its delegates', () => { + bus.on('foo', onTestEvent); + bus.on('bar', () => ({})); + bus2.on('foo', onTestEvent); + + expect(bus.listenerCount).toEqual(3); + }); + }); + + describe('and delegates have no listeners', () => { + it('counts listeners for the instance and its delegates', () => { + bus.on('foo', onTestEvent); + bus.on('bar', () => ({})); + + expect(bus.listenerCount).toEqual(2); + }); + }); + }); + }); + + describe('#listenerCount', () => { + describe('given an instance has no delegates', () => { + it('counts listeners for the instance', () => { + const sub1 = bus.on('foo', onTestEvent); + const sub2 = bus.on('bar', () => ({})); + + expect(bus.listenerCount).toEqual(2); + sub1.unsubscribe(); + expect(bus.listenerCount).toEqual(1); + sub2.unsubscribe(); + expect(bus.listenerCount).toEqual(0); + }); + }); + + describe('given an instance has delegates', () => { + let bus2: DelegateTestBus; + beforeEach(() => { + bus2 = new DelegateTestBus({emulateListenerCount: true}); + bus.pipe(bus2); + }); + + describe('and a delegate has listeners', () => { + it('counts listeners for the instance and its delegates', () => { + const sub1 = bus.on('foo', onTestEvent); + const sub2 = bus.on('bar', () => ({})); + const sub3 = bus2.on('foo', onTestEvent); + + expect(bus.listenerCount).toEqual(3); + sub1.unsubscribe(); + expect(bus.listenerCount).toEqual(2); + sub2.unsubscribe(); + expect(bus.listenerCount).toEqual(1); + sub3.unsubscribe(); + expect(bus.listenerCount).toEqual(0); + sub1.unsubscribe(); + expect(bus.listenerCount).toEqual(0); + }); + }); + + describe('and delegates have no listeners', () => { + it('counts listeners for the instance and its delegates', () => { + bus.on('foo', onTestEvent); + bus.on('bar', () => ({})); + + expect(bus.listenerCount).toEqual(2); + }); + }); + }); + }); + describe('#destroy', () => { it('removes all event listeners, triggering proper lifecycle events', () => { bus = new Strongbus.Bus({allowUnhandledEvents: false});