From b4af99569809e7cef2453143c4585a9c9bdfb53d Mon Sep 17 00:00:00 2001 From: lorenzofox3 Date: Wed, 3 Jan 2018 01:30:59 +0100 Subject: [PATCH 01/21] fix xo and test setting --- package.json | 129 ++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 63 deletions(-) diff --git a/package.json b/package.json index 2bcb037..80c8ff6 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,68 @@ { - "name": "emittery", - "version": "0.4.1", - "description": "Simple and modern async event emitter", - "license": "MIT", - "repository": "sindresorhus/emittery", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "sindresorhus.com" - }, - "engines": { - "node": ">=8" - }, - "scripts": { - "test": "xo && nyc ava && tsd" - }, - "files": [ - "index.js", - "index.d.ts" - ], - "keywords": [ - "event", - "emitter", - "eventemitter", - "events", - "async", - "emit", - "on", - "once", - "off", - "listener", - "subscribe", - "unsubscribe", - "pubsub", - "tiny", - "addlistener", - "addeventlistener", - "dispatch", - "dispatcher", - "observer", - "trigger", - "await", - "promise", - "typescript", - "ts", - "typed" - ], - "devDependencies": { - "@types/node": "^12.0.8", - "ava": "^2.1.0", - "codecov": "^3.1.0", - "delay": "^4.1.0", - "nyc": "^14.1.1", - "tsd": "^0.7.3", - "xo": "^0.24.0" - }, - "nyc": { - "reporter": [ - "html", - "lcov", - "text" - ] - } + "name": "emittery", + "version": "0.4.1", + "description": "Simple and modern async event emitter", + "license": "MIT", + "repository": "sindresorhus/emittery", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=8" + }, + "scripts": { + "test": "xo && nyc ava && tsd", + "build": "babel --out-file=legacy.js index.js", + "build:watch": "npm run build -- --watch", + "prepublish": "npm run build" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "event", + "emitter", + "eventemitter", + "events", + "async", + "emit", + "on", + "once", + "off", + "listener", + "subscribe", + "unsubscribe", + "pubsub", + "tiny", + "addlistener", + "addeventlistener", + "dispatch", + "dispatcher", + "observer", + "trigger", + "await", + "promise", + "typescript", + "ts", + "typed" + ], + "devDependencies": { + "@types/node": "^12.0.8", + "ava": "^2.1.0", + "codecov": "^3.1.0", + "delay": "^4.1.0", + "nyc": "^14.1.1", + "tsd": "^0.7.3", + "xo": "^0.24.0" + }, + "nyc": { + "reporter": [ + "html", + "lcov", + "text" + ] + } } From b05696cede9851db08fab7cbccbae948bd97c46f Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 14:58:00 +0000 Subject: [PATCH 02/21] Simplify next() implementation --- index.js | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index e666671..1ce3cb3 100644 --- a/index.js +++ b/index.js @@ -47,6 +47,40 @@ function defaultMethodNamesOrAssert(methodNames) { return methodNames; } +function iterator(emitter, eventName) { + let flush = () => { + }; + let queue = []; + const off = emitter.on(eventName, data => { + queue.push(data); + flush(); + }); + + return { + async next() { + if (!queue) { + return {done: true}; + } + + if (queue.length === 0) { + await new Promise(resolve => { + flush = resolve; + }); + return this.next(); + } + + return {done: false, value: queue.shift()}; + }, + return() { + off(); + queue = null; + }, + [Symbol.asyncIterator]() { + return this; + } + }; +} + class Emittery { static mixin(emitteryPropertyName, methodNames) { methodNames = defaultMethodNamesOrAssert(methodNames); @@ -94,6 +128,7 @@ class Emittery { eventsMap.set(this, new Map()); } + on(eventName, listener) { assertEventName(eventName); assertListener(listener); @@ -227,7 +262,8 @@ class Emittery { const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor'); // Subclass used to encourage TS users to type their events. -Emittery.Typed = class extends Emittery {}; +Emittery.Typed = class extends Emittery { +}; Object.defineProperty(Emittery.Typed, 'Typed', { enumerable: false, value: undefined From 24d2af71fefd2d1371522369adb6b0c527972368 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 15:07:16 +0000 Subject: [PATCH 03/21] Implement return() according to spec --- index.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 1ce3cb3..8b575bf 100644 --- a/index.js +++ b/index.js @@ -71,9 +71,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; From 5e3e7b353c74d7f69118f52318f685deb9a5a30c Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:00:06 +0000 Subject: [PATCH 04/21] Change how TypeScript is loaded in types test --- test/types.js | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 test/types.js diff --git a/test/types.js b/test/types.js new file mode 100644 index 0000000..52cbc27 --- /dev/null +++ b/test/types.js @@ -0,0 +1,63 @@ +import path from 'path'; + +import test from 'ava'; +import glob from 'glob'; + +// Import syntax trips up Atom with ide-typescript loaded. +const ts = require('typescript'); + +const compilerOptions = { + target: ts.ScriptTarget.ES2017, + module: ts.ModuleKind.CommonJS, + strict: true, + noEmit: true +}; + +test('TS can compile valid Emittery method calls', assertAllCompile, 'test/fixtures/compiles'); +test('TS warns about invalid Emittery method calls', assertEachFail, 'test/fixtures/fails'); + +function assertAllCompile(t, srcDir) { + const fileNames = listFiles(srcDir); + const errors = compile(fileNames); + + t.is(errors.length, 0, errorMessage(errors)); +} + +function assertEachFail(t, srcDir) { + const fileNames = listFiles(srcDir).sort(); + const errors = compile(fileNames); + const filesWithErrors = errors + .map(err => (err.file ? err.file.fileName : null)) + .filter(Boolean); + + t.deepEqual(new Set(filesWithErrors), new Set(fileNames), 'Some files did not emit any compile error.'); + t.snapshot(errorMessage(errors)); +} + +function listFiles(srcRoot) { + return glob.sync('{*.js,*.ts}', { + cwd: path.resolve(srcRoot), + absolute: true + }); +} + +function compile(fileNames, options = compilerOptions) { + const program = ts.createProgram(fileNames, options); + const emitResult = program.emit(); + + return ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); +} + +function errorMessage(diagnosticList) { + return diagnosticList.map(diagnostic => { + if (!diagnostic.file) { + return `${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; + } + + const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + const fileName = path.relative(process.cwd(), diagnostic.file.fileName); + + return `${fileName} (${line + 1},${character + 1}): ${message}`; + }).join('\n'); +} From a40d287e8d54197f1f021163faed7edbb0f68570 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:02:14 +0000 Subject: [PATCH 05/21] Return async iterator from .events(), not .on() Add to TS definition and README. --- index.js | 8 ++++++++ readme.md | 10 ++++++++++ test/types.js | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 8b575bf..8159edf 100644 --- a/index.js +++ b/index.js @@ -134,8 +134,12 @@ class Emittery { on(eventName, listener) { assertEventName(eventName); +<<<<<<< HEAD assertListener(listener); getListeners(this, eventName).add(listener); +======= + this._getListeners(eventName).add(listener); +>>>>>>> Return async iterator from .events(), not .on() return this.off.bind(this, eventName, listener); } @@ -155,6 +159,10 @@ class Emittery { }); } + events(eventName) { + return iterator(this, eventName); + } + async emit(eventName, eventData) { assertEventName(eventName); diff --git a/readme.md b/readme.md index 88a3792..3b1e646 100644 --- a/readme.md +++ b/readme.md @@ -69,7 +69,17 @@ emitter.once('🦄').then(data => { emitter.emit('🦄', '🌈'); ``` +<<<<<<< HEAD #### emit(eventName, data?) +======= +#### 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]) +>>>>>>> Return async iterator from .events(), not .on() Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but execute concurrently. 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 From eb6a43e83039ac2a2c2b961b88bd0c0ec1d2a1e0 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 6 Jan 2018 16:25:22 +0000 Subject: [PATCH 06/21] Implement .anyEvent() --- Emittery.d.ts | 159 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 30 ++++++++++ readme.md | 12 ++++ 3 files changed, 201 insertions(+) create mode 100644 Emittery.d.ts diff --git a/Emittery.d.ts b/Emittery.d.ts new file mode 100644 index 0000000..d124505 --- /dev/null +++ b/Emittery.d.ts @@ -0,0 +1,159 @@ +export = Emittery; + +declare class Emittery { + /** + * Subscribe to an event. + * + * Returns an unsubscribe method. + * + * Using the same listener multiple times for the same event will result + * in only one method call per emitted event. + */ + on(eventName: string, listener: (eventData?: any) => any): Emittery.UnsubscribeFn; + + /** + * Remove an event subscription. + * + * If you don't pass in a `listener`, it will remove all listeners for that + * event. + */ + off(eventName: string, listener?: (eventData?: any) => any): void; + + /** + * Subscribe to an event only once. It will be unsubscribed after the first + * event. + * + * Returns a promise for the event data when `eventName` is emitted. + */ + 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. + * + * Returns a promise for when all the event listeners are done. *Done* + * meaning executed if synchronous or resolved when an + * async/promise-returning function. You usually wouldn't want to wait for + * this, but you could for example catch possible errors. If any of the + * listeners throw/reject, the returned promise will be rejected with the + * error, but the other listeners will not be affected. + * + * Returns a promise for when all the event listeners are done. + */ + emit(eventName: string, eventData?: any): Promise; + + /** + * Same as `emit()`, but it waits for each listener to resolve before + * triggering the next one. This can be useful if your events depend on each + * other. Although ideally they should not. Prefer `emit()` whenever + * possible. + * + * If any of the listeners throw/reject, the returned promise will be + * rejected with the error and the remaining listeners will *not* be called. + * + * Returns a promise for when all the event listeners are done. + */ + emitSerial(eventName: string, eventData?: any): Promise; + + /** + * Subscribe to be notified about any event. + * + * Returns a method to unsubscribe. + */ + onAny(listener: (eventName: string, eventData?: any) => any): Emittery.UnsubscribeFn; + + /** + Remove an `onAny` subscription. + + If you don't pass in a `listener`, it will remove all `onAny` subscriptions. + */ + 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. + */ + clear(): void; + + /** + * The number of listeners for the `eventName` or all events if not + * specified. + */ + listenerCount(eventName?: string): number; +} + +declare namespace Emittery { + /** + * Removes an event subscription. + */ + type UnsubscribeFn = () => void; + + /** + * Maps event names to their emitted data type. + */ + interface Events { + [eventName: string]: any; + } + + /** + * Async event emitter. + * + * Must list supported events and the data type they emit, if any. + * + * For example: + * + * ```ts + * import Emittery = require('emittery'); + * + * const ee = new Emittery.Typed<{value: string}, 'open' | 'close'>(); + * + * ee.emit('open'); + * ee.emit('value', 'foo\n'); + * ee.emit('value', 1); // TS compilation error + * ee.emit('end'); // TS compilation error + * ``` + */ + class Typed extends Emittery { + on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; + on(eventName: Name, listener: () => any): Emittery.UnsubscribeFn; + + once(eventName: Name): Promise; + once(eventName: Name): Promise; + + 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; + + 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; + + emitSerial(eventName: Name, eventData: EventDataMap[Name]): Promise; + emitSerial(eventName: Name): Promise; + } +} diff --git a/index.js b/index.js index 8159edf..824e24b 100644 --- a/index.js +++ b/index.js @@ -51,10 +51,29 @@ function iterator(emitter, eventName) { let flush = () => { }; let queue = []; +<<<<<<< HEAD const off = emitter.on(eventName, data => { 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(); + }); + } +>>>>>>> Implement .anyEvent() return { async next() { @@ -160,6 +179,7 @@ class Emittery { } events(eventName) { + assertEventName(eventName); return iterator(this, eventName); } @@ -221,6 +241,7 @@ class Emittery { anyMap.get(this).delete(listener); } +<<<<<<< HEAD clearListeners(eventName) { if (typeof eventName === 'string') { getListeners(this, eventName).clear(); @@ -230,6 +251,15 @@ class Emittery { listeners.clear(); } } +======= + anyEvent() { + return iterator(this); + } + + clear() { + this._events.clear(); + this._anyEvents.clear(); +>>>>>>> Implement .anyEvent() } listenerCount(eventName) { diff --git a/readme.md b/readme.md index 3b1e646..cdfca71 100644 --- a/readme.md +++ b/readme.md @@ -103,7 +103,19 @@ Returns a method to unsubscribe. Remove an `onAny` subscription. +<<<<<<< HEAD #### clearListeners() +======= +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() +>>>>>>> Implement .anyEvent() Clear all event listeners on the instance. From e0645c91cb4aff9019a15a2f04aa19c4f5610004 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sun, 7 Jan 2018 14:47:04 +0000 Subject: [PATCH 07/21] Ensure async iterators return non-promise values Follows https://tc39.github.io/proposal-async-iteration/#sec-asynciterator-interface --- index.js | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index 824e24b..a53c9a3 100644 --- a/index.js +++ b/index.js @@ -51,12 +51,6 @@ function iterator(emitter, eventName) { let flush = () => { }; let queue = []; -<<<<<<< HEAD - const off = emitter.on(eventName, data => { - queue.push(data); - flush(); - }); -======= let off; if (typeof eventName === 'string') { off = emitter.on(eventName, data => { @@ -68,12 +62,11 @@ function iterator(emitter, eventName) { } else { off = emitter.onAny((eventName, data) => { if (queue) { - queue.push([eventName, data]); + queue.push(Promise.all([eventName, data])); } flush(); }); } ->>>>>>> Implement .anyEvent() return { async next() { @@ -88,7 +81,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(); From 260dc62def925b66208c7e0d4dd06a972f16e2cb Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Sat, 20 Jan 2018 16:40:52 +0000 Subject: [PATCH 08/21] 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 a53c9a3..7a54f7f 100644 --- a/index.js +++ b/index.js @@ -54,16 +54,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(); }); } @@ -84,8 +80,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 e882ecd5b9904bb2ac3405d77b81da4b7cdee97d Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 11:15:05 -0400 Subject: [PATCH 09/21] process rebase --- index.js | 122 ++++++++++++++++++++++++++++++++++++------------------- 1 file changed, 80 insertions(+), 42 deletions(-) diff --git a/index.js b/index.js index 7a54f7f..326f313 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) { @@ -25,45 +27,47 @@ function getListeners(instance, eventName) { return events.get(eventName); } -function defaultMethodNamesOrAssert(methodNames) { - if (methodNames === undefined) { - return allEmitteryMethods; +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); +} - if (!Array.isArray(methodNames)) { - throw new TypeError('`methodNames` must be an array of strings'); +function enqueueProducers(instance, eventName, eventData) { + const producers = producersMap.get(instance); + if (producers.has(eventName)) { + for (const producer of producers.get(eventName)) { + producer.enqueue(eventData); + } } - - for (const methodName of methodNames) { - if (!allEmitteryMethods.includes(methodName)) { - if (typeof methodName !== 'string') { - throw new TypeError('`methodNames` element must be a string'); - } - - throw new Error(`${methodName} is not Emittery method`); + if (producers.has(anyProducer)) { + const item = Promise.all([eventName, eventData]); + for (const producer of producers.get(anyProducer)) { + producer.enqueue(item); } } - - return methodNames; } -function iterator(emitter, eventName) { +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) { @@ -71,6 +75,11 @@ function iterator(emitter, eventName) { } if (queue.length === 0) { + if (finished) { + queue = null; + return this.next(); + } + await new Promise(resolve => { flush = resolve; }); @@ -81,7 +90,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} : @@ -93,6 +102,28 @@ function iterator(emitter, eventName) { }; } +function defaultMethodNamesOrAssert(methodNames) { + if (methodNames === undefined) { + return allEmitteryMethods; + } + + if (!Array.isArray(methodNames)) { + throw new TypeError('`methodNames` must be an array of strings'); + } + + for (const methodName of methodNames) { + if (!allEmitteryMethods.includes(methodName)) { + if (typeof methodName !== 'string') { + throw new TypeError('`methodNames` element must be a string'); + } + + throw new Error(`${methodName} is not Emittery method`); + } + } + + return methodNames; +} + class Emittery { static mixin(emitteryPropertyName, methodNames) { methodNames = defaultMethodNamesOrAssert(methodNames); @@ -143,12 +174,8 @@ class Emittery { on(eventName, listener) { assertEventName(eventName); -<<<<<<< HEAD assertListener(listener); getListeners(this, eventName).add(listener); -======= - this._getListeners(eventName).add(listener); ->>>>>>> Return async iterator from .events(), not .on() return this.off.bind(this, eventName, listener); } @@ -176,6 +203,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]; @@ -196,6 +225,10 @@ class Emittery { ]); } + anyEvent() { + return iterator(this); + } + async emitSerial(eventName, eventData) { assertEventName(eventName); @@ -231,30 +264,32 @@ class Emittery { anyMap.get(this).delete(listener); } -<<<<<<< HEAD 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(); + } } -======= - anyEvent() { - return iterator(this); - } - - clear() { - this._events.clear(); - this._anyEvents.clear(); ->>>>>>> Implement .anyEvent() } 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') { @@ -266,6 +301,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; } From 38c8359fb83de7223c2d1ec572836e81878d5d8c Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 11:48:31 -0400 Subject: [PATCH 10/21] add tests --- index.js | 21 ++++-- test/index.js | 188 +++++++++++++++++++++++++++++++++++++++++++++++--- test/types.js | 63 ----------------- 3 files changed, 191 insertions(+), 81 deletions(-) delete mode 100644 test/types.js diff --git a/index.js b/index.js index 326f313..50f4101 100644 --- a/index.js +++ b/index.js @@ -33,6 +33,7 @@ function getEventProducers(instance, eventName) { if (!producers.has(key)) { producers.set(key, new Set()); } + return producers.get(key); } @@ -43,6 +44,7 @@ function enqueueProducers(instance, eventName, eventData) { producer.enqueue(eventData); } } + if (producers.has(anyProducer)) { const item = Promise.all([eventName, eventData]); for (const producer of producers.get(anyProducer)) { @@ -53,8 +55,7 @@ function enqueueProducers(instance, eventName, eventData) { function iterator(instance, eventName) { let finished = false; - let flush = () => { - }; + let flush = () => {}; let queue = []; const producer = { enqueue(item) { @@ -169,9 +170,9 @@ class Emittery { constructor() { anyMap.set(this, new Set()); eventsMap.set(this, new Map()); + producersMap.set(this, new Map()); } - on(eventName, listener) { assertEventName(eventName); assertListener(listener); @@ -225,10 +226,6 @@ class Emittery { ]); } - anyEvent() { - return iterator(this); - } - async emitSerial(eventName, eventData) { assertEventName(eventName); @@ -259,6 +256,10 @@ class Emittery { return this.offAny.bind(this, listener); } + anyEvent() { + return iterator(this); + } + offAny(listener) { assertListener(listener); anyMap.get(this).delete(listener); @@ -268,19 +269,24 @@ class Emittery { 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(); } } @@ -301,6 +307,7 @@ class Emittery { for (const value of eventsMap.get(this).values()) { count += value.size; } + for (const value of producersMap.get(this).values()) { count += value.size; } diff --git a/test/index.js b/test/index.js index 1a62cb7..bdb47eb 100644 --- a/test/index.js +++ b/test/index.js @@ -17,7 +17,8 @@ test('on() - eventName must be a string', t => { const emitter = new Emittery(); t.throws(() => { - emitter.on(42, () => {}); + emitter.on(42, () => { + }); }, TypeError); }); @@ -55,6 +56,70 @@ test('on() - dedupes identical listeners', async t => { t.deepEqual(calls, [1]); }); +test.serial('events()', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', Promise.resolve('🌟')); + }, 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('events() - return() called during emit', async t => { + const emitter = new Emittery(); + let iterator = null; + emitter.on('🦄', () => { + iterator.return(); + }); + iterator = emitter.events('🦄'); + emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {done: false, value: '🌈'}); + t.deepEqual(await iterator.next(), {done: true}); +}); + +test('events() - return() awaits its argument', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + t.deepEqual(await iterator.return(Promise.resolve(1)), {done: true, value: 1}); +}); + +test('events() - return() without argument', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + t.deepEqual(await iterator.return(), {done: true}); +}); + +test('events() - discarded iterators should stop receiving events', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + + await emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {value: '🌈', done: false}); + await iterator.return(); + await emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {done: true}); + + setTimeout(() => { + emitter.emit('🦄', '🌟'); + }, 10); + + await new Promise(resolve => setTimeout(resolve, 20)); + + t.deepEqual(await iterator.next(), {done: true}); +}); + test('off()', async t => { const emitter = new Emittery(); const calls = []; @@ -332,6 +397,58 @@ test('onAny() - must have a listener', t => { }, TypeError); }); +test.serial('anyEvent()', async t => { + const emitter = new Emittery(); + const iterator = emitter.anyEvent(); + + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', Promise.resolve('🌟')); + }, 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: false, value: ['🦄', '🌈']}); + t.deepEqual(await iterator.next(), {done: true}); +}); + +test('anyEvents() - discarded iterators should stop receiving events', async t => { + const emitter = new Emittery(); + const iterator = emitter.anyEvent(); + + await emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {value: ['🦄', '🌈'], done: false}); + await iterator.return(); + await emitter.emit('🦄', '🌈'); + t.deepEqual(await iterator.next(), {done: true}); + + setTimeout(() => { + emitter.emit('🦄', '🌟'); + }, 10); + + await new Promise(resolve => setTimeout(resolve, 20)); + + t.deepEqual(await iterator.next(), {done: true}); +}); + test('offAny()', async t => { const emitter = new Emittery(); const calls = []; @@ -369,6 +486,24 @@ test('clearListeners()', async t => { 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 = []; @@ -386,13 +521,36 @@ test('clearListeners() - with event name', async t => { 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.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); @@ -400,7 +558,8 @@ test('listenerCount()', t => { test('listenerCount() - works with empty eventName strings', t => { const emitter = new Emittery(); - emitter.on('', () => {}); + emitter.on('', () => { + }); t.is(emitter.listenerCount(''), 1); }); @@ -457,7 +616,7 @@ test('bindMethods() - methodNames must be array of strings or undefined', t => { }); test('bindMethods() - must bind all methods if no array supplied', t => { - const methodsExpected = ['on', 'off', 'once', 'emit', 'emitSerial', 'onAny', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods']; + const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods']; const emitter = new Emittery(); const target = {}; @@ -501,6 +660,7 @@ test('mixin()', t => { this.v = v; } } + const TestClassWithMixin = Emittery.mixin('emitter', ['on', 'off', 'once', 'emit', 'emitSerial', 'onAny', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods'])(TestClass); const symbol = Symbol('test symbol'); const instance = new TestClassWithMixin(symbol); @@ -514,6 +674,7 @@ test('mixin()', t => { test('mixin() - methodNames must be array of strings or undefined', t => { class TestClass { } + t.throws(() => Emittery.mixin('emitter', null)(TestClass)); t.throws(() => Emittery.mixin('emitter', 'string')(TestClass)); t.throws(() => Emittery.mixin('emitter', {})(TestClass)); @@ -523,16 +684,20 @@ test('mixin() - methodNames must be array of strings or undefined', t => { }); test('mixin() - must mixin all methods if no array supplied', t => { - const methodsExpected = ['on', 'off', 'once', 'emit', 'emitSerial', 'onAny', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods']; + const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods']; + + class TestClass { + } - class TestClass {} const TestClassWithMixin = Emittery.mixin('emitter')(TestClass); t.deepEqual(Object.getOwnPropertyNames(TestClassWithMixin.prototype).sort(), methodsExpected.concat(['constructor', 'emitter']).sort()); }); test('mixin() - methodNames must only include Emittery methods', t => { - class TestClass {} + class TestClass { + } + t.throws(() => Emittery.mixin('emitter', ['nonexistent'])(TestClass)); }); @@ -542,6 +707,7 @@ test('mixin() - must not set already existing methods', t => { return true; } } + t.throws(() => Emittery.mixin('emitter', ['on'])(TestClass)); }); diff --git a/test/types.js b/test/types.js deleted file mode 100644 index d834ff7..0000000 --- a/test/types.js +++ /dev/null @@ -1,63 +0,0 @@ -import path from 'path'; - -import test from 'ava'; -import glob from 'glob'; - -// Import syntax trips up Atom with ide-typescript loaded. -const ts = require('typescript'); - -const compilerOptions = { - target: ts.ScriptTarget.ESNext, - module: ts.ModuleKind.CommonJS, - strict: true, - noEmit: true -}; - -test('TS can compile valid Emittery method calls', assertAllCompile, 'test/fixtures/compiles'); -test('TS warns about invalid Emittery method calls', assertEachFail, 'test/fixtures/fails'); - -function assertAllCompile(t, srcDir) { - const fileNames = listFiles(srcDir); - const errors = compile(fileNames); - - t.is(errors.length, 0, errorMessage(errors)); -} - -function assertEachFail(t, srcDir) { - const fileNames = listFiles(srcDir).sort(); - const errors = compile(fileNames); - const filesWithErrors = errors - .map(err => (err.file ? err.file.fileName : null)) - .filter(Boolean); - - t.deepEqual(new Set(filesWithErrors), new Set(fileNames), 'Some files did not emit any compile error.'); - t.snapshot(errorMessage(errors)); -} - -function listFiles(srcRoot) { - return glob.sync('{*.js,*.ts}', { - cwd: path.resolve(srcRoot), - absolute: true - }); -} - -function compile(fileNames, options = compilerOptions) { - const program = ts.createProgram(fileNames, options); - const emitResult = program.emit(); - - return ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); -} - -function errorMessage(diagnosticList) { - return diagnosticList.map(diagnostic => { - if (!diagnostic.file) { - return `${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; - } - - const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); - const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - const fileName = path.relative(process.cwd(), diagnostic.file.fileName); - - return `${fileName} (${line + 1},${character + 1}): ${message}`; - }).join('\n'); -} From 3a70d6598cab3d18b8165b80cfcd7f542ca73987 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 11:56:34 -0400 Subject: [PATCH 11/21] fix readme merge conflict --- readme.md | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/readme.md b/readme.md index cdfca71..0957354 100644 --- a/readme.md +++ b/readme.md @@ -68,10 +68,6 @@ emitter.once('🦄').then(data => { emitter.emit('🦄', '🌈'); ``` - -<<<<<<< HEAD -#### emit(eventName, data?) -======= #### events(eventName) Get an asynchronous iterator which buffers data each time an event is emitted. @@ -79,7 +75,6 @@ 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]) ->>>>>>> Return async iterator from .events(), not .on() Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but execute concurrently. @@ -103,19 +98,13 @@ Returns a method to unsubscribe. Remove an `onAny` subscription. -<<<<<<< HEAD -#### clearListeners() -======= -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() ->>>>>>> Implement .anyEvent() +#### clearListeners() Clear all event listeners on the instance. From b371aa281ff8b4fb0bd4439a9975740aca8e6f5f Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 12:04:40 -0400 Subject: [PATCH 12/21] remove unrelevant file --- Emittery.d.ts | 159 -------------------------------------------------- 1 file changed, 159 deletions(-) delete mode 100644 Emittery.d.ts diff --git a/Emittery.d.ts b/Emittery.d.ts deleted file mode 100644 index d124505..0000000 --- a/Emittery.d.ts +++ /dev/null @@ -1,159 +0,0 @@ -export = Emittery; - -declare class Emittery { - /** - * Subscribe to an event. - * - * Returns an unsubscribe method. - * - * Using the same listener multiple times for the same event will result - * in only one method call per emitted event. - */ - on(eventName: string, listener: (eventData?: any) => any): Emittery.UnsubscribeFn; - - /** - * Remove an event subscription. - * - * If you don't pass in a `listener`, it will remove all listeners for that - * event. - */ - off(eventName: string, listener?: (eventData?: any) => any): void; - - /** - * Subscribe to an event only once. It will be unsubscribed after the first - * event. - * - * Returns a promise for the event data when `eventName` is emitted. - */ - 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. - * - * Returns a promise for when all the event listeners are done. *Done* - * meaning executed if synchronous or resolved when an - * async/promise-returning function. You usually wouldn't want to wait for - * this, but you could for example catch possible errors. If any of the - * listeners throw/reject, the returned promise will be rejected with the - * error, but the other listeners will not be affected. - * - * Returns a promise for when all the event listeners are done. - */ - emit(eventName: string, eventData?: any): Promise; - - /** - * Same as `emit()`, but it waits for each listener to resolve before - * triggering the next one. This can be useful if your events depend on each - * other. Although ideally they should not. Prefer `emit()` whenever - * possible. - * - * If any of the listeners throw/reject, the returned promise will be - * rejected with the error and the remaining listeners will *not* be called. - * - * Returns a promise for when all the event listeners are done. - */ - emitSerial(eventName: string, eventData?: any): Promise; - - /** - * Subscribe to be notified about any event. - * - * Returns a method to unsubscribe. - */ - onAny(listener: (eventName: string, eventData?: any) => any): Emittery.UnsubscribeFn; - - /** - Remove an `onAny` subscription. - - If you don't pass in a `listener`, it will remove all `onAny` subscriptions. - */ - 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. - */ - clear(): void; - - /** - * The number of listeners for the `eventName` or all events if not - * specified. - */ - listenerCount(eventName?: string): number; -} - -declare namespace Emittery { - /** - * Removes an event subscription. - */ - type UnsubscribeFn = () => void; - - /** - * Maps event names to their emitted data type. - */ - interface Events { - [eventName: string]: any; - } - - /** - * Async event emitter. - * - * Must list supported events and the data type they emit, if any. - * - * For example: - * - * ```ts - * import Emittery = require('emittery'); - * - * const ee = new Emittery.Typed<{value: string}, 'open' | 'close'>(); - * - * ee.emit('open'); - * ee.emit('value', 'foo\n'); - * ee.emit('value', 1); // TS compilation error - * ee.emit('end'); // TS compilation error - * ``` - */ - class Typed extends Emittery { - on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; - on(eventName: Name, listener: () => any): Emittery.UnsubscribeFn; - - once(eventName: Name): Promise; - once(eventName: Name): Promise; - - 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; - - 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; - - emitSerial(eventName: Name, eventData: EventDataMap[Name]): Promise; - emitSerial(eventName: Name): Promise; - } -} From 2118e1634325c9ef6bd2afd98eed0febf973fa1b Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 12:08:17 -0400 Subject: [PATCH 13/21] remove unrelevant file and follow formatting rules for package.json --- package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/package.json b/package.json index 80c8ff6..86b8c0d 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,7 @@ "node": ">=8" }, "scripts": { - "test": "xo && nyc ava && tsd", - "build": "babel --out-file=legacy.js index.js", - "build:watch": "npm run build -- --watch", - "prepublish": "npm run build" + "test": "xo && nyc ava && tsd" }, "files": [ "index.js", From 06393899d155e76c1f56e67ceb10d3e2c7308943 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 12:14:30 -0400 Subject: [PATCH 14/21] solve formatting issue --- package.json | 126 +++++++++++++++++++++++++------------------------- test/index.js | 28 ++++------- 2 files changed, 72 insertions(+), 82 deletions(-) diff --git a/package.json b/package.json index 86b8c0d..2bcb037 100644 --- a/package.json +++ b/package.json @@ -1,65 +1,65 @@ { - "name": "emittery", - "version": "0.4.1", - "description": "Simple and modern async event emitter", - "license": "MIT", - "repository": "sindresorhus/emittery", - "author": { - "name": "Sindre Sorhus", - "email": "sindresorhus@gmail.com", - "url": "sindresorhus.com" - }, - "engines": { - "node": ">=8" - }, - "scripts": { - "test": "xo && nyc ava && tsd" - }, - "files": [ - "index.js", - "index.d.ts" - ], - "keywords": [ - "event", - "emitter", - "eventemitter", - "events", - "async", - "emit", - "on", - "once", - "off", - "listener", - "subscribe", - "unsubscribe", - "pubsub", - "tiny", - "addlistener", - "addeventlistener", - "dispatch", - "dispatcher", - "observer", - "trigger", - "await", - "promise", - "typescript", - "ts", - "typed" - ], - "devDependencies": { - "@types/node": "^12.0.8", - "ava": "^2.1.0", - "codecov": "^3.1.0", - "delay": "^4.1.0", - "nyc": "^14.1.1", - "tsd": "^0.7.3", - "xo": "^0.24.0" - }, - "nyc": { - "reporter": [ - "html", - "lcov", - "text" - ] - } + "name": "emittery", + "version": "0.4.1", + "description": "Simple and modern async event emitter", + "license": "MIT", + "repository": "sindresorhus/emittery", + "author": { + "name": "Sindre Sorhus", + "email": "sindresorhus@gmail.com", + "url": "sindresorhus.com" + }, + "engines": { + "node": ">=8" + }, + "scripts": { + "test": "xo && nyc ava && tsd" + }, + "files": [ + "index.js", + "index.d.ts" + ], + "keywords": [ + "event", + "emitter", + "eventemitter", + "events", + "async", + "emit", + "on", + "once", + "off", + "listener", + "subscribe", + "unsubscribe", + "pubsub", + "tiny", + "addlistener", + "addeventlistener", + "dispatch", + "dispatcher", + "observer", + "trigger", + "await", + "promise", + "typescript", + "ts", + "typed" + ], + "devDependencies": { + "@types/node": "^12.0.8", + "ava": "^2.1.0", + "codecov": "^3.1.0", + "delay": "^4.1.0", + "nyc": "^14.1.1", + "tsd": "^0.7.3", + "xo": "^0.24.0" + }, + "nyc": { + "reporter": [ + "html", + "lcov", + "text" + ] + } } diff --git a/test/index.js b/test/index.js index bdb47eb..d4d5abf 100644 --- a/test/index.js +++ b/test/index.js @@ -17,8 +17,7 @@ test('on() - eventName must be a string', t => { const emitter = new Emittery(); t.throws(() => { - emitter.on(42, () => { - }); + emitter.on(42, () => {}); }, TypeError); }); @@ -541,16 +540,11 @@ test('clearListeners() - with event name - clears iterators for that event', asy 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); @@ -558,8 +552,7 @@ test('listenerCount()', t => { test('listenerCount() - works with empty eventName strings', t => { const emitter = new Emittery(); - emitter.on('', () => { - }); + emitter.on('', () => {}); t.is(emitter.listenerCount(''), 1); }); @@ -672,8 +665,7 @@ test('mixin()', t => { }); test('mixin() - methodNames must be array of strings or undefined', t => { - class TestClass { - } + class TestClass {} t.throws(() => Emittery.mixin('emitter', null)(TestClass)); t.throws(() => Emittery.mixin('emitter', 'string')(TestClass)); @@ -695,8 +687,7 @@ test('mixin() - must mixin all methods if no array supplied', t => { }); test('mixin() - methodNames must only include Emittery methods', t => { - class TestClass { - } + class TestClass {} t.throws(() => Emittery.mixin('emitter', ['nonexistent'])(TestClass)); }); @@ -707,7 +698,6 @@ test('mixin() - must not set already existing methods', t => { return true; } } - t.throws(() => Emittery.mixin('emitter', ['on'])(TestClass)); }); From 1b14834ebd0646cc1cd9009743b1c41c5d9401d3 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 12:31:04 -0400 Subject: [PATCH 15/21] skip unrelvant test in v8 --- test/index.js | 72 ++++++++++++++++++++++++++++----------------------- 1 file changed, 40 insertions(+), 32 deletions(-) diff --git a/test/index.js b/test/index.js index d4d5abf..08aefb2 100644 --- a/test/index.js +++ b/test/index.js @@ -2,6 +2,10 @@ import test from 'ava'; import delay from 'delay'; import Emittery from '..'; +const versionRegexp= /^v8/; + +const shouldSkip = versionRegexp.test(process.version); + test('on()', async t => { const emitter = new Emittery(); const calls = []; @@ -55,26 +59,28 @@ test('on() - dedupes identical listeners', async t => { t.deepEqual(calls, [1]); }); -test.serial('events()', async t => { - const emitter = new Emittery(); - const iterator = emitter.events('🦄'); +if(!shouldSkip) { + test.serial('events()', async t => { + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); - await emitter.emit('🦄', '🌈'); - setTimeout(() => { - emitter.emit('🦄', Promise.resolve('🌟')); - }, 10); + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', Promise.resolve('🌟')); + }, 10); - t.plan(3); - const expected = ['🌈', '🌟']; - for await (const data of iterator) { - t.deepEqual(data, expected.shift()); - if (expected.length === 0) { - break; + 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}); -}); + t.deepEqual(await iterator.next(), {done: true}); + }); +} test('events() - return() called during emit', async t => { const emitter = new Emittery(); @@ -396,26 +402,28 @@ test('onAny() - must have a listener', t => { }, TypeError); }); -test.serial('anyEvent()', async t => { - const emitter = new Emittery(); - const iterator = emitter.anyEvent(); +if(!shouldSkip) { + test.serial('anyEvent()', async t => { + const emitter = new Emittery(); + const iterator = emitter.anyEvent(); - await emitter.emit('🦄', '🌈'); - setTimeout(() => { - emitter.emit('🦄', Promise.resolve('🌟')); - }, 10); + await emitter.emit('🦄', '🌈'); + setTimeout(() => { + emitter.emit('🦄', Promise.resolve('🌟')); + }, 10); - t.plan(3); - const expected = [['🦄', '🌈'], ['🦄', '🌟']]; - for await (const data of iterator) { - t.deepEqual(data, expected.shift()); - if (expected.length === 0) { - break; + 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}); -}); + t.deepEqual(await iterator.next(), {done: true}); + }); +} test('anyEvent() - return() called during emit', async t => { const emitter = new Emittery(); From eece4d53637ba2afb7a7de9fd7229f64c421e798 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Tue, 18 Jun 2019 12:38:17 -0400 Subject: [PATCH 16/21] fix xo formatting --- test/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/index.js b/test/index.js index 08aefb2..6af8d26 100644 --- a/test/index.js +++ b/test/index.js @@ -2,7 +2,7 @@ import test from 'ava'; import delay from 'delay'; import Emittery from '..'; -const versionRegexp= /^v8/; +const versionRegexp = /^v8/; const shouldSkip = versionRegexp.test(process.version); @@ -59,7 +59,7 @@ test('on() - dedupes identical listeners', async t => { t.deepEqual(calls, [1]); }); -if(!shouldSkip) { +if (!shouldSkip) { test.serial('events()', async t => { const emitter = new Emittery(); const iterator = emitter.events('🦄'); @@ -402,7 +402,7 @@ test('onAny() - must have a listener', t => { }, TypeError); }); -if(!shouldSkip) { +if (!shouldSkip) { test.serial('anyEvent()', async t => { const emitter = new Emittery(); const iterator = emitter.anyEvent(); From 70c90d05fcb43f54bd7cd74b99022969fd335215 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 23 Jun 2019 20:58:00 +0700 Subject: [PATCH 17/21] Update index.js --- index.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/index.js b/index.js index 50f4101..5de838c 100644 --- a/index.js +++ b/index.js @@ -338,8 +338,7 @@ class Emittery { const allEmitteryMethods = Object.getOwnPropertyNames(Emittery.prototype).filter(v => v !== 'constructor'); // Subclass used to encourage TS users to type their events. -Emittery.Typed = class extends Emittery { -}; +Emittery.Typed = class extends Emittery {}; Object.defineProperty(Emittery.Typed, 'Typed', { enumerable: false, value: undefined From bcde875d681598b8b17a3e0521289816b98c306e Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 23 Jun 2019 21:01:00 +0700 Subject: [PATCH 18/21] Update index.js --- test/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/index.js b/test/index.js index 6af8d26..5889c5e 100644 --- a/test/index.js +++ b/test/index.js @@ -2,9 +2,7 @@ import test from 'ava'; import delay from 'delay'; import Emittery from '..'; -const versionRegexp = /^v8/; - -const shouldSkip = versionRegexp.test(process.version); +const shouldSkip = process.version.startsWith('v8.'); test('on()', async t => { const emitter = new Emittery(); @@ -686,8 +684,7 @@ test('mixin() - methodNames must be array of strings or undefined', t => { test('mixin() - must mixin all methods if no array supplied', t => { const methodsExpected = ['on', 'off', 'once', 'events', 'emit', 'emitSerial', 'onAny', 'anyEvent', 'offAny', 'clearListeners', 'listenerCount', 'bindMethods']; - class TestClass { - } + class TestClass {} const TestClassWithMixin = Emittery.mixin('emitter')(TestClass); From e0fb1475ebe3198962b1774f632f090508fa2450 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Sun, 23 Jun 2019 21:03:34 +0700 Subject: [PATCH 19/21] Update readme.md --- readme.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 0957354..8a9201f 100644 --- a/readme.md +++ b/readme.md @@ -70,7 +70,7 @@ emitter.emit('🦄', '🌈'); ``` #### events(eventName) -Get an asynchronous iterator which buffers data each time an event is emitted. +Get an async iterator which buffers data each time an event is emitted. Call `return()` on the iterator to remove the subscription. @@ -100,7 +100,7 @@ Remove an `onAny` subscription. #### anyEvent() -Get an asynchronous iterator which buffers a tuple of an event name and data each time an event is emitted. +Get an async 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. From 419aace4f1bce7f0f87cbafc4599e5c4c3e23c19 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Wed, 26 Jun 2019 11:08:59 -0400 Subject: [PATCH 20/21] add usage example in the readme.md --- readme.md | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/readme.md b/readme.md index 8a9201f..dd07083 100644 --- a/readme.md +++ b/readme.md @@ -68,12 +68,54 @@ emitter.once('🦄').then(data => { emitter.emit('🦄', '🌈'); ``` + #### events(eventName) Get an async iterator which buffers data each time an event is emitted. Call `return()` on the iterator to remove the subscription. +```js +const iterator = emitter.events('🦄'); + +emitter.emit('🦄', '🌈1'); // buffered +emitter.emit('🦄', '🌈2'); // buffered + +iterator.next() + .then( ({value, done}) => { + // done is false + // value === '🌈1' + return iterator.next(); + }) + .then( ({value, done}) => { + // done is false + // value === '🌈2' + + // revoke subscription + return iterator.return(); + }) + .then(({done}) => { + // done is true + }); +``` + +In practice you would usually consume the events using the [for await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) statement. +In that case, to revoke the subscription simply break the loop + +```js +// in an async context +const iterator = emitter.events('🦄'); + +emitter.emit('🦄', '🌈1'); // buffered +emitter.emit('🦄', '🌈2'); // buffered + +for await (const data of iterator){ + if(data === '🌈2') + break; // revoke the subscription when we see the value '🌈2' +} + +``` + #### emit(eventName, [data]) Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but execute concurrently. @@ -104,6 +146,32 @@ Get an async iterator which buffers a tuple of an event name and data each time Call `return()` on the iterator to remove the subscription. +```js +const iterator = emitter.anyEvent(); + +emitter.emit('🦄', '🌈1'); // buffered +emitter.emit('🌟', '🌈2'); // buffered + +iterator.next() + .then( ({value, done}) => { + // done is false + // value is ['🦄', '🌈1'] + return iterator.next(); + }) + .then( ({value, done}) => { + // done is false + // value is ['🌟', '🌈2'] + + // revoke subscription + return iterator.return(); + }) + .then(({done}) => { + // done is true + }); +``` + +In the same way as for ``events`` you can subscribe by using the ``for await`` statement + #### clearListeners() Clear all event listeners on the instance. From cb47ab4bf82801bac64020294062157fef3cd6f1 Mon Sep 17 00:00:00 2001 From: Laurent Renard Date: Fri, 12 Jul 2019 14:55:19 -0400 Subject: [PATCH 21/21] add type definition & documentation --- index.d.ts | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 17 +++++------ 2 files changed, 94 insertions(+), 9 deletions(-) diff --git a/index.d.ts b/index.d.ts index 95eaee2..473a918 100644 --- a/index.d.ts +++ b/index.d.ts @@ -26,6 +26,55 @@ declare class Emittery { */ on(eventName: string, listener: (eventData?: any) => any): Emittery.UnsubscribeFn; + /** + * Get an async iterator which buffers data each time an event is emitted. + * + * Call `return()` on the iterator to remove the subscription. + * + * @example + * ``` + * const iterator = emitter.events('🦄'); + * + * emitter.emit('🦄', '🌈1'); // buffered + * emitter.emit('🦄', '🌈2'); // buffered + * + * iterator + * .next() + * .then( ({value, done}) => { + * // done is false + * // value === '🌈1' + * return iterator.next(); + * }) + * .then( ({value, done}) => { + * // done is false + * // value === '🌈2' + * // revoke subscription + * return iterator.return(); + * }) + * .then(({done}) => { + * // done is true + * }); + * ``` + * + * In practice you would usually consume the events using the [for await](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of) statement. + * In that case, to revoke the subscription simply break the loop + * + * @example + * ``` + * // in an async context + * const iterator = emitter.events('🦄'); + * + * emitter.emit('🦄', '🌈1'); // buffered + * emitter.emit('🦄', '🌈2'); // buffered + * + * for await (const data of iterator){ + * if(data === '🌈2') + * break; // revoke the subscription when we see the value '🌈2' + * } + * ``` + */ + events(eventName:string): AsyncIterableIterator + /** * Remove an event subscription. */ @@ -74,6 +123,39 @@ declare class Emittery { */ onAny(listener: (eventName: string, eventData?: any) => any): Emittery.UnsubscribeFn; + /* + * Get an async 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. + * + * @example + * ``` + * const iterator = emitter.anyEvent(); + * + * emitter.emit('🦄', '🌈1'); // buffered + * emitter.emit('🌟', '🌈2'); // buffered + * + * iterator.next() + * .then( ({value, done}) => { + * // done is false + * // value is ['🦄', '🌈1'] + * return iterator.next(); + * }) + * .then( ({value, done}) => { + * // done is false + * // value is ['🌟', '🌈2'] + * // revoke subscription + * return iterator.return(); + * }) + * .then(({done}) => { + * // done is true + * }); + * ``` + * + * In the same way as for ``events`` you can subscribe by using the ``for await`` statement + */ + anyEvent(): AsyncIterableIterator + /** * Remove an `onAny` subscription. */ @@ -144,6 +226,8 @@ declare namespace Emittery { on>(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; on(eventName: Name, listener: () => any): Emittery.UnsubscribeFn; + events>(eventName: Name): AsyncIterableIterator; + once>(eventName: Name): Promise; once(eventName: Name): Promise; @@ -151,6 +235,8 @@ declare namespace Emittery { off(eventName: Name, listener: () => any): void; onAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => any): Emittery.UnsubscribeFn; + anyEvent(): AsyncIterableIterator<[Extract, EventDataMap[Extract]]>; + offAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => any): void; emit>(eventName: Name, eventData: EventDataMap[Name]): Promise; diff --git a/readme.md b/readme.md index dd07083..b646425 100644 --- a/readme.md +++ b/readme.md @@ -81,7 +81,8 @@ const iterator = emitter.events('🦄'); emitter.emit('🦄', '🌈1'); // buffered emitter.emit('🦄', '🌈2'); // buffered -iterator.next() +iterator + .next() .then( ({value, done}) => { // done is false // value === '🌈1' @@ -89,10 +90,9 @@ iterator.next() }) .then( ({value, done}) => { // done is false - // value === '🌈2' - - // revoke subscription - return iterator.return(); + // value === '🌈2' + // revoke subscription + return iterator.return(); }) .then(({done}) => { // done is true @@ -160,10 +160,9 @@ iterator.next() }) .then( ({value, done}) => { // done is false - // value is ['🌟', '🌈2'] - - // revoke subscription - return iterator.return(); + // value is ['🌟', '🌈2'] + // revoke subscription + return iterator.return(); }) .then(({done}) => { // done is true