Skip to content

Commit

Permalink
Merge pull request #82 from browserify/once-event-target
Browse files Browse the repository at this point in the history
  • Loading branch information
goto-bus-stop committed Feb 27, 2021
2 parents 1e934b7 + 0a32360 commit ec60f7b
Show file tree
Hide file tree
Showing 4 changed files with 190 additions and 26 deletions.
57 changes: 39 additions & 18 deletions events.js
Expand Up @@ -448,29 +448,50 @@ function unwrapListeners(arr) {

function once(emitter, name) {
return new Promise(function (resolve, reject) {
function eventListener() {
if (errorListener !== undefined) {
function errorListener(err) {
emitter.removeListener(name, resolver);
reject(err);
}

function resolver() {
if (typeof emitter.removeListener === 'function') {
emitter.removeListener('error', errorListener);
}
resolve([].slice.call(arguments));
};
var errorListener;

// Adding an error listener is not optional because
// if an error is thrown on an event emitter we cannot
// guarantee that the actual event we are waiting will
// be fired. The result could be a silent way to create
// memory or file descriptor leaks, which is something
// we should avoid.
if (name !== 'error') {
errorListener = function errorListener(err) {
emitter.removeListener(name, eventListener);
reject(err);
};

emitter.once('error', errorListener);
eventTargetAgnosticAddListener(emitter, name, resolver, { once: true });
if (name !== 'error') {
addErrorHandlerIfEventEmitter(emitter, errorListener, { once: true });
}

emitter.once(name, eventListener);
});
}

function addErrorHandlerIfEventEmitter(emitter, handler, flags) {
if (typeof emitter.on === 'function') {
eventTargetAgnosticAddListener(emitter, 'error', handler, flags);
}
}

function eventTargetAgnosticAddListener(emitter, name, listener, flags) {
if (typeof emitter.on === 'function') {
if (flags.once) {
emitter.once(name, listener);
} else {
emitter.on(name, listener);
}
} else if (typeof emitter.addEventListener === 'function') {
// EventTarget does not have `error` event semantics like Node
// EventEmitters, we do not listen for `error` events here.
emitter.addEventListener(name, function wrapListener(arg) {
// IE does not have builtin `{ once: true }` support so we
// have to do it manually.
if (flags.once) {
emitter.removeEventListener(name, wrapListener);
}
listener(arg);
});
} else {
throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type ' + typeof emitter);
}
}
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -24,6 +24,7 @@
"devDependencies": {
"airtap": "^1.0.0",
"functions-have-names": "^1.2.1",
"has": "^1.0.3",
"has-symbols": "^1.0.1",
"isarray": "^2.0.5",
"tape": "^5.0.0"
Expand Down
148 changes: 141 additions & 7 deletions tests/events-once.js
Expand Up @@ -3,8 +3,62 @@
var common = require('./common');
var EventEmitter = require('../').EventEmitter;
var once = require('../').once;
var has = require('has');
var assert = require('assert');

function Event(type) {
this.type = type;
}

function EventTargetMock() {
this.events = {};

this.addEventListener = common.mustCall(this.addEventListener);
this.removeEventListener = common.mustCall(this.removeEventListener);
}

EventTargetMock.prototype.addEventListener = function addEventListener(name, listener, options) {
if (!(name in this.events)) {
this.events[name] = { listeners: [], options: options || {} }
}
this.events[name].listeners.push(listener);
};

EventTargetMock.prototype.removeEventListener = function removeEventListener(name, callback) {
if (!(name in this.events)) {
return;
}
var event = this.events[name];
var stack = event.listeners;

for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback) {
stack.splice(i, 1);
if (stack.length === 0) {
delete this.events[name];
}
return;
}
}
};

EventTargetMock.prototype.dispatchEvent = function dispatchEvent(arg) {
if (!(arg.type in this.events)) {
return true;
}

var event = this.events[arg.type];
var stack = event.listeners.slice();

for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(null, arg);
if (event.options.once) {
this.removeEventListener(arg.type, stack[i]);
}
}
return !arg.defaultPrevented;
};

function onceAnEvent() {
var ee = new EventEmitter();

Expand Down Expand Up @@ -80,21 +134,101 @@ function onceError() {
ee.emit('error', expected);
});

return once(ee, 'error').then(function (args) {
var promise = once(ee, 'error');
assert.strictEqual(ee.listenerCount('error'), 1);
return promise.then(function (args) {
var err = args[0]
assert.strictEqual(err, expected);
assert.strictEqual(ee.listenerCount('error'), 0);
assert.strictEqual(ee.listenerCount('myevent'), 0);
});
}

Promise.all([
function onceWithEventTarget() {
var et = new EventTargetMock();
var event = new Event('myevent');
process.nextTick(function () {
et.dispatchEvent(event);
});
return once(et, 'myevent').then(function (args) {
var value = args[0];
assert.strictEqual(value, event);
assert.strictEqual(has(et.events, 'myevent'), false);
});
}

function onceWithEventTargetError() {
var et = new EventTargetMock();
var error = new Event('error');
process.nextTick(function () {
et.dispatchEvent(error);
});
return once(et, 'error').then(function (args) {
var err = args[0];
assert.strictEqual(err, error);
assert.strictEqual(has(et.events, 'error'), false);
});
}

function prioritizesEventEmitter() {
var ee = new EventEmitter();
ee.addEventListener = assert.fail;
ee.removeAllListeners = assert.fail;
process.nextTick(function () {
ee.emit('foo');
});
return once(ee, 'foo');
}

var allTests = [
onceAnEvent(),
onceAnEventWithTwoArgs(),
catchesErrors(),
stopListeningAfterCatchingError(),
onceError()
]).catch(function (err) {
console.error(err.stack)
process.exit(1)
});
onceError(),
onceWithEventTarget(),
onceWithEventTargetError(),
prioritizesEventEmitter()
];

var hasBrowserEventTarget = false;
try {
hasBrowserEventTarget = typeof (new window.EventTarget().addEventListener) === 'function' &&
new window.Event('xyz').type === 'xyz';
} catch (err) {}

if (hasBrowserEventTarget) {
var onceWithBrowserEventTarget = function onceWithBrowserEventTarget() {
var et = new window.EventTarget();
var event = new window.Event('myevent');
process.nextTick(function () {
et.dispatchEvent(event);
});
return once(et, 'myevent').then(function (args) {
var value = args[0];
assert.strictEqual(value, event);
assert.strictEqual(has(et.events, 'myevent'), false);
});
}

var onceWithBrowserEventTargetError = function onceWithBrowserEventTargetError() {
var et = new window.EventTarget();
var error = new window.Event('error');
process.nextTick(function () {
et.dispatchEvent(error);
});
return once(et, 'error').then(function (args) {
var err = args[0];
assert.strictEqual(err, error);
assert.strictEqual(has(et.events, 'error'), false);
});
}

common.test.comment('Testing with browser built-in EventTarget');
allTests.push([
onceWithBrowserEventTarget(),
onceWithBrowserEventTargetError()
]);
}

module.exports = Promise.all(allTests);
10 changes: 9 additions & 1 deletion tests/index.js
Expand Up @@ -14,7 +14,15 @@ var require = function(file) {
t.on('end', function () { delete common.test; });
common.test = t;

try { orig_require(file); } catch (err) { t.fail(err); }
try {
var exp = orig_require(file);
if (exp && exp.then) {
exp.then(function () { t.end(); }, t.fail);
return;
}
} catch (err) {
t.fail(err);
}
t.end();
});
};
Expand Down

0 comments on commit ec60f7b

Please sign in to comment.