Source: task.js

import { setOwner, getOwner } from '@ember/application';
import EmberObject, { get, set } from '@ember/object';
import { isDestroying, registerDestructor } from '@ember/destroyable';
import { Task as BaseTask } from './external/task/task';
import { TaskInstance } from './task-instance';
import {
  PERFORM_TYPE_DEFAULT,
  TaskInstanceExecutor,
  PERFORM_TYPE_LINKED,
} from './external/task-instance/executor';
import { EMBER_ENVIRONMENT } from './ember-environment';
import { TASKABLE_MIXIN } from './taskable-mixin';
import { TRACKED_INITIAL_TASK_STATE } from './tracked-state';
import { CANCEL_KIND_LIFESPAN_END } from './external/task-instance/cancelation';

/**
  The `Task` object lives on a host Ember object (e.g.
  a Component, Route, or Controller). You call the
  {@linkcode Task#perform .perform()} method on this object
  to create run individual {@linkcode TaskInstance}s,
  and at any point, you can call the {@linkcode Task#cancelAll .cancelAll()}
  method on this object to cancel all running or enqueued
  {@linkcode TaskInstance}s.

  <style>
    .ignore-this--this-is-here-to-hide-constructor,
    #Task { display: none }
  </style>

  @class Task
*/
export class Task extends BaseTask {
  /**
   * `true` if any current task instances are running.
   *
   * @memberof Task
   * @member {boolean} isRunning
   * @instance
   * @readOnly
   */
  /**
   * `true` if any future task instances are queued.
   *
   * @memberof Task
   * @member {boolean} isQueued
   * @instance
   * @readOnly
   */
  /**
   * `true` if the task is not in the running or queued state.
   *
   * @memberof Task
   * @member {boolean} isIdle
   * @instance
   * @readOnly
   */
  /**
   * The current state of the task: `"running"`, `"queued"` or `"idle"`.
   *
   * @memberof Task
   * @member {string} state
   * @instance
   * @readOnly
   */
  /**
   * The most recently started task instance.
   *
   * @memberof Task
   * @member {TaskInstance} last
   * @instance
   * @readOnly
   */
  /**
   * The most recent task instance that is currently running.
   *
   * @memberof Task
   * @member {TaskInstance} lastRunning
   * @instance
   * @readOnly
   */
  /**
   * The most recently performed task instance.
   *
   * @memberof Task
   * @member {TaskInstance} lastPerformed
   * @instance
   * @readOnly
   */
  /**
   * The most recent task instance that succeeded.
   *
   * @memberof Task
   * @member {TaskInstance} lastSuccessful
   * @instance
   * @readOnly
   */
  /**
   * The most recently completed task instance.
   *
   * @memberof Task
   * @member {TaskInstance} lastComplete
   * @instance
   * @readOnly
   */
  /**
   * The most recent task instance that errored.
   *
   * @memberof Task
   * @member {TaskInstance} lastErrored
   * @instance
   * @readOnly
   */
  /**
   * The most recently canceled task instance.
   *
   * @memberof Task
   * @member {TaskInstance} lastCanceled
   * @instance
   * @readOnly
   */
  /**
   * The most recent task instance that is incomplete.
   *
   * @memberof Task
   * @member {TaskInstance} lastIncomplete
   * @instance
   * @readOnly
   */
  /**
   * The number of times this task has been performed.
   *
   * @memberof Task
   * @member {number} performCount
   * @instance
   * @readOnly
   */

  constructor(options) {
    super(options);

    if (!isDestroying(this.context)) {
      registerDestructor(this.context, () => {
        this.cancelAll({
          reason: 'the object it lives on was destroyed or unrendered',
          cancelRequestKind: CANCEL_KIND_LIFESPAN_END,
        });
      });
    }
  }

  /**
   * Flags the task as linked to the parent task's lifetime. Must be called
   * within another task's perform function. The task will be cancelled if the
   * parent task is canceled as well.
   *
   * ember-concurrency will indicate when this may be needed.
   *
   * @method linked
   * @memberof Task
   * @instance
   *
   */

  /**
   * Flags the task as not linked to the parent task's lifetime. Must be called
   * within another task's perform function. The task will NOT be cancelled if the
   * parent task is canceled.
   *
   * This is useful for avoiding the so-called "self-cancel loop" for tasks.
   * ember-concurrency will indicate when this may be needed.
   *
   * @method unlinked
   * @memberof Task
   * @instance
   *
   */

  /**
   * Creates a new {@linkcode TaskInstance} and attempts to run it right away.
   * If running this task instance would increase the task's concurrency
   * to a number greater than the task's maxConcurrency, this task
   * instance might be immediately canceled (dropped), or enqueued
   * to run at later time, after the currently running task(s) have finished.
   *
   * @method perform
   * @memberof Task
   * @param {*} arg* - args to pass to the task function
   * @instance
   *
   * @fires TaskInstance#TASK_NAME:started
   * @fires TaskInstance#TASK_NAME:succeeded
   * @fires TaskInstance#TASK_NAME:errored
   * @fires TaskInstance#TASK_NAME:canceled
   *
   */

  /**
   * Cancels all running or queued `TaskInstance`s for this Task.
   * If you're trying to cancel a specific TaskInstance (rather
   * than all of the instances running under this task) call
   * `.cancel()` on the specific TaskInstance.
   *
   * @method cancelAll
   * @memberof Task
   * @param options.reason A descriptive reason the task was
   *   cancelled. Defaults to `".cancelAll() was explicitly called
   *   on the Task"`.
   * @param options.resetState If true, will clear the task state
   *   (`last*` and `performCount` properties will be set to initial
   *   values). Defaults to false.
   * @instance
   * @async
   *
   */

  _perform(...args) {
    return this._performShared(args, PERFORM_TYPE_DEFAULT, null);
  }

  _performShared(args, performType, linkedObject) {
    let fullArgs = this._curryArgs ? [...this._curryArgs, ...args] : args;
    let taskInstance = this._taskInstanceFactory(
      fullArgs,
      performType,
      linkedObject
    );

    if (performType === PERFORM_TYPE_LINKED) {
      linkedObject._expectsLinkedYield = true;
    }

    if (isDestroying(this.context)) {
      // TODO: express this in terms of lifetimes; a task linked to
      // a dead lifetime should immediately cancel.
      taskInstance.cancel();
    }

    this.scheduler.perform(taskInstance);
    return taskInstance;
  }

  _taskInstanceFactory(args, performType) {
    let generatorFactory = () => this.generatorFactory(args);
    let taskInstance = new TaskInstance({
      task: this,
      args,
      executor: new TaskInstanceExecutor({
        generatorFactory,
        env: EMBER_ENVIRONMENT,
        debug: this.debug,
      }),
      performType,
      hasEnabledEvents: this.hasEnabledEvents,
    });

    return taskInstance;
  }

  _curry(...args) {
    let task = this._clone();
    task._curryArgs = [...(this._curryArgs || []), ...args];
    return task;
  }

  _clone() {
    return new Task({
      context: this.context,
      debug: this.debug,
      generatorFactory: this.generatorFactory,
      group: this.group,
      hasEnabledEvents: this.hasEnabledEvents,
      name: this.name,
      onStateCallback: this.onStateCallback,
      scheduler: this.scheduler,
    });
  }

  toString() {
    return `<Task:${this.name}>`;
  }
}

if (TRACKED_INITIAL_TASK_STATE) {
  Object.defineProperties(Task.prototype, TRACKED_INITIAL_TASK_STATE);
}

Object.assign(Task.prototype, TASKABLE_MIXIN);

