sindresorhus / p-event

Promisify an event by waiting for it to be emitted

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Handle multiple events

sindresorhus opened this issue · comments

Sometimes it might be useful to handle multiple events and gather them. The problem is determining when it's done. Either the user would choose how many events to wait for, or they would choose an event that indicates it's done. I'm not entirely sure how to design this. Should it be part of the existing API or a pEvent.multi() method? What should the options be?

If anyone needs this, please add your use-case so I can be better informed on how to design this.

Proposal:

// Gathers `data` events until it gets a `finish` event
pEvent.multi(emitter, 'data', {
	doneEvent: 'finish'
}).then(data => {
	console.log(data);
	//=> ['a', 'b']
});

// Gathers 4 `data` events
pEvent.multi(emitter, 'data', {
	count: 4
}).then(data => {
	console.log(data);
	//=> ['a', 'b', 'c', 'd']
});

// Gathers `data` events until it reaches 4 or gets a `finish` event
pEvent.multi(emitter, 'data', {
	count: 4,
	doneEvent: 'finish'
}).then(data => {
	console.log(data);
	//=> ['a', 'b', 'c', 'd']
});

The rejectionEvents option applies here too, but not the multiArgs option.

Could use some opinions on this.


@SamVerschueren @alextes @schnittstabil In case you have time to give an opinion on this. No worries if not.

I've done some scientific research in the area of complex event processing (CEP) some years ago.
And because of that, I would say handle multiple events and gather them can mean many things. One may want to select/filter, group, join, etc. events (similar to query languages like SQL).

Determining when it's done can become easy with the right tools, e.g. using some non-existent functions/classes:

// e.g. emits every 4 events a 'done' event:
const emitter2 = new CollectEmitter(emitter, {count: 4, doneEvent: 'done'});

pEvent(emitter2, 'done').then(data => {
	//=> ['a', 'b', 'c', 'd']
});

Well, similar can be done with observables – the point I want to make is: doing counting/grouping/etc in the world of emitters/observables would be much more flexible.

@schnittstabil Thanks for chiming in. Interesting. I hadn't considered that. Although I think that's a bit out of scope of this module, and as you said, better handled by emitters/Observables. I guess I could document that as a tip.

Nice idea! I'm just going back and forth on adding it to the current API or using something like pEvent.multi(). The thing is that if you pass in doneEvent or something as count, it implies that you will receive multiple events. But then again, explicit is (almost) always better than implicit. So if I follow that though, pEvent.multi() makes perfect sense.

I agree that the CollectEmitter idea is out of scope. Events are always easier to cover with Observables. So for this library, the proposed idea from @sindresorhus looks good.

Additionally, functions could be supported as well:

pEvent.multi(fibonacciEmitter, 'data', data => data >= 42).then(data => {
	console.log(data);
	//=> [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
});

That would allow arbitrary complex done conditions.


A question which comes to my mind: what should be done with the arguments of the doneEvent? E.g. em.emit('finish', 'unicorn')?

Additionally, functions could be supported as well:

Good idea. Yeah, that looks flexible enough to solve most problems, while being simple.

A question which comes to my mind: what should be done with the arguments of the doneEvent? E.g. em.emit('finish', 'unicorn')?

I have yet to encounter a done/finish event that emits a value. Maybe we should just see if it actually comes up in real world situations before considering it.

In short, this is feasible as:

await pEvent(emitter, 'data');
await pEvent(emitter, 'data');
await pEvent(emitter, 'data');
await pEvent(emitter, 'data');

Or

for (let i = 0; i < 4; i++) {
    await pEvent(emitter, 'data');
}

Or

const waitForIt = () => pEvent(emitter, 'data');
waitForIt()
.then(waitForIt)
.then(waitForIt)
.then(waitForIt)

However perhaps promises are not exactly the best way to handle these. Maybe generators?

@bfred-it Sure, that works too, and it gives more flexibility. The idea with pEvent.multi is to make some common patterns super easy. Like gathering all events or a set amount.

My proposal:

pEvent.multi(emitter, event, {
	count: Infinity,
	timeout: Infinity,
	filter: undefined,
	rejectionEvents: ['error'],
	resolveImmediately: true,
	multiArgs: false
});

Examples:

await pEvent.multi(emitter, event, {count: 3});
// => ['a', 'b', 'c']

await pEvent.multi(emitter, event, {rejectionEvents: ['finish']});
// => ['a', 'b', 'c', 'd']

await pEvent.multi(emitter, event, {count: 3, rejectionEvents: ['finish']});
// => ['a', 'b', 'c']

await pEvent.multi(emitter, event, {count: 5, rejectionEvents: ['finish']});
// => ['a', 'b', 'c']

const data = await pEvent.multi(emitter, event, {resolveImmediately: true});
// => []
// 4s later => ['a', 'b', 'c', 'd']

await pEvent.multi(emitter, event, {multiArgs: true, count: 3});
// => [['a', 1], ['b', 2], ['c', 3]]

await pEvent.multi(emitter, event, {count: 3, filter: letter => letter !== 'b'});
// => ['a', 'c', 'd']

@szmarczak Perfect. I agree with everything.