function iterate(finalCallback) {
    let handleResultsInProgress = 0;
    let currentDocumentIndex = 0;

    let error = null;
    for (let i = 0; i < parallel; ++i) {
      enqueue(createFetch());
    }

    function createFetch() {
      let documentsBatch = [];
      let drained = false;

      return fetch;

      function fetch(done) {
        if (drained || aborted) {
          return done();
        } else if (error) {
          return done();
        }

        next(function(err, doc) {
          if (error != null) {
            return done();
          }
          if (err != null) {
            if (err.name === 'MongoCursorExhaustedError') {
              // We may end up calling `next()` multiple times on an exhausted
              // cursor, which leads to an error. In case cursor is exhausted,
              // just treat it as if the cursor returned no document, which is
              // how a cursor indicates it is exhausted.
              doc = null;
            } else if (continueOnError) {
              aggregatedErrors.push(err);
            } else {
              error = err;
              finalCallback(err);
              return done();
            }
          }
          if (doc == null) {
            drained = true;
            if (handleResultsInProgress <= 0) {
              const finalErr = continueOnError ?
                createEachAsyncMultiError(aggregatedErrors) :
                error;

              finalCallback(finalErr);
            } else if (batchSize && documentsBatch.length) {
              handleNextResult(documentsBatch, currentDocumentIndex++, handleNextResultCallBack);
            }
            return done();
          }

          ++handleResultsInProgress;

          // Kick off the subsequent `next()` before handling the result, but
          // make sure we know that we still have a result to handle re: #8422
          immediate(() => done());

          if (batchSize) {
            documentsBatch.push(doc);
          }

          // If the current documents size is less than the provided batch size don't process the documents yet
          if (batchSize && documentsBatch.length !== batchSize) {
            immediate(() => enqueue(fetch));
            return;
          }

          const docsToProcess = batchSize ? documentsBatch : doc;

          function handleNextResultCallBack(err) {
            if (batchSize) {
              handleResultsInProgress -= documentsBatch.length;
              documentsBatch = [];
            } else {
              --handleResultsInProgress;
            }
            if (err != null) {
              if (continueOnError) {
                aggregatedErrors.push(err);
              } else {
                error = err;
                return finalCallback(err);
              }
            }
            if ((drained || aborted) && handleResultsInProgress <= 0) {
              const finalErr = continueOnError ?
                createEachAsyncMultiError(aggregatedErrors) :
                error;
              return finalCallback(finalErr);
            }

            immediate(() => enqueue(fetch));
          }

          handleNextResult(docsToProcess, currentDocumentIndex++, handleNextResultCallBack);
        });
      }
    }
  }
Execute `fn` for every document in the cursor. If `fn` returns a promise,
will wait for the promise to resolve before iterating on to the next one.
Returns a promise that resolves when done.

@param {Function} next the thunk to call to get the next document
@param {Function} fn
@param {Object} options
@param {Number} [options.batchSize=null] if set, Mongoose will call `fn` with an array of at most `batchSize` documents, instead of a single document
@param {Number} [options.parallel=1] maximum number of `fn` calls that Mongoose will run in parallel
@param {AbortSignal} [options.signal] allow cancelling this eachAsync(). Once the abort signal is fired, `eachAsync()` will immediately fulfill the returned promise (or call the callback) and not fetch any more documents.
@return {Promise}
@api public
@method eachAsync