const currentTaskInstanceSymbol = '__ec__encap_current_ti';
export class EncapsulatedTask extends Task {
  constructor(options) {
    super(options);
    this.taskObj = options.taskObj;
    this._encapsulatedTaskStates = new WeakMap();
    this._encapsulatedTaskInstanceProxies = new WeakMap();
  }

  _getEncapsulatedTaskClass() {
    let encapsulatedTaskImplClass = this._encapsulatedTaskImplClass;

    if (!encapsulatedTaskImplClass) {
      // eslint-disable-next-line ember/no-classic-classes
      encapsulatedTaskImplClass = EmberObject.extend(this.taskObj, {
        unknownProperty(key) {
          let currentInstance = this[currentTaskInstanceSymbol];
          return currentInstance ? currentInstance[key] : undefined;
        },
      });
    }

    return encapsulatedTaskImplClass;
  }

  _taskInstanceFactory(args, performType) {
    let owner = getOwner(this.context);
    let taskInstanceProxy;
    let encapsulatedTaskImpl = this._getEncapsulatedTaskClass().create({
      context: this.context,
    });
    setOwner(encapsulatedTaskImpl, owner);

    let generatorFactory = () =>
      encapsulatedTaskImpl.perform.apply(taskInstanceProxy, args);
    let taskInstance = new TaskInstance({
      task: this,
      args,
      executor: new TaskInstanceExecutor({
        generatorFactory,
        env: EMBER_ENVIRONMENT,
        debug: this.debug,
      }),
      performType,
      hasEnabledEvents: this.hasEnabledEvents,
    });
    encapsulatedTaskImpl[currentTaskInstanceSymbol] = taskInstance;

    this._encapsulatedTaskStates.set(taskInstance, encapsulatedTaskImpl);

    taskInstanceProxy = this._wrappedEncapsulatedTaskInstance(taskInstance);

    return taskInstanceProxy;
  }

  _wrappedEncapsulatedTaskInstance(taskInstance) {
    if (!taskInstance) {
      return null;
    }

    let _encapsulatedTaskInstanceProxies =
      this._encapsulatedTaskInstanceProxies;
    let proxy = _encapsulatedTaskInstanceProxies.get(taskInstance);

    if (!proxy) {
      let encapsulatedTaskImpl = this._encapsulatedTaskStates.get(taskInstance);

      proxy = new Proxy(taskInstance, {
        get(obj, prop) {
          return prop in obj
            ? obj[prop]
            : get(encapsulatedTaskImpl, prop.toString());
        },
        set(obj, prop, value) {
          if (prop in obj) {
            obj[prop] = value;
          } else {
            set(encapsulatedTaskImpl, prop.toString(), value);
          }
          return true;
        },
        has(obj, prop) {
          return prop in obj || prop in encapsulatedTaskImpl;
        },
        ownKeys(obj) {
          return Reflect.ownKeys(obj).concat(
            Reflect.ownKeys(encapsulatedTaskImpl)
          );
        },
        defineProperty(obj, prop, descriptor) {
          // Ember < 3.16 uses a WeakMap for value storage, keyed to the proxy.
          // We need to ensure that when we use setProperties to update it, and
          // it creates Meta, that it uses the proxy to key, otherwise we'll
          // have two different values stores in Meta, one which won't render.
          let proxy = _encapsulatedTaskInstanceProxies.get(taskInstance);
          if (proxy) {
            if (descriptor.get) {
              descriptor.get = descriptor.get.bind(proxy);
            } else if (proxy && descriptor.set) {
              descriptor.set = descriptor.set.bind(proxy);
            }
          }

          return Reflect.defineProperty(encapsulatedTaskImpl, prop, descriptor);
        },
        getOwnPropertyDescriptor(obj, prop) {
          return prop in obj
            ? Reflect.getOwnPropertyDescriptor(obj, prop)
            : Reflect.getOwnPropertyDescriptor(encapsulatedTaskImpl, prop);
        },
      });

      _encapsulatedTaskInstanceProxies.set(taskInstance, proxy);
    }

    return proxy;
  }
}