Async post - handling a reply - javascript

I am trying to upload a list of images. I have the images stored in an array (called images).
I have the previews displayed on the screen.
What I want to do is upload them sequentially, and as they complete their upload, I want to set a flag. When this flag is set (thanks to the power of Knockout), the image disappears from the list.
However, I think due to the async nature of the post command .. I'm not achieving the desired results.
Below is what I am trying to do:
for(var i = 0; i < self.images().length; i++) {
var photo = self.images()[i];
var thisItem = photo;
var object = JSON.stringify({
Image: thisItem.Image,
AlbumID: albumId,
Filesize: thisItem.Filesize,
Filetype: thisItem.Filetype,
Description: thisItem.Description,
UniqueID: thisItem.UniqueID
});
var uri = "/api/Photo/Upload";
$.post({
url: uri,
contentType: "application/json"
}, object).done(function(data) {
if(data.IsSuccess) {
photo.Status(1);
}
}).fail(function() {
// Handle here
}).always(function() {
remainingImages = remainingImages - 1;
if(remainingImages == 0) self.isUploading(false);
});
}
self.isUploading(false);
But I think what's happening is that the for loop ends before all the posts have received a reply. Because online one image is removed.
I tried with a async: false ajax post, but that locked up the screen and then they all disappeared.
I thought the 'done' method would only execute once the post is completed, but I think the whole method just ends once the post commands have been sent, and then I never get the done.
How can I achieve what I'm trying to do... Set each image's status once the post gets a reply?

Your first problem is that you are losing the reference you think you have to each photo object because the loop finishes before your AJAX calls return, so that when they do return, photo is a reference to the last item in self.images().
What we need to do to solve this is to create a new scope for each iteration of the loop and each of those scopes will have its own reference to a particular photo. JavaScript has function scopes, so we can achieve our goal by passing each photo to a function. I will use an Immediately Invoked Function Expression (IIFE) as an example:
for (var i = 0; i < self.images().length; i++) {
var photo = self.images()[i];
(function (thisItem) {
/* Everything else from within your for loop goes here. */
/* Note: the done handler must reference `thisItem`, not `photo`. */
})(photo);
}
Note that you should remove self.isUploading(false); from the last line. This should not be set to false until all of the POST requests have returned.
I have created a functioning fiddle that you can see here.
However, this solution will not perform the POST requests "sequentially". I am not sure why you would want to wait for one POST to return before sending the next as this will only increase the time the user must wait. But for the sake of completeness, I will explain how to do it.
To fire a POST after the previous POST returns you will need to remove the for loop. You will need a way to call the next POST in the always handler of the previous POST. This is a good candidate for a recursive function.
In my solution, I use an index to track which item from images was last POSTed. I recursively call the function to perform the POST on the next item in images until we have POSTed all items in images. The following code replaces the for loop:
(function postNextImage (index) {
var photo = self.images()[i];
var thisItem = photo;
var object = JSON.stringify({
Image: thisItem.Image,
AlbumID: albumId,
Filesize: thisItem.Filesize,
Filetype: thisItem.Filetype,
Description: thisItem.Description,
UniqueID: thisItem.UniqueID
});
var uri = "/api/Photo/Upload";
$.post({
url: uri,
contentType: "application/json"
}, object)
.done(function (data) {
if(data.IsSuccess) {
thisItem.Status(1);
}
})
.fail(function () {
// Handle here
})
.always(function () {
if (index < (self.images().length - 1)) {
index += 1;
postNextImage(index);
} else {
self.isUploading(false);
}
});
})(0);
I have created a fiddle of this solution also, and it can be found here.

Related

Twitch TV JSON API Issue

So,I am trying to use the twitch API:
https://codepen.io/sterg/pen/yJmzrN
If you check my codepen page you'll see that each time I refresh the page the status order changes and I can't figure out why is this happening.
Here is my javascript:
$(document).ready(function(){
var ur="";
var tw=["freecodecamp","nightblue3","imaqtpie","bunnyfufuu","mushisgosu","tsm_dyrus","esl_sc2"];
var j=0;
for(var i=0;i<tw.length;i++){
ur="https://api.twitch.tv/kraken/streams/"+tw[i];
$.getJSON(ur,function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> "+tw[j]+"<p>"+""+"</p></li>");
if(json.stream==null){
$(".stat").append("<li>"+"Offline"+"</li>");
}
else{
$(".stat").append("<li>"+json.stream.game+"</li>");
}
j++;
})
}
});
$.getJSON() works asynchronously. The JSON won't be returned until the results come back. The API can return in different orders than the requests were made, so you have to handle this.
One way to do this is use the promise API, along with $.when() to bundle up all requests as one big promise, which will succeed or fail as one whole block. This also ensures that the response data is returned to your code in the expected order.
Try this:
var channelIds = ['freecodecamp', 'nightblue3', 'imaqtpie', 'bunnyfufuu', 'mushisgosu', 'tsm_dyrus', 'esl_sc2'];
$(function () {
$.when.apply(
$,
$.map(channelIds, function (channelId) {
return $.getJSON(
'https://api.twitch.tv/kraken/streams/' + encodeURIComponent(channelId)
).then(function (res) {
return {
channelId: channelId,
stream: res.stream
}
});
})
).then(function () {
console.log(arguments);
var $playersBody = $('table.players tbody');
$.each(arguments, function (index, data) {
$playersBody.append(
$('<tr>').append([
$('<td>'),
$('<td>').append(
$('<a>')
.text(data.channelId)
.attr('href', 'https://www.twitch.tv/' + encodeURIComponent(data.channelId))
),
$('<td>').text(data.stream ? data.stream.game : 'Offline')
])
)
})
})
});
https://codepen.io/anon/pen/KrOxwo
Here, I'm using $.when.apply() to use $.when with an array, rather than list of parameters. Next, I'm using $.map() to convert the array of channel IDs into an array of promises for each ID. After that, I have a simple helper function with handles the normal response (res), pulls out the relevant stream data, while attaching the channelId for use later on. (Without this, we would have to go back to the original array to get the ID. You can do this, but in my opinion, that isn't the best practice. I'd much prefer to keep the data with the response so that later refactoring is less likely to break something. This is a matter of preference.)
Next, I have a .then() handler which takes all of the data and loops through them. This data is returned as arguments to the function, so I simply use $.each() to iterate over each argument rather than having to name them out.
I made some changes in how I'm handling the HTML as well. You'll note that I'm using $.text() and $.attr() to set the dynamic values. This ensures that your HTML is valid (as you're not really using HTML for the dynamic bit at all). Otherwise, someone might have the username of <script src="somethingEvil.js"></script> and it'd run on your page. This avoids that problem entirely.
It looks like you're appending the "Display Name" in the same order every time you refresh, by using the j counter variable.
However, you're appending the "Status" as each request returns. Since these HTTP requests are asynchronous, the order in which they are appended to the document will vary each time you reload the page.
If you want the statuses to remain in the same order (matching the order of the Display Names), you'll need to store the response data from each API call as they return, and order it yourself before appending it to the body.
At first, I changed the last else condition (the one that prints out the streamed game) as $(".stat").append("<li>"+jtw[j]+": "+json.stream.game+"</li>"); - it was identical in meaning to what you tried to achieve, yet produced the same error.
There's a discrepancy in the list you've created and the data you receive. They are not directly associated.
It is a preferred way to use $(".stat").append("<li>"+json.stream._links.self+": "+json.stream.game+"</li>");, you may even get the name of the user with regex or substr in the worst case.
As long as you don't run separate loops for uploading the columns "DisplayName" and "Status", you might even be able to separate them, in case you do not desire to write them into the same line, as my example does.
Whatever way you're choosing, in the end, the problem is that the "Status" column's order of uploading is not identical to the one you're doing in "Status Name".
This code will not preserve the order, but will preserve which array entry is being processed
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
$(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
if (json.stream == null) {
$(".stat").append("<li>" + "Offline" + "</li>");
} else {
$(".stat").append("<li>" + json.stream.game + "</li>");
}
})
}(i));
}
});
This code will preserve the order fully - the layout needs tweaking though
$(document).ready(function() {
var ur = "";
var tw = ["freecodecamp", "nightblue3", "imaqtpie", "bunnyfufuu", "mushisgosu", "tsm_dyrus", "esl_sc2"];
for (var i = 0; i < tw.length; i++) {
ur = "https://api.twitch.tv/kraken/streams/" + tw[i];
(function(j) {
var name = $(".name").append("<li> " + tw[j] + "<p>" + "" + "</p></li>");
var stat = $(".stat").append("<li></li>")[0].lastElementChild;
console.log(stat);
$.getJSON(ur, function(json) {
$(".tst").append(JSON.stringify(json));
if (json.stream == null) {
$(stat).text("Offline");
} else {
$(stat).text(json.stream.game);
}
}).then(function(e) {
console.log(e);
}, function(e) {
console.error(e);
});
}(i));
}
});

Using for loop to create subclass objects with Parse

I am tryng to create posts using a for loop, but when i look at Parse database only the last object of my array get's stored. this is the code i wrote.
var Reggione = Parse.Object.extend("Reggione");
var creaReggione = new Reggione();
var selectobject = $('#searcharea')[0];
for (var i = 2; i < selectobject.length; i++) {
creaReggione.set("name", selectobject.options[i].text);
creaReggione.save();
Thanks, Bye.
Do this by creating an array of new objects, then save them together...
var newObjects = [];
for (var i = 2; i < selectobject.length; i++) {
creaReggione.set("name", selectobject.options[i].text);
newObjects.push(creaReggione);
// ...
}
Parse.Object.saveAll(newObjects);
Remember, if you want something to happen after saveAll completes (like call response.success() if you're in cloud code), then you should use that promise as follows...
Parse.Object.saveAll(newObjects).then(function(result) {
response.success(result);
}, function(error) {
response.error(error);
});
In extension to danhs answer, the reason this does not work is because only one transaction can happen at a time from the JS client to Parse.
Therefore in your loop the first call to .save() is made and the object is saved to Parse at some rate (asynchrounously), in that time the loop continues to run and skips over your other save calls, these objects are NOT queued to be saved. As Danh pointed out, you must use Parse's batch operations to save multiple objects to the server in one go, to do this you can:
var newObjects = [];
for (var i = 2; i < selectobject.length; i++) {
creaReggione.set("name", selectobject.options[i].text);
newObjects.push(creaReggione);
// ...
}
Parse.Object.saveAll(newObjects);
Hope this helps, I'd also recommend taking a look at Parse's callback functions on the save method to get more details on what happened (you can check for errors and success callbacks here to make debugging a little easier)
An example of this would be to extend the previous call with:
Parse.Object.saveAll(newObjects, {
success: function(messages) {
console.log("The objects were successfully saved...")
},
error: function(error) {
console.log("An error occurred when saving the messages array: %s", error.message)
}
})
I hope this is of some help to you

Trying to call a function inside a for loop in javascript

Ill post the code in a second. I am making a call to a service to pull data back. I get the data in an array and each one needs to make another call to another service. so I set my code up like so:
Services.getHelo({
assetSurfaceId: $scope.assetSurfaceId
}).then(function (resp) {
delete resp["$promise"];
delete resp["$resolved"];
$scope.entity.helo = resp;
for (var i = 0; i < $scope.entity.helo.length; i++) {
heloCall($scope.entity.helo, i);
initHelo($scope.entity.helo, i);
}
});
After I delete the promise and $resolved I start my for loop to call to my other functions. the initHelo function just builds a list for me and gets the vars ready for the next call into it. the heloCall makes the other calls i need.
Here is the helocall code:
var heloCall = function (r, i) {
jQuery("#helo").mask("Loading Surface Asset Helos...");
Services.getStatus({
toEntityId: r[i].assetHeloId,
toEntityTypeId: widget_consts.ASSET_HELICOPTER
}).then(function (s) {
delete s["$promise"];
delete s["$resolved"];
$scope.entity.helo[i].status = [];
$scope.entity.helo[i].status = s;
if (s[0].statusLkupShortDesc === "PMC" || s[0].statusLkupShortDesc === "NMC") {
Services.getReason({
toEntityId: s[0].statusId
}).then(function (reason) {
delete reason["$promise"];
delete reason["$resolved"];
if (reason) {
$scope.entity.helo[i].status.reason = [];
$scope.entity.helo[i].status.reason = reason;
initHelo($scope.entity.helo, i);
}
});
Services.getComment({
toEntityId: r[i].assetHeloId
}).then(function (info) {
delete info["$promise"];
delete info["$resolved"];
if (info) {
$scope.entity.helo[i].status.remark = {};
$scope.entity.helo[i].status.remark = info;
initHelo($scope.entity.helo, i);
}
});
}
initHelo($scope.entity.helo, i);
jQuery("#helo").unmask();
});
};
I do a check after the status call is made to make sure it is a status that Would have reasons with it and if it is i make the calls i need.
My issue is this only works when it wants to. I am not sure what i screwed up. I have been trying to get it to work right all day.
It has worked many times for me but I need it to work all the time.
Is there a better way to do this?
I actually have another call in this same file that is set up the same way and works each time with no issues
Any advice would be greatly appreciated
I figured it out. I needed to run a check to make sure that the length of the array returned from the service was not 0. it would cause issues depending on witch call was returned first. after I put this check it in runs fine.

Calling Parse.Cloud.httpRequest inside another httpRequest

I'm trying to call an API to retrieve a list of data, this data will help me get image source. So for each data entry, I try to call the image url and do some image processing using Parse Image. The problem is, the inner httpRequest has never been triggered... I don't know what is the reason... As Parse.com is not that popular, I could hardly find any solution for such a case...
Here's my code:
Parse.Cloud.httpRequest({
url: api_url,
method: 'POST'
}).then(function(data){
data = JSON.parse(data.text);
var entries = data.entries;
for (var i = 0; i < 100; i++) {
entry = entries[i][1];
var image_url = composeImgURL(entry);
Parse.Cloud.httpRequest({
url: image_url,
success: function(httpResponse){
var image = new Image();
image.setData(httpResponse.buffer);
var size = Math.min(image.width(), image.height());
if(size > 300){
var ratio = 300 / size.toFixed(2);
image.scale({
ratio: ratio.toFixed(2)
});
}
image.setFormat("JPEG");
var base64 = image.data().toString("base64");
var cropped = new Parse.File("thumbnail.jpg", { base64: base64 });
cropped.save();
entries[i][1]['url'] = cropped.url;
},
error: function(httpResponse){
console.log(httpResponse);
}
});
}
return entries;
}).then(function(entries){
response.success(entries);
});
Your Current Code
Your code will reach the return entries statement before any of your 100 http requests have completed. As soon as this occurs the line response.success(entries); will be called, and your cloud function will exit.
Parallel Promises
For this situation, what you want to use are parallel promises. The docs are here:
https://parse.com/docs/js_guide#promises-parallel
There is a relevant question/answer on Parse answers here: https://www.parse.com/questions/parallel-promises-with-parsepromisewhen
Future Problems
The above will work for simple functionality, however you are doing 2 more things inside your for loop which will require more time
cropped.save(); you should wait for this function to return
Cropping 100 images inside a cloud function probably won't work. A cloud function only runs for approximately 15 seconds. If crops take too long, some will occur and some won't. You could do something such as queuing images for cropping and performing those crops within a scheduled Job.

Variable scope. Use a closure?

I'm trying to grab all the URLs of my Facebook photos.
I first load the "albums" array with the album id's.
Then I loop through the albums and load the "pictures" array with the photos URLs.
(I see this in Chrome's JS debugger).
But when the code gets to the last statement ("return pictures"), "pictures" is empty.
How should I fix this?
I sense that I should use a closure, but not entirely sure how to best do that.
Thanks.
function getMyPhotos() {
FB.api('/me/albums', function(response) {
var data = response.data;
var albums = [];
var link;
var pictures = [];
// get selected albums id's
$.each(data, function(key, value) {
if ((value.name == 'Wall Photos')) {
albums.push(value.id);
}
});
console.log('albums');
console.log(albums);
// get the photos from those albums
$.each(albums, function(key, value) {
FB.api('/' + value + '/photos', function(resp) {
$.each(resp.data, function(k, val) {
link = val.images[3].source;
pictures.push(link);
});
});
});
console.log('pictures');
console.log(pictures);
return pictures;
});
}
You're thinking about your problem procedurally. However, this logic fails anytime you work with asynchronous requests. I expect what you originally tried to do looked something like this:
var pictures = getMyPhotos();
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
But, that doesn't work since the value of 'pictures' is actually undefined (which is the default return type of any function without an actual return defined -- which is what your getMyPhotos does)
Instead, you want to do something like this:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
callback(pictures);
});
}
// This is the function that actually does the work with your pictures
function oncePhotosReceived(pictures){
for (var i = 0; i < pictures.length; i++) {
// do something with each picture
}
};
// Request the picture data, and give it oncePhotosReceived as a callback.
// This basically lets you say 'hey, once I get my data back, call this function'
getMyPhotos(oncePhotosReceived);
I highly recommend you scrounge around SO for more questions/answers about AJAX callbacks and asynchronous JavaScript programming.
EDIT:
If you want to keep the result of the FB api call handy for other code to use, you can set the return value onto a 'global' variable in the window:
function getMyPhotos(callback) {
FB.api('/me/albums', function (response) {
// process respose data to get a list of pictures, as you have already
// shown in your example
// instead of 'returning' pictures,
// we just call the method that should handle the result
window.pictures = pictures;
});
}
You can now use the global variable 'pictures' (or, explicitly using window.pictures) anywhere you want. The catch, of course, being that you have to call getMyPhotos first, and wait for the response to complete before they are available. No need for localStorage.
As mentioned in the comments, asynchronous code is like Hotel California - you can check any time you like but you can never leave.
Have you noticed how the FB.api does not return a value
//This is NOT how it works:
var result = FB.api('me/albums')
but instead receives a continuation function and passes its results on to it?
FB.api('me/albums', function(result){
Turns out you need to have a similar arrangement for your getMyPhotos function:
function getMyPhotos(onPhotos){
//fetches the photos and calls onPhotos with the
// result when done
FB.api('my/pictures', function(response){
var pictures = //yada yada
onPhotos(pictures);
});
}
Of course, the continuation-passing style is contagious so you now need to call
getMyPhotos(function(pictures){
instead of
var pictures = getMyPhotos();

Categories