I saw similarly asked questions but I am having a hard time applying it to my code.
I have the following piece of code:
createSubmission(sub: Submission, files: Files[]): Promise {
//first add files to storage and create the url array
let urlArray: string[];
return files.forEach(file => {
var storageRef = this.storage.ref(`files/${file.name}`);
return storageRef.put(file).then(()=> {storageRef.getDownloadURL().subscribe(url => {
urlArray.push(url);
if(urlArray.length === files.length){ //we are at the final element
sub.filesUrls = urlArray;
return this.finilize(sub);
}
})
});
});
}
finilize(sub: Submission){ //actually creates the submission
let subRef = this.db.database.ref(`/submissions/${sub.medicUid}`).push();
let subKey = subRef.getKey();
sub.uid = subKey;
return subRef.update(sub);
}
The caller of the first method (createSubmission
) should receive a Promise. My code sequence is finilized when we return from the finilize
method.
I want to continue after all the elements in the loop have been iterated, and the urlArray
has been filled. It is filled when it has the same number of elements as the passed files
array, that's why I placed this condition:
if(urlArray.length === files.length){...}
However, I get an error because the if
condition means that Not all code paths return a value. So I believe I should ditch the if
condition and wait for all promises to resolve before calling finalize
, but I am not very clear on how to do it.
First of all foreach loop does not return anything, instead, it modifies the existing array.
Now come to the solution
you cannot use forEach when you want your code to wait for some async event. Instead, use a for of loop, and with that use await
async createSubmission(sub: Submission, files: Files[]): Promise {
//first add files to storage and create the url array
let urlArray: string[];
for (const file of files) {
const url = await this.getDownloadURL(file)
urlArray.push(url)
}
sub.filesUrls = urlArray
return this.finilize(sub)
}
getDownloadURL(file):Promise{
let storageRef = this.storage.ref(`files/${file.name}`);
return new Promise((resolve, reject) => {
storageRef.put(file).then(() => {
storageRef.getDownloadURL().subscribe(url => {
resolve(url)
})
})
})
}
finilize(sub: Submission){ //actually creates the submission
let subRef = this.db.database.ref(`/submissions/${sub.medicUid}`).push();
let subKey = subRef.getKey();
sub.uid = subKey;
return subRef.update(sub);
}
Note: Code snippet may contain syntactical errors
I don't know what storageRef.getDownloadURL()
is from (I assume it is related to Firebase and Angular) so I leave most of your code untouched assuming that it is correct and that you just want to know how to correctly deal with the Promise case.
There are two ways of solving the problem.
async createSubmission(sub: Submission, files: Files[]): Promise {
//first add files to storage and create the url array
let urlArray: string[];
for( let file of files ) {
const storageRef = this.storage.ref(`files/${file.name}`);
await storageRef.put(file)
const url = await new Promise((resolve, reject) => {
try {
// pass resolve to subscribe. The subscribe will then call resolve with the url
storageRef.getDownloadURL().subscribe(resolve)
// you need to check if the callback to passed to subscribe
// is guaranteed to be called, otherwise you need to
// handle that case otherwise your code flow will
// stuck at this situation
} catch (err) {
reject(err)
}
})
urlArray.push(url)
}
sub.filesUrls = urlArray;
return this.finilize(sub);
}
finilize(sub: Submission) { //actually creates the submission
let subRef = this.db.database.ref(`/submissions/${sub.medicUid}`).push();
let subKey = subRef.getKey();
sub.uid = subKey;
return subRef.update(sub);
}
storageRef.put(file)
does some network stuff and you want to parallelize that (which is not always a good idea), you can use Promise.all
and Array.map
.async createSubmission(sub: Submission, files: Files[]): Promise {
//first add files to storage and create the url array
let urlArray: string[];
urlArray = Promise.all(files.map(async(file) => {
const storageRef = this.storage.ref(`files/${file.name}`);
await storageRef.put(file)
const url = await new Promise((resolve, reject) => {
try {
// pass resolve to subscribe. The subscribe will then call resolve with the url
storageRef.getDownloadURL().subscribe(resolve)
// you need to check if the callback to passed to subscribe
// is guaranteed to be called, otherwise you need to
// handle that case otherwise your code flow will
// stuck at this situation
} catch (err) {
reject(err)
}
})
return url
}))
sub.filesUrls = urlArray;
return this.finilize(sub);
}
finilize(sub: Submission) { //actually creates the submission
let subRef = this.db.database.ref(`/submissions/${sub.medicUid}`).push();
let subKey = subRef.getKey();
sub.uid = subKey;
return subRef.update(sub);
}
As you use TypeScript you should add the corresponding type information to my code.
YOu further should check if storageRef.getDownloadURL()
provides a Promise API and if so replace new Promise
and the subscribe
with that one.
I think what you're looking for is "Promise.all()"
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
Promise.all resolves an array of Promises.
In this example urlArray will contain the resolved values of getDownloadUrl
let urlArrayTasks = [];
for(let file of files){
urlArrayTasks.push(this.getDownloadURL(file));
}
let urlArray = await Promise.all(urlArrayTasks);