diff --git a/index.d.ts b/index.d.ts index 473a918..4347c71 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,243 +1,237 @@ declare class Emittery { /** - * In TypeScript, returns a decorator which mixins `Emittery` as property `emitteryPropertyName` and `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the target class. - * - * @example - * ``` - * import Emittery = require('emittery'); - * - * @Emittery.mixin('emittery') - * class MyClass {} - * - * let instance = new MyClass(); - * - * instance.emit('event'); - * ``` - */ + In TypeScript, it returns a decorator which mixins `Emittery` as property `emitteryPropertyName` and `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the target class. + + @example + ``` + import Emittery = require('emittery'); + + @Emittery.mixin('emittery') + class MyClass {} + + const instance = new MyClass(); + + instance.emit('event'); + ``` + */ static mixin(emitteryPropertyName: string, methodNames?: readonly string[]): Function; /** - * 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; - - /** - * 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. - */ - 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; - - /** - * 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; - - /* - * 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. - */ - offAny(listener: (eventName: string, eventData?: any) => any): void; - - /** - * Clear all event listeners on the instance. - * - * If `eventName` is given, only the listeners for that event are cleared. - */ + Subscribe to an event. + + Using the same listener multiple times for the same event will result in only one method call per emitted event. + + @returns An unsubscribe method. + */ + on(eventName: string, listener: (eventData?: unknown) => void): 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 + ``` + import Emittery = require('emittery'); + + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + + emitter.emit('🦄', '🌈1'); // Buffered + emitter.emit('🦄', '🌈2'); // Buffered + + iterator + .next() + .then(({value, done}) => { + // done === false + // value === '🌈1' + return iterator.next(); + }) + .then(({value, done}) => { + // done === false + // value === '🌈2' + // Revoke subscription + return iterator.return(); + }) + .then(({done}) => { + // done === 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 + ``` + import Emittery = require('emittery'); + + const emitter = new Emittery(); + const iterator = emitter.events('🦄'); + + emitter.emit('🦄', '🌈1'); // Buffered + emitter.emit('🦄', '🌈2'); // Buffered + + // In an async context. + 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. + */ + off(eventName: string, listener: (eventData?: unknown) => void): void; + + /** + Subscribe to an event only once. It will be unsubscribed after the first + event. + + @returns The event data when `eventName` is emitted. + */ + once(eventName: string): Promise; + + /** + Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently. + + @returns A promise that resolves 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. + */ + emit(eventName: string, eventData?: unknown): 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 that resolves when all the event listeners are done. + */ + emitSerial(eventName: string, eventData?: unknown): Promise; + + /** + Subscribe to be notified about any event. + + @returns A method to unsubscribe. + */ + onAny(listener: (eventName: string, eventData?: unknown) => unknown): 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. + + In the same way as for `events`, you can subscribe by using the `for await` statement. + + @example + ``` + import Emittery = require('emittery'); + + const emitter = new Emittery(); + 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 + }); + ``` + */ + anyEvent(): AsyncIterableIterator + + /** + Remove an `onAny` subscription. + */ + offAny(listener: (eventName: string, eventData?: unknown) => void): void; + + /** + Clear all event listeners on the instance. + + If `eventName` is given, only the 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 listeners for the `eventName` or all events if not specified. + */ listenerCount(eventName?: string): number; /** - * Bind the given `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the `target` object. - * - * @example - * ``` - * import Emittery = require('emittery'); - * - * let object = {}; - * - * new Emittery().bindMethods(object); - * - * object.emit('event'); - * ``` - */ + Bind the given `methodNames`, or all `Emittery` methods if `methodNames` is not defined, into the `target` object. + + @example + ``` + import Emittery = require('emittery'); + + const object = {}; + + new Emittery().bindMethods(object); + + object.emit('event'); + ``` + */ bindMethods(target: object, methodNames?: readonly string[]): void; } declare namespace Emittery { /** - * Removes an event subscription. - */ + Removes an event subscription. + */ type UnsubscribeFn = () => void; /** - * Maps event names to their emitted data type. - */ + Maps event names to their emitted data type. + */ interface Events { - [eventName: string]: any; + [eventName: string]: unknown; } /** - * 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 - * ``` - */ + Async event emitter. + + You must list supported events and the data type they emit, if any. + + @example + ``` + import Emittery = require('emittery'); + + const emitter = new Emittery.Typed<{value: string}, 'open' | 'close'>(); + + emitter.emit('open'); + emitter.emit('value', 'foo\n'); + emitter.emit('value', 1); // TS compilation error + emitter.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; + on>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): Emittery.UnsubscribeFn; + on(eventName: Name, listener: () => void): Emittery.UnsubscribeFn; events>(eventName: Name): AsyncIterableIterator; once>(eventName: Name): Promise; once(eventName: Name): Promise; - off>(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): void; - off(eventName: Name, listener: () => any): void; + off>(eventName: Name, listener: (eventData: EventDataMap[Name]) => void): void; + off(eventName: Name, listener: () => void): void; - onAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => any): Emittery.UnsubscribeFn; + onAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => void): Emittery.UnsubscribeFn; anyEvent(): AsyncIterableIterator<[Extract, EventDataMap[Extract]]>; - offAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => any): void; + offAny(listener: (eventName: Extract | EmptyEvents, eventData?: EventDataMap[Extract]) => void): void; emit>(eventName: Name, eventData: EventDataMap[Name]): Promise; emit(eventName: Name): Promise; diff --git a/index.js b/index.js index 5de838c..0a75dc2 100644 --- a/index.js +++ b/index.js @@ -54,21 +54,23 @@ function enqueueProducers(instance, eventName, eventData) { } function iterator(instance, eventName) { - let finished = false; + let isFinished = false; let flush = () => {}; let queue = []; + const producer = { enqueue(item) { queue.push(item); flush(); }, finish() { - finished = true; + isFinished = true; flush(); } }; getEventProducers(instance, eventName).add(producer); + return { async next() { if (!queue) { @@ -76,27 +78,34 @@ function iterator(instance, eventName) { } if (queue.length === 0) { - if (finished) { - queue = null; + if (isFinished) { + queue = undefined; return this.next(); } await new Promise(resolve => { flush = resolve; }); + return this.next(); } - return {done: false, value: await queue.shift()}; + return { + done: false, + value: await queue.shift() + }; }, + async return(value) { - queue = null; + queue = undefined; getEventProducers(instance, eventName).delete(producer); flush(); + return arguments.length > 0 ? {done: true, value: await value} : {done: true}; }, + [Symbol.asyncIterator]() { return this; } @@ -268,6 +277,7 @@ class Emittery { clearListeners(eventName) { if (typeof eventName === 'string') { getListeners(this, eventName).clear(); + const producers = getEventProducers(this, eventName); for (const producer of producers) { diff --git a/package.json b/package.json index 2bcb037..160e16e 100644 --- a/package.json +++ b/package.json @@ -47,12 +47,12 @@ "typed" ], "devDependencies": { - "@types/node": "^12.0.8", - "ava": "^2.1.0", + "@types/node": "^12.7.5", + "ava": "^2.4.0", "codecov": "^3.1.0", - "delay": "^4.1.0", + "delay": "^4.3.0", "nyc": "^14.1.1", - "tsd": "^0.7.3", + "tsd": "^0.7.4", "xo": "^0.24.0" }, "nyc": { diff --git a/readme.md b/readme.md index b646425..f490098 100644 --- a/readme.md +++ b/readme.md @@ -61,6 +61,10 @@ 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. ```js +const Emittery = require('emittery'); + +const emitter = new Emittery(); + emitter.once('🦄').then(data => { console.log(data); //=> '🌈' @@ -76,51 +80,56 @@ Get an async iterator which buffers data each time an event is emitted. Call `return()` on the iterator to remove the subscription. ```js +const Emittery = require('emittery'); + +const emitter = new Emittery(); const iterator = emitter.events('🦄'); -emitter.emit('🦄', '🌈1'); // buffered -emitter.emit('🦄', '🌈2'); // buffered +emitter.emit('🦄', '🌈1'); // Buffered +emitter.emit('🦄', '🌈2'); // Buffered iterator .next() - .then( ({value, done}) => { - // done is false - // value === '🌈1' + .then(({value, done}) => { + // done === false + // value === '🌈1' return iterator.next(); }) - .then( ({value, done}) => { - // done is false + .then(({value, done}) => { + // done === false // value === '🌈2' - // revoke subscription + // Revoke subscription return iterator.return(); }) .then(({done}) => { - // done is true + // done === 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 +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 Emittery = require('emittery'); + +const emitter = new Emittery(); const iterator = emitter.events('🦄'); -emitter.emit('🦄', '🌈1'); // buffered -emitter.emit('🦄', '🌈2'); // buffered +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' +// In an async context. +for await (const data of iterator) { + if (data === '🌈2') { + break; // Revoke the subscription when we see the value '🌈2'. + } } - ``` -#### emit(eventName, [data]) +#### emit(eventName, data?) -Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but execute concurrently. +Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed 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 that resolves 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. #### emitSerial(eventName, data?) @@ -147,29 +156,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 Emittery = require('emittery'); + +const emitter = new Emittery(); const iterator = emitter.anyEvent(); -emitter.emit('🦄', '🌈1'); // buffered -emitter.emit('🌟', '🌈2'); // buffered +emitter.emit('🦄', '🌈1'); // Buffered +emitter.emit('🌟', '🌈2'); // Buffered iterator.next() - .then( ({value, done}) => { - // done is false + .then(({value, done}) => { + // done === false // value is ['🦄', '🌈1'] return iterator.next(); }) - .then( ({value, done}) => { - // done is false + .then(({value, done}) => { + // done === false // value is ['🌟', '🌈2'] - // revoke subscription + // Revoke subscription return iterator.return(); }) .then(({done}) => { - // done is true + // done === true }); ``` -In the same way as for ``events`` you can subscribe by using the ``for await`` statement +In the same way as for `events`, you can subscribe by using the `for await` statement #### clearListeners() @@ -188,7 +200,7 @@ Bind the given `methodNames`, or all `Emittery` methods if `methodNames` is not ```js import Emittery = require('emittery'); -let object = {}; +const object = {}; new Emittery().bindMethods(object); @@ -203,12 +215,12 @@ The default `Emittery` class does not let you type allowed event names and their ```ts import Emittery = require('emittery'); -const ee = new Emittery.Typed<{value: string}, 'open' | 'close'>(); +const emitter = 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 +emitter.emit('open'); +emitter.emit('value', 'foo\n'); +emitter.emit('value', 1); // TS compilation error +emitter.emit('end'); // TS compilation error ``` ### Emittery.mixin(emitteryPropertyName, methodNames?) @@ -221,7 +233,7 @@ import Emittery = require('emittery'); @Emittery.mixin('emittery') class MyClass {} -let instance = new MyClass(); +const instance = new MyClass(); instance.emit('event'); ```