Skip to content

Instantly share code, notes, and snippets.

@adit-hotstar
Last active September 20, 2021 10:07
Show Gist options
  • Save adit-hotstar/25e006f6b04ab59a4099631cb26cc3ae to your computer and use it in GitHub Desktop.
Save adit-hotstar/25e006f6b04ab59a4099631cb26cc3ae to your computer and use it in GitHub Desktop.
Understanding the Promises/A+ specification
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