Home > OS >  How to implement BluebirdJS synchronous inspection extending a native Promise
How to implement BluebirdJS synchronous inspection extending a native Promise

Time:12-23

I'm updating some old code (not mine originally) that uses Bluebird promises. I'd rather use native ES6 Promises instead, but the old code uses a function Promise doesn't have, one to check if promises have been settled.

This is related to a similar question (Is there a way to tell if an ES6 promise is fulfilled/rejected/resolved?), but the solution given there is very different, so I wanted to know if the following code is a reasonable approach to the problem.

export class QueryablePromise<T> extends Promise<T> {
  private _isRejected = false;
  private _isResolved = false;
  private _isSettled = false;

  then<TResult1 = T, TResult2 = never>(
    onResolved?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null,
    onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null
  ): Promise<TResult1 | TResult2> {
    const newResolved = onResolved && ((value: T): TResult1 | PromiseLike<TResult1> => {
      this._isResolved = true;
      this._isSettled = true;
      return onResolved(value);
    });
    const newRejected = onRejected && ((reason: any): TResult2 | PromiseLike<TResult2> => {
      this._isRejected = true;
      this._isSettled = true;
      return onRejected(reason);
    });

    return super.then(newResolved, newRejected);
  }

  catch<TResult = never>(
    onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null
  ): Promise<T | TResult> {
    const newRejected = onRejected && ((reason: any): TResult | PromiseLike<TResult> => {
      this._isRejected = true;
      this._isSettled = true;
      return onRejected(reason);
    });

    return super.catch(newRejected);
  }

  finally(
    onFinally?: (() => void) | undefined | null
  ): Promise<T> {
    const newFinally = onFinally && ((): void => {
      this._isSettled = true;
      return onFinally();
    });

    return super.finally(newFinally);
  }

  get isRejected(): boolean { return this._isRejected; }
  get isResolved(): boolean { return this._isResolved; }
  get isSettled(): boolean { return this._isSettled; }
}

Basically I'm wrapping each callback passed to then, catch, and finally in another function which sets the appropriate flags and then calls the original callback. Flags could easily be set redundantly many times this way, but I don't see that as being much of a problem.

I tried to think of a way to solve this problem using the constructor for my promise subclass, and somehow put a wrapper around the original executor argument to intercept invocations of resolve and reject, but couldn't quite put my head around a way to implement that type of solution.

I also tried simply adding my own separate then, catch, and finally callbacks in a constructor for this subclass, with each having nothing to do but set my status flags, but oddly enough that resulted in a stack overflow, the constructor being called recursively until it blew up.

CodePudding user response:

This code doesn't seem reliable as presented. You need to remember that Promises resolve automatically when created.

With the code above, this simple test would fail:

const a = new QueryablePromise((res, rej) => {
    setTimeout(() => { console.log('resolved'); res()}, 100)
});

setTimeout(() => {
console.log(a.isSettled)
}, 500);

Will output

// resolved
// false

All cases need to be considered, not only when then, catch or finally are called. I would probably override the constructor and wrap the resolve and reject functions like this:

class QueryablePromise<T> extends Promise<T> {
  private _isRejected: boolean | undefined;
  private _isResolved: boolean | undefined;
  private _isSettled: boolean | undefined;

  constructor (fn: (res: (value: T | PromiseLike<T>) => void, rej: (reason?: any) => void) => void) {
    super((resolve, reject) => fn(
      value => {
        resolve(value)
        this._isResolved = true;
        this._isSettled = true;
      },
      reason => {
        reject(reason)
        this._isRejected = true;
        this._isSettled = true;
      },
    ));

    this._isRejected = false;
    this._isResolved = false;
    this._isSettled = false;
  }

  get isRejected(): boolean { return this._isRejected!; }
  get isResolved(): boolean { return this._isResolved!; }
  get isSettled(): boolean { return this._isSettled!; }
}
  • Related