Promise chaining

Feb 7, 2014 at 4:18 PM
Edited Feb 7, 2014 at 4:19 PM
In order to provide consistent chaining from the returned promise of the core $.SPServices function, I had to implement the always() method instead of the $.ajax complete() method.

I realize that I could omit the complete function from the original $.SPServices call and use the .always() chain in my own code, but since $.SPServices is returning a promise I thought it might be a good idea to be consistent in the callback chain.

The problem was the complete callback in the ajax call does not get pushed into the promise que, so it wouldn't be included in any .then() chaining.

Simple example:
$().SPServices({
                        operation: "GetList",
                        listName: "some list",
                        completefunc: function (xData, Status) {  
                               console.log("I run when I want"); }
                    })
                    .then(function (xml, Status) { 
                           console.log(" I obey promise"); });
Here is the patched code.
            // Finally, make the Ajax call
            promisesCache[msg] = $.ajax({
                // The relative URL for the AJAX call
                url: ajaxURL,
                // By default, the AJAX calls are asynchronous.  You can specify false to require a synchronous call.
                async: opt.async,
                // Before sending the msg, need to send the request header
                beforeSend: function (xhr) {
                    // If we need to pass the SOAPAction, do so
                    if(WSops[opt.operation][1]) {
                        xhr.setRequestHeader("SOAPAction", SOAPAction);
                    }
                },
                // Always a POST
                type: "POST",
                // Here is the SOAP request we've built above
                data: msg,
                // We're getting XML; tell jQuery so that it doesn't need to do a best guess
                dataType: "xml",
                // and this is its content type
                contentType: "text/xml;charset='utf-8'",
                /*
                complete: function(xData, Status) {
                    // When the call is complete, call the completefunc if there is one
                    if($.isFunction(opt.completefunc)) {
                        opt.completefunc(xData, Status);

                    }
                }
                */
            }).always(function (data, Status, xData) {
                // When the call is complete, call the completefunc if there is one
                if ($.isFunction(opt.completefunc)) {
                    opt.completefunc(xData, Status);
                }
            });
Feb 7, 2014 at 5:30 PM
Ricardo,
Thanks for posting here... Others in the community "listen" to this forum and thus you will get more conversation going... :)

I'm going to setup a quick test to see test what you are suggesting above... but just want to make sure: are you saying that your 'simple example' above does not work?
$().SPServices({
    operation: "GetList",
    listName: "some list",
    completefunc: function (xData, Status) {  
           console.log("I run when I want"); }
})
.then(function (xml, Status) { 
       console.log(" I obey promise");
});
Because it should... Or else, IMO, jQuery is broken... I'm pretty sure I have used it this way with no issues... IN just reviewing the code you suggested as a fix, I can see 'completefunc' being triggered twice, which I don't think is a good behavior.

/Paul.
Feb 7, 2014 at 6:12 PM
Hi Paul,

The code I submitted has the complete method commented, so it will not run twice.

My simple example was to illustrate my use case. What I'm trying to say is that you can't guarantee the order in which 'completefunc' will execute nor can you use it as part of the $.when mechanism. Your results from this simple test might do what you expected (which I'm interested in hearing). The more elaborate code I was running resulted in completefunc running after "I obey promise" function.

Excluding completefunc from the promise que could be the wanted behavior, I guess it just needs to be made known.

Ricardo
Feb 7, 2014 at 6:42 PM
Thanks Ricardo... I'll run some tests... I thought I looked into this a little while ago (see this post) and even validated that the .ajax() method does in fact call 'complete' before resolving the Deferred - thus maintaining the correct expected execution order. I'll report back soon... I'm going to use Marc latest ALPHA version (2014.1).

/Paul
Coordinator
Feb 7, 2014 at 6:59 PM
Edited Feb 7, 2014 at 7:00 PM
You wouldn't want to use both the completefunc and the promise. The completefunc is still there for backward compatibility, but promises (as it's clear you know) are the preferred approach.

M.
Feb 7, 2014 at 7:11 PM
Hi Marc,

You are correct that using only promises is the best way going forward. As such, when I was refactoring the bit of code in question to use promise chaining is when I came across this edge case. I took for granted that the completefunc would be included in the promise chain. Like I said, It's just something to be aware of.

