I want to fetch multiple images and turn them in blob. I'm a newbie about promises, I've tried but I can't get through.
Here below, a single .fetch() promise
fetch('http://cors.io/?u=http://alistapart.com/d/_made/d/ALA350_appcache_300_960_472_81.jpg')
.then(function(response) {
return response.blob();
})
.then(function(myBlob) {
var objectURL = URL.createObjectURL(myBlob);
document.getElementById('myImage').src = objectURL;
});
Now multiple .fetch() promise (don't work)
var promises = [];
for (var i = values.length - 1; i >= 0; i--) {
promises.push(fetch(values[i]));
}
Promise
.all(promises)
.then(function(response) {
for (var i = response.length - 1; i >= 0; i--) {
return response[i].blob();
}
})
.then(function(blob) {
console.log(blob.length); //undefined !!!
for (var i = blob.length - 1; i >= 0; i--) {
console.log(blob[i]);
lcl_images[i].value = URL.createObjectURL(blob[i]);
document.getElementById(lcl_images[i].id).src = objectURL;
}
})
.catch(function(error) {
console.log(error);
});
It is a general rule that a wholly synchronous intermediate step in the success path of a promise chain can be amalgamated with the next step, allowing one then() to be omitted from the chain.
There is actually a proviso on that statement, involving intermediate catches, but it will suffice for this answer.
So, if the .blob() method is geuinely synchronous (it returns a value), only one .then() is required, not two.
Here are two approaches, both of which exploit Array.prototype.map(), and both should work (though they will differ under error conditions):
1. Simple .map() with detail in Promise.all()
var promises = values.reverse().map(fetch); // you may need .reverse(), maybe not. I'm not 100% sure.
return Promise.all(promises).then(function(responses) {
responses.forEach(function(r, i) {
var imageObj = lcl_images[i],
element = document.getElementById(imageObj.id);
imageObj.value = URL.createObjectURL(r.blob());
if(element) { //safety
element.src = imageObj.value;
}
});
return responses; // here, return whatever you want to be made available to the caller.
}).catch(function(error) {
console.log(error);
});
If you prefer, you can write :
return Promise.all(values.reverse().map(fetch)).then(function(responses) {
// ...
});
2. Detail in .map() followed a simple Promise.all()
var promises = values.reverse().map(function(val, i) {
return fetch(val).then(function(result) {
var imageObj = lcl_images[i],
element = document.getElementById(imageObj.id);
imageObj.value = URL.createObjectURL(result.blob());
if(element) { //safety
element.src = imageObj.value;
}
return result; // here, return whatever you want to be made available to the caller.
});
});
return Promise.all(promises).catch(function(error) { // return a promise to the caller
console.log(error);
});
Notes:
(1) will fail completely if any one fetch() fails.
(2) will perform all the imageObj.value ... and element.src = ... stuff for all successful fetches even if one or more fetch()... fails. Any single failure will cause Promise.all(promises) to return a rejected promise.
(1) or (2) may be more appropriate depending on what you want.
There are other error handling possibilities.
If neither approach works, then the most reasonable explanation would be that the .blob() method returns a promise, not a value.
You are returning from the then handler after first response, instead what you need to do is to return the list of blobs:
Promise
.all(promises)
.then(function(response) {
// CHANGED HERE
var blobPromises = [];
for (var i = response.length - 1; i >= 0; i--) {
blobPromises.push(response[i].blob());
}
return Promise.all(blobPromises);
})
.then(function(blob) {
console.log(blob.length);
for (var i = blob.length - 1; i >= 0; i--) {
lcl_images[i].value = URL.createObjectURL(blob[i]);
document.getElementById(lcl_images[i].id).src = objectURL;
}
})
.catch(function(error) {
console.log(error);
});
Related
My client-side Javascript app needs a certain data structure from the server side ("lists"). Several different places in the app need access to the lists. On startup, these places all make the request, which results in several concurrent requests to the server for the same data structure.
I'd like to issue only one request to the server and have all of requesters simply get a promise that resolves when the request comes back.
I think the method is to create an array or stack of promises. If the first request is in-flight, then each subsequent request causes a promise to get created and added to the stack. When the response comes back, the system runs through the stack and resolves all the promises with the result.
The problem is that I can't figure out the right syntax to get this to work. This is what I have so far:
let lists = [];
let loading = false;
let promises = [];
function getLists() {
if (lists.length > 0) {
// return cached copy
return Promise.resolve(lists);
}
/*
This method can get called several times in quick succession on startup.
To prevent sending multiple requests to the server, we maintain
a stack of promises. If there is already a request in-flight, then just
add a promise to the stack and return it. Then resolve all promises
in the stack when the request returns.
*/
let prom = new Promise(); // BAD, NOT ALLOWED
promises.push(prom);
if (!loading) {
callListsApi(); // async call, resolves promises
}
return prom;
}
function callListsApi() {
loading = true;
axios.get("/lists").then(
response => {
loading = false;
if (!response.data || response.data.length == 0) {
lists = [];
} else {
lists = response.data;
}
for (let i = 0; i < promises.length; i++) {
promises[i].resolve(lists); // give all the callers their lists
}
promises = [];
},
error => {
loading = false;
util.handleAxiosError(error);
let msg = util.getAxiosErrorText(error);
for (let i = 0; i < promises.length; i++) {
promises[i].reject(msg);
}
promises = [];
}
);
}
This doesn't work because you can't create a bare Promise() without putting some kind of executor function in it.
How can I rewrite this so it works?
Figured it out. The answer isn't to create a stack of promises. It's just to return to each requestor the same promise over and over. We maintain one main promise, which exists only when a request is in-flight. This is way simpler.
let lists = [];
let listPromise = null;
function getLists() {
if (lists.length > 0) {
// return cached copy
return Promise.resolve(lists);
}
if (listPromise != null) {
// if we get here, then there is already a request in-flight
return listPromise;
}
listPromise = callListsApi();
return listPromise;
}
function callListsApi() {
return axios.get("/lists").then(
response => {
if (!response.data || response.data.length == 0) {
lists = [];
} else {
lists = response.data;
}
listPromise = null;
return lists;
},
error => {
util.handleAxiosError(error);
listPromise = null;
return util.getAxiosErrorText(error);
}
);
}
The title is confusing, sorry.
I need to look at the contents of a promise for a subsequent promise.
See my previous thread for context: How to sequentially handle asynchronous results from API?
I have working code below, and I annotated my problem:
var promises = [];
while (count) {
var promise = rp(options);
promises.push(promise);
// BEFORE NEXT PROMISE, I NEED TO GET ID OF AN ELEMENT IN THIS PROMISE'S DATA
// AND THEN CHANGE 'OPTIONS'
}
Promise.all(promises).then(values => {
for (var i = 0; i < values; i++) {
for (var j = 0; j < values[i].length; j++) {
results.push(values[i][j].text);
}
}
return res.json(results);
}, function(reason) {
trace(reason);
return res.send('Error');
});
This is a perfect example of how promises can be chained, because the result of then is a new promise.
while (count) {
var promise = rp(options);
promises.push(promise.then(function(result) {
result.options = modify(result.options); // as needed
return result.options;
});
}
In this way, each promise given to Promise.all can be preprocessed.
If one promise depends upon another (e.g. it can't be executed until the prior one has finished and provided some data), then you need to chain your promises. There is no "reaching into a promise to get some data". If you want its result, you wait for it with .then().
rp(options).then(function(data) {
// only here is the data from the first promise available
// that you can then launch the next promise operation using it
});
If you're trying to do this sequence count times (which is what your code implies), then you can create a wrapper function and call itself from the completion of each promise until the count is reached.
function run(iterations) {
var count = 0;
var options = {...}; // set up first options
var results = []; // accumulate results here
function next() {
return rp(options).then(function(data) {
++count;
if (count < iterations) {
// add anything to the results array here
// modify options here for the next run
// do next iteration
return next();
} else {
// done with iterations
// return any accumulated results here to become
// the final resolved value of the original promise
}
});
}
return next();
}
// sample usage
run(10).then(function(results) {
// process results here
}, function(err) {
// process error here
});
I have a problem with my promise return code, I have a function getTagQuotes which contains a for loop which can make multiple calls an API to return data into an array.
How my code for this begins below:
// If there are tags, then wait for promise here:
if (tags.length > 0) {
// Setting promise var to getTagQuotes:
var promise = getTagQuotes(tags).then(function() {
console.log('promise =',promise);
// This array should contain 1-3 tags:
console.log('tweetArrayObjsContainer =',tweetArrayObjsContainer);
// Loop through to push array objects into chartObj:
for (var i=0; i<tweetArrayObjsContainer.length; i++) {
chartObj.chartData.push(tweetArrayObjsContainer[i]);
}
// Finally draw the chart:
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
});
}
My getTagQuotes function with the promise return:
function getTagQuotes(tags) {
var deferred = $q.defer(); // setting the defer
var url = 'app/api/social/twitter/volume/';
// My for loop, which only returns ONCE, even if there are 3 tags
for (var i=0; i<tags.length; i++) {
var loopStep = i;
rawTagData = [];
// The return statement
return GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
.success(function(data, status, headers, config) {
rawTagData.push(data);
// One the last loop, call formatTagData
// which fills the tweetArrayObjsContainer Array
if (loopStep === (rawTagData.length - 1)) {
formatTagData(rawTagData);
deferred.resolve();
return deferred.promise;
}
});
}
function formatTagData(rawData) {
for (var i=0; i<rawData.length; i++) {
var data_array = [];
var loopNum = i;
for (var j=0; j<rawData[loopNum].frequency_counts.length; j++) {
var data_obj = {};
data_obj.x = rawData[loopNum].frequency_counts[j].start_epoch;
data_obj.y = rawData[loopNum].frequency_counts[j].tweets;
data_array.push(data_obj);
}
var tweetArrayObj = {
"key" : "Quantity"+(loopNum+1), "type" : "area", "yAxis" : 1, "values" : data_array
};
tweetArrayObjsContainer.push(tweetArrayObj);
}
}
}
Take notice of this line
return GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
it's inside my for loop:
for (var i=0; i<tags.length; i++)
Everything works great if I only have to loop through once. However as soon as there is another tag (up to 3) it still only returns the first loop/data. It does not wait till the for loop is done. Then return the promise. So my tweetArrayObjsContainer always only has the first tag.
Three issues:
you didn't return the deferred promise from the getTagQuotes method.
you were looking at i to see if you were through the loop, and the for loop is already completed (i == (tags.length - 1)) before the first success is even called.
you called return in the first iteration of the loop so that you didn't even get to the 2nd item.
Here's corrected code (didn't test it yet)
function getTagQuotes(tags) {
var deferred = $q.defer(); // setting the defer
var url = 'app/api/social/twitter/volume/';
var tagsComplete = 0;
for (var i=0; i<tags.length; i++) {
rawTagData = [];
GetTweetVolFactory.returnTweetVol(url+tags[i].term_id)
.success(function(data, status, headers, config) {
rawTagData.push(data);
tagsComplete++;
if (tagsComplete === tags.length) {
formatTagData(rawTagData);
deferred.resolve();
}
});
}
return deferred.promise;
}
return deferred.promise; should be the return value of your function, not the GetTweetVolFactory.returnTweetVol(), because that's what you intend to promisify.
Your problem is that you are calling several GetTweetVolFactory.returnTweetVol(), and then you need to merge all those async calls to resolve your promise. In order to do that, you should promisify just one GetTweetVolFactory.returnTweetVol() call:
function promisifiedTweetVol(rawTagData, urlStuff) {
var deferred = $q.defer(); // setting the defer
GetTweetVolFactory.returnTweetVol(urlStuff)
.success(function(data, status, headers, config) {
rawTagData.push(data);
// One the last loop, call formatTagData
// which fills the tweetArrayObjsContainer Array
if (loopStep === (rawTagData.length - 1)) {
formatTagData(rawTagData);
deferred.resolve();
}
});
return deferred.promise;
}
And then call each promise in a loop and return the promise that resolves when all promises are completed:
function getTagQuotes(tags) {
var url = 'app/api/social/twitter/volume/';
var promises = [];
// My for loop, which only returns ONCE, even if there are 3 tags
for (var i=0; i<tags.length; i++) {
var loopStep = if;
rawTagData = [];
promises.push( promisifiedTweetVol(rawTagData, url+tags[i].term_id) );
}
// ...
return $.when(promises);
}
There are a few more issues with your code, but you should be able to get this working with my tip.
Put every promise in an array then do:
$q.all(arrayOfPromises).then(function(){
// this runs when every promise is resolved.
});
You should return an array of promises here, which means that you should change getTagsQuotes like this:
function getTagQuotes(tags) {
var url = 'app/api/social/twitter/volume/',
promises = [];
for (var i=0; i<tags.length; i++) {
promises.push( GetTweetVolFactory.returnTweetVol( url+tags[i].term_id ) );
}
return promises;
}
And then loop through this promises like this:
if (tags.length > 0) {
var promises = getTagQuotes(tags);
promises.map( function( promise ) {
promise.then( function( data ) {
//Manipulate data here
});
});
}
Edit: In case you want all promises to be finished as outlined in the comment you should do this:
if (tags.length > 0) {
Promise.all( getTagQuotes(tags) ).then( function( data ) {
//Manipulate data here
});
}
Edit: Full data manipulation:
Promise.all( getTagQuotes(tags) ).then( function( allData ) {
allData.map( function( data, dataIndex ){
var rawData = data.data,
dataLength = rawData.frequency_counts.length,
j = 0,
tweetArrayObj = {
// "key" : "Quantity"+(i+1),
// "color" : tagColorArray[i],
"key" : "Quantity",
"type" : "area",
"yAxis" : 1,
"values" : []
};
for ( j; j < dataLength; j++ ) {
rawData.frequency_counts[j].start_epoch = addZeroes( rawData.frequency_counts[j].start_epoch );
tweetArrayObj.values.push( { x:rawData.frequency_counts[j].start_epoch, y:rawData.frequency_counts[j].tweets } );
}
tweetArrayObjsContainer.push( tweetArrayObj );
});
for ( var i= 0,length = tweetArrayObjsContainer.length; i < length; i++ ) {
chartObj.chartData.push( tweetArrayObjsContainer[ i ] );
}
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
});
Using deferreds is widely considered to be an anti-pattern. If your promise library supports a promise contstructor, that's an easier way to create your own promises.
Rather than trying to resolve all of the promises in one, I usually use a promise implementation that has an all function. Then I create one function that returns a promise for a thing, and then another function that returns a promise for all the things.
Using a map() function is also usually a lot cleaner than using a for loop.
Here's a generic recipe. Assuming your promise implementation has some flavor of an all function:
var fetchOne = function(oneData){
//Use a library that returns a promise
return ajax.get("http://someurl.com/" + oneData);
};
var fetchAll = function(allData){
//map the data onto the promise-returning function to get an
//array of promises. You could also use `_.map` if you're a
//lodash or underscore user.
var allPromises = myData.map(fetchOne);
return Promise.all(allPromises);
};
var allData = ["a", "b", "c"];
var promiseForAll = fetchAll(allData);
//Handle the results for all of the promises.
promiseForAll.then(function(results){
console.log("All done.", results);
});
With reference to this question and the earlier question :
the code in general will be a lot cleaner with array.map() in lieu of for loops, in several places.
getTagQuotes() will be made cleaner by building an array of promises, submitting it to $q.all() and returning an aggregate promise.
formatTagData(), and its relationship with its caller, will be made cleaner by returning the transformed rawData.
With a few assumptions, the code should simplify to something like this :
getTagQuotes(tags).then(function(tweetArrayObjsContainer) {
chartObj.chartData = chartObj.chartData.concat(tweetArrayObjsContainer); // concat() ...
// chartObj.chartData = tweetArrayObjsContainer; // ... or simply assign??
chartDirective = ScopeFactory.getScope('chart');
chartDirective.nvd3.drawChart(chartObj.chartData);
});
function getTagQuotes(tags) {
var url = 'app/api/social/twitter/volume/';
var promises = tags.map(function(tag) {
var deferred = $q.defer();
GetTweetVolFactory.returnTweetVol(url + tag.term_id)
.success(function(data, status, headers, config) {
deferred.resolve(data);
})
.error(function(data, status) {
console.log(tag.term_id + ': error in returning tweet data');
deferred.resolve(null); // resolve() here effectively catches the error
});
return deferred.promise;
});
return $q.all(promises).then(formatTagData); //this is much much cleaner than building an explicit data array and resolving an outer deferred.
function formatTagData(rawData) {
return rawData.filter(function(data) {
return data || false; // filter out any nulls
}).map(function(item, i) {
return {
'key': 'Quantity' + (i+1),
'type': 'area',
'yAxis': 1,
'color': tagColorArray[i],
'values': item.frequency_counts.reverse().map(function(c) {
return {
x: addZeroes(c.start_epoch),
y: c.tweets,
};
})
};
});
}
}
I have tried to do this with q as well as async, but haven't been able to seem to make it work. After trying those I tried my own way. I didn't think this would work, but I thought I would give it a try. I am confused since there is a callback within a callback in a sense. Here is the function I am wanting to do:
var getPrice = function(theData) {
var wep = theData.weapon;
var completed = 0;
for (i = 0; i < theData.skins.length; i++) {
var currSkin = theData.skins[i];
theData.skinData[currSkin] = {};
for (k = 0; k < wears.length; k++) {
csgomarket.getSinglePrice(wep, currSkin, wears[k], false,
function(err, data) {
completed++;
if (!err) {
theData.skinData[data.skin][data.wear] = data;
}
if (completed === theData.skins.length*wears.length) {
return theData;
}
})
}
}
}
I know these kinds of issues are common in javascript as I have ran into them before, but not sure how to solve this one. I am wanting to fill my object with all the data returned by the method:
csgomarket.getSinglePrice(wep, currSkin, wears[k], false,
function(err, data) { });
Since each call to getSinglePrice() sends off a GET request it takes some time for the responses to come back. Any suggestions or help would be greatly appreciated!
First csgomarket.getSinglePrice() needs to be promisified. Here's an adapter function that calls csgomarket.getSinglePrice() and returns a Q promise.
function getSinglePriceAsync(wep, skin, wear, stattrak) {
return Q.Promise(function(resolve, reject) { // may be `Q.promise(...)` (lower case P) depending on Q version.
csgomarket.getSinglePrice(wep, skin, wear, stattrak, function(err, result) {
if(err) {
reject(err);
} else {
resolve(result);
}
});
});
}
Now, you want getPrice() to return a promise that settles when all the individual getSinglePriceAsync() promises settle, which is trivial :
var getPrice = function(theData) {
var promises = [];//array in which to accumulate promises
theData.skins.forEach(function(s) {
theData.skinData[s] = {};
wears.forEach(function(w) {
promises.push(getSinglePriceAsync(theData.weapon, s, w, false).then(function(data) {
theData.skinData[data.skin][data.wear] = data;
}));
});
});
//return a single promise that will settle when all the individual promises settle.
return Q.allSettled(promises).then(function() {
return theData;
});
}
However, theData.skinData[data.skin][data.wear] will simplify slightly to theData.skinData[s][w] :
var getPrice = function(theData) {
var promises = [];//array in which to accumulate promises
theData.skins.forEach(function(s) {
theData.skinData[s] = {}; //
wears.forEach(function(w) {
promises.push(getSinglePriceAsync(theData.weapon, s, w, false).then(function(data) {
theData.skinData[s][w] = data;
}));
});
});
//return a single promise that will settle when all the individual `promises` settle.
return Q.allSettled(promises).then(function() {
return theData;
});
}
This simplification would work because the outer forEach(function() {...}) causes s to be trapped in a closure.
As getPrice() now returns a promise, it must be used as follows :
getPrice(myData).then(function(data) {
// use `data` here.
}).catch(function(e) {
//something went wrong!
console.log(e);
});
Your way to do is very complex.
I think the best way is to do 1 request for all prices. Now, for each price you do a request.
If you have a list (array) with data that's need for the request, the return value should be a list with the prices.
If approach above is not possible, you can read more about batching http requests: http://jonsamwell.com/batching-http-requests-in-angular/
Need some clarification - Are you trying to run this on the Client side? Looks like this is running inside a nodejs program on server side. If so, wouldn't you rather push this logic to the client side and handle with Ajax. I believe the browser is better equipped to handle multiple http request-responses.
Since you didn't post much info about your csgoMarket.getSinglePrice function I wrote one that uses returns a promise. This will then allow you to use Q.all which you should read up on as it would really help in your situation.
I've created an inner and outer loop arrays to hold our promises. This code is completely untested since you didn't put up a fiddle.
var getPrice = function(theData) {
var wep = theData.weapon;
var completed = 0;
var promises_outer = [] //array to hold the arrays of our promises
for (var i = 0; i < theData.skins.length; i++) {
var currSkin = theData.skins[i];
theData.skinData[currSkin] = {};
var promises_inner = [] // an array to hold our promises
for (var k = 0; k < wears.length; k++) { //wears.length is referenced to below but not decalared anywhere in the function. It's either global or this function sits somewhere where it has access to it
promises_inner.push(csgomarket.getSinglePrice(wep, currSkin, wears[k], false))
}
promises_outer.push(promises_inner)
}
promises_outer.forEach(function(el, index){
var currSkin = theData.skins[index]
theData.skinData[currSkin] = {}
Q.all(el).then(function(data){ //data is an array of results in the order you made the calls
if(data){
theData.skinData[data.skin][data.wear] = data
}
})
})
}
var csgomarket = {}
csgomarket.getSinglePrice = function(wep, currSkin, wears, someBoolean){
return Q.promise(function (resolve, reject){
//do your request or whatever you do
var result = true
var data = {
skin : "cool one",
wear : "obviously"
}
var error = new Error('some error that would be generated')
if(result){
resolve(data)
} else {
reject(error)
}
})
}
So i have this function where i need to combine multiple promises responses, but after some reading i realized promises are async so in this case my loop is going to complete before all the responses do. Should i need to use something like $q.all in this case? How can i improve this piece of code? Thanks..
$scope.messages = [];
function getPastMessages(data) {
angular.forEach(data, function(item) {
Message.get(item.id).then(function(msg) {
if (msg.data.is_private === false) {
User.getPictures(msg.data.user.id).then(function(pics) {
msg.data.user.pictures = pics.data;
});
} else {
User.get(msg.data.im.sender).then(function(sender) {
msg.data.im.sender = sender.data;
User.get(msg.data.im.reciever).then(function(reciever) {
msg.data.im.reciever = reciever.data;
});
});
}
console.log(msg.data); // SHOW 4 OBJECTS CORRECT
$scope.messages.push(msg.data);
console.log($scope.messages); // SHOW ARRAY OF 6 OBJECTS ????????
})
});
};
Without a working example it was difficult to fully understand the context of your code, but you can do something similar to this.
The basic idea is to create a list of promises that you need to wait for. Each of the promises within this list should return a result (presumably msg.data). Using $q.all, you'll get a list of results (one from each promise) at the end. Note that things returned within a .then get wrapped in promises if they aren't already promises.
$scope.messages = [];
function getPastMessages(data) {
var promises = [];
angular.forEach(data, function(item) {
promises.push(getMessage(item));
});
return $q.all(promises);
}
function getMessage(item) {
return Message.get(item.id).then(function(msg) {
if (msg.data.is_private === false) {
return User.getPictures(msg.data.user.id).then(function(pics) {
msg.data.user.pictures = pics.data;
return msg.data;
});
} else {
return User.get(msg.data.im.sender).then(function(sender) {
msg.data.im.sender = sender.data;
return User.get(msg.data.im.reciever).then(function(reciever) {
msg.data.im.reciever = reciever.data;
return msg.data;
});
});
}
});
}
Usage:
getPastMessages(data).then(function(results) {
for (var i = 0; i < results.length; i++) {
$scope.messages.push(results[i]);
}
});
You can only rely on promises to have resolved when you're inside their callback functions.
var messages = [];
somethingAsync().then(function(data){
messages.push(data);
});
console.log(messages.length)
might return 0 or 1 depending on how long somethingAsync takes; you can't rely on it having completed.
Instead you should do your debugging from inside the callback function:
var messages = [];
somethingAsync().then(function(data){
messages.push(data);
console.log(messages.length)
});
This will always return 1.