6

I am using streamsaver.js to download larger files and it works perfectly fine.

In the meantime, I want to handle cancel events from the user to stop fetching the content from the server.

Is it possible with AbortController and AbortSignal to stop readable streams fetching data?

The sample code would be below for the download part.

fetch(url, { signal: abortSignal })
    .then(resp => {
        for (var pair of resp.headers.entries()) {
            console.log(pair[0] + ': ' + pair[1]);
        }
        total = Number(resp.headers.get('X-zowe-filesize'));
        return resp.body;
    })
    .then(res => {

        var progress = new TransformStream({
            transform(chunk, controller) {
                loaded += chunk.length;
                controller.enqueue(chunk);
                elem.style.width = (loaded / total) * 100 + '%';
                elem.innerHTML = (loaded / total) * 100 + '%';
            }
        })

        const readableStream = res
        // more optimized
        if (window.WritableStream && readableStream.pipeTo) {
            return readableStream
                .pipeThrough(progress)
                .pipeTo(fileStream, { signal: abortSignal })
                .then(() => console.log('done writing'))
        }

        window.writer = fileStream.getWriter()

        const reader = readableStream.getReader()
        const pump = () => reader.read()
            .then(res => res.done
                ? writer.close()
                : writer.write(res.value).then(pump))

        pump()
    })
n0099
  • 520
  • 1
  • 4
  • 12
  • Does this answer your question? [How do I cancel an HTTP `fetch()` request?](https://stackoverflow.com/q/31061838) – double-beep Jan 25 '23 at 15:28

2 Answers2

1

FWIK, it is possible to use AbortController and AbortSignal to stop this. AbortController allows you to create an AbortSignal that can be passed to the fetch API as the signal option. Once the fetch API receives this signal, it will then stop fetching data from the server.

You can pass the abortSignal to the fetch API like this: fetch(url, { signal: abortSignal }). In the code that you provided in your post, it seems as if you have this configured correctly. You probably also may need to pass the abortSignal to the pipeTo method so that it can be used to stop the writing of the file to the stream if the user cancels the download.

You can pass the abortSignal to the pipeTo method like this: pipeTo(stream, { signal: abortSignal })

To do this, you can create a function that calls AbortController's abort() method to cancel fetching and writing. Here is a basic implementation of how you could go about doing this:

const controller = new AbortController(); // Create a new AbortController
const abortSignal = controller.signal; // And make the signal easier to access

// Do a simple fetch and pass the signal to it
// If we don't call AbortController.abort() it will continue
fetch(url, { signal: abortSignal }).then((res) => {
   // Write to a stream
   if (window.WritableStream && readableStream.pipeTo) {
      return readableStream
         .pipeThrough(progress)
         .pipeTo(fileStream, { signal: abortSignal })
         .then(() => console.log("done writing"));
   }
});

function cancel() {
   // Call the `abort()` method and stop the fetch
   controller.abort();
}

In this code, when the user calls cancel(), the abort() method is called on the AbortController, which then inadvertently stops the fetch() and the readableSteam before it is finished writing the file.

Joe
  • 415
  • 1
  • 5
  • 15
0

Using fetch and AbortController

@double-beep provides a link in a comment to a SO answer that works with the code you provided. Here is what you would do using fetch:

// Create an instance of the AbortController.
const controller = new AbortController();
// Pull out the signal which you refer to as AbortSignal.
const abortSignal = controller.signal;
// Now you can use `abortSignal`:
fetch(url, { signal: abortSignal })
    .then(resp => {
        // Code omitted...

// If you ever want to abort the stream you do so on the controller.
controller.abort();

You can add a button to the page that says cancel and onclick have it call the abort() method on the controller. If you want to use the native AbortController here is the finalized spec and here are examples in the MDN Web Docs.

Using StreamSaver

Since your using StreamSaver.js you should use/hook into the built-in abort() method that does this for you already. Under the best practice section it says this:

Handle [the] unload event when [the] user leaves the page... Because it looks like a regular native download process some might think that it's [okay] to leave the page beforehand since it is downloading in the background directly from some server, but it isn't.

And then provides this code example:

// abort so it dose not look stuck
window.onunload = () => {
  writableStream.abort()
  // also possible to call abort on the writer you got from `getWriter()`
  writer.abort()
}

window.onbeforeunload = evt => {
  if (!done) {
    evt.returnValue = `Are you sure you want to leave?`;
  }
}

The example is addressing a different issue but provides the method you need. You can add a button to the page that says cancel and onclick have it call the abort() method on either the stream itself or the writer object.

Please Note: I would recommend the fetch answer because that is quickly becoming the modern supported solution. The hacky way StreamSaver was doing things is no longer needed:

Just want to let you know that there is this new native way to save files to the HD: https://github.com/whatwg/fs which is more or less going to make FileSaver, StreamSaver and similar packages obsolete...

Blizzardengle
  • 992
  • 1
  • 17
  • 30