Skip to content

Commit

Permalink
feat: added lifecycleEvents.ts from toolbelt: an event listener/emitter
Browse files Browse the repository at this point in the history
  • Loading branch information
moberhauer committed Jun 18, 2020
1 parent 587f229 commit 099478c
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/lifecycleEvents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/

import { AnyJson } from '@salesforce/ts-types';
import * as Debug from 'debug';

type callback = (data: AnyJson) => Promise<void>;

interface CallbackDictionary {
[key: string]: callback[];
}

/**
* An asynchronous event listener and emitter that follows the singleton pattern.
*/
export class Lifecycle {
public static getInstance(): Lifecycle {
if (!this.instance) {
this.instance = new Lifecycle();
}
return this.instance;
}

private static instance: Lifecycle;
private debug = Debug(`sfdx:${this.constructor.name}`);
private listeners: CallbackDictionary;

private constructor() {
this.listeners = {};
}

public removeAllListeners(eventName: string) {
this.listeners[eventName] = [];
}

public getListeners(eventName: string): callback[] {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
return this.listeners[eventName];
}

public on<T extends AnyJson>(eventName: string, cb: (data: T | AnyJson) => Promise<void>) {
if (this.getListeners(eventName).length !== 0) {
this.debug(
`${this.listeners[eventName].length +
1} lifecycle events with the name ${eventName} have now been registered. When this event is emitted all ${this
.listeners[eventName].length + 1} listeners will fire.`
);
}
this.listeners[eventName].push(cb);
}

public async emit(eventName: string, data: AnyJson) {
if (this.getListeners(eventName).length === 0) {
this.debug(
`A lifecycle event with the name ${eventName} does not exist. An event must be registered before it can be emitted.`
);
} else {
this.listeners[eventName].forEach(async cb => {
await cb(data);
});
}
}
}
110 changes: 110 additions & 0 deletions test/unit/lifecycleEventsTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2018, salesforce.com, inc.
* All rights reserved.
* SPDX-License-Identifier: BSD-3-Clause
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
import { stubMethod } from '@salesforce/ts-sinon';
import * as chai from 'chai';
import { Lifecycle } from '../../src/LifecycleEvents';
import { testSetup } from '../../src/testSetup';

const $$ = testSetup();

describe('lifecycleEvents', () => {
let fake;
let fakeSpy;
let loggerSpy;

class Foo {
public bar(name: string, result: string) {
return result[name];
}
}

beforeEach(() => {
loggerSpy = stubMethod($$.SANDBOX, Lifecycle.getInstance(), 'debug');
fake = new Foo();
fakeSpy = stubMethod($$.SANDBOX, fake, 'bar');
});

it('getInstance is a functioning singleton pattern', async () => {
chai.assert(Lifecycle.getInstance() === Lifecycle.getInstance());
});

it('succsssful event registration and emitting causes runHook to be called', async () => {
Lifecycle.getInstance().on('test1', async result => {
fake.bar('test1', result);
});
Lifecycle.getInstance().on('test2', async result => {
fake.bar('test1', result);
});
chai.expect(fakeSpy.callCount).to.be.equal(0);

await Lifecycle.getInstance().emit('test1', 'Success');
chai.expect(fakeSpy.callCount).to.be.equal(1);
chai.expect(fakeSpy.args[0][1]).to.be.equal('Success');

await Lifecycle.getInstance().emit('test2', 'Also Success');
chai.expect(fakeSpy.callCount).to.be.equal(2);
chai.expect(fakeSpy.args[1][1]).to.be.equal('Also Success');
});

it('an event registering twice logs a warning but creates two listeners', async () => {
Lifecycle.getInstance().on('test3', async result => {
fake.bar('test3', result);
});
Lifecycle.getInstance().on('test3', async result => {
fake.bar('test3', result);
});
chai.expect(loggerSpy.callCount).to.be.equal(1);
chai
.expect(loggerSpy.args[0][0])
.to.be.equal(
'2 lifecycle events with the name test3 have now been registered. When this event is emitted all 2 listeners will fire.'
);

await Lifecycle.getInstance().emit('test3', 'Two Listeners');
chai.expect(fakeSpy.callCount).to.be.equal(2);
});

it('emitting an event that is not registered logs a warning and will not call runHook', async () => {
await Lifecycle.getInstance().emit('test4', 'Expect failure');
chai.expect(fakeSpy.callCount).to.be.equal(0);
chai.expect(loggerSpy.callCount).to.be.equal(1);
chai
.expect(loggerSpy.args[0][0])
.to.be.equal(
'A lifecycle event with the name test4 does not exist. An event must be registered before it can be emitted.'
);
});

it('removeAllListeners works', async () => {
Lifecycle.getInstance().on('test5', async result => {
fake.bar('test5', result);
});
await Lifecycle.getInstance().emit('test5', 'Success');
chai.expect(fakeSpy.callCount).to.be.equal(1);
chai.expect(fakeSpy.args[0][1]).to.be.equal('Success');

Lifecycle.getInstance().removeAllListeners('test5');
await Lifecycle.getInstance().emit('test5', 'Failure: Listener Removed');
chai.expect(fakeSpy.callCount).to.be.equal(1);
chai.expect(loggerSpy.callCount).to.be.equal(1);
chai
.expect(loggerSpy.args[0][0])
.to.be.equal(
'A lifecycle event with the name test5 does not exist. An event must be registered before it can be emitted.'
);
});

it('getListeners works', async () => {
const x = async result => {
fake.bar('test6', result);
};
Lifecycle.getInstance().on('test6', x);
chai.expect(Lifecycle.getInstance().getListeners('test6')[0]).to.be.equal(x);

chai.expect(Lifecycle.getInstance().getListeners('undefinedKey').length).to.be.equal(0);
});
});

0 comments on commit 099478c

Please sign in to comment.