8

Problem 1: only one API request is allowed at a given time, so the real network requests are queued while there's one that has not been completed yet. An app can call the API level anytime and expecting a promise in return. When the API call is queued, the promise for the network request would be created at some point in the future - what to return to the app? That's how it can be solved with a deferred "proxy" promise:

var queue = [];
function callAPI (params) {
  if (API_available) {
    API_available = false;
    return doRealNetRequest(params).then(function(data){
      API_available = true;
      continueRequests();
      return data;
    });
  } else {
    var deferred = Promise.defer();
    function makeRequest() {
      API_available = false;
      doRealNetRequest(params).then(function(data) {
        deferred.resolve(data);
        API_available = true;
        continueRequests();
      }, deferred.reject);
    }
    queue.push(makeRequest);
    return deferred.promise;
  }
}

function continueRequests() {
  if (queue.length) {
    var makeRequest = queue.shift();
    makeRequest();
  }
}

Problem 2: some API calls are debounced so that the data to be sent is accumulated over time and then is sent in a batch when a timeout is reached. The app calling the API is expecting a promise in return.

var queue = null;
var timeout = 0;
function callAPI2(data) {
  if (!queue) {
    queue = {data: [], deferred: Promise.defer()};
  }
  queue.data.push(data);
  clearTimeout(timeout);
  timeout = setTimeout(processData, 10);
  return queue.deferred.promise;
}

function processData() {
  callAPI(queue.data).then(queue.deferred.resolve, queue.deferred.reject);
  queue = null;
}

Since deferred is considered an anti-pattern, (see also When would someone need to create a deferred?), the question is - is it possible to achieve the same things without a deferred (or equivalent hacks like new Promise(function (resolve, reject) {outerVar = [resolve, reject]});), using the standard Promise API?

Community
  • 1
  • 1
mderk
  • 796
  • 4
  • 13
  • In your first example, the queue never seems to get started? Please show that part of the code as well - it's what you should get a promise for. – Bergi May 25 '16 at 00:40
  • This really sounds like two separate questions. – Bergi May 25 '16 at 00:40
  • 1
    @Bergi edited, added the code for starting the queue. Two questions - maybe, but it seems to me the pattern is the same - the promise is yet to be created, but the calling code is expecting a promise in return. – mderk May 25 '16 at 00:55
  • _"When the API call is queued, the promise for the network request would be created at some point in the future - what to return to the app?"_ , _"The app calling the API is expecting a promise in return."_ You can return the promise when resolved or rejected. See [Make a jquery function wait till it's previous call has been resolved](http://stackoverflow.com/questions/26859275/make-a-jquery-function-wait-till-its-previous-call-has-been-resolved/) – guest271314 May 25 '16 at 01:00

3 Answers3

6

Promises for promises that are yet to be created

…are easy to build by chaining a then invocation with the callback that creates the promise to a promise represents the availability to create it in the future.

If you are making a promise for a promise, you should never use the deferred pattern. You should use deferreds or the Promise constructor if and only if there is something asynchronous that you want to wait for, and it does not already involve promises. In all other cases, you should compose multiple promises.

When you say

When the API call is queued, the promise for the network request would be created at some point in the future

then you should not create a deferred that you can later resolve with the promise once it is created (or worse, resolve it with the promises results once the promise settles), but rather you should get a promise for the point in the future at which the network reqest will be made. Basically you're going to write

return waitForEndOfQueue().then(makeNetworkRequest);

and of course we're going to need to mutate the queue respectively.

var queue_ready = Promise.resolve(true);
function callAPI(params) {
  var result = queue_ready.then(function(API_available) {
    return doRealNetRequest(params);
  });
  queue_ready = result.then(function() {
    return true;
  });
  return result;
}

This has the additional benefit that you will need to explicitly deal with errors in the queue. Here, every call returns a rejected promise once one request failed (you'll probably want to change that) - in your original code, the queue just got stuck (and you probably didn't notice).

The second case is a bit more complicated, as it does involve a setTimeout call. This is an asynchronous primitive that we need to manually build a promise for - but only for the timeout, and nothing else. Again, we're going to get a promise for the timeout, and then simply chain our API call to that to get the promise that we want to return.

function TimeoutQueue(timeout) {
  var data = [], timer = 0;
  this.promise = new Promise(resolve => {
    this.renew = () => {
      clearTimeout(timer);
      timer = setTimeout(resolve, timeout);
    };
  }).then(() => {
    this.constructor(timeout); // re-initialise
    return data;
  });
  this.add = (datum) => {
    data.push(datum);
    this.renew();
    return this.promise;
  };
}

var queue = new TimeoutQueue(10);
function callAPI2(data) {
  return queue.add(data).then(callAPI);
}

You can see here a) how the debouncing logic is completely factored out of callAPI2 (which might not have been necessary but makes a nice point) and b) how the promise constructor only concerns itself with the timeout and nothing else. It doesn't even need to "leak" the resolve function like a deferred would, the only thing it makes available to the outside is that renew function which allows extending the timer.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Plus, you call `resolve` with no arguments so I don't see how your `.then()` handler ever gets a `data` argument like it appear you're counting on. – jfriend00 May 25 '16 at 03:18
  • @jfriend00: There is no "prior promise" here that would need to be resolved, `add()` returns the same `.promise` every time (until the timeout actually expires, when the promise resolves and a new `.promise` and `.add` method are created). The `timer` is replaced with a new one that calls the same `resolve` but expires a bit later on every `add` call. – Bergi May 25 '16 at 03:18
  • 1
    @jfriend00: Thanks, that `data` parameter needed to be removed indeed, `data` should refer to the array by closure. I had passed the array to `setTimeout` in a prior version of the code that I fiddled around with. Maybe I went a bit over the top with this `TimeoutQueue` thing :-) – Bergi May 25 '16 at 03:21
  • @Bergi Thanks for the answer! So, you basically return the second promise from the resolve handler for the first (that is waiting for the queue), and that particular promise will be handled by `then` in the outer code, right? – mderk May 25 '16 at 10:54
  • @mderk: Yes, that's [how `then` works](http://stackoverflow.com/a/22562045/1048572) :-) – Bergi May 25 '16 at 12:23
3

When the API call is queued, the promise for the network request would be created at some point in the future - what to return to the app?

Your first problem can be solved with promise chaining. You don't want to execute a given request until all prior requests have finished and you want to execute them serially in order. This is exactly the design pattern for promise chaining. You can solve that one like this:

var callAPI = (function() {
    var p = Promise.resolve();
    return function(params) {
        // construct a promise that chains to prior callAPI promises
        var returnP = p.then(function() {
            return doRealNetRequest(params);
        });
        // make sure the promise we are chaining to does not abort chaining on errors
        p = returnP.then(null, function(err) {
            // handle rejection locally for purposes of continuing chaining
            return;
        });
        // return the new promise
        return returnP;
    }
})();

In this solution, a new promise is actually created immediately with .then() so you can return that promise immediately - there is no need to create a promise in the future. The actual call to doRealNetRequest() is chained to this retrurned .then() promise by returning its value in the .then() handler. This works because, the callback we provide to .then() is not called until some time in the future when the prior promises in the chain have resolved, giving us an automatic trigger to execute the next one in the chain when the prior one finishes.

This implementation assumes that you want queued API calls to continue even after one returns an error. The extra few lines of code around the handle rejection comment are there to make sure the chain continues even where a prior promise rejects. Any rejection is returned back to the caller as expected.


Here's a solution to your second one (what you call debounce).

the question is - is it possible to achieve the same things without a deferred (or equivalent hacks like new Promise(function (resolve, reject) {outerVar = [resolve, reject]});), using the standard Promise API?

As far as I know, the debouncer type of problem requires a little bit of a hack to expose the ability to trigger the resolve/reject callbacks somehow from outside the promise executor. It can be done a little cleaner than you propose by exposing a single function that is within the promise executor function rather than directly exposing the resolve and reject handlers.

This solution creates a closure to store private state that can be used to manage things from one call to callAPI2() to the next.

To allow code at an indeterminate time in the future to trigger the final resolution, this creates a local function within the promise executor function (which has access to the resolve and reject functions) and then shares that to the higher (but still private) scope so it can be called from outside the promise executor function, but not from outside of callAPI2.

