I have a Promise.all() that returns when all my download promises are finished, the downloads are basically several images. I’m trying to add the possibility to cancel the remaining downloads in the list of URLs that is passed to Promise.all(). I tried some ways to do this, but the items are always downloaded anyway.
How my code is right now:
async downloadFunction(arrayToDownload: Object[]): Promise<void[]> {
let loading = await this.loadingController.create({
message: 'Touch backgroud to cancel',
backdropDismiss: true
});
await loading.present();
const fileTransfer: FileTransferObject = this.transfer.create();
let canceled: boolean = false;
loading.onDidDismiss().then(obj => {
console.log(obj); // obj.role will be = 'backdrop' if clicked in background
if (obj.role === 'backdrop') {
console.log('canceled');
//fileTransfer.abort(); -> Tried to abort filetransfer here first, but it seems that it need to be fired during download.
canceled = true;
return;
}
});
return Promise.all(arrayToDownload.map(element => {
if (canceled === true) {
return; // Added a return here, so it would skip the rest, but didn't work
}
if (element != undefined) {
// Get data from URL and do other preparations
//...
return fileTransfer.download(encodedURI, this.file.externalDataDirectory + 'randomFolder').then(entry => {
return;
}, error => {
console.log("Error while downloading image: ", error);
return;
}).catch(error => {
console.log("Error while downloading image: ", error);
return;
});
} else {
throw new Error("image url not available");
}
})).then(() => {
loading.dismiss();
return [];
});
}
If you don’t get any better answers, I would suggest ditching FileTransfer in favor of ordinary HttpClient. That way you can cancel requests by terminating your subscriptions to them.
return Promise.all(arrayToDownload.map(element => {
if (element != undefined) {
// Get data from URL and do other preparations
//...
return this.http.get(encodedURI).subscribe(res => {
return saveImageLocally(res);
});
} else {
throw new Error("image url not available");
}
}))
How can I cancel the subscription in this case? Or the way I’m thinking is wrong and the HttpClient method should be implemented in another way?
I just realized that no promise will return to Promise.all() because http.get() is an observable. In this case, what would be the equivalent way to handle a bunch of Observables, like Promise.all() do with Promises?
What I would do is to put a tripwire in your subscriptions. There are a bunch of places to put it: the service that is spawning the HTTP requests, a separate dedicated canceller service (with or without an interceptor), the component that is requesting all of this.
I am trying to implement this option, but I am not having much success. I’m triggering the change of value for the Subject from the backdropDismis event of the LoadingController:
async downloadFunction(arrayToDownload: Object[]): Promise<void> {
// declaring loader...
let cancelTripwire = new Subject<void>();
loading.onDidDismiss().then(obj => {
if (obj.role === 'backdrop') {
console.log('canceled'); // Firing in the correct moment
cancelTripwire.next();;
}
});
return arrayToDownload.forEach(element => {
// Preparing url...
//responseType: 'text' because is an image
this.http.get(encodedURI, {responseType: 'text'}).pipe(takeUntil(cancelTripwire))
.subscribe(res => {
console.log(res);
});
});
}
However, despite triggering the event at the right time, the images continue to be downloaded.
Click the “go” button to start things, and you should instantly see “0 active requests” go up to “3 active requests”. After two seconds, “apple” should fill in, followed by the other fruits at further two-second intervals. Pressing “cancel” at any point in the process should instantly drop the active request count to 0 and cancel any unfulfilled requests. The whole shebang can be repeated as desired.
Well, this is really intriguing. Your method works perfectly, but I tried several ways to implement it in my code, but the result is always the same. I even created a StackBlitz to simulate my code with this methodology and to my frustration the StackBlitz worked correctly too. I tried to break the code into several pieces to make it as succinct as possible, but I still can’t identify the source of the problem.
I will make my real code available here for a limited time, if you have the time and desire to take a more experienced look, you may be able to notice something that I missed. Thanks in advance.
This may be a bit of a long shot, but I am always suspicious of async / await and wonder if completely eliminating any use of it in the relevant code paths might do anything.
The underlying idiom here (using unsubscription to abort an activity being performed by HttpClient) relies on the “coldness” of HttpClient's Observables, which is a concept that can’t really be captured by Promises, so whenever an Observable is mogrified into a Promise, the Observable gets “heated up”. In this case, I would not be surprised to see that manifest as a rogue subscription that would be immune to our cancellation tripwire.
async and await in certain cases litter the transpiled JavaScript with Promises that aren’t visible from the original TypeScript. As I find those sorts of bugs supremely vexing, I am yet to jump aboard the async / await train.
Again, not a guess I feel particularly strongly will help you here, but maybe it’s worth experimenting with if you can’t find anything else.
I’ve been trying to avoid using async/await for a while. In this case I used it only for the loadingController because I couldn’t figure out a way to use it without these operators. Now I made some modifications to this controller and the loading controller worked without the operators, but unfortunately the main problem still persists.
Not sure if you’re going to like this or feel the exact opposite, but I kludged your code into my scratchpad thusly, and still see it behaving as I would expect:
I had already tried something very similar to this. I tried again now exactly the way you show, but the same behavior persists. The worst kind of bug is the one you can’t even figure it out why is happening. I’m very grateful for all the help so far, but if you guys have an alternative method to do the same thing, which is download a list of urls and be able to cancel at any point, I will happily try.
Try whit traditional for intead of .map or .forEach, I’ve a hunch.
If that does not work, try creating an Angular interceptor for all http request, then you can create a global variable that you can use to control downloads inside the interceptor.
How would you propose using this global variable to affect downloads that have already commenced? Can you illustrate your idea with some code we can use in test projects?
I was able to solve this problem by rewriting and fractioning this function further:
saveBasemapInStorage(arrayToDownload: Object[]): Promise<void> {
// loadingController impossible to dismiss...
return this.prepareTilesFolder().then(() => {
return this.prepareTileToDownload(arrayToDownload).then(() => {
prepareLoading.dismiss();
this.downloadTiles(arrayToDownload);
});
})
}
async downloadTiles(arrayToDownload) {
// loadingController where is possible to dismiss and cancel the download...
for (let element of arrayToDownload) {
let obj = this.getXYZ(element);
if (element != undefined && this.downloadWasCanceled === false) { // <-- Canceling element
const fileTransfer: FileTransferObject = this.transfer.create();
let destination:string = this.file.externalDataDirectory + 'offline_tiles/' + obj['x'] + '/' + obj['y'] + '/' + obj['z'];
fileTransfer.download(obj['encodedURI'], destination)
.then(entry => {
console.log("donwloaded successfully: ", entry);
return;
}, error => {
console.log("Error while downloading: ", error);
donwloadLoading.dismiss();
return;
}).catch(error => {
console.log("Error while downloading: ", error);
donwloadLoading.dismiss();
return;
});
} else {
donwloadLoading.dismiss();
}
}
donwloadLoading.dismiss();
}
I basically separated the loading function from the directories structuring part and the download part. I believe that this was preventing me to cancel the remaining downloads. I decided to go back to using Cordova’s FileTransfer because it already does the job of downloading and saving the file in the desired location. In place of Promise.all() I left a standard for loop, considering that it is not possible to cancel a promise after it is triggered. I thank you again for all your help!