Outside promise needs to wait for inner nested promise - javascript

I've this usecase wherein I wish to do the following :
Set metadata in indexDb
Iterate over an array of images
See if img is already set in indexDb
if yes, do nothing, if not, download the img
set the downloaded img (as blob) in indexDb
Raise all images processed event at the end
ads data :
[{
ETag:"",
S3URL:"",
duration:30,
filename:"",
linear-gradient:"",
status:"",
timerRequired:"yes"
}]
My code at the moment :
this.Tvlocalforage.setItem('meta', newMeta).then(() => { //Step 1
for (let idx in ads) { //Step 2
this.localforage.getItem(ads[idx]['filename']).then(blob => {
if(!blob){ //Step 3
LSPromise = imgSrcToBlob(ads[idx]['S3URL'], undefined, 'Anonymous', 1).then((blob) => { //Step 4
return this.localforage.setItem(ads[idx]['filename'], blob); //Step 5
});
LSPromises.push(LSPromise);
}
});
}
}).then(() => {
if(LSPromises.length) {
Promise.all(LSPromises).then((data) => {
this.TvLSkeyCount = LSPromises.length;
this.fireLoadAssetsEvent(); //Step 6
});
}
});
Problems I am facing :
After the promise for setting metadata is resolved, it straightaway goes to then() block and by that time LSPromises is null. Of course I understand that internal nested promises haven't been resolved yet.
Resolution I tried :
Return LSGetter promises and download images later. this did not work either.
Code I tried :
this.Tvlocalforage.setItem('meta', newMeta).then(() => {
for (let idx in ads) {
let p = this.Tvlocalforage.getItem(ads[idx]['filename']);
LSPromises.push({'promise' : p, 'filename' : ads[idx]['filename'], 'url' : ads[idx]['S3URL']});
}
}).then(() => {
if(LSPromises.length){
Promise.all(LSPromises.map(obj => {
obj['promise'].then(blob => {
if(!blob){
imgSrcToBlob(obj['url'], undefined, 'Anonymous', 1).resolve(blob => {
return this.Tvlocalforage.setItem(obj['filename'], blob);
});
}
});
})).then((data) => {this.fireLoadAssetsEvent();});
}
I tried 2 more ways to wrapper up and try to return promise.all of download step from inside & on trying to resolve that, return promise.all of set downloaded images to LS. But it did not work.

Get rid of the for() loop since idx will not be what you want it to be inside the promise callback since loop will complete before promises do
Can use map() instead to create the array using a closure
Something like:
this.Tvlocalforage.setItem('meta', newMeta).then(() => { //Step 1
let LSPromises = ads.map(ad => {
return this.localforage.getItem(ads[idx]['filename']).then(blob => {
if (!blob) { //Step 3
return imgSrcToBlob(ad['S3URL'], undefined, 'Anonymous', 1).then((blob) => { //Step 4
return this.localforage.setItem(ad['filename'], blob); //Step 5
});
}
return null
});
});
return Promise.all(LSPromises).then((data) => {
this.TvLSkeyCount = data.filter(o => o).length;
this.fireLoadAssetsEvent(); //Step 6
// not sure what needs to be returned here
});
});

There might be other errors, but there is a missing return:
this.Tvlocalforage.setItem('meta', newMeta).then(() => { //Step 1
for (let idx in ads) { //Step 2
LSPromises.push(this.localforage.getItem(ads[idx]['filename']).then(blob => {
if(!blob){ //Step 3
return /* added return */ imgSrcToBlob(ads[idx]['S3URL'], undefined, 'Anonymous', 1).then((blob) => { //Step 4
return this.localforage.setItem(ads[idx]['filename'], blob); //Step 5
});
// LSPromises.push(LSPromise);
}
}));
}
// }).then(() => {
if(LSPromises.length) {
return /* <<<=== */ Promise.all(LSPromises).then((data) => {
this.TvLSkeyCount = LSPromises.length;
this.fireLoadAssetsEvent(); //Step 6
});
}
});
If the promise returned from Promise.all() is not returned, the caller can not wait for it to complete.

I could not test this, but you should try to flatten the nesting, chaining the thens at the outermost level. You can use Promise.all even more in order to pass the ad value through the chain together with the resolved values:
this.Tvlocalforage.setItem('meta', newMeta).then(() => // Step 1
Promise.all(ads.map( ad => // Step 2
Promise.all(ad, this.localforage.getItem(ad.filename))
))
).then(blobs =>
blobs.filter( ([ad, blob]) => !blob ) // Step 3
).then(blobs =>
Promise.all(blobs.map( ([ad]) =>
[ad, imgSrcToBlob(ad.S3URL, undefined, 'Anonymous', 1)] // Step 4
))
).then(blobs =>
Promise.all(blobs.map( ([ad, blob]) =>
this.localforage.setItem(ad.filename, blob) // Step 5
))
).then(data => {
this.TvLSkeyCount = data.length;
this.fireLoadAssetsEvent(); // Step 6
});

Related

Need to execute async function for every element of the array but dispatch action with progress only

I am quite new in RxJS, still trying to figure out how to implement different features using it.
I need help regarding the implementation of an observable, tried so many ways but none seems to work.
I have this function:
export function automateParameterEdit(tunId) {
const progress$ = new Subject();
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
progress$.next({ ...tun , progress: '0' });
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });
}),
catchError(e => {
// whatever
}),
finalize(async () => {
// whatever
})
)
)
);
return merge(progress$, process$);
}
So, right now, the action is being dispatched twice, once because of the progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 }); that emits the new tun progress, and the second time I believe it's because of the execution of: concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId))
Let's say there are 4 rules (Object.values(rules).length === 4). In the console I see 4 x 2 = 8 actions dispatched, half of them have invalid payload.
What I want to do is execute the autoEditParameters(tunId, rule.ruleId, tun.rulesetId) which btw is async, and after each execution I want to emit the progress (progress$.next({ ...tun, progress: progress / Object.values(rules).length * 100 });).
How do I stop the invalid actions from being dispatched and only execute the async autoEditParameters and dispatch progress?
You don't need a Subject!
You only need a subject when you need to "manually" push a value through a stream. But, in your case, you just want to modify (map) emissions into a different shape.
So, you can just get rid of the subject. There's no need to merge process$ with progress$; you can simply return progress$;
function automateParameterEdit(tunId) {
const process$ = defer(async () => {
const tun = await updateStatus(tunId, 'autoTun');
return { rules: tun.rules, tun };
}).pipe(
flatMap(({ rules, tun }) =>
from(Object.values(rules)).pipe(
concatMap(rule => autoEditParameters(tunId, rule.ruleId, tun.rulesetId)),
scan((acc, curr) => acc + 1, 0),
map(progress => {
return { ...tun, progress: progress / Object.values(rules).length * 100 };
})
)
)
);
return process$;
}
Here are a couple StackBlitz samples:
Original
Possible Solution
and after each execution I want to emit the progress
Not sure if you meant you just wanted emit the numeric percent (not an object), but that could easily be done. Sometimes breaking it down into smaller functions can make it easier to follow:
function automateParameterEdit(tunId): Observable<number> {
return updateTun(tunId).pipe(
flatMap(processRules)
);
}
function updateTun(tunId): Observable<Tun> {
return defer(async () => updateStatus(tunId, 'autoTun'))
}
function processRules(tun: Tun): Observable<number> {
return from(tun.rules).pipe(
concatMap(rule => autoEditParameters(tun.id, rule.ruleId, tun.rulesetId)),
scan(acc => acc + 1, 0),
map(doneCount => doneCount / tun.rules.length * 100),
startWith(0),
)
}
Here, updateTun() just wraps the async function and returns an observable, so it will be executed whenever it is subscribed to.
processRules() takes a Tun and returns an Observable<number> that is the progress percent. startWith just emits an intial value of 0.
StackBlitz

