@@ -193,28 +193,58 @@ export function pMapIterable(
193193
194194 return {
195195 async * [ Symbol . asyncIterator ] ( ) {
196- const iterator = iterable [ Symbol . asyncIterator ] === undefined ? iterable [ Symbol . iterator ] ( ) : iterable [ Symbol . asyncIterator ] ( ) ;
196+ const isSyncIterator = iterable [ Symbol . asyncIterator ] === undefined ;
197+ const iterator = isSyncIterator ? iterable [ Symbol . iterator ] ( ) : iterable [ Symbol . asyncIterator ] ( ) ;
197198
198199 const promises = [ ] ;
199200 const promisesIndexFromInputIndex = { } ;
200201 const inputIndexFromPromisesIndex = [ ] ;
201202 let runningMappersCount = 0 ;
202203 let isDone = false ;
203204 let inputIndex = 0 ;
204- let outputIndex = 0 ; // Only used when `preserveOrder: false`
205+ let outputIndex = 0 ; // Only used when `preserveOrder: true`
206+
207+ // This event emitter prevents the race conditions that arises when:
208+ // - `preserveOrder: false`
209+ // - `promises` are added after `Promise.race` is invoked, since `Promise.race` only races the promises that existed in its input array at call time
210+ // More specifically, this occurs when (in addition to `preserveOrder: false`):
211+ // - `concurrency === Number.PositiveInfinity && Number.PositiveInfinity === backpressure`
212+ // - this forces us to forgo eagerly filling the `promises` pool to avoid infinite recursion
213+ // - IMO this is the root of this problem, and a problem in and of itself: we should consider requiring a finite concurrency & backpressure
214+ // - given the inability to eagerly filing the `promises` pool with infinite concurrency & backpressure, there are some situations in which specifying
215+ // a finite concurrency & backpressure will be faster than specifying the otherwise faster-sounding infinite concurrency & backpressure
216+ // - an async iterator input iterable
217+ // - `mapNext` can't `trySpawn` until it `await`s its `next`, since the input iterable might be done
218+ // - the initial `trySpawn` thus ends when the execution of `mapNext` is suspended to `await next`
219+ // - the input iterable produces more than one element
220+ // - the (single) running `mapNext`'s `trySpawn` _will necessarily_ (since concurrency and backpressure are infinite)
221+ // start another `mapNext` promise that `trySpawn` adds to `promises`
222+ // - this additional promise does not partake in the already-running `nextPromise`, because its underlying `Promise.race` began without it,
223+ // when the initial `trySpawn` returned and `nextPromise` was invoked from the main loop
224+ const promiseEmitter = new EventTarget ( ) ; // Only used when `preserveOrder: false`
225+ const promiseEmitterEvent = 'promiseFulfilled' ;
205226
206227 const nextPromise = preserveOrder
207228 // Treat `promises` as a queue
208229 ? ( ) => {
209- // May be undefined bc of `pMapSkip`s
230+ // May be ` undefined` bc of `pMapSkip`s
210231 while ( promisesIndexFromInputIndex [ outputIndex ] === undefined ) {
211232 outputIndex += 1 ;
212233 }
213234
214235 return promises [ promisesIndexFromInputIndex [ outputIndex ++ ] ] ;
215236 }
216237 // Treat `promises` as a pool (order doesn't matter)
217- : ( ) => Promise . race ( promises ) ;
238+ : ( ) => Promise . race ( [
239+ // Ensures correctness in the case that mappers resolve between the time that one `await nextPromise()` resolves and the next `nextPromise` call is made
240+ // (these promises would otherwise be lost if an event emitter is not listening - the `promises` pool buffers resolved promises to be processed)
241+ // (I wonder if it may be actually be possible to convert the `preserveOrder: false` case to _exclusively_ event-based,
242+ // but such a solution may get messy since we'd want to `yield` from a callback, likely requiring a resolved promises buffer anyway...)
243+ Promise . race ( promises ) ,
244+ // Ensures correctness in the case that more promises are added to `promises` after the initial `nextPromise` call is made
245+ // (these additional promises are not be included in the above `Promise.race`)
246+ new Promise ( resolve => promiseEmitter . addEventListener ( promiseEmitterEvent , r => resolve ( r . detail ) , { once : true } ) )
247+ ] ) ;
218248
219249 function popPromise ( inputIndex ) {
220250 // Swap the fulfilled promise with the last element to avoid an O(n) shift to the `promises` array
@@ -239,7 +269,7 @@ export function pMapIterable(
239269 let next ;
240270 try {
241271 next = iterator . next ( ) ;
242- if ( isPromiseLike ( next ) ) {
272+ if ( ! isSyncIterator ) { // `!isSyncIterator` iff ` isPromiseLike(next)`, but former is already computed
243273 // Optimization: if our concurrency and/or backpressure is bounded (so that we won't infinitely recurse),
244274 // and we need to `await` the next `iterator` element, we first eagerly spawn more `mapNext` promises,
245275 // so that these promises can begin `await`ing their respective `iterator` elements (if needed) and `mapper` results in parallel.
@@ -250,6 +280,7 @@ export function pMapIterable(
250280 // However, the time needed to `await` and ignore these `done` promises is presumed to be small relative to the time needed to perform common
251281 // `async` operations like disk reads, network requests, etc.
252282 // Overall, this can reduce the total time taken to process all elements.
283+ // TODO: in the `concurrency === Number.POSITIVE_INFINITY` case, we could potentially still optimize here by eagerly spawning some # of promises.
253284 if ( backpressure !== Number . POSITIVE_INFINITY ) {
254285 // Spawn if still below concurrency and backpressure limit
255286 trySpawn ( ) ;
@@ -291,12 +322,15 @@ export function pMapIterable(
291322 if ( returnValue === pMapSkip ) {
292323 // If `preserveOrder: true`, resolve to the next inputIndex's promise, in case we are already being `await`ed
293324 // NOTE: no chance that `myInputIndex + 1`-spawning code is waiting to be executed in another part of the event loop,
294- // but currently `promisesIndexFromInputIndex[myInputIndex + 1] === undefined` (so that we incorrectly `mapNext` and
295- // this potentially-currently-awaited promise resolves to the result of mapping a later element than a different member of
296- // `promises`, i.e. `promises` resolve out of order), because all `trySpawn`/`mapNext` calls execute the bookkeeping synchronously,
297- // before any `await`s.
325+ // but currently `promisesIndexFromInputIndex[myInputIndex + 1] === undefined` (so that we incorrectly skip this `if` condition and
326+ // instead call `mapNext`, causing this potentially-currently-awaited promise to resolve to the result of mapping an element
327+ // of the input iterable that was produced later `myInputIndex + 1`, i.e., no chance `promises` resolve out of order, because:
328+ // all `trySpawn`/`mapNext` calls execute their bookkeeping synchronously, before any `await`s, so we cannot observe an intermediate
329+ // state in which input the promise mapping iterable element `myInputIndex + 1` has not been recorded in the `promisesIndexFromInputIndex` ledger.
298330 if ( preserveOrder && promisesIndexFromInputIndex [ myInputIndex + 1 ] !== undefined ) {
299331 popPromise ( myInputIndex ) ;
332+ // Spawn if still below backpressure limit and just dropped below concurrency limit
333+ trySpawn ( ) ;
300334 return promises [ promisesIndexFromInputIndex [ myInputIndex + 1 ] ] ;
301335 }
302336
@@ -321,24 +355,26 @@ export function pMapIterable(
321355 // Reserve index in `promises` array: we don't actually have the promise to save yet,
322356 // but we don't want recursive `trySpawn` calls to use this same index.
323357 // This is safe (i.e., the empty slot won't be `await`ed) because we replace the value immediately,
324- // without yielding to the event loop, so no consumers (namely `getAndRemoveFromPoolNextPromise `)
358+ // without yielding to the event loop, so no consumers (namely `nextPromise `)
325359 // can observe the intermediate state.
326360 const promisesIndex = promises . length ++ ;
327361 promises [ promisesIndex ] = mapNext ( promisesIndex ) ;
362+ promises [ promisesIndex ] . then ( p => promiseEmitter . dispatchEvent ( new CustomEvent ( promiseEmitterEvent , { detail : p } ) ) ) ;
328363 }
329364
365+ // bootstrap `promises`
330366 trySpawn ( ) ;
331367
332368 while ( promises . length > 0 ) {
333- const { result : { error, done, value} , inputIndex} = await nextPromise ( ) ; // eslint-disable-line no-await-in-loop
369+ const { result : { error, done, value} , inputIndex} = await nextPromise ( ) ; // eslint-disable-line no-await-in-loop
334370 popPromise ( inputIndex ) ;
335371
336372 if ( error ) {
337373 throw error ;
338374 }
339375
340376 if ( done ) {
341- // When `preserveOrder: false`, ignore to consume any remaining pending promises in the pool
377+ // When `preserveOrder: false`, `continue` to consume any remaining pending promises in the pool
342378 if ( ! preserveOrder ) {
343379 continue ;
344380 }
0 commit comments