Skip to content

Commit

Permalink
Merge pull request #28 from epferrari/trackEventListenerCount
Browse files Browse the repository at this point in the history
Track event listener count
  • Loading branch information
epferrari authored Sep 7, 2022
2 parents 8313be6 + 39a33b4 commit 6275223
Show file tree
Hide file tree
Showing 2 changed files with 133 additions and 33 deletions.
78 changes: 45 additions & 33 deletions src/strongbus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ export class Bus<TEventMap extends Events.EventMap = Events.EventMap> implements

private _active = false;
private _delegates = new Map<Bus<TEventMap>, Events.Subscription[]>();
private _delegateListenerTotalCount: number = 0;
private _delegateListenerCountsByEvent = new Map<EventKeys<TEventMap>|Events.WILDCARD, number>();
private readonly subscriptionCache = new Map<string, Events.Subscription>();
private readonly options: Required<Options> & {thresholds: Required<ListenerThresholds>};

Expand Down Expand Up @@ -447,9 +449,9 @@ export class Bus<TEventMap extends Events.EventMap = Events.EventMap> 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))
]);
}
}
Expand Down Expand Up @@ -514,12 +516,7 @@ export class Bus<TEventMap extends Events.EventMap = Events.EventMap> 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<EventKeys<TEventMap>|Events.WILDCARD, ReadonlySet<EventHandlers.GenericHandler>>;
Expand Down Expand Up @@ -559,21 +556,31 @@ export class Bus<TEventMap extends Events.EventMap = Events.EventMap> implements
}

public hasListenersFor(event: EventKeys<TEventMap>|Events.WILDCARD): boolean {
return this.hasOwnListenersFor(event) || this.hasDelegateListenersFor(event);
return this.getListenerCountFor(event) > 0;
}

public hasOwnListenersFor(event: EventKeys<TEventMap>|Events.WILDCARD): boolean {
const handlers = this.bus.get(event);
return handlers?.size > 0;
return this.getOwnListenerCountFor(event) > 0;
}

public hasDelegateListenersFor(event: EventKeys<TEventMap>|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<TEventMap>|Events.WILDCARD): number {
return this.getOwnListenerCountFor(event) + this.getDelegateListenerCountFor(event);
}

public getOwnListenerCountFor(event: EventKeys<TEventMap>|Events.WILDCARD): number {
return this.bus.get(event)?.size ?? 0;
}

public getDelegateListenerCountFor(event: EventKeys<TEventMap>|Events.WILDCARD): number {
return (this._delegateListenerCountsByEvent.get(event) ?? 0);
}

/**
Expand Down Expand Up @@ -710,52 +717,57 @@ export class Bus<TEventMap extends Events.EventMap = Events.EventMap> implements

private willAddListener(event: EventKeys<TEventMap>|Events.WILDCARD) {
this.emitLifecycleEvent(Lifecycle.willAddListener, event);
if(!this.active) {
if(!this._active) {
this.emitLifecycleEvent(Lifecycle.willActivate, null);
}
}

private didAddListener(event: EventKeys<TEventMap>|Events.WILDCARD, invokedByDelegate: boolean = false) {
private didAddListener(event: EventKeys<TEventMap>|Events.WILDCARD, bus: Bus<any> = 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<TEventMap>|Events.WILDCARD): void {
this.didAddListener(event, true);
}

private willRemoveListener(event: EventKeys<TEventMap>|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<TEventMap>|Events.WILDCARD, invokedByDelegate: boolean = false) {
private didRemoveListener(event: EventKeys<TEventMap>|Events.WILDCARD, bus: Bus<any> = 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<TEventMap>|Events.WILDCARD): void {
this.didRemoveListener(event, true);
}
}


Expand Down
88 changes: 88 additions & 0 deletions src/strongbus_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand Down

0 comments on commit 6275223

Please sign in to comment.