Ricardo
Coordinator
Feb 7, 2014 at 8:32 PM
What happens with your approach if just the completefunc is used? (I can't test right now.) One of my goals is always to not change any existing behavior for backward compatibility (whether I like that old capability or not!).

M.
Feb 7, 2014 at 9:32 PM
All the code that has only a completefunc seems to be running as expected.

As I ponder this case further, I realize that nested ajax calls in the traditional completefunc would also get left off the deferred object. I'm starting to think that mixing the traditional ajax callback (event) and the new deferred object is just a bad idea.

It might be better to leave completefunc as is and just advise to use one or the other, but not both.

Ricardo
Feb 8, 2014 at 2:38 AM

Ricardo,
Your loosing me on what you actually want to do - and truthfull, I think SPService already does it. Your last comment - "realize that nested ajax calls in the traditional completefunc would also get left off the deferred object" - tells me you are doing something more complex than the simple example you listed above.

Re: Use of completefunc and Promise callbacks

Although you should not, you can actually use both a completefunc callback and define a promise callback - but you should realize how those are executed and obviously how to use them effectively. jQuery .ajax() method will do the following when it is done, and in this order:
  1. Resolve the ajax Deferred - which turn executes any of the promise callbacks
  2. If a 'complete' callback is defined - execute it
In my opinion, there is really one true use case where I may want to use both a completefunc callback and promise callbacks - to have the ability to change the output of .ajax() and thus control what the callbacks that are queued up see...

So here is an example that will show you this - both the order in with the callbacks are executed and how you could control the XML Document response for down-stream callbacks:
$().SPServices({
    operation: "GetList",
    listName: "Tasks",
    completefunc: function (xData, Status) {  
        console.log("completefunc()");
        console.log(xData.responseXML.paul); // notice I'm printing a property called "paul" of the XML Document returned by .ajajx()/SPServices
    }
})
.done(function(){
    console.log("done()");
})
.then(function (xml, Status) { 
    console.log("then()", xml, Status);
    xml.paul = "tavares"; // pass data to others in the chain via the .ajax() input params objects
})
.then(function(){
    console.log("then(2)");
})
.done(function(){
    console.log("done(1)");
})
.then(function(){
    console.log("then(3)");
});
The output of this call in the console would be:
done()
then() Document success
then(2)
done(1)
then(3)
completefunc()
tavares

Use of multiple SPServices calls with jQuery .when()

The promise returned by SPServices (which is the one returned by .ajax) can in fact be chained and queued up with $.when(). Here is an example:
$.when(
    // Call 1
    $().SPServices({
        operation: "GetList",
        listName: "Tasks",
        completefunc: function (xData, Status) {  
            console.log("completefunc()");
        }
    })
    .done(function(){
        console.log("done()");
    }),

    // Call 2
    $().SPServices({
        operation: "GetList",
        listName: "Tasks",
        completefunc: function (xData, Status) {  
            console.log("completefunc(2)");
        }
    })
    .done(function(){
        console.log("done(2)");
    })
)
.then(function(){

    console.log("ALL PROMISES DONE!!!!!");

});
Then output of this would be:
done()
completefunc()
done(2)
ALL PROMISES DONE!!!!!
completefunc(2)
This again shows that the deferred callbacks that are queued up will executed serially and in the order that they are added to the queue... In looking at the output you may question why 'complete(2)' executed after 'ALL PROMISES DONE!!! and not after "done(2)". That again is because once the second "done(2)" is executed, the .when() Deferred is resolved, and thus its callbacks are executed prior to givein control back to the last .ajax()...


So... :-)
With all that said and tested - can you post a code sample of something that is not working for you?
I'm really having a hard time understanding what is not working.

/Paul
Marked as answer by ricardo_cantu on 2/8/2014 at 10:07 AM
Feb 8, 2014 at 3:19 AM
Hi Paul,

Thank you for the in depth answer, I think everyone will appreciate such a thorough explanation of JQuery deferred object execution order.

To answer your question as to what was not working for me, it was simply the fact that I was expecting the following execution order.
completefunc()
undefined
done()
then() Document success
then(2)
done(1)
then(3)
It just wasn't obvious to me that completefunc() would execute after all promises were resolved.

And about my code. It is a bit more complex than my simple example. I have a function that builds up a promise object based on the needs of various custom JS components on a SP page. I do this because sometimes the components want information from the same lists and libraries, so instead of them calling SPServices directly they receive a promise and wait for it to resolve. Since I expected completefunc() to execute first, it made for some weird results.

Once again that you for the awesome examples.

Ricardo
Feb 8, 2014 at 3:47 PM
Ricardo,
No problem... I too build my services in a similar way: expose a .promise() to the caller handle the multiple ajax calls within the service, only resolving the .promise() when I have all needed data..

Paul.