synctasks
Version:
An explicitly non-A+ Promise library that resolves promises synchronously
699 lines (580 loc) • 27.5 kB
text/typescript
/**
* 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.
*/
export const 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 as (((ex: Error) => void)|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: <(err: any) => void>((err: any) => { throw err; })
};
export interface Es6Thenable<R> {
then<U>(onFulfilled?: (value: R) => U | Es6Thenable<U>, onRejected?: (error: any) => U | Es6Thenable<U>): Es6Thenable<U>;
then<U>(onFulfilled?: (value: R) => U | Es6Thenable<U>, onRejected?: (error: any) => void): Es6Thenable<U>;
}
export function fromThenable<T>(thenable: Es6Thenable<T>): STPromise<T> {
const deferred = Defer<T>();
// 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(
value => { deferred.resolve(value); },
(err: any) => { deferred.reject(err); });
// Force async before this promise resolves to prevent ES6 promises from catching thrown exceptions downstream
return deferred.promise().thenAsync(x => x);
}
function isThenable(object: any): object is Thenable<any> {
return object !== null && object !== void 0 && typeof object.then === 'function';
}
function isCancelable(object: any): object is Cancelable {
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<T, C extends any>(trier: () => T, catcher?: (e: Error) => C): T | C {
if (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();
}
}
let asyncCallbacks: (() => void)[] = [];
// 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.
const useSetImmediate = typeof setImmediate !== 'undefined';
/**
* This function will defer callback of the specified callback lambda until the next JS tick, simulating standard A+ promise behavior
*/
export function asyncCallback(callback: () => void) {
asyncCallbacks.push(callback);
if (asyncCallbacks.length === 1) {
// Start a callback for the next tick
if (useSetImmediate) {
setImmediate(resolveAsyncCallbacks);
} else {
setTimeout(resolveAsyncCallbacks, 0);
}
}
}
function resolveAsyncCallbacks() {
const savedCallbacks = asyncCallbacks;
asyncCallbacks = [];
for (let i = 0; i < savedCallbacks.length; i++) {
savedCallbacks[i]();
}
}
export type SuccessFunc<T, U> = (value: T) => U | Thenable<U>;
export type ErrorFunc<U> = (error: any) => U | Thenable<U>;
export type CancelFunc = (context: any) => void;
export interface Deferred<T> {
resolve(obj: T): Deferred<T>;
reject(obj?: any): Deferred<T>;
promise(): STPromise<T>;
onCancel(callback: CancelFunc): Deferred<T>;
}
export interface Thenable<T> {
then<U>(successFunc: SuccessFunc<T, U>, errorFunc?: ErrorFunc<U>): STPromise<U>;
}
export interface Cancelable {
// Will call any cancellation lambdas up the call chain, and reject a chain up the fail blocks
cancel(context?: any): void;
}
export interface STPromise<T> extends Thenable<T>, Cancelable {
catch(errorFunc: ErrorFunc<T>): STPromise<T>;
finally(func: (value: T|any) => void): STPromise<T>;
always<U>(func: (value: T|any) => U | Thenable<U>): STPromise<U>;
done(successFunc: (value: T) => void): STPromise<T>;
fail(errorFunc: (error: any) => void): STPromise<T>;
// Defer the resolution of the then until the next event loop, simulating standard A+ promise behavior
thenAsync<U>(successFunc: SuccessFunc<T, U>, errorFunc?: ErrorFunc<U>): STPromise<U>;
setTracingEnabled(enabled: boolean): STPromise<T>;
toEs6Promise(): Promise<T>;
}
export { STPromise as Promise };
module Internal {
export interface CallbackSet<T, U> {
successFunc?: SuccessFunc<T, U>;
failFunc?: ErrorFunc<U>;
task?: Deferred<U>;
asyncCallback?: boolean;
wasCanceled?: boolean;
cancelContext?: any;
}
export class SyncTask<T> implements Deferred<T>, STPromise<T> {
private _storedResolution: T|undefined;
private _storedErrResolution: any|undefined;
private _completedSuccess = false;
private _completedFail = false;
private _traceEnabled = false;
// If _traceEnabled is true we save stacktrace of the first resolution of the task in the _completeStack
// we are storing the Error instead of stack because it would be faster in standard scenario.
private _completeStack: Error;
private _cancelCallbacks: CancelFunc[] = [];
private _cancelContext: any;
private _wasCanceled = false;
// The owner of this promise should not call cancel twice. However, cancellation through bubbling is independent of this.
private _wasExplicitlyCanceled = false;
private _resolving = false;
private _storedCallbackSets: CallbackSet<T, any>[] = [];
// '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.
private _mustHandleError = true;
private static _rejectedTasks: SyncTask<any>[] = [];
private static _enforceErrorHandledTimer: number|undefined;
private _addCallbackSet<U>(set: CallbackSet<T, U>, callbackWillChain: boolean): STPromise<U> {
const task = new SyncTask<U>();
task.onCancel(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();
}
onCancel(callback: CancelFunc): Deferred<T> {
// 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;
}
then<U>(successFunc: SuccessFunc<T, U>, errorFunc?: ErrorFunc<U>): STPromise<U> {
return this._addCallbackSet<U>({
successFunc: successFunc,
failFunc: errorFunc
}, true);
}
thenAsync<U>(successFunc: SuccessFunc<T, U>, errorFunc?: ErrorFunc<U>): STPromise<U> {
return this._addCallbackSet<U>({
successFunc: successFunc,
failFunc: errorFunc,
asyncCallback: true
}, true);
}
catch(errorFunc: ErrorFunc<T>): STPromise<T> {
return this._addCallbackSet<T>({
failFunc: errorFunc
}, true);
}
always<U>(func: (value: T|any) => U | STPromise<U>): STPromise<U> {
return this._addCallbackSet<U>({
successFunc: func,
failFunc: func
}, true);
}
setTracingEnabled(enabled: boolean) : STPromise<T> {
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
finally(func: (value: T|any) => void): STPromise<T> {
this._addCallbackSet<T|any>({
successFunc: func,
failFunc: func
}, false);
return this;
}
done(successFunc: (value: T) => void): STPromise<T> {
this._addCallbackSet<void>({
successFunc: successFunc
}, false);
return this;
}
fail(errorFunc: (error: any) => void): STPromise<T> {
this._addCallbackSet<void>({
failFunc: errorFunc
}, false);
return this;
}
resolve(obj: T): Deferred<T> {
this._checkState(true);
this._completedSuccess = true;
this._storedResolution = obj;
// Cannot cancel resolved promise - nuke chain
this._cancelCallbacks = [];
this._resolveSuccesses();
return this;
}
reject(obj?: any): Deferred<T> {
this._checkState(false);
this._completedFail = true;
this._storedErrResolution = obj;
// Cannot cancel resolved promise - nuke chain
this._cancelCallbacks = [];
this._resolveFailures();
SyncTask._enforceErrorHandled(this);
return this;
}
private _checkState(resolve: boolean) {
if (this._completedSuccess || this._completedFail) {
if (this._completeStack) {
console.error(this._completeStack.message, this._completeStack.stack);
}
const message = 'Failed to ' + (resolve ? 'resolve' : 'reject') +
': the task is already ' + (this._completedSuccess ? 'resolved' : 'rejected');
throw new Error(message);
}
if (config.traceEnabled || this._traceEnabled) {
this._completeStack = new Error('Initial ' + resolve ? 'resolve' : 'reject');
}
}
// Make sure any rejected task has its failured handled.
private static _enforceErrorHandled(task: SyncTask<any>): void {
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(() => {
SyncTask._enforceErrorHandledTimer = undefined;
const rejectedTasks = SyncTask._rejectedTasks;
SyncTask._rejectedTasks = [];
rejectedTasks.forEach((rejectedTask, i) => {
if (rejectedTask._mustHandleError) {
// Unhandled!
config.unhandledErrorHandler(rejectedTask._storedErrResolution);
}
});
}, 0);
}
}
cancel(context?: any): void {
if (this._wasExplicitlyCanceled) {
throw new Error('Already Canceled');
}
this._wasExplicitlyCanceled = true;
this._cancelInternal(context);
}
private _cancelInternal(context?: any): void {
if (this._wasCanceled) {
return;
}
this._wasCanceled = true;
this._cancelContext = context;
const callbacks = this._cancelCallbacks;
this._cancelCallbacks = [];
if (callbacks.length > 0) {
callbacks.forEach(callback => {
if (!this._completedSuccess && !this._completedFail) {
callback(this._cancelContext);
}
});
}
}
static cancelOtherInternal(promise: Cancelable, context: any): void {
// 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.
const task = promise as SyncTask<any>;
if (task._storedCallbackSets && task._cancelInternal) {
// Is probably a SyncTask.
task._cancelInternal(context);
} else {
promise.cancel(context);
}
}
promise(): STPromise<T> {
return this;
}
private _resolveSuccesses() {
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.
const callbacks = this._storedCallbackSets;
this._storedCallbackSets = [];
callbacks.forEach(callback => {
if (callback.asyncCallback) {
asyncCallback(() => this._resolveSuccessCallback(callback));
} else {
this._resolveSuccessCallback(callback);
}
});
}
this._resolving = false;
}
private _resolveSuccessCallback(callback: CallbackSet<T, any>) {
if (callback.successFunc) {
run(() => {
const ret = callback.successFunc!!!(this._storedResolution!!!);
if (isCancelable(ret)) {
if (callback.wasCanceled) {
SyncTask.cancelOtherInternal(ret, callback.cancelContext);
} else {
callback.task!!!.onCancel(context => 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(r => { callback.task!!!.resolve(r); }, e => { callback.task!!!.reject(e); });
} else {
callback.task!!!.resolve(ret);
}
}, e => {
this._handleException(e, 'SyncTask caught exception in success block: ' + e.toString());
callback.task!!!.reject(e);
});
} else {
callback.task!!!.resolve(this._storedResolution);
}
}
private _resolveFailures() {
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.
const callbacks = this._storedCallbackSets;
this._storedCallbackSets = [];
callbacks.forEach(callback => {
if (callback.asyncCallback) {
asyncCallback(() => this._resolveFailureCallback(callback));
} else {
this._resolveFailureCallback(callback);
}
});
}
this._resolving = false;
}
private _resolveFailureCallback(callback: CallbackSet<T, any>) {
if (callback.failFunc) {
run(() => {
const ret = callback.failFunc!!!(this._storedErrResolution);
if (isCancelable(ret)) {
if (callback.wasCanceled) {
SyncTask.cancelOtherInternal(ret, callback.cancelContext);
} else {
callback.task!!!.onCancel(context => 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(r => { callback.task!!!.resolve(r); }, e => { callback.task!!!.reject(e); });
} else {
// The failure has been handled: ret is the resolved value.
callback.task!!!.resolve(ret);
}
}, e => {
this._handleException(e, 'SyncTask caught exception in failure block: ' + e.toString());
callback.task!!!.reject(e);
});
} else {
callback.task!!!.reject(this._storedErrResolution);
}
}
private _handleException(e: Error, message: string): void {
if (config.exceptionsToConsole) {
console.error(message);
}
if (config.exceptionHandler) {
config.exceptionHandler(e);
}
}
toEs6Promise(): Promise<T> {
return new Promise<T>((resolve, reject) => this.then(resolve, reject));
}
}
}
export type Raceable<T> = T | Thenable<T> | undefined | null;
// Resolves once all of the given items resolve (non-thenables are 'resolved').
// Rejects once any of the given thenables reject.
// Note: resolves immediately if given no items.
export function all<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>, Raceable<T9>,
Raceable<T10>]
): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10]>;
export function all<T1, T2, T3, T4, T5, T6, T7, T8, T9>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>, Raceable<T9>]
): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8, T9]>;
export function all<T1, T2, T3, T4, T5, T6, T7, T8>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>]
): STPromise<[T1, T2, T3, T4, T5, T6, T7, T8]>;
export function all<T1, T2, T3, T4, T5, T6, T7>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>]
): STPromise<[T1, T2, T3, T4, T5, T6, T7]>;
export function all<T1, T2, T3, T4, T5, T6>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>]
): STPromise<[T1, T2, T3, T4, T5, T6]>;
export function all<T1, T2, T3, T4, T5>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>]
): STPromise<[T1, T2, T3, T4, T5]>;
export function all<T1, T2, T3, T4>(values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>]): STPromise<[T1, T2, T3, T4]>;
export function all<T1, T2, T3>(values: [Raceable<T1>, Raceable<T2>, Raceable<T3>]): STPromise<[T1, T2, T3]>;
export function all<T1, T2>(values: [Raceable<T1>, Raceable<T2>]): STPromise<[T1, T2]>;
export function all<T>(values: (T|Thenable<T>)[]): STPromise<T[]>;
export function all(items: any[]): STPromise<any[]> {
if (items.length === 0) {
return Resolved<any[]>([]);
}
const outTask = Defer<any[]>();
let countRemaining = items.length;
let foundError: any|undefined;
const results = Array(items.length);
outTask.onCancel((val) => {
items.forEach(item => {
if (isCancelable(item)) {
Internal.SyncTask.cancelOtherInternal(item, val);
}
});
});
const checkFinish = () => {
if (--countRemaining === 0) {
if (foundError !== undefined) {
outTask.reject(foundError);
} else {
outTask.resolve(results);
}
}
};
items.forEach((item, index) => {
if (isThenable(item)) {
const task = <Thenable<any>>item;
task.then(res => {
results[index] = res;
checkFinish();
}, 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();
}
export function Defer<T>(): Deferred<T> {
return new Internal.SyncTask<T>();
}
export function Resolved<T extends void>(): STPromise<T>;
export function Resolved<T>(val: T): STPromise<T>;
export function Resolved<T>(val?: T): STPromise<T> {
return new Internal.SyncTask<T>().resolve(val!!!).promise();
}
export function Rejected<T>(val?: any): STPromise<T> {
return new Internal.SyncTask<T>().reject(val).promise();
}
// Resolves/Rejects once any of the given items resolve or reject (non-thenables are 'resolved').
// Note: never resolves if given no items.
export function race<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10>(
values: [
Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>, Raceable<T9>,
Raceable<T10>
]
): STPromise<T1|T2|T3|T4|T5|T6|T7|T8|T9|T10>;
export function race<T1, T2, T3, T4, T5, T6, T7, T8, T9>(
values: [
Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>, Raceable<T9>
]
): STPromise<T1|T2|T3|T4|T5|T6|T7|T8|T9>;
export function race<T1, T2, T3, T4, T5, T6, T7, T8>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>, Raceable<T8>]
): STPromise<T1|T2|T3|T4|T5|T6|T7|T8>;
export function race<T1, T2, T3, T4, T5, T6, T7>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>, Raceable<T7>]
): STPromise<T1|T2|T3|T4|T5|T6|T7>;
export function race<T1, T2, T3, T4, T5, T6>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>, Raceable<T6>]
): STPromise<T1|T2|T3|T4|T5|T6>;
export function race<T1, T2, T3, T4, T5>(
values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>, Raceable<T5>]): STPromise<T1|T2|T3|T4|T5>;
export function race<T1, T2, T3, T4>(values: [Raceable<T1>, Raceable<T2>, Raceable<T3>, Raceable<T4>]): STPromise<T1|T2|T3|T4>;
export function race<T1, T2, T3>(values: [Raceable<T1>, Raceable<T2>, Raceable<T3>]): STPromise<T1|T2|T3>;
export function race<T1, T2>(values: [Raceable<T1>, Raceable<T2>]): STPromise<T1|T2>;
export function race<T>(values: (T|Thenable<T>)[]): STPromise<T[]>;
export function race(items: any[]): STPromise<any> {
const outTask = Defer<any>();
let hasSettled = false;
outTask.onCancel((val) => {
items.forEach(item => {
if (isCancelable(item)) {
Internal.SyncTask.cancelOtherInternal(item, val);
}
});
});
items.forEach(item => {
if (isThenable(item)) {
const task = <Thenable<any>>item;
task.then(res => {
if (!hasSettled) {
hasSettled = true;
outTask.resolve(res);
}
}, 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();
}
export type RaceTimerResponse<T> = { timedOut: boolean, result?: T };
export function raceTimer<T>(promise: STPromise<T>, timeMs: number): STPromise<RaceTimerResponse<T>> {
let timerDef = Defer<RaceTimerResponse<T>>();
const token = setTimeout(() => {
timerDef.resolve({ timedOut: true });
}, timeMs);
const adaptedPromise = promise.then(resp => {
clearTimeout(token);
return { timedOut: false, result: resp } as RaceTimerResponse<T>;
});
return race([adaptedPromise, timerDef.promise()]);
}