0

I have situation where I believe I need to create a Deferred object with a "then" handler, but wait until the "then" handler has completed it's own promise before moving on.

The use case is a record object, and the above function is it's save method. The record object has an attribute called saveQueue, which is set to $.Deferred() on the record's instantiation. The resolve call on saveQueue was supposed to make sure the Deferred there is always executing every new handler attached to it as soon as it could. The idea being that you can call save several times on the record in short succession, but the calls will run one after another, and not overlap.

I am using a Deferred to enqueue Ajax calls, so that one does not run until the previous one call finished. However, from the same method, I want to return a Deferred that can be resolved/rejected by the jQuery Ajax object, like so:

 record.saveQueue = $.Deferred();

 self.save = function( record ){
    var deferredAction = $.Deferred();

    deferredAction.then(function() {
        return $.post("/example_save_endpoint");
    });

    record.saveQueue.always(function(){
      deferredAction.resolve();
    }).resolve();

    return deferredAction;
  }

However, when I use this code, the deferredAction promise always ends up resolved, presumably because the #then handler is returning a "pending" (and thus non-rejecting) promise. Is there any way to force the Deferred to wait until the Ajax promise is complete before resolving/rejecting? Or is there another, better way to thread this needle?

Asherlc
  • 1,111
  • 2
  • 12
  • 28
  • `deferredAction` is resolved as soon as `self.adaptor.create( record, callback )` is resolved, not earlier. Your initial `deferredAction` and the one on the next line are 2 different objects. Just give them different names. – zerkms Mar 25 '15 at 00:30
  • You're doing very odd things, especially overwriting `deferredAction` with a promise, calling `resolve` on an object you don't seem to own (`record.saveQueue`), and returning a deferred not a promise. Also I'm suspecting a [deferred antipattern](http://stackoverflow.com/q/23803743/1048572) in your line of thought. Please tell use more about the situation you try to solve, less about the attempt. Where would that queue be located, who instantiates it? – Bergi Mar 25 '15 at 00:44
  • Your points are well taken. The use case is a record object, and the above function is it's `save` method. The record object has an attribute called `saveQueue`, which is set to `$.Deferred()` on the record's instantiation. The resolve call on `saveQueue` was supposed to make sure the Deferred there is always executing every new handler attached to it as soon as it could. The idea being that you can call `save` several times on the record in short succession, but the calls will run one after another, and not overlap. Does that help clarify my intent? – Asherlc Mar 25 '15 at 00:57
  • Consider using .promise().done({}); as shown in documentation https://api.jquery.com/promise/ – Moishe Lipsker Mar 25 '15 at 05:10

2 Answers2

0

I would probably choose not to do it this way, but a deferred/promise can indeed be used as a queuing device.

You need a slight(?) variation of what you already tried.

self.queue = $.when();//A resolved promise, used to form a queue of functions in a .then() chain.

self.save = function(data) {
    var dfrd = $.Deferred();//A Deferred dedicated to this particular save.
    self.queue = self.queue.then(function() {
        return $.post("/example_save_endpoint", data) //Make the AJAX call, and return a jqXHR to ensure the downstream queue waits for this jqXHR to resolve/reject.
            .then(dfrd.resolve, dfrd.reject) //Resolve/reject the Deferred for the caller's benefit
            .then(null, function() {
                //Force failure down the success path to ensure the queue is not killed by an AJAX failure.
                return $.when();//Return a resolved promsie, for the queue's benefit.
            });
    });
    return dfrd.promise();//allow the caller to do something when the AJAX eventually responds
}

For explanation, see comments in code

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
  • This is still using the deferred antipattern. – Bergi Mar 26 '15 at 14:12
  • @Bergi, I was so waiting for someone to say that. Absolutely not! . It's what I have previously called the "deferred legal-pattern". Think about it - `save()` must return a promise, but it *can't* return the jqXHR returned by `$.post()`, which is not generated until later on, when the queued function emerges at the front of the queue (the end of the then chain). I would be delighted if someone can prove me wrong but I don't think it's possible without the intermediate Deferred. – Roamer-1888 Mar 26 '15 at 15:45
  • You'll want to have a look at my answer :-) – Bergi Mar 26 '15 at 16:59
  • 1
    Btw, did you forget a "`self.queue = `"? Your `.save()` method doesn't seem to queue calls currently. – Bergi Mar 26 '15 at 18:24
0

Your idea might work, but

  • the queue must not be resolved using .resolve() every time the method is called, instead it should be initialised only with a resolved promise.
  • to actually queue on the record.saveQueue, it needs to be changed (overwritten) on every method call, to represent the end of the latest request.

And we don't need any deferreds for that, as we can work with the promises that $.post returns.

So use this:

var emptyQueue = $.when(undefined); // an already fulfilled promise as the start
// equivalent: = $.Deferred().resolve().promise();

function startQueue() {
    return emptyQueue; // yes, this delibaretely returns a constant, the begin
                       // of the queue always looks the same (and is never mutated)
}

// every time you create a record, do
record.saveQueue = startQueue();

// and use that in your methods:
this.save = function(record) {
    var queuedRequestResult = record.saveQueue.then(function() {
        return $.post("/example_save_endpoint");
//      ^^^^^^ promises chain :-)
    });
    // Magic happens here:
    record.saveQueue = queuedRequestResult // we swap the previous queue promise for a new
                                           // one that resolves only after the request
      .then(startQueue, startQueue);       // and make sure it then starts with a fresh
                                           // queue, especially when the request failed
    //.then(null, startQueue) is similar, except unnecessarily remembering the last result
    return queuedRequestResult;
}
Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • OK Bergi, you certainly appear to have found a way, but for me the so-called anti-pattern is far far cleaner. – Roamer-1888 Mar 26 '15 at 18:15
  • @Roamer-1888: Hm, I think it's just the same idea as in your answer - only simpler and shorter. – Bergi Mar 26 '15 at 18:25
  • 1
    Bergi, I think you can avoid the need for `record.saveQueue = startQueue()` every time a record is created, with `var queuedRequestResult = (record.saveQueue || startQueue()).then(...)`. Maybe your solution is starting to grow on me already, after all I said I would be delighted ... :-) – Roamer-1888 Mar 26 '15 at 18:43
  • @Roamer-1888: Yeah, that might be a good idea. I just wanted to emphasize that each `record` has its own queue property (just like in the OPs code), which is typically (and efficiently) created in the record's constructor. – Bergi Mar 26 '15 at 18:47
  • Bergi, yes but that would require `startQueue` (or `emptyQueue`) to be in scope of the constructor, which is not necessarily the case. – Roamer-1888 Mar 26 '15 at 18:50
  • Yeah, it depends a lot on what those `record`s are and the architecture of the app. However, OP seems to be using them explicitly for queuing saves, so I'd expect that to work. – Bergi Mar 26 '15 at 18:53