var callAPI2 = (function() {
    var p, timer, trigger, queue = [];
    return function(data) {
        if (!p) {
            p = new Promise(function(resolve) {
                // share completion function to a higher scope
                trigger = function() {
                    resolve(queue);
                    // reinitialize for future calls
                    p = null;
                    queue = [];
                }
            }).then(callAPI);
        }
        // save data and reset timer
        queue.push(data);
        clearTimeout(timer);
        setTimeout(trigger, 10);
        return p;
    }
})();
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • You can (and imo *should*) simplifiy that to `resolve(callAPI(queue))` to at least avoid some of the perils of the [`Promise` constructor antipattern](http://stackoverflow.com/q/23803743/1048572). Or better move `callAPI` into a `then` call right away. – Bergi May 25 '16 at 03:14
  • @Bergi - Thx. I took your first suggestion. – jfriend00 May 25 '16 at 03:38
  • Yes, you correctly did not forget to pass `reject` as the second argument, but every time I see `.then(resolve …` I shudder because someone else will adopt the pattern and eventually forget about errors. Better not use the pattern at all :-) – Bergi May 25 '16 at 03:41
  • Btw, both your old and current codes are still susceptible to exceptions thrown by the `callAPI` invocation itself. – Bergi May 25 '16 at 03:43
  • 1
    @Bergi - yeah, I already figured that out and changed it again. – jfriend00 May 25 '16 at 03:44
  • @mderk - I added a solution to your first question. – jfriend00 May 25 '16 at 16:58
0

You can create a queue, which resolves promises in the order placed in queue

window.onload = function() {
  (function(window) {
    window.dfd = {};
    that = window.dfd;
    that.queue = queue;

    function queue(message, speed, callback, done) {

      if (!this.hasOwnProperty("_queue")) {
        this._queue = [];
        this.done = [];
        this.res = [];
        this.complete = false;
        this.count = -1;
      };
      q = this._queue,
        msgs = this.res;
      var arr = Array.prototype.concat.apply([], arguments);
      q.push(arr);
      msgs.push(message);
      var fn = function(m, s, cb, d) {

        var j = this;
        if (cb) {
          j.callback = cb;
        }
        if (d) {
          j.done.push([d, j._queue.length])
        }
        // alternatively `Promise.resolve(j)`, `j` : `dfd` object
        // `Promise` constructor not necessary here,
        // included to demonstrate asynchronous processing or
        // returned results
        return new Promise(function(resolve, reject) {
            // do stuff
            setTimeout(function() {
              div.innerHTML += m + "<br>";
              resolve(j)
            }, s || 0)
          })
          // call `cb` here, interrupting queue
          .then(cb ? j.callback.bind(j, j._queue.length) : j)
          .then(function(el) {
            console.log("queue.length:", q.length, "complete:", el.complete);
            if (q.length > 1) {
              q.splice(0, 1);
              fn.apply(el, q[0]);
              return el
            } else {
              el._queue = [];
              console.log("queue.length:", el._queue.length
                          , "complete:", (el.complete = !el._queue.length));
              always(promise(el), ["complete", msgs])
            };
            return el
          });
        return j
      }

      , promise = function(t) {
        ++t.count;
        var len = t._queue.length,
          pending = len + " pending";
        return Promise.resolve(
          len === 1 
          ? fn.apply(t, t._queue[0]) && pending 
          : !(t.complete = len === 0) ? pending : t
        )
      }

      , always = function(elem, args) {
        if (args[0] === "start") {
          console.log(elem, args[0]);
        } else {
          elem.then(function(_completeQueue) {
            console.log(_completeQueue, args);
              // call any `done` callbacks passed as parameter to `.queue()`
              Promise.all(_completeQueue.done.map(function(d) {
                return d[0].call(_completeQueue, d[1])
              }))
              .then(function() {
                console.log(JSON.stringify(_completeQueue.res, null, 2))
              })
          })
        }
      };

      always(promise(this), ["start", message, q.length]);
      return window
    };
  }(window));

  window
    .dfd.queue("chain", 1000)
    .dfd.queue("a", 1000)
    .dfd.queue("b", 2000)
    .dfd.queue("c", 2000, function callback(n) {
      console.log("callback at queue index ", n, this);
      return this
    }, function done(n) {
      console.log("all done callback attached at queue index " + n)
    })
    .dfd.queue("do", 2000)
    .dfd.queue("other", 2000)
    .dfd.queue("stuff", 2000);

  for (var i = 0; i < 10; i++) {
    window.dfd.queue(i, 1000)
  };

  window.dfd.queue.apply(window.dfd, ["test 1", 5000]);
  window.dfd.queue(["test 2", 1000]);

  var div = document.getElementsByTagName("div")[0];
  var input = document.querySelector("input");
  var button = document.querySelector("button");

  button.onclick = function() {
    window.dfd.queue(input.value, 0);
    input.value = "";
  }
}
<input type="text" />
<button>add message</button>
<br>
<div></div>
guest271314
  • 1
  • 15
  • 104
  • 177