Skip to content

Commit

Permalink
Merge pull request #7 from vitaly-t/names
Browse files Browse the repository at this point in the history
Names
  • Loading branch information
vitaly-t authored Aug 6, 2019
2 parents 01b18f9 + 808bbb9 commit 67feb23
Show file tree
Hide file tree
Showing 4 changed files with 116 additions and 11 deletions.
65 changes: 64 additions & 1 deletion src/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export interface ISubContext<T = unknown> {
*/
readonly event: SubEvent<T>;

/**
* Subscription Name.
*/
readonly name?: string;

/**
* Unknown-type data to let the event wrapper persist any
* context it needs within the event's lifecycle.
Expand Down Expand Up @@ -55,6 +60,14 @@ export interface IEventOptions<T> {
*/
export interface ISubOptions {

/**
* Unique subscription name, to help with diagnosing subscription leaks.
* It should identify place in the code where the subscription is created.
*
* @see [[getStat]]
*/
name?: string;

/**
* Calling / `this` context for the subscription callback function.
*
Expand All @@ -70,6 +83,22 @@ export interface ISubOptions {
thisArg?: any;
}

/**
* Subscriptions statistics, as returned by method [[getStat]].
*/
export interface ISubStat {

/**
* Map of subscription names to their usage counters.
*/
named: { [name: string]: number };

/**
* Total number of unnamed subscriptions.
*/
unnamed: number;
}

/**
* Subscription callback function type.
*/
Expand Down Expand Up @@ -139,7 +168,8 @@ export class SubEvent<T = unknown> {
// Subscription replaces it with actual cancellation
};
cb = options && 'thisArg' in options ? cb.bind(options.thisArg) : cb;
const sub: ISubscriber<T> = {event: this, data: undefined, cb, cancel};
const name = options && options.name;
const sub: ISubscriber<T> = {event: this, data: undefined, cb, cancel, name};
if (typeof this.options.onSubscribe === 'function') {
this.options.onSubscribe(sub);
}
Expand Down Expand Up @@ -284,6 +314,39 @@ export class SubEvent<T = unknown> {
return this.options.maxSubs || 0;
}

/**
* Retrieves subscriptions statistics, to help with diagnosing subscription leaks.
*
* @param options
* Statistics Options:
*
* - `minUse: number` - Minimum subscription usage/count to be included into the list of named
* subscriptions. If subscription is used less times, it will be excluded from the named list.
*/
public getStat(options?: { minUse?: number }): ISubStat {
const stat: ISubStat = {named: {}, unnamed: 0};
this._subs.forEach(s => {
if (s.name) {
if (s.name in stat.named) {
stat.named[s.name]++;
} else {
stat.named[s.name] = 1;
}
} else {
stat.unnamed++;
}
});
const minUse = (options && options.minUse) || 0;
if (minUse > 1) {
for (const a in stat.named) {
if (stat.named[a] < minUse) {
delete stat.named[a];
}
}
}
return stat;
}

/**
* Cancels all subscriptions.
*
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export {Subscription} from './sub';
export {SubEvent, SubFunction, ISubContext, IEventOptions, ISubOptions} from './event';
export {SubEvent, SubFunction, ISubContext, IEventOptions, ISubOptions, ISubStat} from './event';
export {SubEventCount, ICountOptions, ISubCountChange} from './count';
8 changes: 7 additions & 1 deletion src/sub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@ export class Subscription {
*/
private _cancel: null | (() => void);

/**
* Subscription name, if one was specified with method [[subscribe]].
*/
readonly name?: string;

/**
* @hidden
*/
constructor(cancel: () => void, sub: { cancel?: () => void }) {
constructor(cancel: () => void, sub: { cancel?: () => void, name?: string }) {
this._cancel = cancel;
this.name = sub.name;
sub.cancel = () => {
this._cancel = null;
};
Expand Down
52 changes: 44 additions & 8 deletions test/event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,18 @@ describe('SubEvent', () => {
done();
});
});
it('must pass correct "this" context into subscription functions', () => {
it('must pass subscription options correctly', () => {
const a = new SubEvent<void>();
let context;

function onEvent(this: any) {
context = this;
}

a.subscribe(onEvent, {thisArg: a});
const sub = a.subscribe(onEvent, {thisArg: a, name: 'my-sub'});
a.emitSync();
expect(context).to.eq(a);
expect(sub.name).to.eq('my-sub');
});

it('must track subscription count', () => {
Expand Down Expand Up @@ -84,14 +85,17 @@ describe('SubEvent', () => {
});
});
it('must call onSubscribe during subscription', () => {
let called = false;
let context: ISubContext | undefined;
const onSubscribe = (ctx: ISubContext) => {
called = true;
context = ctx;
};
const a = new SubEvent({onSubscribe});
expect(called).to.be.false;
a.subscribe(dummy);
expect(called).to.be.true;
expect(context).to.be.undefined;
a.subscribe(dummy, {name: 'hello'});
expect(!!context).to.be.true;
if (context) {
expect(context.name).to.eq('hello');
}
});
it('must call onCancel during cancellation', () => {
let data, called = false;
Expand All @@ -103,11 +107,12 @@ describe('SubEvent', () => {
called = true;
};
const a = new SubEvent({onSubscribe, onCancel});
const sub = a.subscribe(dummy);
const sub = a.subscribe(dummy, {name: 'tst'});
expect(called).to.be.false;
sub.cancel();
expect(called).to.be.true;
expect(data).to.eq(123);
expect(sub.name).to.eq('tst');
});
it('must call onFinish when done', done => {
let count: number;
Expand Down Expand Up @@ -236,4 +241,35 @@ describe('SubEvent', () => {
expect(data).to.eq(123);
});
});
describe('getStat', () => {
it('must report all subscriptions correctly', () => {
const a = new SubEvent();
a.subscribe(dummy);
a.subscribe(dummy);
a.subscribe(dummy, {name: 'first'});
a.subscribe(dummy, {name: 'second'});
a.subscribe(dummy, {name: 'third'});
a.subscribe(dummy, {name: 'third'});
expect(a.getStat()).to.eql({
named: {
first: 1,
second: 1,
third: 2
},
unnamed: 2
});
});
it('must limit occurrences according to minUse option', () => {
const a = new SubEvent();
a.subscribe(dummy, {name: 'first'});
a.subscribe(dummy, {name: 'second'});
a.subscribe(dummy, {name: 'second'});
expect(a.getStat({minUse: 2})).to.eql({
named: {
second: 2
},
unnamed: 0
});
});
});
});

0 comments on commit 67feb23

Please sign in to comment.