qiwi/inside-out-promise

View on GitHub
src/main/ts/index.ts

Summary

Maintainability
A
1 hr
Test Coverage
A
96%
import {
  TInsideOutPromise,
  TPromiseExecutor,
  IPromiseFactory,
  TPromiseState,
  TPromiseFactoryOpts,
  TNormalizedPromiseFactoryOpts,
  IPromise,
  IPromiseConstructor,
} from './interface'
import {noop, setProto, bind} from './util'

export * from './interface'

export const factory: IPromiseFactory = (executor?: TPromiseExecutor | TPromiseFactoryOpts, opts?: TPromiseFactoryOpts): InsideOutPromise<any, any> => {
  return new InsideOutPromise(executor, opts)
}

Object.defineProperty(factory, 'Promise', {
  get() {
    return InsideOutPromise.Promise
  },
  set(P: IPromiseConstructor) {
    setProto(InsideOutPromise.prototype, P.prototype)
    InsideOutPromise.Promise = P
  },
})

export class InsideOutPromise<TValue, TReason> implements TInsideOutPromise<TValue, TReason> {

  state: TPromiseState = TPromiseState.PENDING
  value: any
  reason: any

  private _resolve: any
  private _reject: any

  constructor(executor?: TPromiseExecutor<TValue, TReason> | TPromiseFactoryOpts, opts?: TPromiseFactoryOpts) {
    let _resolve: any = undefined
    let _reject: any = undefined
    const _opts = InsideOutPromise.normalizeOpts(executor, opts)
    const _P: PromiseConstructor = _opts.Promise
    const exec: TPromiseExecutor = _opts.executor

    return InsideOutPromise.contextify(new _P((resolve, reject) => {
      _resolve = resolve
      _reject = reject

      exec(resolve, reject)
    }), {
      state: TPromiseState.PENDING,
      _P,
      _resolve,
      _reject,
    })
  }

  get promise(): InsideOutPromise<TValue, TReason> {
    return this
  }

  get status(): TPromiseState {
    return this.state
  }

  get result(): any {
    return this.isFulfilled()
      ? this.value
      : this.isRejected()
        ? this.reason
        : undefined
  }

  @bind
  public resolve(value?: any): InsideOutPromise<TValue, TReason> {
    this._resolve(value)
    return this
  }

  @bind
  public reject(reason?: any): InsideOutPromise<TValue, TReason> {
    this._reject(reason)
    return this
  }

  then(onSuccess?: (value: TValue) => any, onReject?: (reason: any) => any): InsideOutPromise<TValue, TReason> {
    return InsideOutPromise.contextify(this, this, 'then', onSuccess, onReject)
  }

  catch(onReject?: (reason: any) => any): InsideOutPromise<TValue, TReason> {
    return InsideOutPromise.contextify(this, this, 'catch', onReject)
  }

  finally(handler?: () => any): InsideOutPromise<TValue, TReason> {
    // @ts-ignore
    return this._P.prototype.finally
      ? InsideOutPromise.contextify(this, this, 'finally', handler)
      : this.then(handler).catch(handler)
  }

  isPending(): boolean {
    return this.state === TPromiseState.PENDING
  }

  isFulfilled(): boolean {
    return this.state === TPromiseState.FULFILLED
  }

  isRejected(): boolean {
    return this.state === TPromiseState.REJECTED
  }

  isResolved(): boolean {
    return !this.isPending()
  }

  get [Symbol.toStringTag](): string {
    return this.constructor.name
  }

  get [Symbol.species](): any {
    return Promise
  }

  static get [Symbol.species](): any {
    return Promise
  }

  static get [Symbol.toStringTag](): string {
    return this.name
  }

  private static contextify(ref: any, cxt: any, method?: string, ...args: any[]): InsideOutPromise<any, any> {
    const promise = method
      ? cxt._P.prototype[method].call(ref, ...args)
      : ref

    return Object.assign(setProto(this.observe(promise), this.prototype), cxt)
  }

  private static observe(promise: IPromise<any, any>): IPromise<any, any> {
    const fake = promise
      .then((v: any) => {
        Object.assign(fake, {
          value: v,
          state: TPromiseState.FULFILLED,
        })

        return v
      }, (e: any) => {
        Object.assign(fake, {
          reason: e,
          state: TPromiseState.REJECTED,
        })

        throw e
      })

    return fake
  }

  static normalizeOpts(executor?: TPromiseExecutor | TPromiseFactoryOpts, opts: TPromiseFactoryOpts = {}): TNormalizedPromiseFactoryOpts {
    const mixin = typeof executor === 'function'
      ? {
        ...opts,
        executor,
      }
      : executor || opts

    return {
      Promise: InsideOutPromise.Promise,
      executor: noop,
      ...mixin,
    }
  }

  static Promise = Promise

}

// NOTE proto chaining
factory.Promise = Promise