474 lines
11 KiB
JavaScript
474 lines
11 KiB
JavaScript
/* 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
|
|
};
|