Last active
September 20, 2021 10:07
-
-
Save adit-hotstar/25e006f6b04ab59a4099631cb26cc3ae to your computer and use it in GitHub Desktop.
Understanding the Promises/A+ specification
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
type Then<A> = { | |
(onFulfilled?: null, onRejected?: ((reason: unknown) => A | Thenable<A>) | null): Thenable<A>; | |
<B>(onFulfilled: (value: A) => B | Thenable<B>, onRejected?: ((reason: unknown) => B | Thenable<B>) | null): Thenable<B>; | |
}; | |
interface Thenable<A> { | |
then: Then<A>; | |
} | |
type PromiseState<A> = | |
| { status: 'fulfilled', value: A } | |
| { status: 'rejected', reason: unknown } | |
| { status: 'pending', resolveOnFulfilledArray: ((value: A) => void)[], resolveOnRejectedArray: ((reason: unknown) => void)[] }; | |
const compose = <A, B>(resolve: (value: B | Thenable<B>) => void, reject: (reason: unknown) => void, handler: (result: A) => B | Thenable<B>) => (result: A) => { | |
try { | |
resolve(handler(result)); | |
} catch (error: unknown) { | |
reject(error); | |
} | |
}; | |
const dispatch = <A>(callbacks: ((result: A) => void)[], result: A) => { | |
for (const callback of callbacks) callback(result); | |
}; | |
const isThenable = <A>(then: Then<A>, _value: A | Thenable<A>): _value is Thenable<A> => typeof then === 'function'; | |
const isPromise = <A>(thenable: Thenable<A>): thenable is Promise<A> => thenable instanceof Promise; | |
export class Promise<A> implements Thenable<A> { | |
#state: PromiseState<A>; | |
constructor(executor: (resolve: (value: A | Thenable<A>) => void, reject: (reason: unknown) => void) => void) { | |
this.#state = { status: 'pending', resolveOnFulfilledArray: [], resolveOnRejectedArray: [] }; | |
this.#run(executor, null); | |
} | |
static resolve(): Promise<void>; | |
static resolve<A>(value: A | Thenable<A>): Promise<A>; | |
static resolve<A>(value?: A | Thenable<A>): Promise<void> | Promise<A> { | |
if (typeof value === 'undefined') { | |
return new Promise<void>((resolve) => resolve()); | |
} else { | |
return new Promise<A>((resolve) => resolve(value)); | |
} | |
} | |
static reject<A = never>(reason?: unknown): Promise<A> { | |
return new Promise((_resolve, reject) => reject(reason)); | |
} | |
then(onFulfilled?: null, onRejected?: ((reason: unknown) => A | Thenable<A>) | null): Promise<A>; | |
then<B>(onFulfilled: (value: A) => B | Thenable<B>, onRejected?: ((reason: unknown) => B | Thenable<B>) | null): Promise<B>; | |
then<B>(onFulfilled?: ((value: A) => B | Thenable<B>) | null, onRejected?: ((reason: unknown) => A | Thenable<A>) | ((reason: unknown) => B | Thenable<B>) | null): Promise<A> | Promise<B> { | |
if (typeof onFulfilled !== 'function') { | |
return this.catch(onRejected as ((reason: unknown) => A | Thenable<A>) | null | undefined); | |
} | |
const _onRejected = onRejected as ((reason: unknown) => B | Thenable<B>) | null | undefined; | |
return new Promise<B>((resolve, reject) => { | |
switch (this.#state.status) { | |
case 'fulfilled': | |
setTimeout(compose(resolve, reject, onFulfilled), 0, this.#state.value); | |
break; | |
case 'rejected': | |
setTimeout(typeof _onRejected === 'function' ? compose(resolve, reject, _onRejected) : reject, 0, this.#state.reason); | |
break; | |
case 'pending': | |
this.#state.resolveOnFulfilledArray.push(compose(resolve, reject, onFulfilled)); | |
this.#state.resolveOnRejectedArray.push(typeof _onRejected === 'function' ? compose(resolve, reject, _onRejected) : reject); | |
} | |
}); | |
} | |
catch(onRejected?: ((reason: unknown) => A | Thenable<A>) | null): Promise<A> { | |
return new Promise((resolve, reject) => { | |
switch (this.#state.status) { | |
case 'fulfilled': | |
setTimeout(resolve, 0, this.#state.value); | |
break; | |
case 'rejected': | |
setTimeout(typeof onRejected === 'function' ? compose(resolve, reject, onRejected) : reject, 0, this.#state.reason); | |
break; | |
case 'pending': | |
this.#state.resolveOnFulfilledArray.push(resolve); | |
this.#state.resolveOnRejectedArray.push(typeof onRejected === 'function' ? compose(resolve, reject, onRejected) : reject); | |
} | |
}); | |
} | |
finally<B>(onFinally?: (() => B | Thenable<B>) | null): Promise<A> { | |
if (typeof onFinally !== 'function') return this.then(); | |
return this.then( | |
value => Promise.resolve(onFinally()).then(() => value), | |
reason => Promise.resolve(onFinally()).then<A>(() => { throw reason; }) | |
); | |
} | |
#run<Context>(executor: (this: Context, resolve: (value: A | Thenable<A>) => void, reject: (reason: unknown) => void) => void, context: Context) { | |
let once = true; | |
const resolve = (value: A | Thenable<A>) => { | |
if (once) { | |
once = false; | |
this.#resolve(value); | |
} | |
}; | |
const reject = (reason: unknown) => { | |
if (once) { | |
once = false; | |
this.#reject(reason); | |
} | |
}; | |
try { | |
executor.call(context, resolve, reject); | |
} catch (error: unknown) { | |
reject(error); | |
} | |
} | |
#resolve(value: A | Thenable<A>) { | |
if (value === this) return this.#reject(new TypeError('Self resolve')); | |
if (value === null || typeof value !== 'object' && typeof value !== 'function') return this.#fulfill(value); | |
let then: Then<A>; | |
try { | |
then = (value as any).then; | |
} catch (error: unknown) { | |
return this.#reject(error); | |
} | |
if (!isThenable(then, value)) return this.#fulfill(value); | |
if (!isPromise(value)) return this.#run(then, value); | |
switch (value.#state.status) { | |
case 'fulfilled': | |
this.#fulfill(value.#state.value); | |
break; | |
case 'rejected': | |
this.#reject(value.#state.reason); | |
break; | |
case 'pending': | |
value.#state.resolveOnFulfilledArray.push((value) => this.#fulfill(value)); | |
value.#state.resolveOnRejectedArray.push((reason) => this.#reject(reason)); | |
} | |
} | |
#fulfill(value: A) { | |
if (this.#state.status === 'pending') { | |
setTimeout(dispatch, 0, this.#state.resolveOnFulfilledArray, value); | |
this.#state = { status: 'fulfilled', value }; | |
} | |
} | |
#reject(reason: unknown) { | |
if (this.#state.status === 'pending') { | |
setTimeout(dispatch, 0, this.#state.resolveOnRejectedArray, reason); | |
this.#state = { status: 'rejected', reason }; | |
} | |
} | |
} | |
export const resolved = Promise.resolve; | |
export const rejected = Promise.reject; | |
export const deferred = <A>() => { | |
let resolve!: (value: A | Thenable<A>) => void; | |
let reject!: (reason: unknown) => void; | |
const promise = new Promise<A>((_resolve, _reject) => { | |
resolve = _resolve; | |
reject = _reject; | |
}); | |
return { promise, resolve, reject }; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment