/* jshint node: true */ "use strict"; function makeArrayFrom(obj) { return Array.prototype.slice.apply(obj); } var PENDING = "pending", RESOLVED = "resolved", REJECTED = "rejected"; function SynchronousPromise(handler) { this.status = PENDING; this._continuations = []; this._parent = null; this._paused = false; if (handler) { handler.call( this, this._continueWith.bind(this), this._failWith.bind(this) ); } } function looksLikeAPromise(obj) { return obj && typeof (obj.then) === "function"; } function passThrough(value) { return value; } SynchronousPromise.prototype = { then: function (nextFn, catchFn) { var next = SynchronousPromise.unresolved()._setParent(this); if (this._isRejected()) { if (this._paused) { this._continuations.push({ promise: next, nextFn: nextFn, catchFn: catchFn }); return next; } if (catchFn) { try { var catchResult = catchFn(this._error); if (looksLikeAPromise(catchResult)) { this._chainPromiseData(catchResult, next); return next; } else { return SynchronousPromise.resolve(catchResult)._setParent(this); } } catch (e) { return SynchronousPromise.reject(e)._setParent(this); } } return SynchronousPromise.reject(this._error)._setParent(this); } this._continuations.push({ promise: next, nextFn: nextFn, catchFn: catchFn }); this._runResolutions(); return next; }, catch: function (handler) { if (this._isResolved()) { return SynchronousPromise.resolve(this._data)._setParent(this); } var next = SynchronousPromise.unresolved()._setParent(this); this._continuations.push({ promise: next, catchFn: handler }); this._runRejections(); return next; }, finally: function (callback) { var ran = false; function runFinally(result, err) { if (!ran) { ran = true; if (!callback) { callback = passThrough; } var callbackResult = callback(result); if (looksLikeAPromise(callbackResult)) { return callbackResult.then(function () { if (err) { throw err; } return result; }); } else { return result; } } } return this .then(function (result) { return runFinally(result); }) .catch(function (err) { return runFinally(null, err); }); }, pause: function () { this._paused = true; return this; }, resume: function () { var firstPaused = this._findFirstPaused(); if (firstPaused) { firstPaused._paused = false; firstPaused._runResolutions(); firstPaused._runRejections(); } return this; }, _findAncestry: function () { return this._continuations.reduce(function (acc, cur) { if (cur.promise) { var node = { promise: cur.promise, children: cur.promise._findAncestry() }; acc.push(node); } return acc; }, []); }, _setParent: function (parent) { if (this._parent) { throw new Error("parent already set"); } this._parent = parent; return this; }, _continueWith: function (data) { var firstPending = this._findFirstPending(); if (firstPending) { firstPending._data = data; firstPending._setResolved(); } }, _findFirstPending: function () { return this._findFirstAncestor(function (test) { return test._isPending && test._isPending(); }); }, _findFirstPaused: function () { return this._findFirstAncestor(function (test) { return test._paused; }); }, _findFirstAncestor: function (matching) { var test = this; var result; while (test) { if (matching(test)) { result = test; } test = test._parent; } return result; }, _failWith: function (error) { var firstRejected = this._findFirstPending(); if (firstRejected) { firstRejected._error = error; firstRejected._setRejected(); } }, _takeContinuations: function () { return this._continuations.splice(0, this._continuations.length); }, _runRejections: function () { if (this._paused || !this._isRejected()) { return; } var error = this._error, continuations = this._takeContinuations(), self = this; continuations.forEach(function (cont) { if (cont.catchFn) { try { var catchResult = cont.catchFn(error); self._handleUserFunctionResult(catchResult, cont.promise); } catch (e) { cont.promise.reject(e); } } else { cont.promise.reject(error); } }); }, _runResolutions: function () { if (this._paused || !this._isResolved() || this._isPending()) { return; } var continuations = this._takeContinuations(); var data = this._data; var self = this; continuations.forEach(function (cont) { if (cont.nextFn) { try { var result = cont.nextFn(data); self._handleUserFunctionResult(result, cont.promise); } catch (e) { self._handleResolutionError(e, cont); } } else if (cont.promise) { cont.promise.resolve(data); } }); if (looksLikeAPromise(this._data)) { return this._handleWhenResolvedDataIsPromise(this._data); } }, _handleResolutionError: function (e, continuation) { this._setRejected(); if (continuation.catchFn) { try { continuation.catchFn(e); return; } catch (e2) { e = e2; } } if (continuation.promise) { continuation.promise.reject(e); } }, _handleWhenResolvedDataIsPromise: function (data) { var self = this; return data.then(function (result) { self._data = result; self._runResolutions(); }).catch(function (error) { self._error = error; self._setRejected(); self._runRejections(); }); }, _handleUserFunctionResult: function (data, nextSynchronousPromise) { if (looksLikeAPromise(data)) { this._chainPromiseData(data, nextSynchronousPromise); } else { nextSynchronousPromise.resolve(data); } }, _chainPromiseData: function (promiseData, nextSynchronousPromise) { promiseData.then(function (newData) { nextSynchronousPromise.resolve(newData); }).catch(function (newError) { nextSynchronousPromise.reject(newError); }); }, _setResolved: function () { this.status = RESOLVED; if (!this._paused) { this._runResolutions(); } }, _setRejected: function () { this.status = REJECTED; if (!this._paused) { this._runRejections(); } }, _isPending: function () { return this.status === PENDING; }, _isResolved: function () { return this.status === RESOLVED; }, _isRejected: function () { return this.status === REJECTED; } }; SynchronousPromise.resolve = function (result) { return new SynchronousPromise(function (resolve, reject) { if (looksLikeAPromise(result)) { result.then(function (newResult) { resolve(newResult); }).catch(function (error) { reject(error); }); } else { resolve(result); } }); }; SynchronousPromise.reject = function (result) { return new SynchronousPromise(function (resolve, reject) { reject(result); }); }; SynchronousPromise.unresolved = function () { return new SynchronousPromise(function (resolve, reject) { this.resolve = resolve; this.reject = reject; }); }; SynchronousPromise.all = function () { var args = makeArrayFrom(arguments); if (Array.isArray(args[0])) { args = args[0]; } if (!args.length) { return SynchronousPromise.resolve([]); } return new SynchronousPromise(function (resolve, reject) { var allData = [], numResolved = 0, doResolve = function () { if (numResolved === args.length) { resolve(allData); } }, rejected = false, doReject = function (err) { if (rejected) { return; } rejected = true; reject(err); }; args.forEach(function (arg, idx) { SynchronousPromise.resolve(arg).then(function (thisResult) { allData[idx] = thisResult; numResolved += 1; doResolve(); }).catch(function (err) { doReject(err); }); }); }); }; function createAggregateErrorFrom(errors) { /* jshint ignore:start */ if (typeof window !== "undefined" && "AggregateError" in window) { return new window.AggregateError(errors); } /* jshint ignore:end */ return { errors: errors }; } SynchronousPromise.any = function () { var args = makeArrayFrom(arguments); if (Array.isArray(args[0])) { args = args[0]; } if (!args.length) { return SynchronousPromise.reject(createAggregateErrorFrom([])); } return new SynchronousPromise(function (resolve, reject) { var allErrors = [], numRejected = 0, doReject = function () { if (numRejected === args.length) { reject(createAggregateErrorFrom(allErrors)); } }, resolved = false, doResolve = function (result) { if (resolved) { return; } resolved = true; resolve(result); }; args.forEach(function (arg, idx) { SynchronousPromise.resolve(arg).then(function (thisResult) { doResolve(thisResult); }).catch(function (err) { allErrors[idx] = err; numRejected += 1; doReject(); }); }); }); }; SynchronousPromise.allSettled = function () { var args = makeArrayFrom(arguments); if (Array.isArray(args[0])) { args = args[0]; } if (!args.length) { return SynchronousPromise.resolve([]); } return new SynchronousPromise(function (resolve) { var allData = [], numSettled = 0, doSettled = function () { numSettled += 1; if (numSettled === args.length) { resolve(allData); } }; args.forEach(function (arg, idx) { SynchronousPromise.resolve(arg).then(function (thisResult) { allData[idx] = { status: "fulfilled", value: thisResult }; doSettled(); }).catch(function (err) { allData[idx] = { status: "rejected", reason: err }; doSettled(); }); }); }); }; /* jshint ignore:start */ if (Promise === SynchronousPromise) { throw new Error("Please use SynchronousPromise.installGlobally() to install globally"); } var RealPromise = Promise; SynchronousPromise.installGlobally = function (__awaiter) { if (Promise === SynchronousPromise) { return __awaiter; } var result = patchAwaiterIfRequired(__awaiter); Promise = SynchronousPromise; return result; }; SynchronousPromise.uninstallGlobally = function () { if (Promise === SynchronousPromise) { Promise = RealPromise; } }; function patchAwaiterIfRequired(__awaiter) { if (typeof (__awaiter) === "undefined" || __awaiter.__patched) { return __awaiter; } var originalAwaiter = __awaiter; __awaiter = function () { var Promise = RealPromise; originalAwaiter.apply(this, makeArrayFrom(arguments)); }; __awaiter.__patched = true; return __awaiter; } /* jshint ignore:end */ module.exports = { SynchronousPromise: SynchronousPromise };