Access current iteration (index) during for await of loop javascript

There is a question about for await ... of loop.
I have the following code:
for await (let V of reagentItems.map(objectElement => getPricing(objectElement))) {
console.log(objectElement) //I'd like to have access to the current iteration.
//or
console.log(reagentItems[i]) //current fulfilled element from reagent.map
}
So the problem is that this array (from .map) of functions (V as a single response of getPricing) doesn't return the objectElement itself but I need to have access to this objectElement inside for await ... of loop.
Does it possible somehow or not? And if it doesn't, using of Promise.all handles this problem somehow? Or I should modify the original getPricing and return current objectElement with other results?
From what I can understand from your question and your comments, you want to be able to access both object_element and the result of getPricing(object_element) within the for loop.
Your problem right now is that as a result of map, you only have the result of getPricing, but not the original object_element. As a solution, simply return both from map in an object:
// just a random getPricing function
function getPricing(objectElement) {
return {
item_id: objectElement.id,
price: Math.random()
};
}
const reagent_items = [
{
id: 'a'
},
{
id: 'b'
}
];
for (let V of reagent_items.map(object_element => ({object_element, pricing: getPricing(object_element)}))) {
console.log(`object_element: ${JSON.stringify(V.object_element)}, pricing: ${JSON.stringify(V.pricing)}`);
}
You should not be using for await here at all - that is meant for asynchronous generators. Instead, do
for (const object_element of reagent_items) {
const V = await getPricing(object_element);
console.log(object_element, V)
…
}
With Promise.all, the code would look like this:
Promise.all(reagent_items.map(object_element =>
getPricing(object_element).then(V => {
console.log(object_element, V);
…
})
)).then(…)
// or:
await Promise.all(reagent_items.map(async object_element => {
const V = await getPricing(object_element)
console.log(object_element, V);
…
}));

Promises returned data '<pending>'

my data which is fetched from youtube is using nodejs is -
[ { title:
'Dil Zaffran Video Song | Rahat Fateh Ali Khan | Ravi Shankar | Kamal Chandra | Shivin | Palak',
id: 'vChX3XZXqN4',
view: '28652353' },
{ title:
'Full Video: Akh Lad Jaave | Loveyatri | Aayush S|Warina H |Badshah, Tanishk Bagchi,Jubin N, ,Asees K',
id: 'e_vl5aFXB4Q',
view: '17328447'
}]
Now I want to search 5 related videos to each video
my api
getVids(videosID)
.then(result => {
console.log(result) //this is the data which I showed above
result.sort((a,b) => b.view - a.view);
return result;
})
.then(result => {
return result.map(function(el) {
// console.log(el);
let o = Object.assign({}, el);
o.relatedVideos = relatedVideos(el.id); //function is called below
return o;
});
})
.then(result => {
console.log(result);
})
.catch(err => {
console.log(err)
})
and api to 5 related Videos is given below
function relatedVideos(videoId) {
return new Promise((resolve, reject) => {
let urls = 'https://www.googleapis.com/youtube/v3/search?relatedToVideoId=' + videoId + '&key=' + API_KEY + '&part=snippet&type=video';
request(urls, (err, result) => {
const data = JSON.parse(result.body);
const relatedVideos = [];
data.items.forEach(related => {
relatedVideos.push({
title: related.snippet.title
});
if(relatedVideos.length === 5){
resolve(relatedVideos);
}
});
});
});
}
It is giving the output like
[ { title:
'Dil Zaffran Video Song | Rahat Fateh Ali Khan | Ravi Shankar | Kamal Chandra | Shivin | Palak',
id: 'vChX3XZXqN4',
view: '28655496',
relatedVideos: Promise { <pending> } },
{ title:
'Full Video: Akh Lad Jaave | Loveyatri | Aayush S|Warina H |Badshah, Tanishk Bagchi,Jubin N, ,Asees K',
id: 'e_vl5aFXB4Q',
view: '17334681',
relatedVideos: Promise { <pending> }
}]
How to solve this pending problem or how to wait so it gets full data.
It happens because you assign the promise returned by relatedVideos() to the property relatedVideos, you never actually wait for it to resolve. You need to await all of the promises before you proceed, this is most easily done with Promise.all(). Let's focus on this part of your code:
return result.map(function(el) {
// console.log(el);
let o = Object.assign({}, el);
o.relatedVideos = relatedVideos(el.id); //function is called below
return o;
});
What you need to do is to wait for each promise to resolve in order to get its resolved value before you assign it to the object. Then you also need to create a promise that doesn't resolve until all of those promises have resolved. This is what Promise.all() is for. Change the code mentioned above into this:
return Promise.all(result.map(function(el) {
// console.log(el);
let o = Object.assign({}, el);
return relatedVideos(el.id)
.then(function(relatedVideo) {
o.relatedVideos = relatedVideo;
return o;
});
}));
Please note that ALL of the promises mapped to Promise.all() needs to resolve in order for the chain to continue with the next then. If even one of those promises rejects or don't resolve at all it will never continue. You have a condition for your resolve() call in the relatedVideos() function. This implies that in some cases a promise might end up never resolving. You should always resolve or reject a promise, perhaps resolve(null) would be appropriate in your case.
This happens when the promise has not been resolved, so your resolve function isn't being called. It would probably be a good idea to test this without the below conditional.
if(relatedVideos.length === 5){
resolve(relatedVideos);
}

