In my previous question I thought I'd got it sorted, but I've found an intermittent edge condition where the "then" part is being carried out before all the promises resolve in the Q.all call.
Simply, I have the following set up where a generic calculation is called multiple times with different parameters: (code simplified a bit to remove irrelevant code and actual parameter names)
var promiseArray = [];
promiseArray.push(genericCalc(param1, param2, param3));
promiseArray.push(genericCalc(param1, param2, param3));
promiseArray.push(genericCalc(param1, param2, param3));
var calcPromise = Q.all(promiseArray);
return calcPromise
.then(calcsDone)
.fail(calcsFailed);
function calcsDone(res) {
calcTotals();
setChart(selectedRow());
console.log("done recalculation routine");
}
function calcsFailed() {
logger.logError("Failure in Calculations", "", null, true);
}
genericCalc (with some irrelevant code removed)
var genericCalc = function (param1, param2, param3) {
//specific calculation is determined using parameters passed in and varies but is same structure for each as below
calcPromise = specificCalc(params...);
return calcPromise
.then(calcsDone)
.fail(calcsFailed);
function calcsDone(res) {
//some minor subtotalling in here using "res" and flag setting
return Q.resolve();
}
function calcsFailed() {
//handle error....
}
};
There are 3 or 4 different specific calculations and they all have roughly the same sort of layout:
function specificCalc1(params...) {
var promise = calcContext.ajaxCalc(params);
return promise
.then(calcsDone)
.fail(calcsFailed);
function calcsDone(res) {
rv(res);
console.log("done specific calc 1");
return Q.resolve();
}
function calcsFailed() {
logger.logError("Failure in specific calc 1", "", null, true);
return Q.resolve();
}
};
Now I know that this is not a great idea to push the result of the ajax calc into a return value, but at present I don't want to change this as there is just too much code change involved and I feel that, even if not the best methodology at present, this is a learning curve for me and I'll address that part once I have my head around this strange edge condition.
So... what throws me is that every now and again when I change some values on screen which trigger a recalculation, one of the "done" console log messages from one of the specific calcs appears AFTER the "done recalculation routine" one from the first section!
I thought it was being caused by a poorly-constructed promise leading to the function being executed immediately, but the REALLY weird thing is that when I put a debug stop in the server code for that calculation, all works correctly and the client browser is paused until the server code is continued and then the client side debugs show that the next points are being hit.
9 times out of 10 it all works perfectly. On the 10th one, I see "done recalculation routine" in the console immediately followed by "done specific calc 2" and this causes the totals to be wrong.
I cannot see how this can be happening. I've artificially put time delay loops in all the specific calc functions to make them take several seconds and never once do I see the out of sequence debug messages, but when there is no artificial slowdown in place, I see it 1 in 10 times approximately.
Please would someone try and put me out of my misery here... I just want to know I can rely on the "Q.all" call working in the first block of code, such that when it hits "calcsDone" it has ALWAYS done the generic and specific calcs!
Edit 1:
To explain the "rv" or returnvalue a bit. In "genericCalc", I declare an observable:
var res = ko.observable(); //holds returned results
And as part of the call to "specificCalc" I pass that return value in, for example:
calcPromise = specificCalc1(isPackaging, res, thisArticle);
In the specificCalc I just put the ajax result into the observable, so it is available in "calcsDone" in "genericCalc" and, once all calcs are completed from the "Q.all()" function, the calculation routines can tot up the individual results from each specific Calc.
Edit 2
The console log, when it goes wrong is:
done specificCalc1
done specificCalc2, isPackaging=true
Done specificCalc3, redraw = false
done calctotals
chart replotted
done recalculation routine
done specificCalc2, isPackaging=false
...you can see the "done specificCalc2" after "done recalculation routine" there.
Edit 3
I reduced the promiseArray to just one item to check, using the parameters for what seems to be the troublesome call (specificCalc2):
var promiseArray = [];
promiseArray.push(genericCalc(param1, param2, param3));
And then I put a stop point in the server code.
The result is that the stop in the server code happens and the console already says "done" so it's a problem with the promise construction after all and was somehow being masked by one of the other calls being done. Once I release the stop in server code, I get my console messages from the ajax call function AND the generic Calc function saying "done".
So, it appears to me as if the problem is in the main Q.all call from what I can tell, as that doesn't wait for the generic calc function being completed before it carries on with its "calcsDone" function.
I just tried returning the genericCalc promise:
return genericCalc("eol_", true, false, false)
//return calcPromise
.then(calcsDone)
.fail(calcsFailed);
...and it instantly fails with "cannot call method "then" of undefined, thus confirming my problem is in the generic calc function, so off to look at that now.
Edit 4
Narrowed the problem down to a further function call within genericCalc. As part of the calculation, this calls a function to remove the impact value as stands before the calculation is done. When the calculation returns it then adds the result back in to the overall amount.
If I "return Q.resolve()" from genericCalc on the line before I do:
impactRemove(currentPrefix, currentArticle); //remove impacts for this prefix
then the promise works, if I do it on the line after, it fails. So for some reason calling that function seems to resolve the promise. Investigating further now.
Edit 5
So the problem is caused when I call a standard function midway through the genericCalc routine. The logic behind this is:
Change on browser form retriggers calculation
Function is called that sets up array of promises (several of which call the same function with different parameters)
Inside that generic function (genericCalc) I call a standard non-promise function that removes the current calculation totals from the project total
Calculation is complete
Standard non-promise function called to add results of calculation back to project total
GenericCalc returns promise to main function
Overall totals updated with latest project totals, graphics are updated
What actually seems to happen is that when I call the standard javascript functions with genericCalc, they execute immediately, therefore resolving the promise and although the ajax call is still done, the Q.all call does not wait as it believes the promise is resolved as "genericCalc" is returning "undefined" and not a promise.
At this point, Bergi is screaming at me about my hideous anti-pattern noob coding and I tend to agree with him. Trouble is, I'd like to get it working this way so I have something to test against when I finally adapt it to work correctly.
So... if I have two functions called from within "genericCalc" like so:
impactRemove(currentPrefix, currentArticle); //remove impacts for this prefix
and
impactAdd(currentPrefix, currentArticle); //addimpacts for this prefix
Which are basically like this:
function impactAdd(prefix, prjArt) {
if (!prjArt) {return} //just get out as there's nothing to calculate
factorArray.forEach(function (f) {
var orig_projValue = pGlobalVar.editProject()[prefix + f]();
var new_projArtValue = prjArt[prefix + f](); //must be set first!
pGlobalVar.editProject()[prefix + f](orig_projValue + new_projArtValue); //add new value back in to project value
});
}
...then how do I call these "midpoint" functions within the promise of genericCalc so that I Can still return a promise when a) impactRemove has been completed, b) the remote ajax call has been completed and c) the impactAdd function has been completed?
Do I need to set up code within genericCalc to do something like:
impactRemove(params...).then(<ajax function that returns new calculation results>).then(impactAdd(params))
...or will (params) after my functions automatically invoke those functions, thus resolving the promise too early? This is all so confusing compared to what I'm used to!
Edit6
All genericCalc does is this:
var genericCalc = function (param1, param2, param3) {
//specific calculation is determined using parameters passed in and varies but is same structure for each as below
calcPromise = specificCalc(params...);
impactRemove(currentPrefix, currentArticle); //remove impacts for this prefix
return calcPromise
.then(calcsDone)
.fail(calcsFailed);
function calcsDone(res) {
//some minor subtotalling in here using "res" and flag setting
impactAdd(currentPrefix, currentArticle); //addimpacts for this prefix
return Q.resolve();
}
function calcsFailed() {
//handle error....
}
};
"specificCalc" returns a promise - that one works as I've checked the contents of the promise at a breakpoint. If I remove the calls to "impactRemove" and "impactAdd" above, then "genericCalc" also works. It is those calls that cause the problem.
This is why I think I need something like:
impactRemove(params)
.then(return calcPromise(params)
.then(impactAdd(params);
...but neither impactAdd nor impactRemove do anything asynchronously and I'm also not sure how I can set this up as I need to use params and yet you said those params will mean the functions are immediately invoked...
Edit 7
So, as mentioned in the lengthy comments section, this is being caused by a "forEach" loop in genericCalc:
var genericCalc = function (calcPrefix, other params...) {
gcCount++;
console.log("generic calc starting for: " + calcPrefix + ", count=" + gcCount);
pGlobalVar.projectIsCalculating(true); //controls spinner gif
var res_Array = ko.observable(); //holds returned results
var _prjArticleArray = []; //create local array to use
if (currentArticle == true) {
_prjArticleArray.push(pGlobalVar.editProjectArticle());
} else {
projectResults().projectArticles().forEach(function (pa) {
_prjArticleArray.push(pa);
});
};
_prjArticleArray.forEach(function (thisArticle) {
var calcPromise;
switch (calcPrefix) {
case "eol_":
calcPromise = Q.all([calcEOL(isPackaging, res_Array, thisArticle)]);
break;
case "use_":
calcPromise = Q.all([calcUse(isPackaging, res_Array, thisArticle)]);
break;
case "air_":
calcPromise = Q.all([calcFlight(isPackaging, res_Array, thisArticle)]);
break;
default:
break;
}
impactRemove(calcPrefix, thisArticle); //remove impacts for this prefix
return calcPromise
.then(calcsDone)
.fail(calcsFailed);
function calcsDone(args) {
//do some calcs and totalling based on returned results
impactAdd(calcPrefix, thisArticle); //add this article impact into project total
console.log("generic calc done for: " + calcPrefix + ", count=" + gcCount);
calcTotals(); //accmulates individual results into the "total_xxx" used on the results table and in the chart
setChart(selectedRow());
pGlobalVar.projectIsCalculating(false);
}
function calcsFailed() {
logger.logError("Failure in " + calcPrefix + "calculation", "", null, true);
impactAdd(calcPrefix); //will add back in results as they were at start of calc
pGlobalVar.projectIsCalculating(false);
}
});
};
The only reason I've posted it in all its ugly glory is to point out that all works perfectly IF I remove the "forEach" loop and just run this for one article. Why on earth would the forEach loop make it die a horrible death ?
I think you just want to exchange the order of the specificCalc() and impactRemove() calls. Even if the first is asynchronous, it will start doing its task right now - only the results will arrive in the next turn. If you want to chain anything after a synchronous task, just put it on the next line ("semicolon monad").
Also, if impactRemove does assign to your global (!) variable calcPromise, it might not be a promise any more and throw when calling a .then() method on it. What you want seems to be
function genericCalc(param1, param2, param3) {
impactRemove(currentPrefix, currentArticle); //remove impacts for this prefix
return specificCalc(params...).finally(function() {
impactAdd(currentPrefix, currentArticle); // add impacts for this prefix
}).then(function calcsDone(res) {
// some minor subtotalling in here using "res" and flag setting
return subtotal;
}, function calcsFailed() {
// handle error....
});
}
Why on earth would the forEach loop make it die a horrible death?
Because you're not returning a promise. A forEach loop has its own callback, from which you can return, but from the genericCalc function nothing is returned. Q.all will not fret about that, but just take the undefined as a value. However, the async action is to be started and you get your callbacks, but Q.all won't wait for it, because it does not know of it.
The solution is a quite simple change and has already been explained here.
Related
Newbie to JS. I have this function. First, it checks for data in cache. If it's not there, it calls backend for this data and saves it to cache.
function doSomething() {
return getCachedData()
.catch(function() {
return getData()
.then(saveDataToCache);
});
}
I need to test how many times cache and backend are called on the first and the second time the method doSomething is executed. Here is my test:
it('should test', function() {
spyOn(this.service, 'doSomething').andCallThrough();
spyOn(this.service, 'getCachedData').andCallThrough();
spyOn(this.service, 'getData').andCallThrough();
this.service.doSomething();
this.$rootScope.$apply();
expect(this.service.doSomething.callCount).toBe(1);
expect(this.service.getCachedData.callCount).toBe(1);
expect(this.service.getData.callCount).toBe(1);
this.service.doSomething();
this.$rootScope.$apply();
expect(this.service.doSomething.callCount).toBe(2);
expect(this.service.getCachedData.callCount).toBe(2);
expect(this.service.getData.callCount).toBe(1);
});
However, I am getting an error saying that call count for getCachedData and getData is always 0, both the first and the second time.
Changing this.$rootScope.$apply(); to this.$rootScope.$digest(); doesn't improve anything. I also tested it manually and everything seems to be working fine.
I also noticed that if I change the function doSomething as below then the counts for getCachedData are 2 & 2, which is correct.
function doSomething() {
this.getCachedData();
return getCachedData()
.catch(function() {
return getData()
.then(saveDataToCache);
});
}
create an object. for example:
system_metrics ={
cache_calls:0,
backend_calls:0,
reset_cache:function(){this.cache_calls=0;},
reset_backend_calls:function(){this.backend_calls=0;},
add_cache_call:function(){this.cache_calls++;}
add_backend_call:function(){this.cache_calls++;}
}
in
function getCachedData(){
//add this line
system_metrics.add_cache_call();
}
in
function getData(){
//add this line
system_metrics.add_backend_call();
}
I need to test how many times cache and backend are called on the first and the second time the method doSomething is executed
in
function doSomething(){
if(system_metrics.cache_calls === 1){
//the first time the method doSomething is executed
}
if(system_metrics.cache_calls === 2){
//the second time the method doSomething is executed
}
if(system_metrics.backend_calls === 1){
//the first time the method doSomething is executed
}
if(system_metrics.backend_calls === 2){
//the second time the method doSomething is executed
}
}
at anytime during execution.. lets say at the end of the day
you can now check system_metrics.cache_calls to give you total number of cache calls for the day.. thats if you never resetted the cache calls
at anytime during execution you can clear the number of cache calls with system_metrics.reset_cache_calls
if your question is to check how many times cache was called
when doSomething runs for the first time or second time.
if your question is to check how many times backend was called
when doSomething runs for the first time or second time.
add the following to your system_metrics
do_something_calls
add_do_something_call
reset_do_something_call
& add add_do_something_call to your do_something function
i think you get the picture
with this approach you can track any metric that you want anytime that you want to
I'm using the npm library jsdiff, which has a function that determines the difference between two strings. This is a synchronous function, but given two large, very different strings, it will take extremely long periods of time to compute.
diff = jsdiff.diffWords(article[revision_comparison.field], content[revision_comparison.comparison]);
This function is called in a stack that handles an request through Express. How can I, for the sake of the user, make the experience more bearable? I think my two options are:
Cancelling the synchronous function somehow.
Cancelling the user request somehow. (But would this keep the function still running?)
Edit: I should note that given two very large and different strings, I want a different logic to take place in the code. Therefore, simply waiting for the process to finish is unnecessary and cumbersome on the load - I definitely don't want it to run for any long period of time.
fork a child process for that specific task, you can even create a queu to limit the number of child process that can be running in a given moment.
Here you have a basic example of a worker that sends the original express req and res to a child that performs heavy sync. operations without blocking the main (master) thread, and once it has finished returns back to the master the outcome.
Worker (Fork Example) :
process.on('message', function(req,res) {
/* > Your jsdiff logic goes here */
//change this for your heavy synchronous :
var input = req.params.input;
var outcome = false;
if(input=='testlongerstring'){outcome = true;}
// Pass results back to parent process :
process.send(req,res,outcome);
});
And from your Master :
var cp = require('child_process');
var child = cp.fork(__dirname+'/worker.js');
child.on('message', function(req,res,outcome) {
// Receive results from child process
console.log('received: ' + outcome);
res.send(outcome); // end response with data
});
You can perfectly send some work to the child along with the req and res like this (from the Master): (imagine app = express)
app.get('/stringCheck/:input',function(req,res){
child.send(req,res);
});
I found this on jsdiff's repository:
All methods above which accept the optional callback method will run in sync mode when that parameter is omitted and in async mode when supplied. This allows for larger diffs without blocking the event loop. This may be passed either directly as the final parameter or as the callback field in the options object.
This means that you should be able to add a callback as the last parameter, making the function asynchronous. It will look something like this:
jsdiff.diffWords(article[x], content[y], function(err, diff) {
//add whatever you need
});
Now, you have several choices:
Return directly to the user and keep the function running in the background.
Set a 2 second timeout (or whatever limit fits your application) using setTimeout as outlined in this
answer.
If you go with option 2, your code should look something like this
jsdiff.diffWords(article[x], content[y], function(err, diff) {
//add whatever you need
return callback(err, diff);
});
//if this was called, it means that the above operation took more than 2000ms (2 seconds)
setTimeout(function() { return callback(); }, 2000);
While waiting for the back end devs to implement a "cancel all" feature, which cancels all tasks tracked by the back end, I am attempting to makeshift it by cancelling each individual task. The cancel REST service accepts an ID in the form of a data object {transferID: someID}.
I use a FOR loop to iterate over an array of IDs that I have stored elsewhere. Anticipating that people MAY end up with dozens or hundreds of tasks, I wanted to implement a small delay that will theoretically not overflow the number of HTTP requests the browser can handle and will also reduce a blast of load on the back end CPU. Here is some code with comments for the purpose of this discussion:
ta.api.cancel = function (taskArray, successCallback, errorCallback) {
// taskArray is ["task1","task2"]
// this is just the latest attempt. I had an attempt where I didn't bother
// with this and the results were the same. I THOUGHT there was a "back image"
// type issue so I tried to instantiate $.ajax into two different variables.
// It is not a back image issue, though, but one to do with setTimeout.
ta.xhrObjs = ta.xhrObjs || {};
for (var i = 0; i < taskArray.length; i++) {
console.log(taskArray); // confirm that both task1 and task2 are there.
var theID = taskArray[i];
var id = {transferID: theID}; // convert to the format understood by REST
console.log(id); // I see "task1" and then "task2" consecutively... odd,
// because I expect to see the "inside the setTimeout" logging line next
setTimeout(function () {
console.log('inside the setTimeout, my id is: ')
console.log(id.transferID);
// "inside the setTimeout, my id is: task2" twice consecutively! Y NO task1?
ta.xhrObjs[theID] = doCancel(id);
}, 20 * i);
}
function doCancel(id) {
// a $.Ajax call for "task2" twice, instead of "task1" then "task2" 20ms
// later. No point debugging the Ajax (though for the record, cache is
// false!) because the problem is already seen in the 'setTimeout' and
// fixed by not setting a timeout.
}
}
Thing is: I know setTimeout makes the containing function execute asynchronously. If I take out the timeout, and just call doCancel in the iterator, it will call it on task1 and then task2. But although it makes the call async, I don't understand why it just does task2 twice. Can't wrap my head around it.
I am looking for a way to get the iterator to make the Ajax calls with a 20ms delay. But I need it to call on both! Anybody see a glaring error that I can fix, or know of a technique?
You must wrap your function setTimeout and pass the id variable into it, like this:
(function(myId, i) {
setTimeout(function () {
console.log('inside the setTimeout, my id is: ', myId);
}, 20 * i);
}(theId, i));
This pattern does not create a unique variable1 for each instance of the loop as one might expect.
function () {
for (var i = 0; i < length; i++) {
var variable1;
}
}
In javascript variables are "hoisted". To quote Mozilla:
"Because variable declarations (and declarations in general) are
processed before any code is executed, declaring a variable anywhere
in the code is equivalent to declaring it at the top."
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var
So it should be re-written as:
function () {
var variable1;
for (var i = 0; i < length; i++) {
}
}
What this means is that after the loop has finished, any asynchronous callbacks that reference this variable will see the last value of the loop.
I have the following setup, and I'm curious if this is the correct way to do it. It works correctly, but I'm just making sure that I'm doing it right, or if there is a better way to accomplish the same task.
//custom ajax wrapper
var pageLoadPromise = ajax({
url: //call to webmethod
});
//this is the way I have been doing it
pageLoadPromise.done(cB1)
.done(cB2)
.done(cB3)
.done(cB4)
.done(function(){cB5(args);});
//this function requires that cB1 has been completed
//I tried this and it worked as well
pageLoadPromise.done(cB1,cB2,cB3,cB4)
.done(function(){cB5(agrs)});
Doing it both ways works, but like I said, am I wondering if it is this the correct way to accomplish this?
UPDATE:
I have made a small adjustment to my code, specifically for cB1 and the callback to cB5
pageloadPromise.done(
function(data){
cB1(data).done(function(){
cB5(args);
});
},cB2,cB3,cB4
);
function cB1(data){
var cB1Promise = $.Deferred();
...
cB1Promise.resolve();
return cB1Promise;
}
As pointed out by #Bergi, regardless of how you add the callbacks, they are all run in the order they are attached using done. So, promise.done(cb1, cb2, cb3, cb4).done(cb5) is the same as promise.done(cb1).done(cb2).done(cb3).done(cb4).done(cb5).
To make sure cb5 runs after cb1 use:
promise.done( function(data) {cb1(data).done(cb5);}, cb2, cb3, cb4);
Remove data if you don't need it.
I played around with the scenarios in http://jsbin.com/moqiko/4/edit?js,console,output
Doing it both ways works
Yes, they are pretty much equivalent (except for the .done(function(){cB5}); which doesn't work).
I am wondering if it is this the correct way to accomplish this?
Use the one you like better. This is more a design question than one of "correctness". However, both ways look quite odd in my eyes, and I've seen lots of promise code. I would recommend two different structures, depending on how your app is structured:
You use the pageLoadPromise as a global cache for your initial data. It is then consumed in very different places, possibly at different times, for multiple different things (or maybe even repeatedly for the same thing). Then use pageLoadPromise repeatedly in each module:
var pageLoadPromise = ajax({url: …}); // initialisation
pageLoadPromise.done(cB1); // somewhere
…
pageLoadPromise.done(cB2); // somewhere else
…
pageLoadPromise.done(cB3); // other place or time
…
You use the pageLoadPromise in one place only, and want to basically do one thing when it's loaded, except that it is structured in multiple subtasks; and each needs only a part of, not the whole structure. Then use a single callback only:
ajax({url: …}).then(function(data) {
cb1(data.d.cb1data);
cb2(data.d.cb2data);
cb3(data.d.cb3data);
cb4(data.d.cb4data);
cb5(data.d.cb5data, some_additional_data);
});
I have made a small adjustment to my code, specifically for cB1 and the callback to cB5
You should not make cb1 return a promise when it doesn't do anything asynchronous. Don't modify it. If you want to express explicitly that cb5 needs to be executed with the result of cb1, then you should use .then for chaining:
var pageLoadPromise = ajax({url: …}); // initialisation
var cB1promise = pageLoadPromise.then(cB1);
cB1promise.done(cb5); // does get called with the return value of cB1
or
ajax({url: …}).then(function(data) {
var res1 = cb1(data.d.cb1data);
…
cb5(data.d.cb5data, some_additional_data, res1);
});
Update. Thanks to #Bergi who pointed out that jQuery's done() returns in fact the same promise. I've updated the answer based on that.
If cB2,cB3,cB4 are not interconnected and all of them process the same data from the ajax call, then you can add them to the same promise (pageloadPromise).
With the above assumption in mind, your second version of code can be simplified without involving a new promise to be created in cB1(), and without having to go one extra indentation level:
pageloadPromise.then(cB1).done(cB5);
pageloadPromise.done(cB2, cB3, cB4);
function cB1(data){
// ...
//data2 would be the argument value passed when resolving
// your original cB1Promise
return data2;
}
What happens here is that the .then() call creates a new promise that gets resolved with whatever data cB1 returns, allowing cB5 to receive that data without creating an extra callback and without involving another promise (as we already have one in hand).
However if cB1 needs another ajax then your original implementation of cB1 would be more appropriate (the callback scheduling remains the same though).
And one final note, I didn't noticed any failure handlers, in case the ajax call fails.
So I've got this basket functionality where you enter, say, an author's name and it lists the available books. You select what you want and then you can click to select another author. When you do, you get a list looking roughly like this:
Stephen King
The Stand [remove]
The Talisman [remove]
Pet Sematary [remove]
Terry Pratchett
Mort [remove]
Guards Guards [remove]
In the example above, the Stephen King books have been stored in Session, the Terry Pratchett books have not. If I click the remove button on a Pratchett book, some jquery will just hide those. If I remove a Stephen King book, an ajax request is triggered to remove it from Session before jquery hides it.
So my javascript looks something like this:
$('.book-remove').click(removeBook);
function deleteFromBasket(bookId) {
var ajaxArgs = { BookId : bookId };
return $.ajax({
// blah blah, no problems here
success: function(e) {
hideRow();
}
});
}
function hideRow(bookId) {
$('.book-id-' + bookId).hide();
}
function removeBook(e) {
e.preventDefault();
if ($(this).hasClass('needs-ajax-call') {
var promise = deleteFromBasket($(this).prop("id").replace("book-id-", ""));
// this is the problem. how do I wait here for the ajax to complete?
}
else
hideRow();
// if i put promise.done(hideRow) up there, it still runs this too soon.
doMoreStuff();
}
You can structure all your code (both code paths with and without ajax) to use promises. The code that doesn't have to do an ajax call can just start with a promise that is already resolved and both code paths will execute the same sequence (one will be faster to execute because it doesn't have to wait for the ajax call), but both will execute things in the same order:
Any stuff you want done AFTER the ajax call simply has to be moved into the promise.then() handler:
function removeBook(e) {
e.preventDefault();
var promise;
if ($(this).hasClass('needs-ajax-call') {
promise = deleteFromBasket($(this).prop("id").replace("book-id-", ""));
} else {
// get an already resolved promise since no ajax call is required
promise = $.Deferred().resolve();
}
// you can return this promise if you want the calling code
// to be able to know when you're done with everything
return promise.then(function() {
hideRow(bookId); // assumes you've calculated a bookId somewhere
doOtherStuff();
});
}
This has the advantage that the lions share of your code is in one code path rather than two separate code paths and it solves your issue because all the code in the .then() handler will not execute until after the ajax call is done.
Here's an answer to the question plus some suggestions for tidier DOM/javascript.
First, let's make sure that :
the "session" entries are in static container(s) (eg a <div>) with class="sessionResultsWrapper"
the "non session" entries are in static container(s) (eg a <div>) with class="otherResultsWrapper"
each entry is an element (eg an <li>) with class="entry" and data-bookID="xxxx"
Now you are in a better position :
to select elements without the need for cumbersome id parsing
to establish a click handler that will fire on all existing "remove" buttons and any that are added later to the static containers.
$(".sessionResultsWrapper, .otherResultsWrapper").on('click', '.book-remove', function(e) {
e.preventDefault();
var $this = $(this),
promise;
if ($this.parents(".sessionResultsWrapper").length) {
promise = deleteFromBasket($this.closest('entry').data('bookID'));
} else {
promise = $.when();//this is the most compact syntax for a resolved promise
});
// At this point, you have a promise regardless of whether a "session" or "non-session" button was clicked.
// The only difference is that a "non-session" promise will be already resolved, while a "session" promise will be resolved later.
// Because of the way promises work, we can simply chain a .then(fn), which will fire immediately (non session, Terry Pratchett title) or later (when ajax successfully returns, Stephen King title).
promise.then(function() {
$this.closest(".entry").hide();//or, if the entry will never be re-shown then .remove()
doMoreStuff();
});
});
You could add the doMoreStuff as a callback to the promise.
Something like this
function removeBook(e) {
e.preventDefault();
if ($(this).hasClass('needs-ajax-call') {
var promise = deleteFromBasket($(this).prop("id").replace("book-id-", ""));
promise.done(doMoreStuff);
}
else{
hideRow();
doMoreStuff();
}
}