mirror of https://github.com/embarklabs/embark.git
feat(@embark/core): Add events oracle
Add the ability to request an event before it’s command handler has been set. Also, add the ability to emit an event before a listener has been set. The most useful use case for this is to allow modules/plugins to load asynchronously and without need to know their load order beforehand. This let’s us request events in advance of having the event command handler set by the module/plugin. BEFORE: ``` this.events.request(“event:name”, () => { // never fired }); this.events.setCommandHandler(“event:name”, cb); ``` AFTER: ``` this.events.request(“event:name”, () => { // YAY it fires! }); this.events.setCommandHandler(“event:name”, cb); ```
This commit is contained in:
parent
e943d03ce0
commit
84ca98f962
|
@ -1,4 +1,5 @@
|
|||
var EventEmitter = require('events');
|
||||
const cloneDeep = require('lodash.clonedeep');
|
||||
|
||||
function warnIfLegacy(eventName) {
|
||||
const legacyEvents = [];
|
||||
|
@ -20,6 +21,14 @@ EventEmitter.prototype._maxListeners = 350;
|
|||
const _on = EventEmitter.prototype.on;
|
||||
const _once = EventEmitter.prototype.once;
|
||||
const _setHandler = EventEmitter.prototype.setHandler;
|
||||
const _removeAllListeners = EventEmitter.prototype.removeAllListeners;
|
||||
|
||||
const toFire = [];
|
||||
|
||||
EventEmitter.prototype.removeAllListeners = function(requestName) {
|
||||
delete toFire[requestName];
|
||||
return _removeAllListeners.call(this, requestName);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.on = function(requestName, cb) {
|
||||
log("listening to event: ", requestName);
|
||||
|
@ -45,7 +54,19 @@ EventEmitter.prototype.request = function() {
|
|||
|
||||
log("requesting: ", requestName);
|
||||
warnIfLegacy(requestName);
|
||||
return this.emit('request:' + requestName, ...other_args);
|
||||
const listenerName = 'request:' + requestName;
|
||||
|
||||
// if we don't have a command handler set for this event yet,
|
||||
// store it and fire it once a command handler is set
|
||||
if (!this.listeners(listenerName).length) {
|
||||
if(!toFire[listenerName]) {
|
||||
toFire[listenerName] = [];
|
||||
}
|
||||
toFire[listenerName].push(other_args);
|
||||
return;
|
||||
}
|
||||
|
||||
return this.emit(listenerName, ...other_args);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.setCommandHandler = function(requestName, cb) {
|
||||
|
@ -53,14 +74,51 @@ EventEmitter.prototype.setCommandHandler = function(requestName, cb) {
|
|||
let listener = function(_cb) {
|
||||
cb.call(this, ...arguments);
|
||||
};
|
||||
const listenerName = 'request:' + requestName;
|
||||
|
||||
// unlike events, commands can only have 1 handler
|
||||
this.removeAllListeners('request:' + requestName);
|
||||
return this.on('request:' + requestName, listener);
|
||||
_removeAllListeners.call(this, listenerName);
|
||||
|
||||
// if this event was requested prior to the command handler
|
||||
// being set up,
|
||||
// 1. delete the premature request(s) from the toFire array so they are not fired again
|
||||
// 2. Add an event listener for future requests
|
||||
// 3. call the premature request(s) bound
|
||||
const prematureListenerArgs = cloneDeep(toFire[listenerName]);
|
||||
if (prematureListenerArgs) {
|
||||
delete toFire[listenerName];
|
||||
// Assign listener here so that any requests bound inside the
|
||||
// initial listener callback will be bound (see unit tests for an example)
|
||||
this.on(listenerName, listener);
|
||||
prematureListenerArgs.forEach((prematureArgs) => {
|
||||
cb.call(this, ...prematureArgs);
|
||||
});
|
||||
return;
|
||||
}
|
||||
return this.on(listenerName, listener);
|
||||
};
|
||||
|
||||
EventEmitter.prototype.setCommandHandlerOnce = function(requestName, cb) {
|
||||
log("setting command handler for: ", requestName);
|
||||
return this.once('request:' + requestName, function(_cb) {
|
||||
|
||||
const listenerName = 'request:' + requestName;
|
||||
|
||||
// if this event was requested prior to the command handler
|
||||
// being set up,
|
||||
// 1. delete the premature request(s) from the toFire array so they are not fired again
|
||||
// 2. call the premature request(s) bound
|
||||
// Do not bind an event listener for future requests as this is meant to be fired
|
||||
// only once.
|
||||
const prematureListenerArgs = cloneDeep(toFire[listenerName]);
|
||||
if (prematureListenerArgs) {
|
||||
delete toFire[listenerName];
|
||||
prematureListenerArgs.forEach((prematureArgs) => {
|
||||
cb.call(this, ...prematureArgs);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
return this.once(listenerName, function(_cb) {
|
||||
cb.call(this, ...arguments);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*globals describe, it, before, beforeEach*/
|
||||
const {File, Types} = require("../lib/core/file");
|
||||
const Assert = require("assert");
|
||||
const {expect} = require("chai");
|
||||
const fs = require("../lib/core/fs");
|
||||
const Events = require("../lib/core/events");
|
||||
|
||||
let events;
|
||||
const testEventName = "testevent";
|
||||
|
||||
describe('embark.Events', function () {
|
||||
this.timeout(10000);
|
||||
before(() => {
|
||||
events = new Events();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
events.removeAllListeners(testEventName);
|
||||
events.removeAllListeners(`request:${testEventName}`);
|
||||
});
|
||||
|
||||
describe('Set event listeners', function () {
|
||||
it('should be able to listen to an event emission', (done) => {
|
||||
events.on(testEventName, ({isTest}) => {
|
||||
expect(isTest).to.be.true;
|
||||
done();
|
||||
});
|
||||
events.emit(testEventName, { isTest: true });
|
||||
});
|
||||
|
||||
it('should be able to listen to an event emission once', (done) => {
|
||||
events.once(testEventName, ({isTest}) => {
|
||||
expect(isTest).to.be.true;
|
||||
done();
|
||||
});
|
||||
events.emit(testEventName, { isTest: true });
|
||||
});
|
||||
});
|
||||
describe('Set command handlers', function() {
|
||||
it('should be able to set a command handler and request the event', (done) => {
|
||||
events.setCommandHandler(testEventName, () => {
|
||||
Assert.ok(true);
|
||||
done();
|
||||
});
|
||||
events.request(testEventName);
|
||||
});
|
||||
|
||||
it('should be able to set a command handler with data and request the event', (done) => {
|
||||
events.setCommandHandler(testEventName, (options, cb) => {
|
||||
expect(options.isTest).to.be.true;
|
||||
cb(options);
|
||||
});
|
||||
events.request(testEventName, { isTest: true }, (data) => {
|
||||
expect(data.isTest).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to set a command handler with data and request the event once', (done) => {
|
||||
events.setCommandHandlerOnce(testEventName, (options, cb) => {
|
||||
expect(options.isTest).to.be.true;
|
||||
cb(options);
|
||||
});
|
||||
events.request(testEventName, { isTest: true }, (data) => {
|
||||
expect(data.isTest).to.be.true;
|
||||
events.request(testEventName, { isTest: true }, (data) => {
|
||||
Assert.fail("Should not call the requested event again, as it was set with once only");
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to request an event before a command handler has been set', (done) => {
|
||||
let testData = { isTest: true, manipulatedCount: 0 };
|
||||
events.request(testEventName, testData, (dataFirst) => {
|
||||
expect(dataFirst.isTest).to.be.true;
|
||||
expect(dataFirst.manipulatedCount).to.equal(1);
|
||||
expect(dataFirst.isAnotherTest).to.be.undefined;
|
||||
});
|
||||
events.request(testEventName, testData, (dataSecond) => {
|
||||
expect(dataSecond.isTest).to.be.true;
|
||||
expect(dataSecond.manipulatedCount).to.equal(2);
|
||||
expect(dataSecond.isAnotherTest).to.be.undefined;
|
||||
|
||||
dataSecond.isAnotherTest = true;
|
||||
events.request(testEventName, dataSecond, (dataThird) => {
|
||||
expect(dataThird.isTest).to.be.true;
|
||||
expect(dataThird.manipulatedCount).to.equal(3);
|
||||
expect(dataThird.isAnotherTest).to.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
events.setCommandHandler(testEventName, (options, cb) => {
|
||||
expect(options.isTest).to.be.true;
|
||||
options.manipulatedCount++;
|
||||
cb(options);
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to request an event before a command handler has been set once', (done) => {
|
||||
let testData = { isTest: true, manipulatedCount: 0 };
|
||||
events.request(testEventName, testData, (data) => {
|
||||
expect(data.isTest).to.be.true;
|
||||
expect(data.manipulatedCount).to.equal(1);
|
||||
expect(data.isAnotherTest).to.be.undefined;
|
||||
events.request(testEventName, data, (_dataSecondRun) => {
|
||||
Assert.fail("Should not call the requested event again, as it was set with once only");
|
||||
});
|
||||
});
|
||||
events.request(testEventName, testData, (_dataThirdRun) => {
|
||||
done();
|
||||
});
|
||||
events.setCommandHandlerOnce(testEventName, (options, cb) => {
|
||||
expect(options.isTest).to.be.true;
|
||||
options.manipulatedCount = options.manipulatedCount + 1;
|
||||
cb(options);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue