I'm working with the last.fm API and queue.js to dynamically merge a user's Weekly Track List and Track Info datasets together. I'm running into a problem when my AJAX call for a certain song returns an error. My main question is, how do I skip these errors? Specifically, how do I alter Mike Bostock's taskThatSometimesFails function to continue onto the next d3.json call?
Here's my original code:
d3.json(top, function(json) {
weeklyChart = json.weeklytrackchart.track;
var q = queue()
.defer(d3.json, top);
weeklyChart.forEach(function(d) {
tracks = 'http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=...&artist='+ d.artist['#text'] + '&track=' + d.name + '&format=json';
q.defer(d3.json, tracks);
});
q.awaitAll(function(error) {
if (error) throw error;
console.log(arguments);
Here's Mike Bostock's suggested way of skipping over error/null responses:
queue(1)
.defer(ignoreError, taskThatSometimesFails)
.defer(ignoreError, taskThatSometimesFails)
.awaitAll(function(error, results) {
if (error) throw error; // never happens
console.log(results); // e.g., [0.23068457026965916, undefined]
});
function ignoreError(task, callback) {
task(function(error, result) {
return callback(null, result); // ignore error
});
}
function taskThatSometimesFails(callback) {
setTimeout(function() {
var x = Math.random();
if (x > .5) return callback(new Error("failed"));
callback(null, x);
}, 250);
}
I don't know node.js, and am pretty confused by how callback and setTimeout work. I tried adapting Mike's code to my situation, but I'm getting stuck on what to do with taskThatSometimesFails (where the magic is, I suppose). Here's my current, crappy adaptation:
d3.json(top, function(json) {
weeklyChart = json.weeklytrackchart.track;
var q = queue()
.defer(d3.json, top);
weeklyChart.forEach(function(d) {
tracks = 'http://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=...&artist='+ d.artist['#text'] + '&track=' + d.name + '&format=json';
q.defer(d3.json, tracks);
});
function ignoreError(task, callback) {
task(function(error, result) {
return callback(null, result);
});
}
function taskThatSometimesFails(callback) {
setTimeout(function() {
if (error) return callback(new Error("failed"));
return callback(null, tracks); //or maybe return d3.json(tracks)?
}, 1000);
}
q.awaitAll(function(error) {
if (error) throw error;
For now, any null responses are still breaking the whole queue. Any help, as always, appreciated. Thanks, guys.
Related
I've been playing with promises and trying to build some sort of progress notification.
The code is executing all functions in the right order, but the progress updates execute just before the resolve as opposed to when they actually happen.
Can anyone point out what I'm doing wrong?
function start(x) {
console.log("Start: " + x);
var promise = process(x);
console.log("promise returned");
promise.then(function(data) {
console.log("Completed: " + data);
}, function(data) {
console.log("Cancelled: " + data);
}, function(data) {
console.log("In Progress: " + data);
});
}
function process(x) {
var deferred = $.Deferred();
var promise = deferred.promise();
// process asynchronously
setTimeout(function() {
for (var i=0 ; i<x ; i++) {
sleep(1000);
deferred.notify(i);
}
if (x % 2 === 0) {
deferred.reject(x);
} else {
deferred.resolve(x);
}
}, 0);
return promise;
}
function sleep(sleepDuration) {
var now = new Date().getTime();
while(new Date().getTime() < now + sleepDuration){ /* do nothing */ }
}
start(3);
Fiddle here:
https://jsfiddle.net/n86mr9tL/
A delay timer implemented with while(), will "block" - ie hog the processor.
Blocking not only prevents other javascript from running but also inhibits reliable refreshing of the browser screen including the console. So whereas those deferred.notify(i) and console.log("In Progress: " + data) statements are firing, the console isn't refreshed until the processor becomes free to do so.
Unsurprisingly, the solution lies in not using while().
Fortunately, javascript imcludes two built-in methods window.setTimeout() and window.setInterval(), which are conceptually different from a while() idler but fulfil the same role .... without blocking.
window.setInterval(fn, t) fires function fn every t milliseconds,
window.setTimeout(fn, t) fires function fn once, after t milliseconds.
Both methods return an opaque reference, which allows them to be cancelled.
In the code below, start() is unmodified, process() is heavily modified and sleep() has disappeared.
process() now does the following :
creates a jQuery Deferred and returns a promise derived from it,
establises a setInterval() of 1000 milliseconds (1 second), whose function :
keeps count of how many times it has been called,
calls deferred.notify() every second until the counter i reaches the specified maximum x,
when the specified maximum is reached :
the interval, which would otherwise silently tick away ad infinitum, is cleared,
deferred.resolve() or deferred.reject() are called to settle the Deferred (and its promise),
function start(x) {
console.log("Start: " + x);
process(x).then(function(data) {
console.log("Completed: " + data);
}, function(data) {
console.log("Cancelled: " + data);
}, function(data) {
console.log("In Progress: " + data);
});
}
function process(x) {
return $.Deferred(function(dfd) {
var i = 1;
var intervalRef = setInterval(function() {
if(i < x) {
dfd.notify(i++);
} else {
clearInterval(intervalRef);
dfd[(x % 2 === 0)?'reject':'resolve'](x);
}
}, 1000);
}).promise();
}
console.clear();
start(3);
Updated fiddle
I'm using Parse to represent the state of a beer keg (among other things). I'd like to check the user's notifications, stored in a "Notifications" table, to see if they'd like to receive a notification when the keg is filled.
I have all of the logic for setting the user's notification settings as well as sending notifications in cloud/notifications.js. All of the logic for updating the keg is in cloud/beer.js. I created an exported function called "sendKegRefillNotification" which performs a query.find() on the Notifications table and gets called from beer.js.
The problem is that it doesn't seem to be executing query.find() when I call the function from beer.js, however when I call the same function from a job within notifications.js, it works just fine.
main.js:
require("cloud/beer.js");
require("cloud/notifications.js");
beer.js:
var notify = require("cloud/notifications.js");
var Keg = Parse.Object.extend("Keg");
var fillKeg = function(beerName) {
var promise = new Parse.Promise();
var keg = new Keg();
keg.set("beerName", beerName)
keg.set("kickedReports", []);
keg.save(null, { useMasterKey: true }).then(function(keg) {
console.log("Keg updated to " + beerName + ".");
promise.resolve(keg);
notify.sendKegRefillNotification(keg);
},
function(keg, error) {
promise.reject(error);
});
return promise;
}
Parse.Cloud.define("beerFillKeg", function(request, response) {
var beerName = request.params.name;
if (!beerName) {
response.error("No beer was specified.");
return;
}
if (!util.isUserAdmin(request.user)) {
response.error("User does not have permission to update the keg.");
return;
}
fillKeg(beerName).then(function(keg) {
kegResponse(keg).then(function(result) {
response.success(result);
});
},
function(error) {
response.error(error);
});
});
function kegResponse(keg) {
var promise = new Parse.Promise();
var result = {
id: keg.id,
beer: {
name: keg.get("beerName")
},
filled: keg.createdAt,
kickedReports: []
};
var kickedReports = keg.get("kickedReports");
if (!kickedReports || kickedReports.length == 0) {
promise.resolve(result);
} else {
util.findUsers(kickedReports).then(function(users) {
result.kickedReports = util.infoForUsers(users);
promise.resolve(result);
}, function(users, error) {
console.log(error);
promise.resolve(result);
});
}
return promise;
}
notifications.js:
var Keg = Parse.Object.extend("Keg");
var Notification = Parse.Object.extend("Notifications");
exports.sendKegRefillNotification = function(keg) {
var beerName = keg.get("beerName");
console.log("Sending notifications that keg is refilled to '" + beerName + "'.");
var promise = new Parse.Promise();
var query = new Parse.Query(Notification);
query.include("user");
query.equalTo("keg_filled", true);
query.find({ useMasterKey: true }).then(function(notifications) {
console.log("Found notifications!");
promise.resolve("Found notifications!");
},
function(notifications, error) {
console.error("No notifications");
console.error(error);
promise.reject(error);
});
return promise;
}
Parse.Cloud.job("beerSendRefillNotification", function(request, status) {
var query = new Parse.Query(Keg);
query.descending("createdAt");
query.first().then(function(keg) {
if (!keg) {
status.error("No keg");
return;
}
exports.sendKegRefillNotification(keg);
},
function(keg, error) {
response.error(error);
});
});
When I run the job "beerSendRefillNotification" from the Parse dashboard, I can tell that query.find() is getting called because it prints "Found notifications!":
E2015-02-23T06:59:49.006Z]v1564 Ran job beerSendRefillNotification with:
Input: {}
Result: success/error was not called
I2015-02-23T06:59:49.055Z]false
I2015-02-23T06:59:49.190Z]Sending notifications that keg is refilled to 'test'.
I2015-02-23T06:59:49.243Z]Found notifications!
However, when I call the cloud function "beerFillKeg", it isn't because it's not printing "Found notifications!" or "No notifications":
I2015-02-23T07:00:17.414Z]v1564 Ran cloud function beerFillKeg for user HKePOEWZvC with:
Input: {"name":"Duff"}
Result: {"beer":{"name":"Duff"},"filled":{"__type":"Date","iso":"2015-02-23T07:00:17.485Z"},"id":"olLXh0F54E","kickedReports":[]}
I2015-02-23T07:00:17.438Z]false
I2015-02-23T07:00:17.523Z]Keg updated to Duff.
I2015-02-23T07:00:17.525Z]Sending notifications that keg is refilled to 'Duff'.
I finally understand it. In sendKegRefillNotification, you're calling query.find({...}), then returning an object. That find is asynchronous, and you're doing nothing to wait for the result. I think you need to return the find function call, rather than an object you set within that method.
In other words, you're running along, leaving some async running code behind you.
Edit: I understand what you tried to do. It sort of makes sense. You defined a promise, and thought the caller would wait for the promise. The problem is, the promise is defined in an asynchronous block. It doesn't yet have any meaning at the moment the caller gets it.
It looks like Parse doesn't allow you to run a query from inside a callback from save(). When I moved "notify.sendKegRefillNotification(keg);" to outside of the callback, it worked.
var fillKeg = function(beerName) {
var promise = new Parse.Promise();
var keg = new Keg();
keg.set("beerName", beerName)
keg.set("kickedReports", []);
keg.save(null, { useMasterKey: true }).then(function(keg) {
console.log("Keg updated to " + beerName + ".");
console.log("Send notifications.");
promise.resolve(keg);
},
function(keg, error) {
promise.reject(error);
});
notify.sendKegRefillNotification(keg); // Now this works
return promise;
}
Can anyone shed some more light on why this worked?
I'm trying to use promise to get in promise2
But if I have an object Widgets with several elements in it...
Why can't I have been able to get my console.log's output
Parse.Cloud.define("extract", function(request, response) {
var user = request.params.user;
var promise = Parse.Promise.as();
[...]
}).then(function() {
return query.find().then(function(results) {
_.each(results, function(result) {
[...]
Widget.objectId = result.id;
Widgets[timestamp] = Widget;
});
return promise;
}).then(function(results) {
for (var key in Widgets) {
var Widget = Widgets[key];
var widget_data = Widgets[key].widget_data;
var promise2 = Parse.Promise.as();
promise2 = promise2.then(function() {
return Parse.Cloud.run('extractWidgetData', {
'widget_data': widget_data,
}).then(function(newresult) {
Widgets[key].data = newresult.data;
console.log('--------WHY NOT HERE ALL TIME ?--------');
});
});
return promise2;
}
}).then(function() {
response.success(Widgets);
},
function(error) {
response.error("Error: " + error.code + " " + error.message);
});
});
});
I'm becoming crazy to run this damn Code
EDIT : I finally followed Roamer's advices to implement something but I'm not sure if it's the good way to work with Promise in series...
Parse.Cloud.define("extract", function(request, response) {
var user = request.params.user;
var Widgets = {};
...
... .then(function() {
return query.find().then(function(results) {
return Parse.Promise.when(results.map(function(result) {
var Widget = ...;//some transform of `result`
Widget.id = ...;//some transform of `result`
var timestamp = createdAtDate.getTime();
...
return Parse.Cloud.run('extractData', {
'widget_data': Widget.widget_data,
}).then(function(newresult) {
Widget.stat = newresult.stats;
return Widget;//<<<<<<< important! This ensures that results.map() returns an array of promises, each of which delivers a Widget objects.
});
}));
}).then(function() {
var promisedWidget = Array.prototype.slice.apply(arguments);
return Parse.Promise.when(promisedWidget.map(function(Widget) {
return Parse.Cloud.run('getWineStats', {
'id': Widget.data.id
}).then(function(stat) {
Widget.stat = stat;
return Widget;
});
}));
}).then(function() {
var promisedWidget = Array.prototype.slice.apply(arguments);
_.each(promisedWidget, function(Widget) {
var createdAtObject = Widget.createdAt;
var strDate = createdAtObject.toString();
var createdAtDate = new Date(strDate);
timestamp = createdAtDate.getTime();
Widgets[timestamp] = Widget;
});
return Widgets;
}).then(function(Widgets) {
response.success(Widgets);
},
function(error) {
response.error("Error: " + error.code + " " + error.message);
});
});
});
First, I echo Bergi's comment on indentation/matching parenthesis.
But ignoring that for a moment, at the heart of the code you have return query.find().then(...).then(...).then(...) but the flow from the first .then() to the second is incorrect. Besides which, only two .then()s are necessary as the code in the first then is synchronous, so can be merged with the second.
Delete the two lines above for (var key in Widgets) { then at least Widgets will be available to be processed further.
Going slightly further, you should be able to do all the required processing of results in a single loop. There seems to be little pont in building Widgets with _.each(...) then looping through the resulting object with for (var key in Widgets) {...}.
In the single loop, you probably want a Parse.Promise.when(results.map(...)) pattern, each turn of the map returning a promise of a Widget. This way, you are passing the required data down the promise chain rather than building a Widgets object in an outer scope.
Do all this and you will end up with something like this :
Parse.Cloud.define("extract", function(request, response) {
var user = request.params.user;
...
... .then(function() {
return query.find().then(function(results) {
return Parse.Promise.when(results.map(function(result) {
var Widget = ...;//some transform of `result`
...
return Parse.Cloud.run('extractWidgetData', {
'widget_data': Widget.widget_data,
}).then(function(newresult) {
Widget.data = newresult.data;
return Widget;//<<<<<<< important! This ensures that results.map() returns an array of promises, each of which delivers a Widget objects.
});
}));
}).then(function() {
//Here, compose the required Widgets array from this function's arguments
var Widgets = Array.prototype.slice.apply(arguments);//Yay, we got Widgets
response.success(Widgets);
}, function(error) {
response.error("Error: " + error.code + " " + error.message);
});
});
});
I have a query, and they query may return many items.
I can go through all of them and destroy them.
The problem is since destroy is Async, the response.success(); part is executed before all the destroys are executed, so not all items are really deleted.
How can I make it wait until the loop is done and then only response.success();
Thanks.
garageQuery2.find({
success: function(results) {
alert("Successfully retrieved " + results.length + " garages to delete.");
// Do something with the returned Parse.Object values
for (var i = 0; i < results.length; i++) {
var object = results[i];
object.destroy({
success: function(myObject) {
// The object was deleted from the Parse Cloud.
},
error: function(myObject, error) {
// The delete failed.
// error is a Parse.Error with an error code and description.
}
});
}
response.success();
},
error: function(error) {
alert("Error: " + error.code + " " + error.message);
}
});
Try to work with Promises
This code is based on this: https://www.parse.com/docs/js_guide#promises-series
garageQuery2.find().then(function(results) {
// Create a trivial resolved promise as a base case.
var promiseSeries = Parse.Promise.as();
// the "_" is given by declaring "var _ = require('underscore');" on the top of your module. You'll use Underscore JS library, natively supported by parse.com
_.each(results, function(objToKill) {
// For each item, extend the promise with a function to delete it.
promiseSeries = promiseSeries.then(function() {
// Return a promise that will be resolved when the delete is finished.
return objToKill.destroy();
});
});
return promiseSeries;
}).then(function() {
// All items have been deleted, return to the client
response.success();
});
Hope it helps
I have 2 lists in my application and the user is supposed to drag and drop items from one list to another.
When the user drops an element from one of the lists to the other list a request has to be made to the server side code to update a field in the database (SelectedForDiscussion).
This is the code in my controller:
$scope.$watch("questionsDiscuss", function (value) {
var question = $.Enumerable.From($scope.questionsDiscuss).Where(function (item) { return !item.SelectedForDiscussion }).FirstOrDefault()
if (question != undefined) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, true)
.then(function (output) {
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) { return item.Id == output.data.questionId })
.FirstOrDefault();
var index = $.Enumerable.From($scope.questionsDiscuss).IndexOf(question);
if (question != undefined)
if (output.data.result != "success") {
$scope.questionsDiscuss.splice(index, 1);
$scope.questionsReceived.splice(0, 0, question);
}
else {
question.SelectedForDiscussion = true;
$scope.questionsDiscuss[index] = question;
}
});
}
else {
var question = $.Enumerable.From($scope.questionsReceived).Where(function (item) { return item.SelectedForDiscussion }).FirstOrDefault();
if (question != undefined) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, false)
.then(function (output) {
var question = $.Enumerable.From($scope.questionsReceived)
.Where(function (item) { return item.Id == output.data.questionId })
.FirstOrDefault();
var index = $.Enumerable.From($scope.questionsReceived).IndexOf(question);
if (question != undefined)
if (output.data.result != "success") {
$scope.questionsReceived.splice(index, 1);
$scope.questionsDiscuss.splice(0, 0, question);
}
else {
question.SelectedForDiscussion = false;
$scope.questionsReceived[index] = question;
}
});
}
}
}, true);
I have 4 javascript breakpoint placed at the following lines within Firebug:
2 of them at the following lines:
if (question != undefined) {
One at:
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
And the other at:
var question = $.Enumerable.From($scope.questionsReceived)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
The following happens:
The breakpoints at:
if (question != undefined) {
are always reached.
The breakpoint at
var question = $.Enumerable.From($scope.questionsDiscuss)
.Where(function (item) {
return item.Id == output.data.questionId
})
.FirstOrDefault();
is also reached.
The other is never reached.
Both responses are OK(response code 200).
Everything should work perfectly but the then clause in the second promise is never reached.
Can anyone tell me what I am doing wrong?
The serverside appplication is an ASP.NET MVC application written in C#.
Edit 1:
I figured out why this was happening and I have a work around for it. I am stil interested in an actual solution.
The problem is angularjs throws an error then swallows it when calling $http for the second time. The error is:
digest alredy in progress
I think this is because in my directive I have this code:
dndfunc = function (scope, element, attrs) {
// contains the args for this component
var args = attrs.dndBetweenList.split(',');
// contains the args for the target
var targetArgs = $('#' + args[1]).attr('dnd-between-list').split(',');
// variables used for dnd
var toUpdate;
var target;
var startIndex = -1;
// watch the model, so we always know what element
// is at a specific position
scope.$watch(args[0], function (value) {
toUpdate = value;
}, true);
// also watch for changes in the target list
scope.$watch(targetArgs[0], function (value) {
target = value;
}, true);
// use jquery to make the element sortable (dnd). This is called
// when the element is rendered
$(element[0]).sortable({
items: 'div',
start: function (event, ui) {
// on start we define where the item is dragged from
startIndex = ($(ui.item).index());
},
stop: function (event, ui) {
var newParent = ui.item[0].parentNode.id;
// on stop we determine the new index of the
// item and store it there
var newIndex = ($(ui.item).index());
var toMove = toUpdate[startIndex];
// we need to remove him from the configured model
toUpdate.splice(startIndex, 1);
if (newParent == args[1]) {
// and add it to the linked list
target.splice(newIndex, 0, toMove);
} else {
toUpdate.splice(newIndex, 0, toMove);
}
// we move items in the array, if we want
// to trigger an update in angular use $apply()
// since we're outside angulars lifecycle
scope.$apply(targetArgs[0]);
scope.$apply(args[0]);
},
connectWith: '#' + args[1]
})
}
And there are 2 calls to apply at the end which trigger a new digest cycle I think.
Anyway I fixed it by adding this call before the calls to apply:
if (scope.updateLists != undefined)
scope.updateLists();
And moved all the code from the watch into the updateLists function.
Also because people have mentioned the service as having something to do with it I am pasting the relevant code within it:
GetQuestionsReceived: function (eid, criteria, page, rows) {
var promise = this.GetQuestionsReceivedInternal(eid,criteria, page, rows).then(function (response) {
// The return value gets picked up by the then in the controller.
return response;
});
// Return the promise to the controller
return promise;
},
GetQuestionsReceivedInternal: function (eid, criteria, page, rows) {
return $http({ method: 'GET',
url: '../QuestionManagement/GetQuestions?eventId='+eid+'&page=1&rows=5&'+serialize(criteria)
}).
success(function (data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
results = data;
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
if (window.console && console.log) {
console.log("Could not obtain questions received. Error:" + data + "Status:" + status + "Headers:" + headers + "Config:" + config);
}
});
},
GetQuestionsDiscuss: function (eid,criteria, page, rows) {
var promise = this.GetQuestionsDiscussInternal(eid,criteria, page, rows).then(function (response) {
// The return value gets picked up by the then in the controller.
return response;
});
// Return the promise to the controller
return promise;
},
GetQuestionsDiscussInternal: function (eid,criteria, page, rows) {
return $http({ method: 'GET',
url: '../QuestionManagement/GetQuestions?eventId=' + eid + '&page=1&rows=5&' + serialize(criteria)
}).
success(function (data, status, headers, config) {
// this callback will be called asynchronously
// when the response is available
response = data;
}).
error(function (data, status, headers, config) {
// called asynchronously if an error occurs
// or server returns response with an error status.
if (window.console && console.log) {
console.log("Could not obtain questions received. Error:" + data + "Status:" + status + "Headers:" + headers + "Config:" + config);
}
});
},
You have two very similar blocks of code, which could be generalized and placed in a function wrapper, leaving behind a very simple calling function.
If you can get everything into that form, then I think you will find it easier to debug.
Here is an attempt to do so :
function updateSelectionStatus(qA, qB, bool) {
var en = $.Enumerable.From(qA);
var question = en.Where(function (item) {
return bool ? !item.SelectedForDiscussion : item.SelectedForDiscussion;
}).FirstOrDefault();
if(question) {
questionSelectionService.UpdateQuestionSelectionStatus(question.Id, bool).then(function (output) {
if (output.data.result == "success") {
question.SelectedForDiscussion = bool;
}
else {
qA.splice(en.IndexOf(question), 1);
qB.unshift(question);
}
});
}
return question;
}
$scope.$watch("questionsDiscuss", function (value) {
if (!updateSelectionStatus($scope.questionsDiscuss, $scope.questionsReceived, true) {
updateSelectionStatus($scope.questionsReceived, $scope.questionsDiscuss, false);
}
}, true);
I may have made some false assumptions and simplified too much (eg. purging the inner $.Enumerable.From, which appears to reselect the same question as the outer), so you may well need to rework my code.
I'm advocating a principle here, rather than offering a solution.