Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand All @@ -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<Name extends keyof AllEventData>(eventName: Name | readonly Name[]): EmitteryOncePromise<AllEventData[Name]>;
once<Name extends keyof AllEventData>(eventName: Name | readonly Name[], predicate?: (eventData: AllEventData[Name]) => boolean): EmitteryOncePromise<AllEventData[Name]>;

/**
Trigger an event asynchronously, optionally with some data. Listeners are called in the order they were added, but executed concurrently.
Expand Down
10 changes: 9 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
14 changes: 11 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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)
Expand Down
53 changes: 53 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down