Passing value into next Promises argument

Basically, i'm so confused with the Promises. Because, I'm still new to moved on from Callback method for handling Asynchronous.
So, i have a code like these
const Search = function(name) { //
return new Promise((resolve, reject) => { //
let o = [{
"name": "Apple",
"quantity": 10
},
{
"name": "Grape",
"quantity": 5
}
]
let result = o.filter((n) => n.name == name)
if (result.length < 1) {
reject()
return
}
resolve(result[0])
})
}
const Buy = function(data, qty) {
return new Promise((resolve, reject) => {
let o = {
name,
quantity
} = data
o.quantity = parseInt(o.quantity) - qty
if (o.quantity < 0) {
throw new Error("Oops. Quantity is Empty!")
}
resolve(o)
})
}
const Result = function(test) {
console.log(test)
}
The main purpose, how i can input a value into a qty arguments on Buy function?
I was do something like this, but the result is not expected what i want. I'm sure, my code has something missing but i don't know.
Search("Apple")
.then(Buy, 4)
.then(Result)
The result is :
{ name: 'Apple', quantity: NaN }
Main goal is :
{ name: 'Apple', quantity: 6 }
Anyway thanks :)
Search("Apple")
.then(function(result){return Buy(result, 4)})
.then(Result)
You were trying to pass Buy directly into .then, but the function in .then always gets passed only 1 argument. So you can call and return Buy in an anonymous function, where you can apply yourself as many arguments you want.
You can take advantage of scope.
function executeSearch() {
Search("Apple").then(function (searchResult) {
// feel free to use the result of the first promise
// as input parameters to the second if desired
const name = searchResult.name;
const quantity = searchResult.quantity;
Buy(searchResult, 4).then(Result);
});
}
This is similar to the other answer but demonstrates that you can use the result of the first promise in any number of ways to execute the second promise.
The then method accepts a function, so what you can do is change your 'buy' to the following:
const Buy = function(qty) {
return function(data){
return new Promise((resolve, reject) => {
let o = {
name,
quantity
} = data
o.quantity = parseInt(o.quantity) - qty
if (o.quantity < 0) {
throw new Error("Oops. Quantity is Empty!")
}
resolve(o)
})
}
}
Then you can use it like:
Search("Apple")
.then(Buy(4))
.then(Result)

