UNPKG

synctasks

Version:

An explicitly non-A+ Promise library that resolves promises synchronously

514 lines (513 loc) 21.6 kB
"use strict"; /** * SyncTasks.ts * Author: David de Regt * Copyright: Microsoft 2015 * * A very simple promise library that resolves all promises synchronously instead of * kicking them back to the main ticking thread. This affirmatively rejects the A+ * standard for promises, and is used for a combination of performance (wrapping * things back to the main thread is really slow) and because indexeddb loses * context for its calls if you send them around the event loop and transactions * automatically close. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.config = { // If we catch exceptions in success/fail blocks, it silently falls back to the fail case of the outer promise. // If this is global variable is true, it will also spit out a console.error with the exception for debugging. exceptionsToConsole: true, // Whether or not to actually attempt to catch exceptions with try/catch blocks inside the resolution cases. // Disable this for debugging when you'd rather the debugger caught the exception synchronously rather than // digging through a stack trace. catchExceptions: true, // Use this option in order to debug double resolution asserts locally. // Enabling this option in the release would have a negative impact on the application performance. traceEnabled: false, exceptionHandler: undefined, // If an ErrorFunc is not added to the task (then, catch, always) before the task rejects or synchonously // after that, then this function is called with the error. Default throws the error. unhandledErrorHandler: (function (err) { throw err; }) }; function fromThenable(thenable) { var deferred = Defer(); // NOTE: The {} around the error handling is critical to ensure that // we do not trigger "Possible unhandled rejection" warnings. By adding // the braces, the error handler rejects the outer promise, but returns // void. If we remove the braces, it would *also* return something which // would be unhandled thenable.then(function (value) { deferred.resolve(value); }, function (err) { deferred.reject(err); }); // Force async before this promise resolves to prevent ES6 promises from catching thrown exceptions downstream return deferred.promise().thenAsync(function (x) { return x; }); } exports.fromThenable = fromThenable; function isThenable(object) { return object !== null && object !== void 0 && typeof object.then === 'function'; } function isCancelable(object) { return object !== null && object !== void 0 && typeof object.cancel === 'function'; } // Runs trier(). If config.catchExceptions is set then any exception is caught and handed to catcher. function run(trier, catcher) { if (exports.config.catchExceptions) { // Any try/catch/finally block in a function makes the entire function ineligible for optimization is most JS engines. // Make sure this stays in a small/quick function, or break out into its own function. try { return trier(); } catch (e) { return catcher(e); } } else { return trier(); } } var asyncCallbacks = []; // Ideally, we use setImmediate, but that's only supported on some environments. // Suggestion: Use the "setimmediate" NPM package to polyfill where it's not available. var useSetImmediate = typeof setImmediate !== 'undefined'; /** * This function will defer callback of the specified callback lambda until the next JS tick, simulating standard A+ promise behavior */ function asyncCallback(callback) { asyncCallbacks.push(callback); if (asyncCallbacks.length === 1) { // Start a callback for the next tick if (useSetImmediate) { setImmediate(resolveAsyncCallbacks); } else { setTimeout(resolveAsyncCallbacks, 0); } } } exports.asyncCallback = asyncCallback; function resolveAsyncCallbacks() { var savedCallbacks = asyncCallbacks; asyncCallbacks = []; for (var i = 0; i < savedCallbacks.length; i++) { savedCallbacks[i](); } } var Internal; (function (Internal) { var SyncTask = /** @class */ (function () { function SyncTask() { this._completedSuccess = false; this._completedFail = false; this._traceEnabled = false; this._cancelCallbacks = []; this._wasCanceled = false; // The owner of this promise should not call cancel twice. However, cancellation through bubbling is independent of this. this._wasExplicitlyCanceled = false; this._resolving = false; this._storedCallbackSets = []; // 'Handled' just means there was a callback set added. // Note: If that callback does not handle the error then that callback's task will be 'unhandled' instead of this one. this._mustHandleError = true; } SyncTask.prototype._addCallbackSet = function (set, callbackWillChain) { var _this = this; var task = new SyncTask(); task.onCancel(function (context) { set.wasCanceled = true; set.cancelContext = context; // Note: Cancel due to bubbling should not throw if the public cancel is called before/after. _this._cancelInternal(context); }); set.task = task; this._storedCallbackSets.push(set); if (callbackWillChain) { // The callback inherits responsibility for "handling" errors. this._mustHandleError = false; } else { // The callback can never "handle" errors since nothing can chain to it. task._mustHandleError = false; } // The _resolve* functions handle callbacks being added while they are running. if (!this._resolving) { if (this._completedSuccess) { this._resolveSuccesses(); } else if (this._completedFail) { this._resolveFailures(); } } return task.promise(); }; SyncTask.prototype.onCancel = function (callback) { // Only register cancel callback handler on promise that hasn't been completed if (!this._completedSuccess && !this._completedFail) { if (this._wasCanceled) { callback(this._cancelContext); } else { this._cancelCallbacks.push(callback); } } return this; }; SyncTask.prototype.then = function (successFunc, errorFunc) { return this._addCallbackSet({ successFunc: successFunc, failFunc: errorFunc }, true); }; SyncTask.prototype.thenAsync = function (successFunc, errorFunc) { return this._addCallbackSet({ successFunc: successFunc, failFunc: errorFunc, asyncCallback: true }, true); }; SyncTask.prototype.catch = function (errorFunc) { return this._addCallbackSet({ failFunc: errorFunc }, true); }; SyncTask.prototype.always = function (func) { return this._addCallbackSet({ successFunc: func, failFunc: func }, true); }; SyncTask.prototype.setTracingEnabled = function (enabled) { this._traceEnabled = enabled; return this; }; // Finally should let you inspect the value of the promise as it passes through without affecting the then chaining // i.e. a failed promise with a finally after it should then chain to the fail case of the next then SyncTask.prototype.finally = function (func) { this._addCallbackSet({ successFunc: func, failFunc: func }, false); return this; }; SyncTask.prototype.done = function (successFunc) { this._addCallbackSet({ successFunc: successFunc }, false); return this; }; SyncTask.prototype.fail = function (errorFunc) { this._addCallbackSet({ failFunc: errorFunc }, false); return this; }; SyncTask.prototype.resolve = function (obj) { this._checkState(true); this._completedSuccess = true; this._storedResolution = obj; // Cannot cancel resolved promise - nuke chain this._cancelCallbacks = []; this._resolveSuccesses(); return this; }; SyncTask.prototype.reject = function (obj) { this._checkState(false); this._completedFail = true; this._storedErrResolution = obj; // Cannot cancel resolved promise - nuke chain this._cancelCallbacks = []; this._resolveFailures(); SyncTask._enforceErrorHandled(this); return this; }; SyncTask.prototype._checkState = function (resolve) { if (this._completedSuccess || this._completedFail) { if (this._completeStack) { console.error(this._completeStack.message, this._completeStack.stack); } var message = 'Failed to ' + (resolve ? 'resolve' : 'reject') + ': the task is already ' + (this._completedSuccess ? 'resolved' : 'rejected'); throw new Error(message); } if (exports.config.traceEnabled || this._traceEnabled) { this._completeStack = new Error('Initial ' + resolve ? 'resolve' : 'reject'); } }; // Make sure any rejected task has its failured handled. SyncTask._enforceErrorHandled = function (task) { if (!task._mustHandleError) { return; } SyncTask._rejectedTasks.push(task); // Wait for some async time in the future to check these tasks. if (!SyncTask._enforceErrorHandledTimer) { SyncTask._enforceErrorHandledTimer = setTimeout(function () { SyncTask._enforceErrorHandledTimer = undefined; var rejectedTasks = SyncTask._rejectedTasks; SyncTask._rejectedTasks = []; rejectedTasks.forEach(function (rejectedTask, i) { if (rejectedTask._mustHandleError) { // Unhandled! exports.config.unhandledErrorHandler(rejectedTask._storedErrResolution); } }); }, 0); } }; SyncTask.prototype.cancel = function (context) { if (this._wasExplicitlyCanceled) { throw new Error('Already Canceled'); } this._wasExplicitlyCanceled = true; this._cancelInternal(context); }; SyncTask.prototype._cancelInternal = function (context) { var _this = this; if (this._wasCanceled) { return; } this._wasCanceled = true; this._cancelContext = context; var callbacks = this._cancelCallbacks; this._cancelCallbacks = []; if (callbacks.length > 0) { callbacks.forEach(function (callback) { if (!_this._completedSuccess && !_this._completedFail) { callback(_this._cancelContext); } }); } }; SyncTask.cancelOtherInternal = function (promise, context) { // Warning: this cast is a bit dirty, but we need to avoid .cancel for SyncTasks. // Note: Cancel due to bubbling should not throw if the public cancel is called before/after. var task = promise; if (task._storedCallbackSets && task._cancelInternal) { // Is probably a SyncTask. task._cancelInternal(context); } else { promise.cancel(context); } }; SyncTask.prototype.promise = function () { return this; }; SyncTask.prototype._resolveSuccesses = function () { var _this = this; this._resolving = true; // New callbacks can be added as the current callbacks run: use a loop to get through all of them. while (this._storedCallbackSets.length) { // Only iterate over the current list of callbacks. var callbacks = this._storedCallbackSets; this._storedCallbackSets = []; callbacks.forEach(function (callback) { if (callback.asyncCallback) { asyncCallback(function () { return _this._resolveSuccessCallback(callback); }); } else { _this._resolveSuccessCallback(callback); } }); } this._resolving = false; }; SyncTask.prototype._resolveSuccessCallback = function (callback) { var _this = this; if (callback.successFunc) { run(function () { var ret = callback.successFunc(_this._storedResolution); if (isCancelable(ret)) { if (callback.wasCanceled) { SyncTask.cancelOtherInternal(ret, callback.cancelContext); } else { callback.task.onCancel(function (context) { return SyncTask.cancelOtherInternal(ret, context); }); } // Note: don't care if ret is canceled. We don't need to bubble out since this is already resolved. } if (isThenable(ret)) { // The success block of a then returned a new promise, so ret.then(function (r) { callback.task.resolve(r); }, function (e) { callback.task.reject(e); }); } else { callback.task.resolve(ret); } }, function (e) { _this._handleException(e, 'SyncTask caught exception in success block: ' + e.toString()); callback.task.reject(e); }); } else { callback.task.resolve(this._storedResolution); } }; SyncTask.prototype._resolveFailures = function () { var _this = this; this._resolving = true; // New callbacks can be added as the current callbacks run: use a loop to get through all of them. while (this._storedCallbackSets.length) { // Only iterate over the current list of callbacks. var callbacks = this._storedCallbackSets; this._storedCallbackSets = []; callbacks.forEach(function (callback) { if (callback.asyncCallback) { asyncCallback(function () { return _this._resolveFailureCallback(callback); }); } else { _this._resolveFailureCallback(callback); } }); } this._resolving = false; }; SyncTask.prototype._resolveFailureCallback = function (callback) { var _this = this; if (callback.failFunc) { run(function () { var ret = callback.failFunc(_this._storedErrResolution); if (isCancelable(ret)) { if (callback.wasCanceled) { SyncTask.cancelOtherInternal(ret, callback.cancelContext); } else { callback.task.onCancel(function (context) { return SyncTask.cancelOtherInternal(ret, context); }); } // Note: don't care if ret is canceled. We don't need to bubble out since this is already rejected. } if (isThenable(ret)) { ret.then(function (r) { callback.task.resolve(r); }, function (e) { callback.task.reject(e); }); } else { // The failure has been handled: ret is the resolved value. callback.task.resolve(ret); } }, function (e) { _this._handleException(e, 'SyncTask caught exception in failure block: ' + e.toString()); callback.task.reject(e); }); } else { callback.task.reject(this._storedErrResolution); } }; SyncTask.prototype._handleException = function (e, message) { if (exports.config.exceptionsToConsole) { console.error(message); } if (exports.config.exceptionHandler) { exports.config.exceptionHandler(e); } }; SyncTask.prototype.toEs6Promise = function () { var _this = this; return new Promise(function (resolve, reject) { return _this.then(resolve, reject); }); }; SyncTask._rejectedTasks = []; return SyncTask; }()); Internal.SyncTask = SyncTask; })(Internal || (Internal = {})); function all(items) { if (items.length === 0) { return Resolved([]); } var outTask = Defer(); var countRemaining = items.length; var foundError; var results = Array(items.length); outTask.onCancel(function (val) { items.forEach(function (item) { if (isCancelable(item)) { Internal.SyncTask.cancelOtherInternal(item, val); } }); }); var checkFinish = function () { if (--countRemaining === 0) { if (foundError !== undefined) { outTask.reject(foundError); } else { outTask.resolve(results); } } }; items.forEach(function (item, index) { if (isThenable(item)) { var task = item; task.then(function (res) { results[index] = res; checkFinish(); }, function (err) { if (foundError === undefined) { foundError = (err !== undefined) ? err : true; } checkFinish(); }); } else { // Not a task, so resolve directly with the item results[index] = item; checkFinish(); } }); return outTask.promise(); } exports.all = all; function Defer() { return new Internal.SyncTask(); } exports.Defer = Defer; function Resolved(val) { return new Internal.SyncTask().resolve(val).promise(); } exports.Resolved = Resolved; function Rejected(val) { return new Internal.SyncTask().reject(val).promise(); } exports.Rejected = Rejected; function race(items) { var outTask = Defer(); var hasSettled = false; outTask.onCancel(function (val) { items.forEach(function (item) { if (isCancelable(item)) { Internal.SyncTask.cancelOtherInternal(item, val); } }); }); items.forEach(function (item) { if (isThenable(item)) { var task = item; task.then(function (res) { if (!hasSettled) { hasSettled = true; outTask.resolve(res); } }, function (err) { if (!hasSettled) { hasSettled = true; outTask.reject(err); } }); } else { // Not a task, so resolve directly with the item if (!hasSettled) { hasSettled = true; outTask.resolve(item); } } }); return outTask.promise(); } exports.race = race; function raceTimer(promise, timeMs) { var timerDef = Defer(); var token = setTimeout(function () { timerDef.resolve({ timedOut: true }); }, timeMs); var adaptedPromise = promise.then(function (resp) { clearTimeout(token); return { timedOut: false, result: resp }; }); return race([adaptedPromise, timerDef.promise()]); } exports.raceTimer = raceTimer;