From 565635765258be392488c7d86cd87144d82044f8 Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Tue, 2 Jan 2018 17:24:19 +0100 Subject: [PATCH 01/23] feat: register event listener with asynchronous iterator protocol - close #3 --- index.js | 29 +++++++++++++-- package.json | 8 ++++- readme.md | 22 ++++++++++++ test/_run.js | 100 +++++++++++++++++++++++++++++++++++++++------------ 4 files changed, 133 insertions(+), 26 deletions(-) diff --git a/index.js b/index.js index 35f07a4..67de2fa 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,26 @@ function assertEventName(eventName) { } } +async function* iterator(emitter, eventName) { + const queue = []; + const off = emitter.on(eventName, function (data) { + queue.push(data); + }); + + try { + while (true) { + if (queue.length) { + yield queue.shift(); + } else { + yield await emitter.once(eventName); + } + } + } finally { + off(); + } + +} + module.exports = class Emittery { constructor() { this._events = new Map(); @@ -24,8 +44,13 @@ module.exports = class Emittery { on(eventName, listener) { assertEventName(eventName); - this._getListeners(eventName).add(listener); - return this.off.bind(this, eventName, listener); + + if (typeof listener === 'function') { + this._getListeners(eventName).add(listener); + return this.off.bind(this, eventName, listener); + } + + return iterator(this, eventName); } off(eventName, listener) { diff --git a/package.json b/package.json index c3d6b39..085e70a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "ava": "*", "babel-cli": "^6.26.0", "babel-core": "^6.26.0", + "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "codecov": "^3.0.0", @@ -60,9 +61,14 @@ "babel": { "plugins": [ "transform-async-to-generator", - "transform-es2015-spread" + "transform-es2015-spread", + "transform-async-generator-functions" ] }, + "ava": { + "require": "babel-register", + "babel": "inherit" + }, "nyc": { "reporter": [ "html", diff --git a/readme.md b/readme.md index 5f4900f..e7da2cb 100644 --- a/readme.md +++ b/readme.md @@ -47,6 +47,28 @@ Returns an unsubscribe method. Using the same listener multiple times for the same event will result in only one method call per emitted event. +If you use the method with only the first argument, it will return an asynchronous iterator. Your listener will therefore be the loop body and you'll be able to +unsubscribe to the event simply by breaking the loop. + +```Javascript + const emitter = new Emittery(); + //subscribe + (async function () { + for await (let t of emitter.on('foo')) { + if(t >10){ + break;//unsubscribe + } + console.log(t); + } + })(); + + let count = 0; + setInterval(function () { + count++; + emitter.emit('foo', count); + }, 1000); +``` + ##### listener(data) #### off(eventName, [listener]) diff --git a/test/_run.js b/test/_run.js index db5c61f..3e051f0 100644 --- a/test/_run.js +++ b/test/_run.js @@ -4,8 +4,10 @@ import delay from 'delay'; module.exports = Emittery => { test('on()', t => { const emitter = new Emittery(); - const listener1 = () => {}; - const listener2 = () => {}; + const listener1 = () => { + }; + const listener2 = () => { + }; emitter.on('🦄', listener1); emitter.on('🦄', listener2); t.deepEqual([...emitter._events.get('🦄')], [listener1, listener2]); @@ -13,12 +15,14 @@ module.exports = Emittery => { test('on() - eventName must be a string', t => { const emitter = new Emittery(); - t.throws(() => emitter.on(42, () => {}), TypeError); + t.throws(() => emitter.on(42, () => { + }), TypeError); }); test('on() - returns a unsubcribe method', t => { const emitter = new Emittery(); - const listener = () => {}; + const listener = () => { + }; const off = emitter.on('🦄', listener); t.true(emitter._events.get('🦄').has(listener)); @@ -29,7 +33,8 @@ module.exports = Emittery => { test('on() - dedupes identical listeners', t => { const emitter = new Emittery(); - const listener = () => {}; + const listener = () => { + }; emitter.on('🦄', listener); emitter.on('🦄', listener); @@ -37,9 +42,41 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); + test.cb('on() - async iterator (queued)', async t => { + t.plan(3); + const fixture = '🌈'; + const emitter = new Emittery(); + emitter.emit('🦄', fixture); + emitter.emit('🦄', fixture); + emitter.emit('🦄', fixture); + let count = 0; + for await (let data of emitter.on('🦄')) { + count++; + if (count > 3) { + break; + } + t.deepEqual(data, fixture); + } + t.end(); + }); + + test.cb('on() - async iterator (non queued)', async t => { + t.plan(1); + const fixture = '🌈'; + const emitter = new Emittery(); + setTimeout(function () { + emitter.emit('🦄', fixture) + }, 300); + for await (let data of emitter.on('🦄')) { + t.deepEqual(data, fixture); + t.end(); + } + }); + test('off()', t => { const emitter = new Emittery(); - const listener = () => {}; + const listener = () => { + }; emitter.on('🦄', listener); t.is(emitter._events.get('🦄').size, 1); @@ -56,8 +93,10 @@ module.exports = Emittery => { test('off() - all listeners', t => { const emitter = new Emittery(); - emitter.on('🦄', () => {}); - emitter.on('🦄', () => {}); + emitter.on('🦄', () => { + }); + emitter.on('🦄', () => { + }); t.is(emitter._events.get('🦄').size, 2); emitter.off('🦄'); @@ -199,7 +238,8 @@ module.exports = Emittery => { test('offAny()', t => { const emitter = new Emittery(); - const listener = () => {}; + const listener = () => { + }; emitter.onAny(listener); t.is(emitter._anyEvents.size, 1); emitter.offAny(listener); @@ -208,9 +248,12 @@ module.exports = Emittery => { test('offAny() - all listeners', t => { const emitter = new Emittery(); - emitter.onAny(() => {}); - emitter.onAny(() => {}); - emitter.onAny(() => {}); + emitter.onAny(() => { + }); + emitter.onAny(() => { + }); + emitter.onAny(() => { + }); t.is(emitter._anyEvents.size, 3); emitter.offAny(); t.is(emitter._anyEvents.size, 0); @@ -218,11 +261,16 @@ module.exports = Emittery => { test('clear()', t => { const emitter = new Emittery(); - emitter.on('🦄', () => {}); - emitter.on('🌈', () => {}); - emitter.on('🦄', () => {}); - emitter.onAny(() => {}); - emitter.onAny(() => {}); + emitter.on('🦄', () => { + }); + emitter.on('🌈', () => { + }); + emitter.on('🦄', () => { + }); + emitter.onAny(() => { + }); + emitter.onAny(() => { + }); t.is(emitter._events.size, 2); t.is(emitter._anyEvents.size, 2); emitter.clear(); @@ -232,11 +280,16 @@ module.exports = Emittery => { test('listenerCount()', t => { const emitter = new Emittery(); - emitter.on('🦄', () => {}); - emitter.on('🌈', () => {}); - emitter.on('🦄', () => {}); - emitter.onAny(() => {}); - emitter.onAny(() => {}); + emitter.on('🦄', () => { + }); + emitter.on('🌈', () => { + }); + emitter.on('🦄', () => { + }); + emitter.onAny(() => { + }); + emitter.onAny(() => { + }); t.is(emitter.listenerCount('🦄'), 4); t.is(emitter.listenerCount('🌈'), 3); t.is(emitter.listenerCount(), 5); @@ -244,7 +297,8 @@ module.exports = Emittery => { test('listenerCount() - works with empty eventName strings', t => { const emitter = new Emittery(); - emitter.on('', () => {}); + emitter.on('', () => { + }); t.is(emitter.listenerCount(''), 1); }); From 78cbcd678d962df5ffe192102ef44ed847d5b09a Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Tue, 2 Jan 2018 17:32:42 +0100 Subject: [PATCH 02/23] keep single line block formatting --- test/_run.js | 69 ++++++++++++++++++---------------------------------- 1 file changed, 23 insertions(+), 46 deletions(-) diff --git a/test/_run.js b/test/_run.js index 3e051f0..8707dac 100644 --- a/test/_run.js +++ b/test/_run.js @@ -4,10 +4,8 @@ import delay from 'delay'; module.exports = Emittery => { test('on()', t => { const emitter = new Emittery(); - const listener1 = () => { - }; - const listener2 = () => { - }; + const listener1 = () => {}; + const listener2 = () => {}; emitter.on('🦄', listener1); emitter.on('🦄', listener2); t.deepEqual([...emitter._events.get('🦄')], [listener1, listener2]); @@ -15,14 +13,12 @@ module.exports = Emittery => { test('on() - eventName must be a string', t => { const emitter = new Emittery(); - t.throws(() => emitter.on(42, () => { - }), TypeError); + t.throws(() => emitter.on(42, () => {}), TypeError); }); test('on() - returns a unsubcribe method', t => { const emitter = new Emittery(); - const listener = () => { - }; + const listener = () => {}; const off = emitter.on('🦄', listener); t.true(emitter._events.get('🦄').has(listener)); @@ -33,8 +29,7 @@ module.exports = Emittery => { test('on() - dedupes identical listeners', t => { const emitter = new Emittery(); - const listener = () => { - }; + const listener = () => {}; emitter.on('🦄', listener); emitter.on('🦄', listener); @@ -75,8 +70,7 @@ module.exports = Emittery => { test('off()', t => { const emitter = new Emittery(); - const listener = () => { - }; + const listener = () => {}; emitter.on('🦄', listener); t.is(emitter._events.get('🦄').size, 1); @@ -93,10 +87,8 @@ module.exports = Emittery => { test('off() - all listeners', t => { const emitter = new Emittery(); - emitter.on('🦄', () => { - }); - emitter.on('🦄', () => { - }); + emitter.on('🦄', () => {}); + emitter.on('🦄', () => {}); t.is(emitter._events.get('🦄').size, 2); emitter.off('🦄'); @@ -238,8 +230,7 @@ module.exports = Emittery => { test('offAny()', t => { const emitter = new Emittery(); - const listener = () => { - }; + const listener = () => {}; emitter.onAny(listener); t.is(emitter._anyEvents.size, 1); emitter.offAny(listener); @@ -248,12 +239,9 @@ module.exports = Emittery => { test('offAny() - all listeners', t => { const emitter = new Emittery(); - emitter.onAny(() => { - }); - emitter.onAny(() => { - }); - emitter.onAny(() => { - }); + emitter.onAny(() => {}); + emitter.onAny(() => {}); + emitter.onAny(() => {}); t.is(emitter._anyEvents.size, 3); emitter.offAny(); t.is(emitter._anyEvents.size, 0); @@ -261,16 +249,11 @@ module.exports = Emittery => { test('clear()', t => { const emitter = new Emittery(); - emitter.on('🦄', () => { - }); - emitter.on('🌈', () => { - }); - emitter.on('🦄', () => { - }); - emitter.onAny(() => { - }); - emitter.onAny(() => { - }); + emitter.on('🦄', () => {}); + emitter.on('🌈', () => {}); + emitter.on('🦄', () => {}); + emitter.onAny(() => {}); + emitter.onAny(() => {}); t.is(emitter._events.size, 2); t.is(emitter._anyEvents.size, 2); emitter.clear(); @@ -280,16 +263,11 @@ module.exports = Emittery => { test('listenerCount()', t => { const emitter = new Emittery(); - emitter.on('🦄', () => { - }); - emitter.on('🌈', () => { - }); - emitter.on('🦄', () => { - }); - emitter.onAny(() => { - }); - emitter.onAny(() => { - }); + emitter.on('🦄', () => {}); + emitter.on('🌈', () => {}); + emitter.on('🦄', () => {}); + emitter.onAny(() => {}); + emitter.onAny(() => {}); t.is(emitter.listenerCount('🦄'), 4); t.is(emitter.listenerCount('🌈'), 3); t.is(emitter.listenerCount(), 5); @@ -297,8 +275,7 @@ module.exports = Emittery => { test('listenerCount() - works with empty eventName strings', t => { const emitter = new Emittery(); - emitter.on('', () => { - }); + emitter.on('', () => {}); t.is(emitter.listenerCount(''), 1); }); From b50ec91c2dfb773b471e7ce6e4356de9771bdbdf Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Wed, 3 Jan 2018 01:30:59 +0100 Subject: [PATCH 03/23] fix xo and test setting --- index.js | 11 +++++++---- package.json | 9 ++++++++- test/_run.js | 42 ++++++++++++++++++++---------------------- 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/index.js b/index.js index 67de2fa..d0a1b2c 100644 --- a/index.js +++ b/index.js @@ -8,24 +8,27 @@ function assertEventName(eventName) { } } -async function* iterator(emitter, eventName) { +async function * iterator(emitter, eventName) { const queue = []; - const off = emitter.on(eventName, function (data) { + const off = emitter.on(eventName, data => { queue.push(data); }); try { + /* eslint-disable no-constant-condition */ + /* eslint-disable no-await-in-loop */ while (true) { - if (queue.length) { + if (queue.length > 0) { yield queue.shift(); } else { yield await emitter.once(eventName); } } + /* eslint-enable no-constant-condition */ + /* eslint-enable no-await-in-loop */ } finally { off(); } - } module.exports = class Emittery { diff --git a/package.json b/package.json index 085e70a..e930e6c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "babel --out-file=legacy.js index.js", "build:watch": "npm run build -- --watch", "prepublish": "npm run build", - "test": "xo && nyc ava" + "test": "nyc ava" }, "files": [ "index.js", @@ -50,6 +50,7 @@ "ava": "*", "babel-cli": "^6.26.0", "babel-core": "^6.26.0", + "babel-eslint": "^8.1.2", "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", @@ -58,11 +59,17 @@ "nyc": "^11.3.0", "xo": "*" }, + "xo": { + "parser": "babel-eslint" + }, "babel": { "plugins": [ "transform-async-to-generator", "transform-es2015-spread", "transform-async-generator-functions" + ], + "presets":[ + "@ava/stage-4" ] }, "ava": { diff --git a/test/_run.js b/test/_run.js index 8707dac..b2595a9 100644 --- a/test/_run.js +++ b/test/_run.js @@ -37,36 +37,34 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); - test.cb('on() - async iterator (queued)', async t => { - t.plan(3); + /* eslint-disable ava/no-async-fn-without-await */ + test('on() - async iterator', async t => { const fixture = '🌈'; const emitter = new Emittery(); - emitter.emit('🦄', fixture); - emitter.emit('🦄', fixture); - emitter.emit('🦄', fixture); - let count = 0; - for await (let data of emitter.on('🦄')) { - count++; - if (count > 3) { - break; - } - t.deepEqual(data, fixture); - } - t.end(); + setTimeout(() => { + emitter.emit('🦄', fixture); + }, 300); + const iterator = emitter.on('🦄'); + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); }); + /* eslint-enable ava/no-async-fn-without-await */ - test.cb('on() - async iterator (non queued)', async t => { - t.plan(1); + /* eslint-disable ava/no-async-fn-without-await */ + test.cb('on() - async iterator (queued)', t => { const fixture = '🌈'; const emitter = new Emittery(); - setTimeout(function () { - emitter.emit('🦄', fixture) - }, 300); - for await (let data of emitter.on('🦄')) { - t.deepEqual(data, fixture); + const iterator = emitter.on('🦄'); + emitter.emit('🦄', fixture); + setTimeout(async () => { + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); t.end(); - } + }, 300); }); + /* eslint-enable ava/no-async-fn-without-await */ test('off()', t => { const emitter = new Emittery(); From c904739d0fa23552c4a01ce41b99a2eccdc6420b Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Wed, 3 Jan 2018 01:43:34 +0100 Subject: [PATCH 04/23] enable xo --- package.json | 2 +- test/_run.js | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index e930e6c..904e90e 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build": "babel --out-file=legacy.js index.js", "build:watch": "npm run build -- --watch", "prepublish": "npm run build", - "test": "nyc ava" + "test": "xo && nyc ava" }, "files": [ "index.js", diff --git a/test/_run.js b/test/_run.js index b2595a9..a8d0f2b 100644 --- a/test/_run.js +++ b/test/_run.js @@ -52,17 +52,14 @@ module.exports = Emittery => { /* eslint-enable ava/no-async-fn-without-await */ /* eslint-disable ava/no-async-fn-without-await */ - test.cb('on() - async iterator (queued)', t => { + test('on() - async iterator (queued)', async t => { const fixture = '🌈'; const emitter = new Emittery(); const iterator = emitter.on('🦄'); emitter.emit('🦄', fixture); - setTimeout(async () => { - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - t.end(); - }, 300); + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); }); /* eslint-enable ava/no-async-fn-without-await */ From c9515119f4870df7ad3bfc0d342f37251091eaf1 Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Wed, 3 Jan 2018 01:46:24 +0100 Subject: [PATCH 05/23] removed useless eslint flag comments --- test/_run.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/_run.js b/test/_run.js index a8d0f2b..1891c05 100644 --- a/test/_run.js +++ b/test/_run.js @@ -37,7 +37,6 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); - /* eslint-disable ava/no-async-fn-without-await */ test('on() - async iterator', async t => { const fixture = '🌈'; const emitter = new Emittery(); @@ -49,9 +48,7 @@ module.exports = Emittery => { t.deepEqual(done, false); t.deepEqual(value, fixture); }); - /* eslint-enable ava/no-async-fn-without-await */ - /* eslint-disable ava/no-async-fn-without-await */ test('on() - async iterator (queued)', async t => { const fixture = '🌈'; const emitter = new Emittery(); @@ -61,7 +58,6 @@ module.exports = Emittery => { t.deepEqual(done, false); t.deepEqual(value, fixture); }); - /* eslint-enable ava/no-async-fn-without-await */ test('off()', t => { const emitter = new Emittery(); From d9b617a122660f3b556818eb266716d2f4dcc9f6 Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Wed, 3 Jan 2018 16:44:24 +0100 Subject: [PATCH 06/23] split async iterator behavior to different file --- index.js | 39 +++------------------------------------ iterator.js | 37 +++++++++++++++++++++++++++++++++++++ package.json | 14 +++----------- readme.md | 6 ++++++ test/_run.js | 22 ---------------------- test/iterator.js | 43 +++++++++++++++++++++++++++++++++++++++++++ util.js | 6 ++++++ 7 files changed, 98 insertions(+), 69 deletions(-) create mode 100644 iterator.js create mode 100644 test/iterator.js create mode 100644 util.js diff --git a/index.js b/index.js index d0a1b2c..c08f4b9 100644 --- a/index.js +++ b/index.js @@ -1,36 +1,8 @@ 'use strict'; +const assertEventName = require('./util'); const resolvedPromise = Promise.resolve(); -function assertEventName(eventName) { - if (typeof eventName !== 'string') { - throw new TypeError('eventName must be a string'); - } -} - -async function * iterator(emitter, eventName) { - const queue = []; - const off = emitter.on(eventName, data => { - queue.push(data); - }); - - try { - /* eslint-disable no-constant-condition */ - /* eslint-disable no-await-in-loop */ - while (true) { - if (queue.length > 0) { - yield queue.shift(); - } else { - yield await emitter.once(eventName); - } - } - /* eslint-enable no-constant-condition */ - /* eslint-enable no-await-in-loop */ - } finally { - off(); - } -} - module.exports = class Emittery { constructor() { this._events = new Map(); @@ -47,13 +19,8 @@ module.exports = class Emittery { on(eventName, listener) { assertEventName(eventName); - - if (typeof listener === 'function') { - this._getListeners(eventName).add(listener); - return this.off.bind(this, eventName, listener); - } - - return iterator(this, eventName); + this._getListeners(eventName).add(listener); + return this.off.bind(this, eventName, listener); } off(eventName, listener) { diff --git a/iterator.js b/iterator.js new file mode 100644 index 0000000..418cb35 --- /dev/null +++ b/iterator.js @@ -0,0 +1,37 @@ +'use strict'; +const assertEventName = require('./util'); +const EmitteryClass = require('./index'); + +async function * iterator(emitter, eventName) { + const queue = []; + const off = emitter.on(eventName, data => { + queue.push(data); + }); + + try { + /* eslint-disable no-constant-condition */ + /* eslint-disable no-await-in-loop */ + while (true) { + if (queue.length > 0) { + yield queue.shift(); + } else { + await emitter.once(eventName); + } + } + /* eslint-enable no-constant-condition */ + /* eslint-enable no-await-in-loop */ + } finally { + off(); + } +} + +module.exports = class IterableEmittery extends EmitteryClass { + on(eventName, listener) { + assertEventName(eventName); + if (typeof listener === 'function') { + this._getListeners(eventName).add(listener); + return this.off.bind(this, eventName, listener); + } + return iterator(this, eventName); + } +}; diff --git a/package.json b/package.json index 904e90e..79fa67c 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "build": "babel --out-file=legacy.js index.js", "build:watch": "npm run build -- --watch", "prepublish": "npm run build", - "test": "xo && nyc ava" + "test:next": "xo && node --harmony ./node_modules/.bin/ava", + "test": "xo && nyc ava ./test/main.js ./test/legacy.js" }, "files": [ "index.js", @@ -51,7 +52,6 @@ "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-eslint": "^8.1.2", - "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "codecov": "^3.0.0", @@ -65,17 +65,9 @@ "babel": { "plugins": [ "transform-async-to-generator", - "transform-es2015-spread", - "transform-async-generator-functions" - ], - "presets":[ - "@ava/stage-4" + "transform-es2015-spread" ] }, - "ava": { - "require": "babel-register", - "babel": "inherit" - }, "nyc": { "reporter": [ "html", diff --git a/readme.md b/readme.md index e7da2cb..059fae2 100644 --- a/readme.md +++ b/readme.md @@ -34,6 +34,10 @@ emitter.emit('🦄', '🌈'); The above only works in Node.js 8 or newer. For older Node.js versions you can use `require('emittery/legacy')`. +### Node.js 9+ + +If you want the benefits of async iterators syntax your can use emittery from `require('emittery/iterator')`. Note you'll need to pass the relevant harmony flag to your nodejs process. +[see API#on for more details](#oneventname-listener) ## API @@ -47,6 +51,8 @@ Returns an unsubscribe method. Using the same listener multiple times for the same event will result in only one method call per emitted event. +##### Async iterator syntax + If you use the method with only the first argument, it will return an asynchronous iterator. Your listener will therefore be the loop body and you'll be able to unsubscribe to the event simply by breaking the loop. diff --git a/test/_run.js b/test/_run.js index 1891c05..db5c61f 100644 --- a/test/_run.js +++ b/test/_run.js @@ -37,28 +37,6 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); - test('on() - async iterator', async t => { - const fixture = '🌈'; - const emitter = new Emittery(); - setTimeout(() => { - emitter.emit('🦄', fixture); - }, 300); - const iterator = emitter.on('🦄'); - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - }); - - test('on() - async iterator (queued)', async t => { - const fixture = '🌈'; - const emitter = new Emittery(); - const iterator = emitter.on('🦄'); - emitter.emit('🦄', fixture); - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - }); - test('off()', t => { const emitter = new Emittery(); const listener = () => {}; diff --git a/test/iterator.js b/test/iterator.js new file mode 100644 index 0000000..4cb7b6a --- /dev/null +++ b/test/iterator.js @@ -0,0 +1,43 @@ +import test from 'ava'; + +let Emittery; +try { + Emittery = require('../iterator'); +} catch (err) { + test('does not work due to syntax errors', t => { + t.is(err.name, 'SyntaxError'); + }); +} + +if (Emittery) { + require('./_run')(Emittery); + /* eslint-disable ava/no-async-fn-without-await */ + test('on() - async iterator (for await)', async t => { + t.plan(3); + const fixture = '🌈'; + const emitter = new Emittery(); + setInterval(() => { + emitter.emit('🦄', fixture); + }, 50); + let count = 0; + for await (const data of emitter.on('🦄')) { + count++; + if (count >= 3) { + break; + } + t.deepEqual(data, fixture); + } + }); + /* eslint-enable ava/no-async-fn-without-await */ + + test('on() - async iterator', async t => { + const fixture = '🌈'; + const emitter = new Emittery(); + const iterator = emitter.on('🦄'); + emitter.emit('🦄', fixture); + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); + }); +} + diff --git a/util.js b/util.js new file mode 100644 index 0000000..689a409 --- /dev/null +++ b/util.js @@ -0,0 +1,6 @@ +'use strict'; +module.exports = function (eventName) { + if (typeof eventName !== 'string') { + throw new TypeError('eventName must be a string'); + } +}; From cac49ab9c98336baee8dbbaece1a9a19a570082d Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Thu, 4 Jan 2018 14:42:09 +0000 Subject: [PATCH 07/23] Revert "split async iterator behavior to different file" This reverts commit d9b617a122660f3b556818eb266716d2f4dcc9f6. --- index.js | 39 ++++++++++++++++++++++++++++++++++++--- iterator.js | 37 ------------------------------------- package.json | 14 +++++++++++--- readme.md | 6 ------ test/_run.js | 22 ++++++++++++++++++++++ test/iterator.js | 43 ------------------------------------------- util.js | 6 ------ 7 files changed, 69 insertions(+), 98 deletions(-) delete mode 100644 iterator.js delete mode 100644 test/iterator.js delete mode 100644 util.js diff --git a/index.js b/index.js index c08f4b9..d0a1b2c 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,36 @@ 'use strict'; -const assertEventName = require('./util'); const resolvedPromise = Promise.resolve(); +function assertEventName(eventName) { + if (typeof eventName !== 'string') { + throw new TypeError('eventName must be a string'); + } +} + +async function * iterator(emitter, eventName) { + const queue = []; + const off = emitter.on(eventName, data => { + queue.push(data); + }); + + try { + /* eslint-disable no-constant-condition */ + /* eslint-disable no-await-in-loop */ + while (true) { + if (queue.length > 0) { + yield queue.shift(); + } else { + yield await emitter.once(eventName); + } + } + /* eslint-enable no-constant-condition */ + /* eslint-enable no-await-in-loop */ + } finally { + off(); + } +} + module.exports = class Emittery { constructor() { this._events = new Map(); @@ -19,8 +47,13 @@ module.exports = class Emittery { on(eventName, listener) { assertEventName(eventName); - this._getListeners(eventName).add(listener); - return this.off.bind(this, eventName, listener); + + if (typeof listener === 'function') { + this._getListeners(eventName).add(listener); + return this.off.bind(this, eventName, listener); + } + + return iterator(this, eventName); } off(eventName, listener) { diff --git a/iterator.js b/iterator.js deleted file mode 100644 index 418cb35..0000000 --- a/iterator.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; -const assertEventName = require('./util'); -const EmitteryClass = require('./index'); - -async function * iterator(emitter, eventName) { - const queue = []; - const off = emitter.on(eventName, data => { - queue.push(data); - }); - - try { - /* eslint-disable no-constant-condition */ - /* eslint-disable no-await-in-loop */ - while (true) { - if (queue.length > 0) { - yield queue.shift(); - } else { - await emitter.once(eventName); - } - } - /* eslint-enable no-constant-condition */ - /* eslint-enable no-await-in-loop */ - } finally { - off(); - } -} - -module.exports = class IterableEmittery extends EmitteryClass { - on(eventName, listener) { - assertEventName(eventName); - if (typeof listener === 'function') { - this._getListeners(eventName).add(listener); - return this.off.bind(this, eventName, listener); - } - return iterator(this, eventName); - } -}; diff --git a/package.json b/package.json index 79fa67c..904e90e 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,7 @@ "build": "babel --out-file=legacy.js index.js", "build:watch": "npm run build -- --watch", "prepublish": "npm run build", - "test:next": "xo && node --harmony ./node_modules/.bin/ava", - "test": "xo && nyc ava ./test/main.js ./test/legacy.js" + "test": "xo && nyc ava" }, "files": [ "index.js", @@ -52,6 +51,7 @@ "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-eslint": "^8.1.2", + "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "codecov": "^3.0.0", @@ -65,9 +65,17 @@ "babel": { "plugins": [ "transform-async-to-generator", - "transform-es2015-spread" + "transform-es2015-spread", + "transform-async-generator-functions" + ], + "presets":[ + "@ava/stage-4" ] }, + "ava": { + "require": "babel-register", + "babel": "inherit" + }, "nyc": { "reporter": [ "html", diff --git a/readme.md b/readme.md index 059fae2..e7da2cb 100644 --- a/readme.md +++ b/readme.md @@ -34,10 +34,6 @@ emitter.emit('🦄', '🌈'); The above only works in Node.js 8 or newer. For older Node.js versions you can use `require('emittery/legacy')`. -### Node.js 9+ - -If you want the benefits of async iterators syntax your can use emittery from `require('emittery/iterator')`. Note you'll need to pass the relevant harmony flag to your nodejs process. -[see API#on for more details](#oneventname-listener) ## API @@ -51,8 +47,6 @@ Returns an unsubscribe method. Using the same listener multiple times for the same event will result in only one method call per emitted event. -##### Async iterator syntax - If you use the method with only the first argument, it will return an asynchronous iterator. Your listener will therefore be the loop body and you'll be able to unsubscribe to the event simply by breaking the loop. diff --git a/test/_run.js b/test/_run.js index db5c61f..1891c05 100644 --- a/test/_run.js +++ b/test/_run.js @@ -37,6 +37,28 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); + test('on() - async iterator', async t => { + const fixture = '🌈'; + const emitter = new Emittery(); + setTimeout(() => { + emitter.emit('🦄', fixture); + }, 300); + const iterator = emitter.on('🦄'); + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); + }); + + test('on() - async iterator (queued)', async t => { + const fixture = '🌈'; + const emitter = new Emittery(); + const iterator = emitter.on('🦄'); + emitter.emit('🦄', fixture); + const {value, done} = await iterator.next(); + t.deepEqual(done, false); + t.deepEqual(value, fixture); + }); + test('off()', t => { const emitter = new Emittery(); const listener = () => {}; diff --git a/test/iterator.js b/test/iterator.js deleted file mode 100644 index 4cb7b6a..0000000 --- a/test/iterator.js +++ /dev/null @@ -1,43 +0,0 @@ -import test from 'ava'; - -let Emittery; -try { - Emittery = require('../iterator'); -} catch (err) { - test('does not work due to syntax errors', t => { - t.is(err.name, 'SyntaxError'); - }); -} - -if (Emittery) { - require('./_run')(Emittery); - /* eslint-disable ava/no-async-fn-without-await */ - test('on() - async iterator (for await)', async t => { - t.plan(3); - const fixture = '🌈'; - const emitter = new Emittery(); - setInterval(() => { - emitter.emit('🦄', fixture); - }, 50); - let count = 0; - for await (const data of emitter.on('🦄')) { - count++; - if (count >= 3) { - break; - } - t.deepEqual(data, fixture); - } - }); - /* eslint-enable ava/no-async-fn-without-await */ - - test('on() - async iterator', async t => { - const fixture = '🌈'; - const emitter = new Emittery(); - const iterator = emitter.on('🦄'); - emitter.emit('🦄', fixture); - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - }); -} - diff --git a/util.js b/util.js deleted file mode 100644 index 689a409..0000000 --- a/util.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; -module.exports = function (eventName) { - if (typeof eventName !== 'string') { - throw new TypeError('eventName must be a string'); - } -}; From a4d414d76f764bb1d9daa9b3688dcda36b953d8e Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Thu, 4 Jan 2018 15:23:39 +0000 Subject: [PATCH 08/23] Implement async iterator protocol without generators, fix tests --- index.js | 46 ++++++++++++++++++++++++++++++--------------- package.json | 25 ++++++++++++++----------- test/_run.js | 53 ++++++++++++++++++++++++++++++++++------------------ 3 files changed, 80 insertions(+), 44 deletions(-) diff --git a/index.js b/index.js index 0bbacfe..a9983e7 100644 --- a/index.js +++ b/index.js @@ -8,27 +8,43 @@ function assertEventName(eventName) { } } -async function * iterator(emitter, eventName) { - const queue = []; +function iterator(emitter, eventName) { + let flush = null; + let queue = []; const off = emitter.on(eventName, data => { - queue.push(data); + if (flush) { + flush(data); + } else { + queue.push(data); + } }); - try { - /* eslint-disable no-constant-condition */ - /* eslint-disable no-await-in-loop */ - while (true) { + return { + async next() { + if (!queue) { + return {done: true}; + } + if (queue.length > 0) { - yield queue.shift(); - } else { - yield await emitter.once(eventName); + return {done: false, value: queue.shift()}; } + + const value = await new Promise(resolve => { + flush = data => { + resolve(data); + flush = null; + }; + }); + return {done: false, value}; + }, + return() { + off(); + queue = null; + }, + [Symbol.asyncIterator]() { + return this; } - /* eslint-enable no-constant-condition */ - /* eslint-enable no-await-in-loop */ - } finally { - off(); - } + }; } class Emittery { diff --git a/package.json b/package.json index 1f3ce34..8779055 100644 --- a/package.json +++ b/package.json @@ -66,28 +66,31 @@ "typescript": "^2.6.2", "xo": "*" }, - "xo": { - "parser": "babel-eslint" + "ava": { + "babel": { + "plugins": [ + "transform-async-generator-functions" + ], + "presets": [ + "@ava/stage-4", + "@ava/transform-test-files" + ] + } }, "babel": { "plugins": [ "transform-async-to-generator", - "transform-es2015-spread", - "transform-async-generator-functions" - ], - "presets":[ - "@ava/stage-4" + "transform-es2015-spread" ] }, - "ava": { - "require": "babel-register", - "babel": "inherit" - }, "nyc": { "reporter": [ "html", "lcov", "text" ] + }, + "xo": { + "parser": "babel-eslint" } } diff --git a/test/_run.js b/test/_run.js index 1891c05..4b0e191 100644 --- a/test/_run.js +++ b/test/_run.js @@ -1,6 +1,24 @@ import test from 'ava'; import delay from 'delay'; +// babel-plugin-transform-async-generator-functions assumes +// `Symbol.asyncIterator` exists, so stub it for iterator tests. +function stubAsyncIteratorSymbol(next) { + return async (...args) => { + if (!Symbol.asyncIterator) { + Symbol.asyncIterator = Symbol.for('Emittery.asyncIterator'); + } + + try { + return await next(...args); + } finally { + if (Symbol.asyncIterator === Symbol.for('Emittery.asyncIterator')) { + delete Symbol.asyncIterator; + } + } + }; +} + module.exports = Emittery => { test('on()', t => { const emitter = new Emittery(); @@ -37,27 +55,26 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); - test('on() - async iterator', async t => { - const fixture = '🌈'; + test.serial('on() - async iterator', stubAsyncIteratorSymbol(async t => { const emitter = new Emittery(); - setTimeout(() => { - emitter.emit('🦄', fixture); - }, 300); const iterator = emitter.on('🦄'); - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - }); - test('on() - async iterator (queued)', async t => { - const fixture = '🌈'; - const emitter = new Emittery(); - const iterator = emitter.on('🦄'); - emitter.emit('🦄', fixture); - const {value, done} = await iterator.next(); - t.deepEqual(done, false); - t.deepEqual(value, fixture); - }); + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', '🌟'); + }, 10); + + t.plan(3); + const expected = ['🌈', '🌟']; + for await (const data of iterator) { + t.deepEqual(data, expected.shift()); + if (expected.length === 0) { + break; + } + } + + t.deepEqual(await iterator.next(), {done: true}); + })); test('off()', t => { const emitter = new Emittery(); From a6a3476fd3f77836e73feaf6f37075bff513dc1b Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 14:57:38 +0000 Subject: [PATCH 09/23] Fix linting error --- test/_run.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/_run.js b/test/_run.js index 4b0e191..baf4d21 100644 --- a/test/_run.js +++ b/test/_run.js @@ -1,7 +1,7 @@ import test from 'ava'; import delay from 'delay'; -// babel-plugin-transform-async-generator-functions assumes +// The babel-plugin-transform-async-generator-functions plugin assumes // `Symbol.asyncIterator` exists, so stub it for iterator tests. function stubAsyncIteratorSymbol(next) { return async (...args) => { From e91559c24c4418c521393c49125bc8e668d8c018 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 14:58:00 +0000 Subject: [PATCH 10/23] Simplify next() implementation --- index.js | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index a9983e7..c6fcf55 100644 --- a/index.js +++ b/index.js @@ -9,14 +9,11 @@ function assertEventName(eventName) { } function iterator(emitter, eventName) { - let flush = null; + let flush = () => {}; let queue = []; const off = emitter.on(eventName, data => { - if (flush) { - flush(data); - } else { - queue.push(data); - } + queue.push(data); + flush(); }); return { @@ -25,17 +22,14 @@ function iterator(emitter, eventName) { return {done: true}; } - if (queue.length > 0) { - return {done: false, value: queue.shift()}; + if (queue.length === 0) { + await new Promise(resolve => { + flush = resolve; + }); + return this.next(); } - const value = await new Promise(resolve => { - flush = data => { - resolve(data); - flush = null; - }; - }); - return {done: false, value}; + return {done: false, value: queue.shift()}; }, return() { off(); From c4ca867bbd8a43c136f65b30a34be0f2dc6b6a80 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 15:04:07 +0000 Subject: [PATCH 11/23] Handle edge case where return() is called by an earlier listener for the same event This clears 'queue' before the iterator pushes to it, which shouldn't cause a crash. --- index.js | 4 +++- test/_run.js | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index c6fcf55..52bb2b2 100644 --- a/index.js +++ b/index.js @@ -12,7 +12,9 @@ function iterator(emitter, eventName) { let flush = () => {}; let queue = []; const off = emitter.on(eventName, data => { - queue.push(data); + if (queue) { + queue.push(data); + } flush(); }); diff --git a/test/_run.js b/test/_run.js index baf4d21..23be314 100644 --- a/test/_run.js +++ b/test/_run.js @@ -76,6 +76,17 @@ module.exports = Emittery => { t.deepEqual(await iterator.next(), {done: true}); })); + test('on() - async iterator - return() called during emit', async t => { + const emitter = new Emittery(); + let iterator = null; + emitter.on('🦄', () => { + iterator.return(); + }); + iterator = emitter.on('🦄'); + emitter.emit('🦄'); + t.deepEqual(await iterator.next(), {done: true}); + }); + test('off()', t => { const emitter = new Emittery(); const listener = () => {}; From e18c260c0f419014c701623ec5b42309da267d21 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 15:07:16 +0000 Subject: [PATCH 12/23] Implement return() according to spec --- index.js | 5 ++++- test/_run.js | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 52bb2b2..e76be1d 100644 --- a/index.js +++ b/index.js @@ -33,9 +33,12 @@ function iterator(emitter, eventName) { return {done: false, value: queue.shift()}; }, - return() { + async return(value) { off(); queue = null; + return arguments.length > 0 ? + {done: true, value: await value} : + {done: true}; }, [Symbol.asyncIterator]() { return this; diff --git a/test/_run.js b/test/_run.js index 23be314..83ff02e 100644 --- a/test/_run.js +++ b/test/_run.js @@ -87,6 +87,18 @@ module.exports = Emittery => { t.deepEqual(await iterator.next(), {done: true}); }); + test('on() - async iterator - return() awaits its argument', async t => { + const emitter = new Emittery(); + const iterator = emitter.on('🦄'); + t.deepEqual(await iterator.return(Promise.resolve(1)), {done: true, value: 1}); + }); + + test('on() - async iterator - return() without argument', async t => { + const emitter = new Emittery(); + const iterator = emitter.on('🦄'); + t.deepEqual(await iterator.return(), {done: true}); + }); + test('off()', t => { const emitter = new Emittery(); const listener = () => {}; From edb7ac5f2a476f30e3023e911ddc17bfbcf0a94a Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 15:59:43 +0000 Subject: [PATCH 13/23] Remove unnecessary tsconfig files in test fixtures --- test/fixtures/compiles/tsconfig.json | 11 ----------- test/fixtures/tsconfig.json | 11 ----------- 2 files changed, 22 deletions(-) delete mode 100644 test/fixtures/compiles/tsconfig.json delete mode 100644 test/fixtures/tsconfig.json diff --git a/test/fixtures/compiles/tsconfig.json b/test/fixtures/compiles/tsconfig.json deleted file mode 100644 index 27a6731..0000000 --- a/test/fixtures/compiles/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es2017", - "lib": ["es2017"], - "module": "commonjs", - "strict": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true - }, - "include": ["*.ts"] -} diff --git a/test/fixtures/tsconfig.json b/test/fixtures/tsconfig.json deleted file mode 100644 index 27a6731..0000000 --- a/test/fixtures/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "compilerOptions": { - "target": "es2017", - "lib": ["es2017"], - "module": "commonjs", - "strict": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true - }, - "include": ["*.ts"] -} From 756b47a9620ac2699ce10c990bbf1a35af851b2d Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:00:06 +0000 Subject: [PATCH 14/23] Change how TypeScript is loaded in types test --- test/types.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/types.js b/test/types.js index ff27ab4..52cbc27 100644 --- a/test/types.js +++ b/test/types.js @@ -2,7 +2,9 @@ import path from 'path'; import test from 'ava'; import glob from 'glob'; -import * as ts from 'typescript'; + +// Import syntax trips up Atom with ide-typescript loaded. +const ts = require('typescript'); const compilerOptions = { target: ts.ScriptTarget.ES2017, From 7ef298341473fd61c23bc5d95e6fcd4167050814 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:02:14 +0000 Subject: [PATCH 15/23] Return async iterator from .events(), not .on() Add to TS definition and README. --- Emittery.d.ts | 11 +++++++++++ index.js | 13 ++++++------- readme.md | 6 ++++++ test/_run.js | 16 ++++++++-------- test/types.js | 2 +- tsconfig.json | 5 ++++- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index 76d7d92..1c74235 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -27,6 +27,14 @@ declare class Emittery { */ once(eventName: string): Promise; + /** + * Get an asynchronous iterator which buffers data each time an event is + * emitted. + * + * Call `return()` on the iterator to remove the subscription. + */ + events(eventName: string): AsyncIterableIterator; + /** * Trigger an event asynchronously, optionally with some data. Listeners * are called in the order they were added, but execute concurrently. @@ -122,6 +130,9 @@ declare namespace Emittery { off(eventName: Name, listener?: (eventData: EventDataMap[Name]) => any): void; off(eventName: Name, listener?: () => any): void; + events(eventName: Name): AsyncIterableIterator; + events(eventName: Name): AsyncIterableIterator; + onAny(listener: (eventName: Name, eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; onAny(listener: (eventName: Name) => any): Emittery.UnsubscribeFn; diff --git a/index.js b/index.js index e76be1d..f959629 100644 --- a/index.js +++ b/index.js @@ -62,13 +62,8 @@ class Emittery { on(eventName, listener) { assertEventName(eventName); - - if (typeof listener === 'function') { - this._getListeners(eventName).add(listener); - return this.off.bind(this, eventName, listener); - } - - return iterator(this, eventName); + this._getListeners(eventName).add(listener); + return this.off.bind(this, eventName, listener); } off(eventName, listener) { @@ -90,6 +85,10 @@ class Emittery { }); } + events(eventName) { + return iterator(this, eventName); + } + async emit(eventName, eventData) { assertEventName(eventName); await resolvedPromise; diff --git a/readme.md b/readme.md index bed7217..36ac0ff 100644 --- a/readme.md +++ b/readme.md @@ -94,6 +94,12 @@ emitter.once('🦄').then(data => { emitter.emit('🦄', '🌈'); ``` +#### events(eventName) + +Get an asynchronous iterator which buffers data each time an event is emitted. + +Call `return()` on the iterator to remove the subscription. + #### emit(eventName, [data]) Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but execute concurrently. diff --git a/test/_run.js b/test/_run.js index 83ff02e..c679523 100644 --- a/test/_run.js +++ b/test/_run.js @@ -55,9 +55,9 @@ module.exports = Emittery => { t.is(emitter._events.get('🦄').size, 1); }); - test.serial('on() - async iterator', stubAsyncIteratorSymbol(async t => { + test.serial('events()', stubAsyncIteratorSymbol(async t => { const emitter = new Emittery(); - const iterator = emitter.on('🦄'); + const iterator = emitter.events('🦄'); await emitter.emit('🦄', '🌈'); setTimeout(() => { @@ -76,26 +76,26 @@ module.exports = Emittery => { t.deepEqual(await iterator.next(), {done: true}); })); - test('on() - async iterator - return() called during emit', async t => { + test('events() - return() called during emit', async t => { const emitter = new Emittery(); let iterator = null; emitter.on('🦄', () => { iterator.return(); }); - iterator = emitter.on('🦄'); + iterator = emitter.events('🦄'); emitter.emit('🦄'); t.deepEqual(await iterator.next(), {done: true}); }); - test('on() - async iterator - return() awaits its argument', async t => { + test('events() - return() awaits its argument', async t => { const emitter = new Emittery(); - const iterator = emitter.on('🦄'); + const iterator = emitter.events('🦄'); t.deepEqual(await iterator.return(Promise.resolve(1)), {done: true, value: 1}); }); - test('on() - async iterator - return() without argument', async t => { + test('events() - return() without argument', async t => { const emitter = new Emittery(); - const iterator = emitter.on('🦄'); + const iterator = emitter.events('🦄'); t.deepEqual(await iterator.return(), {done: true}); }); diff --git a/test/types.js b/test/types.js index 52cbc27..d834ff7 100644 --- a/test/types.js +++ b/test/types.js @@ -7,7 +7,7 @@ import glob from 'glob'; const ts = require('typescript'); const compilerOptions = { - target: ts.ScriptTarget.ES2017, + target: ts.ScriptTarget.ESNext, module: ts.ModuleKind.CommonJS, strict: true, noEmit: true diff --git a/tsconfig.json b/tsconfig.json index 21b3bd7..e8ebc14 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,10 @@ "compilerOptions": { "allowSyntheticDefaultImports": true, "alwaysStrict": true, - "lib": ["es2017"], + "lib": [ + "es2017", + "esnext.asynciterable" + ], "module": "commonjs", "moduleResolution": "node", "noImplicitAny": true, From 463c4e6535ef87ca6098eaad9fde43acef7f6e06 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:25:22 +0000 Subject: [PATCH 16/23] Implement .anyEvent() --- Emittery.d.ts | 11 +++++++++++ index.js | 27 +++++++++++++++++++++------ readme.md | 6 ++++++ test/_run.js | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 6 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index 1c74235..d124505 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -77,6 +77,14 @@ declare class Emittery { */ offAny(listener?: (eventName: string, eventData?: any) => any): void; + /** + * Get an asynchronous iterator which buffers a tuple of an event name and + * data each time an event is emitted. + * + * Call `return()` on the iterator to remove the subscription. + */ + anyEvent(): AsyncIterableIterator<[string, any]>; + /** * Clear all event listeners on the instance. */ @@ -139,6 +147,9 @@ declare namespace Emittery { offAny(listener?: (eventName: Name, eventData: EventDataMap[Name]) => any): void; offAny(listener?: (eventName: Name) => any): void; + anyEvent(): AsyncIterableIterator<[Name, EventDataMap[Name]]>; + anyEvent(): AsyncIterableIterator<[Name, void]>; + emit(eventName: Name, eventData: EventDataMap[Name]): Promise; emit(eventName: Name): Promise; diff --git a/index.js b/index.js index f959629..be16d54 100644 --- a/index.js +++ b/index.js @@ -11,12 +11,22 @@ function assertEventName(eventName) { function iterator(emitter, eventName) { let flush = () => {}; let queue = []; - const off = emitter.on(eventName, data => { - if (queue) { - queue.push(data); - } - flush(); - }); + let off; + if (typeof eventName === 'string') { + off = emitter.on(eventName, data => { + if (queue) { + queue.push(data); + } + flush(); + }); + } else { + off = emitter.onAny((eventName, data) => { + if (queue) { + queue.push([eventName, data]); + } + flush(); + }); + } return { async next() { @@ -86,6 +96,7 @@ class Emittery { } events(eventName) { + assertEventName(eventName); return iterator(this, eventName); } @@ -125,6 +136,10 @@ class Emittery { } } + anyEvent() { + return iterator(this); + } + clear() { this._events.clear(); this._anyEvents.clear(); diff --git a/readme.md b/readme.md index 36ac0ff..8b7b235 100644 --- a/readme.md +++ b/readme.md @@ -126,6 +126,12 @@ Remove an `onAny` subscription. If you don't pass in a `listener`, it will remove all `onAny` subscriptions. +#### anyEvent() + +Get an asynchronous iterator which buffers a tuple of an event name and data each time an event is emitted. + +Call `return()` on the iterator to remove the subscription. + #### clear() Clear all event listeners on the instance. diff --git a/test/_run.js b/test/_run.js index c679523..163a553 100644 --- a/test/_run.js +++ b/test/_run.js @@ -278,6 +278,38 @@ module.exports = Emittery => { t.is(emitter._anyEvents.size, 0); }); + test.serial('anyEvent()', stubAsyncIteratorSymbol(async t => { + const emitter = new Emittery(); + const iterator = emitter.anyEvent(); + + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', '🌟'); + }, 10); + + t.plan(3); + const expected = [['🦄', '🌈'], ['🦄', '🌟']]; + for await (const data of iterator) { + t.deepEqual(data, expected.shift()); + if (expected.length === 0) { + break; + } + } + + t.deepEqual(await iterator.next(), {done: true}); + })); + + test('anyEvent() - return() called during emit', async t => { + const emitter = new Emittery(); + let iterator = null; + emitter.onAny(() => { + iterator.return(); + }); + iterator = emitter.anyEvent(); + emitter.emit('🦄'); + t.deepEqual(await iterator.next(), {done: true}); + }); + test('clear()', t => { const emitter = new Emittery(); emitter.on('🦄', () => {}); From 3f9aa37a77fdced6dabdc2848bd2893fdd416083 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 17:03:03 +0000 Subject: [PATCH 17/23] Tweak AVA's Babel options --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 8779055..e70c4c3 100644 --- a/package.json +++ b/package.json @@ -68,12 +68,12 @@ }, "ava": { "babel": { + "babelrc": false, "plugins": [ "transform-async-generator-functions" ], "presets": [ - "@ava/stage-4", - "@ava/transform-test-files" + "@ava/stage-4" ] } }, From ef596e650075ea57e0ce3239711dd8d26f157692 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 18:03:27 +0000 Subject: [PATCH 18/23] Support running tests without for-await-of transpilation Feature-detect for-await-of support, and only transpile this feature when necessary. With Node.js 9 you can use the following to run tests with the native implementation: ```console $ node --harmony_async_iteration ./node_modules/.bin/ava ``` --- package.json | 3 ++- test/_for-await.js | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 test/_for-await.js diff --git a/package.json b/package.json index e70c4c3..2003496 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-eslint": "^8.1.2", + "babel-plugin-syntax-async-generators": "^6.13.0", "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", @@ -70,7 +71,7 @@ "babel": { "babelrc": false, "plugins": [ - "transform-async-generator-functions" + "./test/_for-await.js" ], "presets": [ "@ava/stage-4" diff --git a/test/_for-await.js b/test/_for-await.js new file mode 100644 index 0000000..7a0acef --- /dev/null +++ b/test/_for-await.js @@ -0,0 +1,9 @@ +'use strict'; +const vm = require('vm'); + +try { + vm.runInNewContext('async () => { for await (const _ of []) {} }'); + module.exports = require('babel-plugin-syntax-async-generators'); +} catch (err) { + module.exports = require('babel-plugin-transform-async-generator-functions'); +} From 361cddd2bd65336adeee74dac7b3d76d1b9dfa53 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 7 Jan 2018 14:40:55 +0000 Subject: [PATCH 19/23] Fix for-await transpilation This syntax is transformed using async-to-generator, not async-generator-functions. --- package.json | 5 +---- test/_for-await.js | 9 ++++++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 2003496..dd00d5d 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,6 @@ "babel-core": "^6.26.0", "babel-eslint": "^8.1.2", "babel-plugin-syntax-async-generators": "^6.13.0", - "babel-plugin-transform-async-generator-functions": "^6.24.1", "babel-plugin-transform-async-to-generator": "^6.24.1", "babel-plugin-transform-es2015-spread": "^6.22.0", "codecov": "^3.0.0", @@ -70,10 +69,8 @@ "ava": { "babel": { "babelrc": false, - "plugins": [ - "./test/_for-await.js" - ], "presets": [ + "./test/_for-await.js", "@ava/stage-4" ] } diff --git a/test/_for-await.js b/test/_for-await.js index 7a0acef..058d007 100644 --- a/test/_for-await.js +++ b/test/_for-await.js @@ -1,9 +1,12 @@ 'use strict'; const vm = require('vm'); +const syntax = require('babel-plugin-syntax-async-generators'); +const transform = require('babel-plugin-transform-async-to-generator'); + try { - vm.runInNewContext('async () => { for await (const _ of []) {} }'); - module.exports = require('babel-plugin-syntax-async-generators'); + new vm.Script('async () => { for await (const _ of []) {} }'); // eslint-disable-line no-new + module.exports = {plugins: [syntax]}; } catch (err) { - module.exports = require('babel-plugin-transform-async-generator-functions'); + module.exports = {plugins: [syntax, transform]}; } From 735ee107f1fd0b94ae84dcc95f574033d4a54c02 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 7 Jan 2018 14:47:04 +0000 Subject: [PATCH 20/23] Ensure async iterators return non-promise values Follows https://tc39.github.io/proposal-async-iteration/#sec-asynciterator-interface --- index.js | 4 ++-- test/_run.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/index.js b/index.js index be16d54..ff375ae 100644 --- a/index.js +++ b/index.js @@ -22,7 +22,7 @@ function iterator(emitter, eventName) { } else { off = emitter.onAny((eventName, data) => { if (queue) { - queue.push([eventName, data]); + queue.push(Promise.all([eventName, data])); } flush(); }); @@ -41,7 +41,7 @@ function iterator(emitter, eventName) { return this.next(); } - return {done: false, value: queue.shift()}; + return {done: false, value: await queue.shift()}; }, async return(value) { off(); diff --git a/test/_run.js b/test/_run.js index 163a553..fba43a2 100644 --- a/test/_run.js +++ b/test/_run.js @@ -61,7 +61,7 @@ module.exports = Emittery => { await emitter.emit('🦄', '🌈'); setTimeout(() => { - emitter.emit('🦄', '🌟'); + emitter.emit('🦄', Promise.resolve('🌟')); }, 10); t.plan(3); @@ -284,7 +284,7 @@ module.exports = Emittery => { await emitter.emit('🦄', '🌈'); setTimeout(() => { - emitter.emit('🦄', '🌟'); + emitter.emit('🦄', Promise.resolve('🌟')); }, 10); t.plan(3); From 0afc521575b5a9426f62c48a3ae2ec8033dc41e5 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 20 Jan 2018 16:40:52 +0000 Subject: [PATCH 21/23] Tweak iterator implementation now that scheduling is more consistent --- index.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 2633020..3ec72c4 100644 --- a/index.js +++ b/index.js @@ -16,16 +16,12 @@ function iterator(emitter, eventName) { let off; if (typeof eventName === 'string') { off = emitter.on(eventName, data => { - if (queue) { - queue.push(data); - } + queue.push(data); flush(); }); } else { off = emitter.onAny((eventName, data) => { - if (queue) { - queue.push(Promise.all([eventName, data])); - } + queue.push(Promise.all([eventName, data])); flush(); }); } @@ -46,8 +42,9 @@ function iterator(emitter, eventName) { return {done: false, value: await queue.shift()}; }, async return(value) { - off(); queue = null; + off(); + flush(); return arguments.length > 0 ? {done: true, value: await value} : {done: true}; From 4cdcd6fd155f8a3a0d04d451ddeb7813c201a81d Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 20 Jan 2018 16:41:26 +0000 Subject: [PATCH 22/23] Remove wrongly placed documentation --- readme.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/readme.md b/readme.md index b9a2f8b..2ec1891 100644 --- a/readme.md +++ b/readme.md @@ -47,28 +47,6 @@ Returns an unsubscribe method. Using the same listener multiple times for the same event will result in only one method call per emitted event. -If you use the method with only the first argument, it will return an asynchronous iterator. Your listener will therefore be the loop body and you'll be able to -unsubscribe to the event simply by breaking the loop. - -```Javascript - const emitter = new Emittery(); - //subscribe - (async function () { - for await (let t of emitter.on('foo')) { - if(t >10){ - break;//unsubscribe - } - console.log(t); - } - })(); - - let count = 0; - setInterval(function () { - count++; - emitter.emit('foo', count); - }, 1000); -``` - ##### listener(data) #### off(eventName, listener) From 94f54c0207a643e8f7f2d191a9114922274d9128 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 20 Jan 2018 17:34:24 +0000 Subject: [PATCH 23/23] Separate iterator production clearListeners() now signals iterators to not expect further items, without clearing the current queue. Clarify that clearListeners() also impacts iterators. Similarly include the number of active iterators in listenerCount(). Enqueue new iterator items synchronously, before listeners are called. --- Emittery.d.ts | 9 ++--- index.js | 98 ++++++++++++++++++++++++++++++++++++++------------- readme.md | 9 ++--- test/_run.js | 50 +++++++++++++++++++++++--- 4 files changed, 129 insertions(+), 37 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index 1f9d145..6150545 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -81,15 +81,16 @@ declare class Emittery { anyEvent(): AsyncIterableIterator<[string, any]>; /** - * Clear all event listeners on the instance. + * Clear all iterators and event listeners on the instance. * - * If `eventName` is given, only the listeners for that event are cleared. + * If `eventName` is given, only the iterators and listeners for that event + * are cleared. */ clearListeners(eventName?: string): void; /** - * The number of listeners for the `eventName` or all events if not - * specified. + * The number of iterators and listeners for the `eventName` or all events + * if not specified. */ listenerCount(eventName?: string): number; } diff --git a/index.js b/index.js index 3ec72c4..867016d 100644 --- a/index.js +++ b/index.js @@ -2,6 +2,8 @@ const anyMap = new WeakMap(); const eventsMap = new WeakMap(); +const producersMap = new WeakMap(); +const anyProducer = Symbol('anyProducer'); const resolvedPromise = Promise.resolve(); function assertEventName(eventName) { @@ -10,22 +12,45 @@ function assertEventName(eventName) { } } -function iterator(emitter, eventName) { +function assertListener(listener) { + if (typeof listener !== 'function') { + throw new TypeError('listener must be a function'); + } +} + +function getListeners(instance, eventName) { + const events = eventsMap.get(instance); + if (!events.has(eventName)) { + events.set(eventName, new Set()); + } + return events.get(eventName); +} + +function getEventProducers(instance, eventName) { + const key = typeof eventName === 'string' ? eventName : anyProducer; + const producers = producersMap.get(instance); + if (!producers.has(key)) { + producers.set(key, new Set()); + } + return producers.get(key); +} + +function iterator(instance, eventName) { + let finished = false; let flush = () => {}; let queue = []; - let off; - if (typeof eventName === 'string') { - off = emitter.on(eventName, data => { - queue.push(data); + const producer = { + enqueue(item) { + queue.push(item); flush(); - }); - } else { - off = emitter.onAny((eventName, data) => { - queue.push(Promise.all([eventName, data])); + }, + finish() { + finished = true; flush(); - }); - } + } + }; + getEventProducers(instance, eventName).add(producer); return { async next() { if (!queue) { @@ -33,6 +58,11 @@ function iterator(emitter, eventName) { } if (queue.length === 0) { + if (finished) { + queue = null; + return this.next(); + } + await new Promise(resolve => { flush = resolve; }); @@ -43,7 +73,7 @@ function iterator(emitter, eventName) { }, async return(value) { queue = null; - off(); + getEventProducers(instance, eventName).delete(producer); flush(); return arguments.length > 0 ? {done: true, value: await value} : @@ -55,25 +85,26 @@ function iterator(emitter, eventName) { }; } -function assertListener(listener) { - if (typeof listener !== 'function') { - throw new TypeError('listener must be a function'); +function enqueueProducers(instance, eventName, eventData) { + const producers = producersMap.get(instance); + if (producers.has(eventName)) { + for (const producer of producers.get(eventName)) { + producer.enqueue(eventData); + } } -} - -function getListeners(instance, eventName) { - const events = eventsMap.get(instance); - if (!events.has(eventName)) { - events.set(eventName, new Set()); + if (producers.has(anyProducer)) { + const item = Promise.all([eventName, eventData]); + for (const producer of producers.get(anyProducer)) { + producer.enqueue(item); + } } - - return events.get(eventName); } class Emittery { constructor() { anyMap.set(this, new Set()); eventsMap.set(this, new Map()); + producersMap.set(this, new Map()); } on(eventName, listener) { @@ -107,6 +138,8 @@ class Emittery { async emit(eventName, eventData) { assertEventName(eventName); + enqueueProducers(this, eventName, eventData); + const listeners = getListeners(this, eventName); const anyListeners = anyMap.get(this); const staticListeners = [...listeners]; @@ -130,6 +163,8 @@ class Emittery { async emitSerial(eventName, eventData) { assertEventName(eventName); + enqueueProducers(this, eventName, eventData); + const listeners = getListeners(this, eventName); const anyListeners = anyMap.get(this); const staticListeners = [...listeners]; @@ -169,17 +204,29 @@ class Emittery { clearListeners(eventName) { if (typeof eventName === 'string') { getListeners(this, eventName).clear(); + const producers = getEventProducers(this, eventName); + for (const producer of producers) { + producer.finish(); + } + producers.clear(); } else { anyMap.get(this).clear(); for (const listeners of eventsMap.get(this).values()) { listeners.clear(); } + for (const producers of producersMap.get(this).values()) { + for (const producer of producers) { + producer.finish(); + } + producers.clear(); + } } } listenerCount(eventName) { if (typeof eventName === 'string') { - return anyMap.get(this).size + getListeners(this, eventName).size; + return anyMap.get(this).size + getListeners(this, eventName).size + + getEventProducers(this, eventName).size + getEventProducers(this).size; } if (typeof eventName !== 'undefined') { @@ -191,6 +238,9 @@ class Emittery { for (const value of eventsMap.get(this).values()) { count += value.size; } + for (const value of producersMap.get(this).values()) { + count += value.size; + } return count; } diff --git a/readme.md b/readme.md index 2ec1891..9d40987 100644 --- a/readme.md +++ b/readme.md @@ -108,13 +108,13 @@ Call `return()` on the iterator to remove the subscription. #### clearListeners() -Clear all event listeners on the instance. +Clear all iterators and event listeners on the instance. -If `eventName` is given, only the listeners for that event are cleared. +If `eventName` is given, only the iterators and listeners for that event are cleared. #### listenerCount([eventName]) -The number of listeners for the `eventName` or all events if not specified. +The number of iterators and listeners for the `eventName` or all events if not specified. ## TypeScript @@ -138,8 +138,9 @@ ee.emit('end'); // TS compilation error Listeners are not invoked for events emitted *before* the listener was added. Removing a listener will prevent that listener from being invoked, even if events are in the process of being (asynchronously!) emitted. This also applies to `.clearListeners()`, which removes all listeners. Listeners will be called in the order they were added. So-called *any* listeners are called *after* event-specific listeners. -Note that when using `.emitSerial()`, a slow listener will delay invocation of subsequent listeners. It's possible for newer events to overtake older ones. +Asynchronous iterators are fed events *before* listeners are invoked. They will not receive events emitted *before* the iterator was created. Iterators buffer events. Calling `.return()` on the iterator will clear the buffer. `.clearListeners()` causes iterators to complete after their event buffer is exhausted but does *not* clear the buffer. Iterators are fed events in the order they were created. So called *any* iterators are fed events *after* event-specific iterators. +Note that when using `.emitSerial()`, a slow listener will delay invocation of subsequent listeners. It's possible for newer events to overtake older ones. Slow listeners do impact asynchronous iterators. ## FAQ diff --git a/test/_run.js b/test/_run.js index 5ff4abe..70e813a 100644 --- a/test/_run.js +++ b/test/_run.js @@ -95,7 +95,8 @@ module.exports = Emittery => { iterator.return(); }); iterator = emitter.events('🦄'); - emitter.emit('🦄'); + emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {done: false, value: '🌈'}); t.deepEqual(await iterator.next(), {done: true}); }); @@ -424,7 +425,8 @@ module.exports = Emittery => { iterator.return(); }); iterator = emitter.anyEvent(); - emitter.emit('🦄'); + emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {done: false, value: ['🦄', '🌈']}); t.deepEqual(await iterator.next(), {done: true}); }); @@ -445,6 +447,24 @@ module.exports = Emittery => { t.deepEqual(calls, ['🦄1', '🦄2', 'any1', 'any2', '🌈', 'any1', 'any2']); }); + test('clearListeners() - also clears iterators', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + const anyIterator = emitter.anyEvent(); + await emitter.emit('🦄', '🌟'); + await emitter.emit('🌈', '🌟'); + t.deepEqual(await iterator.next(), {done: false, value: '🌟'}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🦄', '🌟']}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🌈', '🌟']}); + await emitter.emit('🦄', '💫'); + emitter.clearListeners(); + await emitter.emit('🌈', '💫'); + t.deepEqual(await iterator.next(), {done: false, value: '💫'}); + t.deepEqual(await iterator.next(), {done: true}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🦄', '💫']}); + t.deepEqual(await anyIterator.next(), {done: true}); + }); + test('clearListeners() - with event name', async t => { const emitter = new Emittery(); const calls = []; @@ -462,16 +482,36 @@ module.exports = Emittery => { t.deepEqual(calls, ['🦄1', '🦄2', 'any1', 'any2', '🌈', 'any1', 'any2', 'any1', 'any2', '🌈', 'any1', 'any2']); }); + test('clearListeners() - with event name - clears iterators for that event', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + const anyIterator = emitter.anyEvent(); + await emitter.emit('🦄', '🌟'); + await emitter.emit('🌈', '🌟'); + t.deepEqual(await iterator.next(), {done: false, value: '🌟'}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🦄', '🌟']}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🌈', '🌟']}); + await emitter.emit('🦄', '💫'); + emitter.clearListeners('🦄'); + await emitter.emit('🌈', '💫'); + t.deepEqual(await iterator.next(), {done: false, value: '💫'}); + t.deepEqual(await iterator.next(), {done: true}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🦄', '💫']}); + t.deepEqual(await anyIterator.next(), {done: false, value: ['🌈', '💫']}); + }); + test('listenerCount()', t => { const emitter = new Emittery(); emitter.on('🦄', () => {}); emitter.on('🌈', () => {}); emitter.on('🦄', () => {}); + emitter.events('🌈'); emitter.onAny(() => {}); emitter.onAny(() => {}); - t.is(emitter.listenerCount('🦄'), 4); - t.is(emitter.listenerCount('🌈'), 3); - t.is(emitter.listenerCount(), 5); + emitter.anyEvent(); + t.is(emitter.listenerCount('🦄'), 5); + t.is(emitter.listenerCount('🌈'), 5); + t.is(emitter.listenerCount(), 7); }); test('listenerCount() - works with empty eventName strings', t => {