import Promise, { Executor, State, Thenable, isThenable } from '../promise'; export const Canceled = 4; /** * Task is an extension of Promise that supports cancelation. */ export default class Task extends Promise { static all(items: (T | Thenable)[]): Task { return super.all(items); } static race(items: (T | Thenable)[]): Task { return super.race(items); } static reject(reason: Error): Task { return super.reject(reason); } static resolve(): Task; static resolve(value: (T | Thenable)): Task; static resolve(value?: any): Task { return super.resolve(value); } protected static copy(other: Promise): Task { const task = > super.copy(other); task.children = []; task.canceler = other instanceof Task ? other.canceler : function () {}; return task; } constructor(executor: Executor, canceler?: () => void) { super((resolve, reject) => { // Don't let the Task resolve if it's been canceled executor( (value) => { if (this._state === Canceled) { return; } resolve(value); }, (reason) => { if (this._state === Canceled) { return; } reject(reason); } ); }); this.children = []; this.canceler = () => { if (canceler) { canceler(); } this._cancel(); }; } /** * A cancelation handler that will be called if this task is canceled. */ private canceler: () => void; /** * Children of this Task (i.e., Tasks that were created from this Task with `then` or `catch`). */ private children: Task[]; /** * The finally callback for this Task (if it was created by a call to `finally`). */ private _finally: () => void | Thenable; /** * Propagates cancelation down through a Task tree. The Task's state is immediately set to canceled. If a Thenable * finally task was passed in, it is resolved before calling this Task's finally callback; otherwise, this Task's * finally callback is immediately executed. `_cancel` is called for each child Task, passing in the value returned * by this Task's finally callback or a Promise chain that will eventually resolve to that value. */ private _cancel(finallyTask?: void | Thenable): void { this._state = Canceled; const runFinally = () => { try { return this._finally(); } catch (error) { // Any errors in a `finally` callback are completely ignored during cancelation } }; if (this._finally) { if (isThenable(finallyTask)) { finallyTask = (> finallyTask).then(runFinally, runFinally); } else { finallyTask = runFinally(); } } this.children.forEach(function (child) { child._cancel(finallyTask); }); } /** * Immediately cancels this task if it has not already resolved. This Task and any descendants are synchronously set * to the Canceled state and any `finally` added downstream from the canceled Task are invoked. */ cancel(): void { if (this._state === State.Pending) { this.canceler(); } } finally(callback: () => void | Thenable): Task { const task = > super.finally(callback); // Keep a reference to the callback; it will be called if the Task is canceled task._finally = callback; return task; } then(onFulfilled?: (value: T) => U | Thenable, onRejected?: (error: Error) => U | Thenable): Task { const task = > super.then( // Don't call the onFulfilled or onRejected handlers if this Task is canceled function (value) { if (task._state === Canceled) { return; } if (onFulfilled) { return onFulfilled(value); } return value; }, function (error) { if (task._state === Canceled) { return; } if (onRejected) { return onRejected(error); } throw error; } ); task.canceler = () => { // If task's parent (this) hasn't been resolved, cancel it; downward propagation will start at the first // unresolved parent if (this._state === State.Pending) { this.cancel(); } // If task's parent has been resolved, propagate cancelation to the task's descendants else { task._cancel(); } }; // Keep track of child Tasks for propogating cancelation back down the chain this.children.push(task); return task; } catch(onRejected: (reason?: Error) => (U | Thenable)): Task { return super.catch(onRejected); } }