Skip to content

Instantly share code, notes, and snippets.

@softwarespot
Last active March 26, 2019 06:05
Show Gist options
  • Save softwarespot/97f2baa7041295b915e8fe8c471cda08 to your computer and use it in GitHub Desktop.
Save softwarespot/97f2baa7041295b915e8fe8c471cda08 to your computer and use it in GitHub Desktop.
Non-spec compliant Promise implementation
// NOTE: The following is a non-spec compliant Promise implementation
// Utils
const asyncRun = createAsyncRunner()
const isFunction = fn => typeof fn === 'function'
const isObject = obj => Object(obj) === obj
const noop = () => {}
const STATE_PENDING = 'pending'
const STATE_RESOLVED = 'resolved'
const STATE_REJECTED = 'rejected'
function Promise(executor) {
var isInstance = this instanceof Promise
if (!isInstance) {
throw new TypeError('"Promise" cannot be called as a function. Use the "new" keyword to construct a new "Promise" object')
}
if (!isFunction(executor)) {
throw new TypeError(`Invalid type for "executor" argument, got "${typeof executor}"`)
}
if (isInstance && (this.state === STATE_PENDING || this.state === STATE_RESOLVED)) {
throw new TypeError('Invalid "this", cannot be a pending or resolved "Promise" object')
}
this.state = STATE_PENDING
this.value = undefined
this.chains = []
this.resolve = createSetStateFn(this, STATE_RESOLVED)
this.reject = createSetStateFn(this, STATE_REJECTED)
try {
executor(this.resolve, this.reject)
// const resolver = createResolutionFn(this)
// executor(resolver, this.reject)
} catch (ex) {
this.reject(ex)
}
}
Promise.prototype.then = function thenFn(onResolved, onRejected) {
const chain = new this.constructor(noop, noop)
chain.onResolved = isFunction(onResolved) ? onResolved : undefined
chain.onRejected = isFunction(onRejected) ? onRejected : undefined
this.chains.push(chain)
handleChains(this)
return chain
}
Promise.prototype.catch = function catchFn(onRejected) {
return this.then(undefined, onRejected)
}
Promise.resolve = function resolveFn(value) {
// Return as-is, if an instance of Promise
if (isObject(value) && value.constructor === this) {
return value
}
// NOTE: "this" is bound to Promise by default
return new this(resolve => resolve(value))
}
Promise.reject = function rejectFn(reason) {
return new this((_, reject) => reject(reason))
}
function handleChains(promise) {
if (promise.state === STATE_PENDING) {
return
}
promise.chains.forEach((chain) => {
// Skip those chains, which have already been handled before
if (chain.state !== STATE_PENDING) {
return
}
const onHandler = promise.state === STATE_RESOLVED ? chain.onResolved : chain.onRejected
if (onHandler) {
asyncRun(() => {
try {
const fn = createResolutionFn(chain)
fn(onHandler(promise.value))
} catch (ex) {
chain.reject(ex)
}
})
} else if (promise.state === STATE_RESOLVED) {
chain.resolve(promise.value)
} else if (promise.state === STATE_REJECTED) {
chain.reject(promise.value)
}
})
}
function createResolutionFn(promise) {
return (value) => {
// Spec: 2.3.1
if (promise === value) {
promise.reject(new TypeError(`A Promise's callback cannot return the same Promise instance`))
return
}
// Spec: 2.3.2
if (value instanceof Promise) {
// Spec: 2.3.2.1
// Spec: 2.3.2.2
// Spec: 2.3.2.3
value.then(promise.resolve, promise.reject)
// const fn = createResolutionFn(promise)
// value.then(fn, promise.reject)
return
}
// Spec: 2.3.4
if (!isFunction(value) && !isObject(value)) {
promise.resolve(value)
return
}
// Spec: 2.3.3
let then
try {
// Spec: 2.3.3.1
then = value.then
} catch (ex) {
// Spec: 2.3.3.2
promise.reject(ex)
return
}
// Spec: 2.3.3.4
if (!isFunction(then)) {
promise.resolve(value)
return
}
// Spec: 2.3.3.3.3
try {
// Spec: 2.3.3.3
// Spec: 2.3.3.3.1
// Spec: 2.3.3.3.2
// Spec: 2.3.3.3.3
then.call(value, promise.resolve, promise.reject)
// const fn = createResolutionFn(promise)
// then.call(value, fn, promise.reject)
} catch (ex) {
// Spec: 2.3.3.3.4
// Spec: 2.3.3.3.4.1
// Spec: 2.3.3.3.4.2
promise.reject(ex)
}
}
}
function createSetStateFn(promise, nextState) {
return (valueOrReason) => {
if (promise.state === STATE_PENDING) {
promise.state = nextState
promise.value = valueOrReason
handleChains(promise)
}
}
}
function createAsyncRunner() {
const fns = []
// Flush the queue by calling all functions pushed to the queue.
function flush() {
// NOTE: Using arr.forEach() does not work due to issues of
// copying the array
for (let i = 0; i < fns.length; i += 1) {
const fn = fns[i]
fn()
}
fns.length = 0
}
return (fn) => {
if (fns.push(fn) === 1) {
setTimeout(flush)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment