EDIT
So I decided to study further the situation. I tried to chain the promises returned by the various function, but so far I had no luck.
This part of my app is designed to work not totally asynchronously, and that is what is causing a massive headache right now.
First of all: the Imagepicker itself returns a promise. In this promise I have all the pictures that the users selects from the gallery - I've put a limit to 5 but it's irrelevant.
this.imagePicker.getPictures(this.options)
// Prima promise, ottengo le foto scelte dalla gallery
.then((res) => {
var count = 1;
for (var i = 0; i < res.length; i++) {
var total = res.length;
let path:string = res[i].toString();
// Estraggo il nome e il percorso del file
var currentName = path.substr(path.lastIndexOf('/') + 1);
var correctPath = path.substr(0, path.lastIndexOf('/') + 1);
})
Now, if I were working on any other language than typescript/javascript, I would call the upload method inside the for loop, one per file, get the result for that specific file and call the second upload method (metadata) that would complete the upload and move to the next file in the for loop.
But I'm not on any other language, so initially I thought: "Hey, let's nest the promises!". Which works, almost flawlessly until you start having some network issues, and then bam, you never know which files are uploaded and which aren't until the very end.
The problem is how do I chain promises that come from different functions?
I tried in this way (the above code is now as shown below)
this.imagePicker.getPictures(this.options)
// Prima promise, ottengo le foto scelte dalla gallery
.then((res) => {
var count = 1;
for (var i = 0; i < res.length; i++) {
var total = res.length;
let path:string = res[i].toString();
// Estraggo il nome e il percorso del file
var currentName = path.substr(path.lastIndexOf('/') + 1);
var correctPath = path.substr(0, path.lastIndexOf('/') + 1);
this.file.readAsArrayBuffer(correctPath.toString(), currentName)
.then( result => {
let blob = new Blob([result], {type: "image/jpeg"});
return blob;
})
}
})
But then again, if I want to use that "blob", I've to chain a .then inside that promise, so I end up nesting them anyway. And worse, i know now that some of the code will be executed before the promise will get back the results. And that's exactly what happens. The loop goes on because after calling this.file.readAsArrayBuffer, the code jumps and close the for loop iteration while waiting for the promise to fulfill, and in the meantime a new promise is fired by the second iteration of the loop.
Both the above, edited snippet of code and the original below, lack the ability to properly show a loadingController that is shown but it disappears before the download is really even started, or, If I remove the duration option and put a this.loadingController.dismiss() in the .then part of the upload promise, the last loadingController stays on forever, it never gets dismissed.
This is the original code (i'm still with this until I found a viable solution).
Recap of the requirements: - The user chooses up to 5 pics from the gallery - Each of the 5 pics gets uploaded ONE-AT-A-TIME with a decent loadingController modal until upload has ended, then the correspondent metadata is uploaded too. - At the end of the download a Toast is shown giving feedback to the user - start over with the next file
In this particular case, I'm not interested that the user can do something else while uploading: I want them "stuck" with the loading until it ends.
Original code follows:
photoPicker()
photoPicker() {
if (!this.uploadForm.invalid) {
this.options = {
width: 4000,
quality: 100,
outputType: 0,
}
this.imagePicker.getPictures(this.options)
.then((res) => {
var count = 1;
for (var i = 0; i < res.length; i++) {
var total = res.length;
let path:string = res[i].toString();
// Estraggo il nome e il percorso del file
var currentName = path.substr(path.lastIndexOf('/') + 1);
var correctPath = path.substr(0, path.lastIndexOf('/') + 1);
if ( CONFIG.DEV == 1) {
let datetime = new Date();
console.log('[objects-docs-multiupload] @ ' + datetime.toISOString() + ' picture path: ');
console.log(path);
}
// Leggo il contenuto in un buffer
this.file.readAsArrayBuffer(correctPath.toString(), currentName)
.then( result => {
if ( CONFIG.DEV == 1) {
let datetime = new Date();
console.log('[objects-docs-multiupload] @ ' + datetime.toISOString() + 'data result: ');
console.log(result);
}
// E uso il contenuto per creare un blob con il binario del file
let blob = new Blob([result], {type: "image/jpeg"});
if ( CONFIG.DEV == 1) {
let datetime = new Date();
console.log('[objects-docs-multiupload] @ ' + datetime.toISOString() + 'data blob: ');
console.log(blob);
}
// invoco il metodo per caricare il file
this.uploadFile(blob, currentName, count, total);
count = count + 1;
});
}
}, (err) => {
alert(err);
});
} else {
this.showToastAlert('Compilare i dati del documento', 'error');
return;
}
if (this.uploadForm.controls.recipient.value != '') {
this.createTask();
}
this.router.navigateByUrl('/objects-dashboard/'+this.obj_id);
}
uploadFile()
async uploadFile(file, fileName, counter, total) {
// Chiamo la funzione asincrona per la mascherina di caricamento
// in modo da dare visibilità al fatto che la app è ferma per fare un upload
this.presentUpLoading(counter, total);
// Contatto il metodo uploadFile del rest
this.restProvider.uploadFile(file, fileName)
.then(data => {
if ( CONFIG.DEV == 1) {
let datetime = new Date();
console.log('[objects-dashboard] @ ' + datetime.toISOString() + 'response from uploadFile: ');
console.log(data);
}
// Qui devo fare un piccolo trucco
// devo ritrasformare in json e quindi rifare il parse
// per otternere un oggetto (recryptData) da usare per assemblare
// l'url cui fare il redirect
let decryptedData = JSON.stringify(data);
let recryptData = JSON.parse(decryptedData);
this.loadDoc(recryptData.uuid, recryptData.uploadName, counter, total);
});
}
loadDoc()
async loadDoc(uuid, uploadName, counter, total) {
let upload = {
name: this.uploadForm.controls.name.value+'_'+counter,
categoryId: this.uploadForm.controls.cat_id.value.substring(3),
description: this.uploadForm.controls.description.value+'_'+counter,
filename: uploadName,
uuid: uuid,
objectId: this.obj_id,
};
if(CONFIG.DEV ==1) { console.log(upload); };
this.restProvider.uploadDoc(upload)
.then( data => {
if (data['Result'] == 'Success') {
// OK: Richiamo la funzione showToastAlert
// per mostrare l'avviso Toast
this.showToastAlert('Documento Caricato '+counter+' di '+total, 'success');
// Rimando alla pagina messages-dashboard
//this.router.navigateByUrl('/objects-dashboard/'+this.obj_id);
} else {
// KO: Richiamo la funzione showToastAlert
// per mostrare l'avviso Toast
this.showToastAlert('Documento NON caricato '+counter+' di '+total, 'error');
}
});
}
I finally managed to get the whole thing working correctly. It turns out I was calling the functions in the wrong way. I solved using await/async in the proper fashion.
The inside the for..loop:
this.imagePicker.getPictures(this.options)
// Prima promise, ottengo le foto scelte dalla gallery
.then(async (res) => {
var count = 1;
for (var i = 0; i < res.length; i++) {
var total = res.length;
let path:string = res[i].toString();
// Estraggo il nome e il percorso del file
var currentName = path.substr(path.lastIndexOf('/') + 1);
var correctPath = path.substr(0, path.lastIndexOf('/') + 1);
// Leggo il contenuto in un buffer
await this.file.readAsArrayBuffer(correctPath.toString(), currentName)
.then( (result) => {
// E uso il contenuto per creare un blob con il binario del file
this.blob = new Blob([result], {type: "image/jpeg"});
return this.blob;
})
// Visualizzo il loading controller che indica il caricamento in corso
if ( CONFIG.DEV == 1 ) console.log("Presenting loading controller for counter "+count+" of "+total);
this.presentUpLoading(count, total);
// Inizio l'upload del file col metodo rest e aspetto l'esito
if ( CONFIG.DEV == 1 ) console.log("Starting upload of "+currentName);
await this.uploadFile(this.blob, currentName,count, total);
if ( CONFIG.DEV == 1 ) console.log("RecryptData: ");
if ( CONFIG.DEV == 1 ) console.log(this.recryptData);
if ( CONFIG.DEV == 1 ) console.log("Loading Doc Metadata");
// Carico i metadati dei files e aspetto l'esito
await this.loadDoc(this.recryptData.uuid, this.recryptData.uploadName, count, total);
if ( CONFIG.DEV == 1 ) console.log("Dismissing loading controller");
// Elimino il loading controller e passo al successivo upload
this.dismissUpLoading();
if ( CONFIG.DEV == 1 ) console.log("incrementing counter");
count++;
}
})
That trick has been to make the promise result from getPictures an async function, so that I could use await in all the other promises I had to run in the loop. In this way, all the promises are fulfilled BEFORE the next execution of the loop, and I have been able also to fix the loadingController, which now stays on for the whole upload time.
To have the actual upload function working with await I had to transform the results of that function into a Promise itself:
uploadFile(file, fileName, counter, total) {
// Chiamo la funzione asincrona per la mascherina di caricamento
// in modo da dare visibilità al fatto che la app è ferma per fare un upload
return new Promise((resolve) => {
// Contatto il metodo uploadFile del rest
this.restProvider.uploadFile(file, fileName)
.then((data) => {
// Qui devo fare un piccolo trucco
// devo ritrasformare in json e quindi rifare il parse
// per otternere un oggetto (recryptData) da usare per assemblare
// l'url cui fare il redirect
let decryptedData = JSON.stringify(data);
this.recryptData = JSON.parse(decryptedData);
//this.loadDoc(this.recryptData.uuid, this.recryptData.uploadName, counter, total);
resolve(this.recryptData);
});
});
}
Thanks to this, the array "recryptData" became available for the next function (loadDoc) and I was able to avoid promise nesting.