sindresorhus / p-map

Map over promises concurrently
MIT License
1.29k stars 58 forks source link

Add support for AsyncIterable as input #46

Closed huntharo closed 3 years ago

huntharo commented 3 years ago

Overview

Open Issues

sindresorhus commented 3 years ago

Duplicating the tests for the async versions doesn't feel right

That's how it's done. There's good way of mixing async and sync in JS.

sindresorhus commented 3 years ago

There is a problem in the original functionality around the stop-on-error test that needs to be resolved

Which test exactly? Can you link to it?

huntharo commented 3 years ago

There is a problem in the original functionality around the stop-on-error test that needs to be resolved

Which test exactly? Can you link to it?

This test: https://github.com/sindresorhus/p-map/blob/main/test.js#L121-L135

Stop on error is problematic... there can be many concurrent mappers running and they will continue to run even if iteration is "stopped" after one of them throws. If the continuing mappers also throw they will have nowhere to throw to that can be caught. So it seems that the best way to handle an exception in one mapper is to check if any other mappers are running, push the exception onto an array, then let the other mappers either succeed or fail, and the last of them throws and AggregateError with the exceptions from all of them or just throws the single error if only one of them failed. It definitely should not throw until all of the mappers have finished though as that could lead to the consumer restarting the pMap while mappers from the prior run are still active.

Ultimately what I ended up thinking was: if you need clean error handling then you should never allow the mapper to throw and instead handle the error yourself in the mapper. However, I don't think there is a way for the mapper to signal that iteration should stop early.

Current form of the test in the PR

test('do not run mapping after stop-on-error happened', async t => {
    const input = [1, async () => delay(300, {value: 2}), 3];
    const mappedValues = [];
    await t.throwsAsync(
        pMap(input, async value => {
            value = typeof value === 'function' ? await value() : value;
            mappedValues.push(value);
            if (value === 1) {
                await delay(100);
                throw new Error('Oops!');
            }
        })
    );
    await delay(500);
    t.deepEqual(mappedValues, [1, 3, 2]);
});
sindresorhus commented 3 years ago

This test: https://github.com/sindresorhus/p-map/blob/main/test.js#L121-L135

// @papb ^

huntharo commented 3 years ago

This test: https://github.com/sindresorhus/p-map/blob/main/test.js#L121-L135

// @papb ^

I added a PR with a failing test case that shows that multiple concurrent mapper exceptions result in only one of those errors being catchable:

49

I think the stopOnError true/false logic can be somewhat consolidated so that both reject with an AggregateError when mappers have all stopped, but stopOnError true immediately stops invoking more mappers and iterating more source items (if applicable, as in concurrency !== Infinity).

sindresorhus commented 3 years ago

I think the stopOnError true/false logic can be somewhat consolidated so that both reject with an AggregateError when mappers have all stopped

Per https://github.com/sindresorhus/p-map/pull/49#issuecomment-916271810, stopOnError: true should only return a single error. This is how Promise.all works too.

huntharo commented 3 years ago

I believe all outstanding issues have been addressed now.

sindresorhus commented 3 years ago

Can you update the readme too?

huntharo commented 3 years ago

Can you update the readme too?

The readme has been updated.

sindresorhus commented 3 years ago

Looks good. Thanks for contributing this :)

huntharo commented 3 years ago

Looks good. Thanks for contributing this :)

No problem. Thanks for this (and other) modules and for the timely reviews and merges!