diff --git a/index.d.ts b/index.d.ts index c2ca0d1..9eb96a9 100644 --- a/index.d.ts +++ b/index.d.ts @@ -462,10 +462,12 @@ export default class Emittery< ): void; /** - Subscribe to one or more events only once. It will be unsubscribed after the first - event. + Subscribe to one or more events only once. It will be unsubscribed after the first event that matches the predicate (if provided). - @returns The promise of event data when `eventName` is emitted. This promise is extended with an `off` method. + @param eventName - The event name(s) to subscribe to. + @param predicate - Optional predicate function to filter event data. The event will only be emitted if the predicate returns true. + + @returns The promise of event data when `eventName` is emitted and predicate matches (if provided). This promise is extended with an `off` method. @example ``` @@ -482,11 +484,19 @@ export default class Emittery< console.log(data); }); + // With predicate + emitter.once('data', data => data.ok === true).then(data => { + console.log(data); + //=> {ok: true, value: 42} + }); + emitter.emit('🦄', '🌈'); // Logs `🌈` twice emitter.emit('🐶', '🍖'); // Nothing happens + emitter.emit('data', {ok: false}); // Nothing happens + emitter.emit('data', {ok: true, value: 42}); // Logs {ok: true, value: 42} ``` */ - once(eventName: Name | readonly Name[]): EmitteryOncePromise; + once(eventName: Name | readonly Name[], predicate?: (eventData: AllEventData[Name]) => boolean): EmitteryOncePromise; /** Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently. diff --git a/index.js b/index.js index e61ca0a..5f4005a 100644 --- a/index.js +++ b/index.js @@ -334,11 +334,19 @@ export default class Emittery { } } - once(eventNames) { + once(eventNames, predicate) { + if (predicate !== undefined && typeof predicate !== 'function') { + throw new TypeError('predicate must be a function'); + } + let off_; const promise = new Promise(resolve => { off_ = this.on(eventNames, data => { + if (predicate && !predicate(data)) { + return; + } + off_(); resolve(data); }); diff --git a/readme.md b/readme.md index 5e596f4..657ac04 100644 --- a/readme.md +++ b/readme.md @@ -297,11 +297,11 @@ await emitter.emit('🦊', 'c'); // Nothing happens ##### listener(data) -#### once(eventName | eventName[]) +#### once(eventName | eventName[], predicate?) -Subscribe to one or more events only once. It will be unsubscribed after the first event. +Subscribe to one or more events only once. It will be unsubscribed after the first event that matches the predicate (if provided). -Returns a promise for the event data when `eventName` is emitted. This promise is extended with an `off` method. +Returns a promise for the event data when `eventName` is emitted and predicate matches (if provided). This promise is extended with an `off` method. ```js import Emittery from 'emittery'; @@ -317,8 +317,16 @@ emitter.once(['🦄', '🐶']).then(data => { console.log(data); }); +// With predicate +emitter.once('data', data => data.ok === true).then(data => { + console.log(data); + //=> {ok: true, value: 42} +}); + emitter.emit('🦄', '🌈'); // Log => '🌈' x2 emitter.emit('🐶', '🍖'); // Nothing happens +emitter.emit('data', {ok: false}); // Nothing happens +emitter.emit('data', {ok: true, value: 42}); // Log => {ok: true, value: 42} ``` #### events(eventName) diff --git a/test/index.js b/test/index.js index bd928df..1645044 100644 --- a/test/index.js +++ b/test/index.js @@ -432,6 +432,59 @@ test('once() - returns a promise with an unsubscribe method', async t => { t.pass(); }); +test('once() - supports filter predicate', async t => { + const emitter = new Emittery(); + + const oncePromise = emitter.once('data', data => data.ok === true); + await emitter.emit('data', {ok: false, foo: 'bar'}); + + const payload = {ok: true, value: 42}; + + await emitter.emit('data', payload); + await emitter.emit('data', {ok: true, other: 'value'}); + + t.is(await oncePromise, payload); +}); + +test('once() - filter predicate must be a function', t => { + const emitter = new Emittery(); + t.throws( + () => emitter.once('data', 'not a function'), + { + instanceOf: TypeError, + message: 'predicate must be a function', + }, + ); +}); + +test('once() - filter predicate with multiple event names', async t => { + const emitter = new Emittery(); + const payload = {ok: true, value: 42}; + + const oncePromise = emitter.once(['data1', 'data2'], data => data.ok === true); + await emitter.emit('data1', {ok: false}); + await emitter.emit('data2', payload); + + t.is(await oncePromise, payload); +}); + +test('once() - filter predicate can be unsubscribed', async t => { + const emitter = new Emittery(); + const oncePromise = emitter.once('data', data => data.ok === true); + + oncePromise.off(); + await emitter.emit('data', {ok: true}); + + const testPromise = Promise.race([ + oncePromise, + new Promise(resolve => { + setTimeout(() => resolve('timeout'), 100); + }), + ]); + + t.is(await testPromise, 'timeout'); +}); + test('emit() - one event', async t => { const emitter = new Emittery(); const eventFixture = {foo: true};