zalgo-promise
Version:
Javascript module template.
462 lines (355 loc) • 12.7 kB
JavaScript
/* @flow */
import { isPromise } from './utils';
import { onPossiblyUnhandledException, dispatchPossiblyUnhandledError } from './exceptions';
import { startActive, endActive, awaitActive } from './flush';
export class ZalgoPromise<R : mixed> {
resolved : boolean
rejected : boolean
errorHandled : boolean
value : R
error : mixed
// eslint-disable-next-line flowtype/no-mutable-array
handlers : Array<{|
promise : ZalgoPromise<*>,
onSuccess : void | (result : R) => mixed,
onError : void | (error : mixed) => mixed
|}>
dispatching : boolean
stack : string
constructor(handler : ?(resolve : (result : R) => void, reject : (error : mixed) => void) => void) {
this.resolved = false;
this.rejected = false;
this.errorHandled = false;
this.handlers = [];
if (handler) {
let result;
let error;
let resolved = false;
let rejected = false;
let isAsync = false;
startActive();
try {
handler(res => {
if (isAsync) {
this.resolve(res);
} else {
resolved = true;
result = res;
}
}, err => {
if (isAsync) {
this.reject(err);
} else {
rejected = true;
error = err;
}
});
} catch (err) {
endActive();
this.reject(err);
return;
}
endActive();
isAsync = true;
if (resolved) {
// $FlowFixMe
this.resolve(result);
} else if (rejected) {
this.reject(error);
}
}
if (__DEBUG__) {
try {
throw new Error(`ZalgoPromise`);
} catch (err) {
this.stack = err.stack;
}
}
}
resolve(result : R) : ZalgoPromise<R> {
if (this.resolved || this.rejected) {
return this;
}
if (isPromise(result)) {
throw new Error('Can not resolve promise with another promise');
}
this.resolved = true;
this.value = result;
this.dispatch();
return this;
}
reject(error : mixed) : ZalgoPromise<R> {
if (this.resolved || this.rejected) {
return this;
}
if (isPromise(error)) {
throw new Error('Can not reject promise with another promise');
}
if (!error) {
// $FlowFixMe
const err = (error && typeof error.toString === 'function' ? error.toString() : Object.prototype.toString.call(error));
error = new Error(`Expected reject to be called with Error, got ${ err }`);
}
this.rejected = true;
this.error = error;
if (!this.errorHandled) {
setTimeout(() => {
if (!this.errorHandled) {
dispatchPossiblyUnhandledError(error, this);
}
}, 1);
}
this.dispatch();
return this;
}
asyncReject(error : mixed) : ZalgoPromise<R> {
this.errorHandled = true;
this.reject(error);
return this;
}
dispatch() {
const { dispatching, resolved, rejected, handlers } = this;
if (dispatching) {
return;
}
if (!resolved && !rejected) {
return;
}
this.dispatching = true;
startActive();
const chain = <T>(firstPromise : ZalgoPromise<T>, secondPromise : ZalgoPromise<T>) => {
return firstPromise.then(res => {
secondPromise.resolve(res);
}, err => {
secondPromise.reject(err);
});
};
for (let i = 0; i < handlers.length; i++) {
const { onSuccess, onError, promise } = handlers[i];
let result;
if (resolved) {
try {
result = onSuccess ? onSuccess(this.value) : this.value;
} catch (err) {
promise.reject(err);
continue;
}
} else if (rejected) {
if (!onError) {
promise.reject(this.error);
continue;
}
try {
result = onError(this.error);
} catch (err) {
promise.reject(err);
continue;
}
}
if (result instanceof ZalgoPromise && (result.resolved || result.rejected)) {
const promiseResult : ZalgoPromise<*> = result;
if (promiseResult.resolved) {
promise.resolve(promiseResult.value);
} else {
promise.reject(promiseResult.error);
}
promiseResult.errorHandled = true;
} else if (isPromise(result)) {
if (result instanceof ZalgoPromise && (result.resolved || result.rejected)) {
if (result.resolved) {
promise.resolve(result.value);
} else {
promise.reject(result.error);
}
} else {
// $FlowFixMe
chain(result, promise);
}
} else {
promise.resolve(result);
}
}
handlers.length = 0;
this.dispatching = false;
endActive();
}
then<X, Y>(onSuccess : void | (result : R) => (ZalgoPromise<X> | Y), onError : void | (error : mixed) => (ZalgoPromise<X> | Y)) : ZalgoPromise<X | Y> {
if (onSuccess && typeof onSuccess !== 'function' && !onSuccess.call) {
throw new Error('Promise.then expected a function for success handler');
}
if (onError && typeof onError !== 'function' && !onError.call) {
throw new Error('Promise.then expected a function for error handler');
}
const promise = new ZalgoPromise();
this.handlers.push({
promise,
onSuccess,
onError
});
this.errorHandled = true;
this.dispatch();
return promise;
}
catch<X, Y>(onError : (error : mixed) => ZalgoPromise<X> | Y) : ZalgoPromise<X | Y> {
// $FlowFixMe incompatible-call
const resultPromise : ZalgoPromise<X | Y> = this.then(undefined, onError);
return resultPromise;
}
finally(onFinally : () => mixed) : ZalgoPromise<R> {
if (onFinally && typeof onFinally !== 'function' && !onFinally.call) {
throw new Error('Promise.finally expected a function');
}
return this.then((result) => {
return ZalgoPromise.try(onFinally)
.then(() => {
return result;
});
}, (err) => {
return ZalgoPromise.try(onFinally)
.then(() => {
throw err;
});
});
}
timeout(time : number, err : ?Error) : ZalgoPromise<R> {
if (this.resolved || this.rejected) {
return this;
}
const timeout = setTimeout(() => {
if (this.resolved || this.rejected) {
return;
}
this.reject(err || new Error(`Promise timed out after ${ time }ms`));
}, time);
return this.then(result => {
clearTimeout(timeout);
return result;
});
}
// $FlowFixMe
toPromise() : Promise<R> {
// $FlowFixMe
if (typeof Promise === 'undefined') {
throw new TypeError(`Could not find Promise`);
}
// $FlowFixMe
return Promise.resolve(this); // eslint-disable-line compat/compat
}
lazy() : ZalgoPromise<R> {
this.errorHandled = true;
return this;
}
static resolve<X, Y>(value : ZalgoPromise<X> | Y) : ZalgoPromise<X | Y> {
if (value instanceof ZalgoPromise) {
// $FlowFixMe incompatible-type-arg
const result : ZalgoPromise<X | Y> = value;
return result;
}
if (isPromise(value)) {
// $FlowFixMe
return new ZalgoPromise((resolve, reject) => value.then(resolve, reject));
}
return new ZalgoPromise().resolve(value);
}
static reject(error : mixed) : ZalgoPromise<R> {
return new ZalgoPromise().reject(error);
}
static asyncReject(error : mixed) : ZalgoPromise<R> {
return new ZalgoPromise().asyncReject(error);
}
static all<X : $ReadOnlyArray<mixed>>(promises : X) : ZalgoPromise<$TupleMap<X, <Y>(ZalgoPromise<Y> | Y) => Y>> { // eslint-disable-line no-undef
const promise = new ZalgoPromise();
let count = promises.length;
// eslint-disable-next-line no-undef
const results = ([] : $TupleMap<X, <Y>(ZalgoPromise<Y> | Y) => Y>).slice();
if (!count) {
promise.resolve(results);
return promise;
}
const chain = <T>(i : number, firstPromise : ZalgoPromise<T>, secondPromise : ZalgoPromise<T>) => {
return firstPromise.then(res => {
results[i] = res;
count -= 1;
if (count === 0) {
promise.resolve(results);
}
}, err => {
secondPromise.reject(err);
});
};
for (let i = 0; i < promises.length; i++) {
const prom = promises[i];
if (prom instanceof ZalgoPromise) {
if (prom.resolved) {
results[i] = prom.value;
count -= 1;
continue;
}
} else if (!isPromise(prom)) {
results[i] = prom;
count -= 1;
continue;
}
chain(i, ZalgoPromise.resolve(prom), promise);
}
if (count === 0) {
promise.resolve(results);
}
return promise;
}
static hash<O : Object>(promises : O) : ZalgoPromise<$ObjMap<O, <Y>(ZalgoPromise<Y> | Y) => Y>> { // eslint-disable-line no-undef
const result = {};
const awaitPromises = [];
for (const key in promises) {
if (promises.hasOwnProperty(key)) {
const value = promises[key];
if (isPromise(value)) {
awaitPromises.push(value.then(res => {
result[key] = res;
}));
} else {
result[key] = value;
}
}
}
return ZalgoPromise.all(awaitPromises).then(() => result);
}
static map<T, X>(items : $ReadOnlyArray<T>, method : (T) => (ZalgoPromise<X> | X)) : ZalgoPromise<$ReadOnlyArray<X>> {
// $FlowFixMe
return ZalgoPromise.all(items.map(method));
}
static onPossiblyUnhandledException(handler : (err : mixed) => void) : {| cancel : () => void |} {
return onPossiblyUnhandledException(handler);
}
static try<X, Y, C : mixed, A : $ReadOnlyArray<mixed>>(method : (...args : $ReadOnlyArray<mixed>) => (ZalgoPromise<X> | Y), context? : C, args? : A) : ZalgoPromise<X | Y> {
if (method && typeof method !== 'function' && !method.call) {
throw new Error('Promise.try expected a function');
}
let result : ZalgoPromise<X> | Y;
startActive();
try {
result = method.apply(context, args || []);
} catch (err) {
endActive();
return ZalgoPromise.reject(err);
}
endActive();
// $FlowFixMe incompatible-call
const resultPromise = ZalgoPromise.resolve(result);
return resultPromise;
}
static delay(delay : number) : ZalgoPromise<void> {
return new ZalgoPromise(resolve => {
setTimeout(resolve, delay);
});
}
static isPromise(value : mixed) : boolean {
if (value && value instanceof ZalgoPromise) {
return true;
}
return isPromise(value);
}
static flush() : ZalgoPromise<void> {
return awaitActive(ZalgoPromise);
}
}