I need to pull data from a series of .csv files off the server. I am converting the csvs into arrays and I am trying to keep them all in an object. The ajax requests are all successful, but for some reason only the data from the last request ends up in the object. Here is my code:
var populate_chart_data = function(){
"use strict";
var genders = ["Boys","Girls"];
var charts = {
WHO: ["HCFA", "IWFA", "LFA", "WFA", "WFL"],
CDC: ["BMIAGE", "HCA", "IWFA", "LFA", "SFA", "WFA", "WFL", "WFS"]
};
var fileName, fileString;
var chart_data = {};
for (var i=0; i < genders.length; i++){
for (var item in charts){
if (charts.hasOwnProperty(item)){
for (var j=0; j<charts[item].length; j++) {
fileName = genders[i] + '_' + item + '_' + charts[item][j];
fileString = pathString + fileName + '.csv';
$.ajax(fileString, {
success: function(data) {
chart_data[fileName] = csvToArray(data);
},
error: function() {
console.log("Failed to retrieve csv");
},
timeout: 300000
});
}
}
}
}
return chart_data;
};
var chart_data = populate_chart_data();
The console in Firebug shows every ajax request successful, but when I step through the loops, my chart_data object is empty until the final loop. This is my first foray into ajax. Is it a timing issue?
There are two things you need to consider here:
The AJAX calls are asynchronous, this means you callback will only be called as soon as you receive the data. Meanwhile your loop keeps going and queueing new requests.
Since you're loop is going on, the value of filename will change before your callback is executed.
So you need to do two things:
Push the requests into an array and only return when the array completes
Create a closure so your filename doesn't change
.
var chart_data = [];
var requests = [];
for (var j=0; j<charts[item].length; j++) {
fileName = genders[i] + '_' + item + '_' + charts[item][j];
fileString = pathString + fileName + '.csv';
var onSuccess = (function(filenameinclosure){ // closure for your filename
return function(data){
chart_data[filenameinclosure] = csvToArray(data);
};
})(fileName);
requests.push( // saving requests
$.ajax(fileString, {
success: onSuccess,
error: function() {
console.log("Failed to retrieve csv");
},
timeout: 300000
})
);
}
$.when.apply(undefined, requests).done(function () {
// chart_data is filled up
});
I'm surprised that any data ends up in the object. The thing about ajax is that you can't depend on ever knowing when the request will complete (or if it even will complete). Therefore any work that depends on the retrieved data must be done in the ajax callbacks. You could so something like this:
var requests = [];
var chart_data = {};
/* snip */
requests.push($.ajax(fileString, {
/* snip */
$.when.apply(undefined, requests).done(function () {
//chart_data should be full
});
Related
What I am trying to do here are:
Remove all contents in a class first, because every day the events.json file will be updated. I have my first question here: is there a better way to remove all contents from a database class on Parse?
Then I will send a request to get the events.json and store "name" and "id" of the result into a 2D array.
Then I will send multiple requests to get json files of each "name" and "id" pairs.
Finally, I will store the event detail into database. (one event per row) But now my code will terminate before it downloaded the json files.
Code:
function newLst(results) {
var event = Parse.Object.extend("event");
for (var i = 0; i < results.length; i++){
Parse.Cloud.httpRequest({
url: 'https://api.example.com/events/'+ results[i].name +'/'+ results[i].id +'.json',
success: function(newLst) {
var newJson = JSON.parse(newLst.text);
var newEvent = new event();
newEvent.set("eventId",newJson.data.id);
newEvent.set("eventName",newJson.data.title);
newEvent.save(null, {
success: function(newEvent) {
alert('New object created with objectId: ' + newEvent.id);
},
error: function(newEvent, error) {
alert('Failed to create new object, with error code: ' + error.message);
}
});
},
error: function(newLst) {
}
});
}
};
Parse.Cloud.job("getevent", function(request, status) {
var event = Parse.Object.extend("event");
var query = new Parse.Query(event);
query.notEqualTo("objectId", "lol");
query.limit(1000);
query.find({
success: function(results) {
for (var i = 0; i < results.length; i++) {
var myObject = results[i];
myObject.destroy({
success: function(myObject) {
},
error: function(myObject, error) {
}
});
}
},
error: function(error) {
alert("Error: " + error.code + " " + error.message);
}
});
var params = { url: 'https://api.example.com/events.json'};
Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var jsonobj = JSON.parse(httpResponse.text);
for (var i = 0; i < jsonobj.data.length; i++) {
var tmp2D = {"name":"id"}
tmp2D.name = [jsonobj.data[i].name];
tmp2D.id = [jsonobj.data[i].id];
results.push(tmp2D);
}
newLst(results);
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
I think my original answer is correct as a standalone. Rather than make it unreadable with the additional code, here it is made very specific to your edit.
The key is to eliminate passed callback functions. Everything below uses promises. Another key idea is decompose the activities into logical chunks.
A couple of caveats: (1) There's a lot of code there, and the chances that either your code is mistaken or mine is are still high, but this should communicate the gist of a better design. (2) We're doing enough work in these functions that we might bump into a parse-imposed timeout. Start out by testing all this with small counts.
Start with your question about destroying all instances of class...
// return a promise to destroy all instances of the "event" class
function destroyEvents() {
// is your event class really named with lowercase? uppercase is conventional
var query = new Parse.Query("event");
query.notEqualTo("objectId", "lol"); // doing this because the OP code did it. not sure why
query.limit(1000);
return query.find().then(function(results) {
return Parse.Object.destroyAll(results);
});
}
Next, get remote events and format them as simple JSON. See the comment. I'm pretty sure your idea of a "2D array" was ill-advised, but I may be misunderstanding your data...
// return a promise to fetch remote events and format them as an array of objects
//
// note - this differs from the OP data. this will evaluate to:
// [ { "name":"someName0", id:"someId0" }, { "name":"someName1", id:"someId1" }, ...]
//
// original code was producing:
// [ { "name":["someName0"], id:["someId0"] }, { "name":["someName1"], id:["someId1"] }, ...]
//
function fetchRemoteEvents() {
var params = { url: 'https://api.example.com/events.json'};
return Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var remoteEvents = JSON.parse(httpResponse.text).data;
for (var i = 0; i < remoteEvents.length; i++) {
var remoteEvent = { "name": remoteEvents[i].name, "id": remoteEvents[i].id };
results.push(remoteEvent);
}
return results;
});
}
Please double check all of my work above regarding the format (e.g. response.text, JSON.parse().data, etc).
Its too easy to get confused when you mix callbacks and promises, and even worse when you're generating promises in a loop. Here again, we break out a simple operation, to create a single parse.com object based on one of the single remote events we got in the function above...
// return a promise to create a new native event based on a remoteEvent
function nativeEventFromRemoteEvent(remoteEvent) {
var url = 'https://api.example.com/events/'+ remoteEvent.name +'/'+ remoteEvent.id +'.json';
return Parse.Cloud.httpRequest({ url:url }).then(function(response) {
var eventDetail = JSON.parse(response.text).data;
var Event = Parse.Object.extend("event");
var event = new Event();
event.set("eventId", eventDetail.id);
event.set("eventName", eventDetail.title);
return event.save();
});
}
Finally, we can bring it together in a job that is simple to read, certain to do things in the desired order, and certain to call success() when (and only when) it finishes successfully...
// the parse job removes all events, fetches remote data that describe events
// then builds events from those descriptions
Parse.Cloud.job("getevent", function(request, status) {
destroyEvents().then(function() {
return fetchRemoteEvents();
}).then(function(remoteEvents) {
var newEventPromises = [];
for (var i = 0; i < remoteEvents.length; i++) {
var remoteEvent = remoteEvents[i];
newEventPromises.push(nativeEventFromRemoteEvent(remoteEvent));
}
return Parse.Promise.when(newEventPromises);
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
The posted code does just one http request so there's no need for an array of promises or the invocation of Promise.when(). The rest of what might be happening is obscured by mixing the callback parameters to httpRequest with the promises and the assignment inside the push.
Here's a clarified rewrite:
Parse.Cloud.job("getevent", function(request, status) {
var promises = [];
var params = { url: 'https://api.example.com'};
Parse.Cloud.httpRequest(params).then(function(httpResponse) {
var results = [];
var jsonobj = JSON.parse(httpResponse.text);
for (var i = 0; i < jsonobj.data.length; i++) {
// some code
}
}).then(function() {
status.success("run job");
}, function(error) {
status.error(error);
});
});
But there's a very strong caveat here: this works only if ("// some code") that appears in your original post doesn't itself try to do any asynch work, database or otherwise.
Lets say you do need to do asynch work in that loop. Move that work to a promise-returning function collect those in an array, and then use Promise.when(). e.g....
// return a promise to look up some object, change it and save it...
function findChangeSave(someJSON) {
var query = new Parse.Query("SomeClass");
query.equalTo("someAttribute", someJSON.lookupAttribute);
return query.first().then(function(object) {
object.set("someOtherAttribute", someJSON.otherAttribute);
return object.save();
});
}
Then, in your loop...
var jsonobj = JSON.parse(httpResponse.text);
var promises = [];
for (var i = 0; i < jsonobj.data.length; i++) {
// some code, which is really:
var someJSON = jsonobj.data[i];
promises.push(findChangeSave(someJSON));
}
return Parse.Promise.when(promises);
I'm not well-versed in JS but have to write some Cloud Code.
I have a few httpRequest operations returning JSON that then create Parse objects. After all those operations complete I want to run a saveAll.
Here's a shortened version of the function for readability. There are *** pointing to the async operations that need to be dependent on each other:
Parse.Cloud.job("importPlaylists", function(request, status) {
// Array to store new objects
var playlistsToSave = [];
// Iterate the countries (Sheets)
for (var j = 0; j < urls.length; j++) {
// *** -- 1. HERE IS WHERE I LOOP THROUGH HTTPREQUESTS, GET JSON, AND CREATE A NUMBER OF PARSE OBJECTS
// Cloud function - http request
Parse.Cloud.httpRequest({
url: url
}).then(function(httpResponse) {
// Parse response
var json = JSON.parse(httpResponse.buffer);
// Create playlist objects
for (var i = 0; i < json.feed.entry.length; i++) {
// Create Playlist objcts from json
var Playlist = Parse.Object.extend("Playlist");
var playlist = new Playlist();
// ...
// Add to caching array
playlistsToSave.push(playlist);
}
},function(httpResponse) {
// error
console.error('Request failed with response code ' + httpResponse.status);
status.error("Scheduled messages error: " + error);
});
}
// *** -- 2. THIS IS THE SAVEALL OPERATION THAT NEEDS TO BE DEPENDENT ON ALL OF THOSE HTTPRESPONSES COMPLETING
// Parse - Save objects
Parse.Object.saveAll(playlistsToSave, {
success: function(saveList) {
status.success("Objects created successfully.");
},
error: function(error) {
status.error("Unable to save objects.");
}
});
});
And here's the full code:
Parse.Cloud.job("importPlaylists", function(request, status) {
// ID of the Google Spreadsheet
var spreadsheetID = "someId";
// List of country codes and their Google Sheets Ids
var territories = {
"us" : "od6",
"gb" : "olbnsti",
"it" : "oa6haa5",
"es" : "oxibv7k",
"fr" : "obisdv5",
"nl" : "ohrgz0b",
"de" : "ocqrxlj",
"pl" : "oi0umg5"
}
// Array to store new objects
var playlistsToSave = [];
// Iterate the countries (Sheets)
for (var j = 0; j < territories.length; j++) {
var countryCode = territories[j];
var sheetID = territories[countryCode];
var url = "https://spreadsheets.google.com/feeds/list/" + spreadsheetID + "/" + sheetID + "/public/values?alt=json"; // Make sure it is public or set to Anyone with link can view
// *** - 1. This loops through each Google Sheet and creates Parse objects from the JSON
// Cloud function - http request
Parse.Cloud.httpRequest({
url: url
}).then(function(httpResponse) {
// Parse response
var json = JSON.parse(httpResponse.buffer);
// Create playlist objects
for (var i = 0; i < json.feed.entry.length; i++) {
var each = json.feed.entry[i];
// TODO: This needs error handling - if "gsx$" doesn't exist will crash
var name = each["gsx$name"]["$t"];
var playlistid = each["gsx$playlistid"]["$t"];
var description = each["gsx$description"]["$t"];
var imageurl = each["gsx$imageurl"]["$t"];
var categories = each["gsx$categories"]["$t"].split(','); // json returns a string from Sheet - split on commas
// Create Parse objects
var Playlist = Parse.Object.extend("Playlist");
var playlist = new Playlist();
playlist.set("name", name);
playlist.set("playlistId", playlistid);
playlist.set("description", description);
playlist.set("imageUrl", imageurl);
playlist.set("categories", categories);
playlist.add("territories", countryCode);
// Add to caching array
playlistsToSave.push(playlist);
}
},function(httpResponse) {
// error
console.error('Request failed with response code ' + httpResponse.status);
status.error("Scheduled messages error: " + error);
});
}
// *** - 2. This is the saveAll operation that needs to be dependent on all the httpRequests completing
// Parse - Save objects
Parse.Object.saveAll(playlistsToSave, {
success: function(saveList) {
status.success("Objects created successfully.");
},
error: function(error) {
status.error("Unable to save objects.");
}
});
});
Use this async.js's each. It will allow you to make sure all httpRequests are completed.
I have a problem concerning the execution of ajax requests in a for loop. I already searched the web for it and found some solutions which I already implemented to avoid running the request synchronously. Unfortunately these solutions don't provide information how to ensure, that the success block gets called in the correct order aswell.
This is my code:
for (var i = 0; i < array.length; i++) {
(function(index) {
var path = array[index].split(";")[1];
var selectedRevision = array[index].split(";")[0];
$.ajax({
url: 'svntojson.jsp?revHistoryForPath=' + path,
dataType:'text',
success:function(data){
console.log(index);
var $li = $("<li/>").text(path).addClass("ui-corner-all")
.prepend("<div class='handle'><span class='ui-icon ui-icon-carat-2-n-s'></span></div>")
.append('<button class="delete"></button>')
.append('<select class="revHistoryOptions" style="float:right;margin-right:5px;">' + data.trim() + '</select>');
$("#list").append($li);
$("#list").sortable('refresh');
$('.revHistoryOptions').eq(index).children('option[value=' + selectedRevision + ']').attr('selected', 'selected');
}
});
})(i);
}
However the order of indices can change because the one ajax request succeeds earlier. This wouldn't be a problem but I am appending some list elements in the success block and I need the exact order.
So my question is how to ensure that the success block of my ajax request will be called in the order of the for loop indices from 0 to n-1.
WRONG DESIGN : If you want a complete synchronized behavior and not involving any user interaction between iterations, you can avoid
looping and use single ajax request.
You can use a single ajax when you want it completely synchronized:
$.ajax({
url: 'svntojson.jsp?inputArray=' + array,
dataType: 'json',//Note I changed this to json to receive the array on outputs
success: function (data) {
var resArray = data;
for (var index = 0; index < resArray.length; index++) {
var res = resArray[index];
var $li = $("<li/>").text(res.path).addClass("ui-corner-all")
.prepend("<div class='handle'><span class='ui-icon ui-icon-carat-2-n-s'></span></div>")
.append('<button class="delete"></button>')
.append('<select class="revHistoryOptions" style="float:right;margin-right:5px;">' + res.data + '</select>');
$("#list").append($li);
$("#list").sortable('refresh');
$('.revHistoryOptions').eq(index).children('option[value=' + res.selectedRevision + ']').attr('selected', 'selected');
}
}
});
on the server:
process the input array and generate output for each element of array. (Psuedocode as following):
var resArray = new Array(inputArr.length);
for (var i = 0; i < inputArr.length; i++) {
var res = new res();
res.path = inputArr[i].split(";")[1];
res.selectedRevision = inputArr[i].split(";")[0];
resArray.push(res);
}
return resArray;
Have been looking into getting callbacks for ajax() responses using $.when I'm still unsure how this works fully but this is what I would like the below to do.
When a user adds a town and country per line, it goes to the url in the .ajax() I get a response and it pushes the array to be usable outside of the .each() loop.
At the moment you will see inside here at jsbin that when the button is pressed firstly the response in console.log is [] then when I press it again the addresses show up. then a 3rd press will add the addresses again which shouldn't happen.
jQuery
var addresses,town;
var arrayLocation = [];
$('button').click(function(){
addresses = function() {
deferred = new $.Deferred();
var arrayOfLines = $('#gps').val().split('\n');
$.each(arrayOfLines, function(index, item) {
town = item.split(',');
$.ajax({
url: 'http://maps.googleapis.com/maps/api/geocode/json?address='+town[0]+'&sensor=false',
dataType: 'json',
success: function (data) {
add = data.results[0].address_components[0].long_name;
lat = data.results[0].geometry.location.lat;
lng = data.results[0].geometry.location.lng;
arrayLocation.push("['"+add+"', "+lat+", "+lng+"]");
console.log("['"+add+"', "+lat+", "+lng+"]");
}
});
});
return arrayLocation;
};
$.when(addresses()).then(function(arrayLocation){
console.log(arrayLocation);
});
});
You are not using $.when correctly. The main problem is that the addresses function returns a bare array. It's true that this array will be populated in the future when an async operation (the set of AJAX calls) completes, but from the point of view of addresses's caller this is impossible to know. Therefore the caller has absolutely no chance of reacting to the operation being completed.
What you would normally do is return the return value of $.ajax to the caller, somewhat like this:
addresses = function() {
return $.ajax({ ... });
};
The caller could then do
$.when(addresses()).then(function(result) { ... });
In this particular example however this is not directly possible because there are multiple AJAX calls being made, so you need some way of "combining" all of them into one package. There are multiple ways to do this, so there is also a matter of what you prefer here.
One solution would be to use an array of AJAX promises:
$('button').click(function(){
var arrayLocation = [];
addresses = function() {
var promises = [];
var arrayOfLines = $('#gps').val().split('\n');
$.each(arrayOfLines, function(index, item) {
town = item.split(',');
promises.push($.ajax({
url: 'http://maps.googleapis.com/...',
dataType: 'json',
success: function (data) {
add = data.results[0].address_components[0].long_name;
lat = data.results[0].geometry.location.lat;
lng = data.results[0].geometry.location.lng;
arrayLocation.push("['"+add+"', "+lat+", "+lng+"]");
console.log("['"+add+"', "+lat+", "+lng+"]");
}
}));
});
return promises;
};
$.when.apply($, addresses()).then(function(){
console.log(arrayLocation);
});
});
There are a couple points of note here:
Returning an array of independent promises means that you cannot feed them to $.when directly because that function is designed to accept multiple individual promises instead of an array; you need to use apply to compensate.
I have moved the declaration of arrayLocation inside the click event handler so that it gets reset each time you click the button. The problem of results being added again after each click was due to this array not being reset.
The final handler does not accept any arguments. That's because the arguments that will be passed are the jqXHR objects representing the individual AJAX requests, which is not really useful. Instead of this it captures arrayLocation by closure, since you know independently that the results will be stored there.
Slightly different approach without a shared global variables
$('button').click(function () {
var addresses = function () {
var arrayOfLines = $('#gps').val().split('\n'),
arrayLocation = [];
$.each(arrayOfLines, function (index, item) {
var town = item.split(',');
var xhr = $.ajax({
url: 'http://maps.googleapis.com/maps/api/geocode/json?address=' + $.trim(town[0]) + '&sensor=false',
dataType: 'json'
});
arrayLocation.push(xhr);
});
return $.when.apply($, arrayLocation).then(function () {
return $.map(arguments, function (args) {
if (!$.isArray(args[0].results) || args[0].results.length == 0) {
return undefined;
}
var data = args[0].results[0];
var location = data.geometry.location;
var add = data.address_components[0].long_name;
var lat = location.lat;
var lng = lng;
return "['" + add + "', " + lat + ", " + lng + "]";
});
});
return arrayLocation;
};
addresses().done(function (arrayLocation) {
console.log(arrayLocation)
})
});
Demo: Fiddle
I have javascript/jquery code which fetches info and updates it into the database with a mixture of while/for loops. While fetching, I have a div which shows a current progress log of whats going on. In Firefox, as the script is running it updates the div at the same time as it should. In Google Chrome, it runs the entire loop, holding back the log, and only outputs it until the script is finished running. Anyone have any idea why this is happening?
Here is my code:
$(document).ready(function() {
add_text("test");
var array_length = num_sets;
for(var i = 0; i < array_length; i = i + 1) {
var setId = sets[i]['id'];
var setName = sets[i]['name'];
var setLinkName = sets[i]['link'];
var setNumCards = sets[i]['num_cards'];
add_text("Beginning to fetch set \"" + setName + "\"");
add_text("Found " + setNumCards + " total cards.");
while(ii < setNumCards) {
var card_name = sets[i]['cards'][ii]['name'];
var card_link = sets[i]['cards'][ii]['link'];
add_text("Fetching card " + sets[i]['cards'][ii]['name']);
fetch_card(sets[i]['cards'][ii]['link'], setId);
}
}
});
add_text function:
function add_text(text) {
$("#status_text").append("<br />" + text);
}
fetch_card function:
function fetch_card(card_link, set_id)
{
$.ajax({
url: "fetch_card.php?link=" + card_link + "&set_id=" + set_id,
context: document.body,
async: false,
success: function(){
ii = ii + 1;
}
});
}
You are using synchronous ajax calls (which are generally not very desirable). The browser can block all activity until that ajax call completes. Whether or not the browser updates the screen during a synchronous ajax call is up to the browser.
Your code would be much better if it was rewritten to use asychronous ajax only. It takes a little more work to structure your code properly to work with asynchronous ajax calls, but the browser remains completely responsive during the asynchronous ajax calls.
I'm not entirely sure how you were using the ii variable in your original implementation (as it wasn't declared or initialized in the code you included), but this is the general structure you could use. It uses the traditional for loop to collect all the data you wanted in an array, then calls the ajax function one a time on that data. It isn't clear to me how you're actually doing anything with the returned ajax info, but perhaps that just isn't something you included here:
$(document).ready(function() {
add_text("test");
var array_length = num_sets;
var fetchData = [];
var fetchIndex = 0;
for(var i = 0; i < array_length; i++) {
var setId = sets[i]['id'];
var setName = sets[i]['name'];
var setLinkName = sets[i]['link'];
var setNumCards = sets[i]['num_cards'];
add_text("Beginning to fetch set \"" + setName + "\"");
add_text("Found " + setNumCards + " total cards.");
for (var ii = 0; ii < setNumCards; ii++) {
var card_name = sets[i]['cards'][ii]['name'];
var card_link = sets[i]['cards'][ii]['link'];
add_text("Fetching card " + sets[i]['cards'][ii]['name']);
fetchData.push({link: sets[i]['cards'][ii]['link'], id: setId});
}
}
function next() {
if (fetchIndex < fetchData.length) {
fetch_card(fetchData[fetchIndex].link, fetchData[fetchIndex].id, next);
fetchIndex++;
}
}
function fetch_card(card_link, set_id, successFn) {
$.ajax({
url: "fetch_card.php?link=" + card_link + "&set_id=" + set_id,
context: document.body,
async: true,
success: successFn
});
}
next();
});