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:
emizzle 2019-02-18 10:25:17 +11:00 committed by Pascal Precht
parent e943d03ce0
commit 84ca98f962
2 changed files with 182 additions and 4 deletions

View File

@ -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);
}); });
}; };

View File

@ -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);
});
});
});
});