-
Notifications
You must be signed in to change notification settings - Fork 69
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added lifecycleEvents.ts from toolbelt: an event listener/emitter
- Loading branch information
1 parent
587f229
commit 099478c
Showing
2 changed files
with
179 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |