I was asked to complete the task described below:
Event Emitters are objects that serve as the core building block in event-driven architectures. They simplify the process of handling asynchronous events and enable clean and decoupled code.
Create an Event Emitter module in JavaScript (as modern of a version as you prefer) with documentation and tests. Your implementation should allow for:
- Emitting named events with any number of arguments.
- Registering handler functions for named events that are passed the appropriate arguments on emission.
- Registering a "one-time" handler that will be called at most one time.
- Removing specific previously-registered event handlers and/or all previously-registered event handlers.
This module should be suitable for publishing to npm, though it is not necessary for you to do so.
Do not subclass or otherwise require an existing Event Emitter module, and do not include any dependencies apart from testing or development dependencies.
Below is the module I created, as well as the unit tests I developed as I created the module. It is similar to the NodeJS Event Emitter API but obviously not as complex.
How does the module and test code look? What would you recommend be done differently?
Emitter module:
"use strict";
function Emitter() {
//constructor
this.eventHandlers = {};
}
/**
* Emit an event
* @param event string
* @param arg1...argN - any arguments to be sent to callback functions
*/
Emitter.prototype.emit = function(event) {
const args = Array.from(arguments).slice(1);
if (this.eventHandlers.hasOwnProperty(event)) {
let indexesToRemove = [];
for (const index in this.eventHandlers[event]) {
const handler = this.eventHandlers[event][index];
handler.callback.apply(null, args);
if (handler.hasOwnProperty('once')) {
indexesToRemove.push(index);
}
}
if (indexesToRemove.length) {
for(const index in indexesToRemove) {
this.eventHandlers[event].splice(index, 1);
}
}
}
};
/**
* Register a callback for an event
* @param event
* @param callback
*/
Emitter.prototype.on = function(event, callback) {
addHandler.call(this, event, {callback: callback});
};
/**
* Register a callback for an event to be called only once
* @param event
* @param callback
*/
Emitter.prototype.once = function(event, callback) {
addHandler.call(this, event, {callback: callback, once: true});
};
/**
* Un-register a single or all callbacks for a given event
* @param event
* @param callback optional
*/
Emitter.prototype.off = function(event, callback) {
if (this.eventHandlers.hasOwnProperty(event)) {
if (callback !== undefined) {
for (const index in this.eventHandlers[event]) {
if (callback.toString() == this.eventHandlers[event][index].callback.toString()) {
this.eventHandlers[event].splice(index, 1);
}
}
}
else {
delete this.eventHandlers[event];
}
}
}
module.exports = Emitter;
/**
* Helper function to add an event handler
* @param event
* @param handlerObject
*/
function addHandler(event, handlerObject) {
if (!(event in this.eventHandlers)) {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(handlerObject);
}
Unit tests
const EventEmitter = require("../src/");
const chai = require("chai");
const sinon = require('sinon');
const sinonChai = require("sinon-chai");
chai.should();
chai.use(sinonChai);
describe("Event Emitter", function() {
let emitter;
before(function() {
emitter = new EventEmitter();
});
it("Allows registering handler functions for named events that are passed the appropriate arguments on emission.", function() {
const callback = sinon.fake();
emitter.on('scrape', callback);
emitter.emit('scrape');
callback.should.have.been.called;
const testValue1 = 'testValue1';
emitter.emit('scrape', testValue1);
callback.should.have.been.calledWith(testValue1);
const multiArgs = ['a', 'scraped', 'value'];
emitter.emit('scrape', ...multiArgs);
callback.should.have.been.calledWith(...multiArgs);
});
it("Allows Registering a \"one-time\" handler that will be called at most one time.", function() {
const callback = sinon.fake();
const callback2 = sinon.fake();
emitter.once('pull', callback);
emitter.on('pull', callback2);
emitter.emit('pull');
emitter.emit('pull');
callback.should.have.been.calledOnce;
callback2.should.have.been.calledTwice;
});
it("Allows Removing specific previously-registered event handlers and/or all previously-registered event handlers.", function() {
const callback = sinon.fake();
const callback2 = sinon.fake();
const callback3 = sinon.fake();
const callback4 = sinon.fake();
emitter.on('push', callback);
emitter.on('push', callback2);
emitter.off('push', callback);
emitter.emit('push');
callback.should.not.have.been.called;
callback2.should.have.been.called;
emitter.on('push', callback3);
emitter.off('push');
emitter.emit('push');
emitter.on('unshift', callback4);
emitter.emit('unshift');
callback3.should.not.have.been.called;
callback4.should.have.been.called;
});
});