Promise not waiting until request is finished [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 5 years ago.
I'm trying to chain promises together to make a series of requests. This is one of the few times I've used promises, so I'm not too sure what I'm doing. Hoping that someone can catch whats going on wrong. Basically, my first promise is working correctly, then the second function runs after the first promise is resolved, however, the third does not wait so the information I'm requesting is incomplete.
I can see that my functions are returning the correct information. So mainly my question is how do I format my promises so they each wait for each other to complete before running?
function getPaths(folderpath) {
return new Promise(function(resolve, reject) {
db.getPaths(folderpath, function(response) {
//get paths for files and folders
for (let i = 0; i < response.entries.length; i++) {
let entries = response.entries[i];
if (entries[".tag"] == "folder") {
folderPaths.push(response.entries[i].path_lower);
}
if (entries[".tag"] == "file") {
filePaths.push(response.entries[i].path_lower);
}
}
resolve();
});
});
}
function openFolders(folders) {
return new Promise(function(resolve, reject) {
for (let i = 0; i < folders.length; i++) {
db.getPaths(folders[i], function(response) {
for (let j = 0; j < response.entries.length; j++) {
let entries = response.entries[j];
if (entries[".tag"] == "file") {
filePaths.push(response.entries[j].path_lower);
}
}
//console.log(filePaths); //returns correct information
});
}
resolve();
});
}
getPaths("/path").then(function() {
openFolders(folderPaths);
}).then(function() {
console.log(filePaths); //returns incomplete information
});
You have a big problem here in bold
function openFolders(folders) {
return new Promise(function(resolve, reject) {
for (let i = 0; i < folders.length; i++) {
db.getPaths(folders[i], function(response) {
// ...
});
}
resolve();
});
}
You're running a synchrounous for loop around an asynchronous function call. The loop finishes iterating synchronously and then immediately calls resolve() - the code in the async handler (eg, filePaths.push(...)) does not run before the Promise is resolved
Your problems don't stop there though. You're using Promises unconventionally by modifying global state and then resolving and empty Promise - resolve() vs resolve(someValue). #FrankModica's answer has more to say about that.
As for remedies, I'd recommend you take a look at Promise.all - and in general, using Promises in a more conventional way
In the snippet below, I've mocked your db function to read from a fake in-memory database, fakedb. Then, I made a getPaths function which wraps db.getPaths but returns a Promise instead. Then I rewrite openFolders to create an array of Promises, pass them to Promise.all, then finally resolve the values I want
Worth noting: Promise.all will run your array of Promises in parallel - if this undesirable, you could run them in serial using .reduce and .then.
const db = {
getPaths: (path, k) => {
setTimeout(k, 50, fakedb[path])
}
}
function getPaths (path) {
return new Promise((resolve, reject) =>
db.getPaths(path, response => resolve(response)))
}
function openFolders (folders) {
return Promise.all(folders.map(getPaths))
.then(responses =>
responses.reduce((acc, {entries}) =>
acc.concat(entries.filter(x => x['.tag'] === 'file').map(x => x.path_lower)), []))
}
const fakedb = {
'cat': {
'entries': [
{ '.tag': 'dir', 'path_lower': './cat/.' },
{ '.tag': 'dir', 'path_lower': './cat/..' },
{ '.tag': 'file', 'path_lower': './cat/a' },
{ '.tag': 'file', 'path_lower': './cat/b' },
{ '.tag': 'file', 'path_lower': './cat/c' }
]
},
'dog': {
'entries': [
{ '.tag': 'dir', 'path_lower': './dog/.' },
{ '.tag': 'dir', 'path_lower': './dog/..' },
{ '.tag': 'file', 'path_lower': './dog/a' },
{ '.tag': 'file', 'path_lower': './dog/b' },
{ '.tag': 'file', 'path_lower': './dog/c' }
]
}
}
openFolders(['cat','dog']).then(console.log, console.error)
The operation I'm performing in openFolders might feel a little complex because it handles all transformations of the responses in a single .then handler - you could (optionally) separate the work into multiple .then calls which might result in a lighter cognitive load
function openFolders (folders) {
return Promise.all(folders.map(getPaths))
// get `.entries` of each response
.then(responses =>
responses.map(x => x.entries))
// flatten array of entries arrays into a single array
.then(arrOfEntries =>
arrOfEntries.reduce((acc, entries) =>
acc.concat(entries), []))
// only keep entries where `.tag` of each entry is `'file'`
.then(entries =>
entries.filter(x => x['.tag'] === 'file'))
// return `path_lower` of the resulting entries
.then(entries =>
entries.map(x => x.path_lower))
}
If I understand correctly, you are using some variables from the outer scope (folderPaths and filePaths). I'm not sure that's the best way to go about this, but I believe it will start working once you return the promise from openFolders. This way you wait until openFolders completes:
getPaths("/path").then(function() {
return openFolders(folderPaths);
}).then(function() {
console.log(filePaths);
});
But I'd recommend resolving the information required for the next call:
resolve(folderPaths);
And:
resolve(filePaths);
So your code can look more like:
getPaths("/path").then(function(folderPaths) {
return openFolders(folderPaths);
}).then(function(filePaths) {
console.log(filePaths);
});
Edit: Looks like you may have another issue mentioned by #naomik. If db.getPaths is async, in your openFolders function you may be resolving before all of the async calls in the loop complete. Unfortunately I don't have time right now to show the solution. Hopefully #naomik can!

Categories