promises-aplus/constructor-spec

discovery of resolver

ForbesLindesay opened this issue · 7 comments

Consider a utility function, timeout:

function timeout(prom, time) {
  let {promise, resolver} = promise();
  resolver.resolve(prom);
  setTimeout(function () {
    resolver.reject(new Error('Operation timed out');
  }, time);
  return promise;
}

The problem with utility functions like these is that I'm not going to get out the same type of promise that I put in. If I passed in something like a Q promise and get back something like a promises-a I loose a lot of functionality.

What if promises came with a property that tells you how to make a new promise, resolver pair:

function timeout(prom, time) {
  let {promise, resolver} = typeof prom.defer === 'function' ?
                            prom.defer() : promise();
  resolver.resolve(prom);
  setTimeout(function () {
    resolver.reject(new Error('Operation timed out');
  }, time);
  return promise;
}

That way if I passed a Q promise in, I'd get a Q promise out.

Do people want this method? What should it be called?

This is a pretty interesting idea. A bit weird, but not bad.

If we went with a promise-constructor approach, though, it would be more natural, via the constructor property:

function timeout(promise, time) {
  var Promise = promise.constructor;

  return new Promise(function (resolve, reject) {
    promise.then(resolve, reject);
    setTimeout(function () {
      reject(new Error('Operation timed out.'));
    }, time);
  });
}

Writing this down actually gives me the strongest argument for the constructor pattern over the deferred pattern.

This seems like a great way to achieve all of the desired features:

    new Promise(function (resolve, reject) {
      resolve(promise);
      setTimeout(function () {
          reject(new Error('Operation timed out.'));
          // reject function has its own "extension" properties
          // so you could also do this (feel free to bikeshed these names):
          // resolve.createRejectedPromise(new Error('Operation timed out.'));
          // or this:
          // resolve.createCancellablePromise(promise);
          // or this:
          // resolve.createSomeObservablePromise(promise);
      }, 100);
    });

Pros:

  1. fulfillment is obvious and straightforward via function signature. 99% of the time, this is what devs will need
  2. first param can be treated as a resolver with several functions, even proprietary extensions
  3. KISS

Cons:

  1. duplicating the reject function arg as a resolve arg property might feel silly to some

I like this hybrid. It makes the most common operations, resolve & reject, totally obvious and easy to use, while still allowing the resolve function (or reject, I suppose) to be used as the extension point for other to-be-spec'd and proprietary features.

@unscriptable @briancavalier both those comments are nothing to do with this issue and everything to do with #7. This issue is about discovering what promise library was used to create an existing promise (so as to be able to create a new promise from the same library). You're comments are about what arguments should be passed to the resolver function.

I worry about the memory footprint of having resolve as both a function and an object with the resolver methods. If we go that way for each promise we'll have to create:

  • All the functions that wrap the the resolver in the promise. If your Promise implementation has getStatus which asks the resolver for its status, then you have an extra function if you want to keep the resolver private
  • Two bound functions: resolve and reject
  • A new function for each method of the resolver that we want to expose to the promise creator

And we don't have __proto__ to avoid creating all those functions. I thought about setting this in the Promise initialization function pointing to the resolver, but it's a little weird.

@juandopazo I don't see how your comments relate to this issue, I assume you are still talking about #7.

Yup, my bad.