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');
|
var EventEmitter = require('events');
|
||||||
|
const cloneDeep = require('lodash.clonedeep');
|
||||||
|
|
||||||
function warnIfLegacy(eventName) {
|
function warnIfLegacy(eventName) {
|
||||||
const legacyEvents = [];
|
const legacyEvents = [];
|
||||||
|
@ -20,6 +21,14 @@ EventEmitter.prototype._maxListeners = 350;
|
||||||
const _on = EventEmitter.prototype.on;
|
const _on = EventEmitter.prototype.on;
|
||||||
const _once = EventEmitter.prototype.once;
|
const _once = EventEmitter.prototype.once;
|
||||||
const _setHandler = EventEmitter.prototype.setHandler;
|
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) {
|
EventEmitter.prototype.on = function(requestName, cb) {
|
||||||
log("listening to event: ", requestName);
|
log("listening to event: ", requestName);
|
||||||
|
@ -45,7 +54,19 @@ EventEmitter.prototype.request = function() {
|
||||||
|
|
||||||
log("requesting: ", requestName);
|
log("requesting: ", requestName);
|
||||||
warnIfLegacy(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) {
|
EventEmitter.prototype.setCommandHandler = function(requestName, cb) {
|
||||||
|
@ -53,14 +74,51 @@ EventEmitter.prototype.setCommandHandler = function(requestName, cb) {
|
||||||
let listener = function(_cb) {
|
let listener = function(_cb) {
|
||||||
cb.call(this, ...arguments);
|
cb.call(this, ...arguments);
|
||||||
};
|
};
|
||||||
|
const listenerName = 'request:' + requestName;
|
||||||
|
|
||||||
// unlike events, commands can only have 1 handler
|
// unlike events, commands can only have 1 handler
|
||||||
this.removeAllListeners('request:' + requestName);
|
_removeAllListeners.call(this, listenerName);
|
||||||
return this.on('request:' + requestName, listener);
|
|
||||||
|
// 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) {
|
EventEmitter.prototype.setCommandHandlerOnce = function(requestName, cb) {
|
||||||
log("setting command handler for: ", requestName);
|
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);
|
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