Last active
April 12, 2024 12:23
-
-
Save lqt0223/751d0d268f6855ce12329952dd1da4cf to your computer and use it in GitHub Desktop.
42. A promise implementation that is A+ conformant
This file contains 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
// (Tested on Node.js 18.15.0) A promise implementation that is A+ conformant | |
// To implement something promise-ish (ignoring all edge-cases from A+ spec), there are some guidelines below: | |
// - manage the state of promise object, to avoid transition between two settled state | |
// - use 'then' method to store callbacks for resolving / rejecting the promise. | |
// - implement 3 kinds of chaining behaviors of a promise | |
// - when resolving a promise, recursively unwrap until a non-thenable value is found | |
// - a promise can invoke then (to add callbacks) multiple times. When the promise is settled, all cbs should be invoked sequentially | |
// - the propagation of promise chain | |
// warn: this does not check if val.then is a function | |
// make sure to do the check outside of function | |
const isThenable = val => val && (typeof val === 'function' || typeof val === 'object') && ('then' in val) | |
class MyPromise { | |
state = 'pending' // 'pending' | 'fulfilled' | 'rejected' | |
value = undefined // the resolved value for promise instance, unwrapped until it is not a thenable and stored here | |
onFulfilledCbs = [] | |
onRejectedCbs = [] | |
// internal methods to resolve an incoming x value | |
// there are basically 3 cases for x | |
// - thenable (but not an instance of MyPromise) | |
// - MyPromise instance | |
// - other non-thenable | |
_resolve = (x) => { | |
// 2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason. | |
if (x === this) { | |
return this._reject(new TypeError()) | |
} | |
// thenable case | |
if (isThenable(x)) { | |
// 2.3.3.2. If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason. | |
try { | |
// if this is a settled MyPromise, pass it on to resolve | |
if (x instanceof MyPromise) { | |
if (x.state === 'fulfilled') { | |
this._resolve(x.value) | |
return | |
} else if (x.state === 'rejected') { | |
this._reject(x.value) | |
return | |
} | |
} | |
// warn: use as less val.then getter as possible, which is a derived test from test 2.3.3.1 | |
const then = x.then | |
// 2.3.3.4. If then is not a function, fulfill promise with x. | |
if (typeof then !== 'function') { | |
this._settleValue(x) | |
return | |
} | |
// other thenable case or cases for pending MyPromise | |
// here, we have many codes for dealing with quirk thenables (which are test cases from 2.3.3.3.1) | |
// the already-settled check is needed for thenable that [tries to fulfill twice] | |
let settled = false | |
// the try-catch is needed for thenable that [fulfills but then throws] | |
try { | |
// warn: call with 'this' correctly bound | |
then.call(x, (_val) => { | |
if (!settled) { | |
settled = true | |
this._resolve(_val) | |
} | |
}, (_val) => { | |
if (!settled) { | |
settled = true | |
this._reject(_val) | |
} | |
}) | |
// if the exception throws after settled, it should be ignored | |
// else, re-throw | |
} catch (e) { | |
if (!settled) { | |
throw e | |
} | |
} | |
} catch (e) { | |
this._reject(e) | |
} | |
return | |
} | |
// for non-thenable value case | |
this._settleValue(x) | |
} | |
// the reject process does not require a recursive unwrapping | |
_reject = async (reason) => { | |
this._settleValue(reason, true) | |
} | |
// resolve or reject a value (without further unwrapping) | |
_settleValue = (valOrReason, doReject = false) => { | |
// do not resolve or reject when promise is already settled | |
if (this.state !== 'pending') return | |
// this is for running codes asynchronously (after the possible quickest 'then' invocation) | |
// we need to make sure that cbs are filled by 'then' invocation on the promise, | |
// even in the extreme case that the promise is immediately settled | |
queueMicrotask(() => { | |
this.state = doReject ? 'rejected' : 'fulfilled' | |
this.value = valOrReason | |
// 'then' might be invoked many times on the same promise, so it's an array | |
const cbs = doReject ? this.onRejectedCbs : this.onFulfilledCbs | |
while (cbs.length) { | |
// for each callback added to this promise | |
const [ cb, nextPromise ] = cbs.shift() | |
// 2.2.7.2. If either onFulfilled or onRejected throws an exception e, promise2 must be rejected with e as the reason. | |
try { | |
if (cb) { | |
const nextVal = cb(valOrReason) | |
// because there is a cb that handles, however the state of the current promise, next promise should try to resolve | |
nextPromise._resolve(nextVal) | |
// if there is no cb, propagate through | |
// 2.2.1. Both onFulfilled and onRejected are optional arguments: | |
} else { | |
if (doReject) { | |
nextPromise._reject(valOrReason) | |
} else { | |
nextPromise._resolve(valOrReason) | |
} | |
} | |
} catch (e) { | |
nextPromise._reject(e) | |
} | |
} | |
}) | |
} | |
constructor(initFn) { | |
initFn(this._resolve, this._reject) | |
} | |
// synchronously adding resolve / reject callback for this promise | |
then(onFulfilled, onRejected) { | |
const promise = new MyPromise(() => {}) | |
// make sure that cb is callable | |
const rejCb = onRejected?.call && onRejected | |
this.onRejectedCbs.push([rejCb, promise]) | |
const fulCb = onFulfilled?.call && onFulfilled | |
this.onFulfilledCbs.push([fulCb, promise]) | |
return promise | |
} | |
} | |
const adapter = { | |
resolved: (val) => new MyPromise((resolve) => resolve(val)), | |
rejected: (val) => new MyPromise((_, reject) => reject(val)), | |
deferred: () => { | |
let res, rej | |
const p = new MyPromise((resolve, reject) => { | |
res = resolve | |
rej = reject | |
}) | |
return { | |
promise: p, | |
resolve: res, | |
reject: rej, | |
} | |
} | |
} | |
var promisesAplusTests = require("promises-aplus-tests") | |
promisesAplusTests(adapter, { grep: '' }) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment