Source: cancelable-promise-helpers.js

import { assert } from '@ember/debug';
import RSVP, { Promise } from 'rsvp';
import { TaskInstance } from './task-instance';
import { cancelableSymbol } from './external/yieldables';

/**
 * A cancelation-aware variant of [Promise.all](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all).
 * The normal version of a `Promise.all` just returns a regular, uncancelable
 * Promise. The `ember-concurrency` variant of `all()` has the following
 * additional behavior:
 *
 * - if the task that `yield`ed `all()` is canceled, any of the
 *   {@linkcode TaskInstance}s passed in to `all` will be canceled
 * - if any of the {@linkcode TaskInstance}s (or regular promises) passed in reject (or
 *   are canceled), all of the other unfinished `TaskInstance`s will
 *   be automatically canceled.
 *
 * [Check out the "Awaiting Multiple Child Tasks example"](/docs/examples/joining-tasks)
 */
export const all = taskAwareVariantOf(RSVP.Promise, 'all', identity);

/**
 * A cancelation-aware variant of [RSVP.allSettled](https://api.emberjs.com/ember/release/functions/rsvp/allSettled).
 * The normal version of a `RSVP.allSettled` just returns a regular, uncancelable
 * Promise. The `ember-concurrency` variant of `allSettled()` has the following
 * additional behavior:
 *
 * - if the task that `yield`ed `allSettled()` is canceled, any of the
 *   {@linkcode TaskInstance}s passed in to `allSettled` will be canceled
 */
export const allSettled = taskAwareVariantOf(RSVP, 'allSettled', identity);

/**
 * A cancelation-aware variant of [Promise.race](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race).
 * The normal version of a `Promise.race` just returns a regular, uncancelable
 * Promise. The `ember-concurrency` variant of `race()` has the following
 * additional behavior:
 *
 * - if the task that `yield`ed `race()` is canceled, any of the
 *   {@linkcode TaskInstance}s passed in to `race` will be canceled
 * - once any of the tasks/promises passed in complete (either success, failure,
 *   or cancelation), any of the {@linkcode TaskInstance}s passed in will be canceled
 *
 * [Check out the "Awaiting Multiple Child Tasks example"](/docs/examples/joining-tasks)
 */
export const race = taskAwareVariantOf(Promise, 'race', identity);

/**
 * A cancelation-aware variant of [RSVP.hash](https://api.emberjs.com/ember/release/functions/rsvp/hash).
 * The normal version of a `RSVP.hash` just returns a regular, uncancelable
 * Promise. The `ember-concurrency` variant of `hash()` has the following
 * additional behavior:
 *
 * - if the task that `yield`ed `hash()` is canceled, any of the
 *   {@linkcode TaskInstance}s passed in to `hash` will be canceled
 * - if any of the items rejects/cancels, all other cancelable items
 *   (e.g. {@linkcode TaskInstance}s) will be canceled
 */
export const hash = taskAwareVariantOf(RSVP, 'hash', getValues);

/**
 * A cancelation-aware variant of [RSVP.hashSettled](https://api.emberjs.com/ember/release/functions/rsvp/hashSettled).
 * The normal version of a `RSVP.hashSettled` just returns a regular, uncancelable
 * Promise. The `ember-concurrency` variant of `hashSettled()` has the following
 * additional behavior:
 *
 * - if the task that `yield`ed `hashSettled()` is canceled, any of the
 *   {@linkcode TaskInstance}s passed in to `hashSettled` will be canceled
 */
export const hashSettled = taskAwareVariantOf(RSVP, 'hashSettled', getValues);


function identity(obj) {
  return obj;
}

function getValues(obj) {
  return Object.keys(obj).map(k => obj[k]);
}

function taskAwareVariantOf(obj, method, getItems) {
  return function(thing) {
    let items = getItems(thing);
    assert(`'${method}' expects an array.`, Array.isArray(items));

    items.forEach((it) => {
      // Mark TaskInstances, including those that performed synchronously and
      // have finished already, as having their errors handled, as if they had
      // been then'd, which this is emulating.
      if (it && it instanceof TaskInstance) {
        it.executor.asyncErrorsHandled = true;
      }
    });

    let defer = RSVP.defer();

    obj[method](thing).then(defer.resolve, defer.reject);

    let hasCancelled = false;
    let cancelAll = () => {
      if (hasCancelled) { return; }
      hasCancelled = true;
      items.forEach(it => {
        if (it) {
          if (it instanceof TaskInstance) {
            it.cancel();
          } else if (typeof it[cancelableSymbol] === 'function') {
            it[cancelableSymbol]();
          }
        }
      });
    };

    let promise = defer.promise.finally(cancelAll);
    promise[cancelableSymbol] = cancelAll;
    return promise;
  };
}