Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cannot abort a streamsaver WriteStream #149

Closed
jat255 opened this issue Mar 27, 2020 · 4 comments
Closed

Cannot abort a streamsaver WriteStream #149

jat255 opened this issue Mar 27, 2020 · 4 comments

Comments

@jat255
Copy link

jat255 commented Mar 27, 2020

I am not really a javascript developer, so forgive me if I'm missing something obvious, but I'm having trouble creating a button that will cancel an in-progress download using StreamSaver.

My application is using StreamSaver.js, zip-stream.js, and web-streams-polyfill (to provide pipes and TranformStreams in Firefox) and web-streams-adapter (to allow use of pipes on a Fetch API call in Firefox). I have different download functions for zipping files and then for some single large files, and neither can be canceled due to locked writers. Specifically, the error I get when attempting to use writeStream.abort() is Promise {<rejected>: TypeError: Failed to execute 'abort' on 'WritableStream': Cannot abort a locked stream. I'm using a TransformStream to accumulate the number of bytes downloaded to update a progress bar.

The code is basically this (for downloading a single large file):

p = new TransformStream({
    transform (chunk, ctrl) {
        bytesDownloaded += chunk.byteLength 
        updateProgressBar(bytesDownloaded, total_to_dl);
        ctrl.enqueue(chunk)
    }
});

toPonyRS = WebStreamsAdapter.createReadableStreamWrapper(ponyfill.ReadableStream)
fileStream = streamSaver.createWriteStream(filename, {size: window.file_sizes[url]});
fetch(url).then(res =>  {
    rs = res.body;
    rs = window.ReadableStream.prototype.pipeTo ?
           rs : toPonyRS(rs);
    return rs.pipeThrough(p).pipeTo(fileStream);
})

I've attached an event handler to a cancel button that tries to call fileStream.abort(), but then this gives an error because the stream is locked. Am I doing something wrong, or is this not possible? When canceling the download from the browser, the actual download still continues (I think this is #13), so I was hoping to provide a means for the users to cancel the download (currently the only thing that works is refreshing or closing the page.

@jat255
Copy link
Author

jat255 commented Mar 31, 2020

@jimmywarting sorry to bug, but any thoughts on this issue?

@jimmywarting
Copy link
Owner

// don't
ws = new WritableStream()
writer = ws.getWriter() // locks the stream
ws.abort() // Failed to execute 'abort' on 'WritableStream': Cannot abort a locked stream

// do
ws = new WritableStream()
writer = ws.getWriter() // locks the stream
writer.abort()

// or
ws = new WritableStream()
writer = ws.getWriter() // locks the stream
writer.releaseLock() // releases the lock
ws.abort()

as for the browser ui when they cancel, it don't properly propegate back to the service worker when it's aborted. so yea, it's related to #13

if you provide them with a abort button then you could perhaps cancel both the fetch and the writeable stream with AbortController/AbortSignal

other (perhaps easier) solution could have you tried just using a link <a download="filename" href="url">download</a>
instead of emulating what a server dose with streamsaver, just provide a content-disposition attachment response header from the backend.

@jat255
Copy link
Author

jat255 commented Mar 31, 2020

Thanks! Can I still use this approach with pipeTo/pipeThrough? It appears those methods use the streams directly, rather than the writers/readers, but perhaps I'm misunderstanding.

I'm doing some individual file downloading this way (which I could do directly in the browser as you mention), but since I'm also using zip-stream.js, I wanted to do it all the same way so I could have one progress bar for all the downloads (regardless of if they're individual files or in a zip).

I'm doing the following for the zips, but I'm not sure where I would use the getWriter() in this example:

let writeStream = streamSaver.createWriteStream(
    zip_title,
    { size: total_bytes});

z = new ZIP({
    pull (ctrl) {
        const it = files.next()
        if (it.done) {
            ctrl.close()
        } else {
           const [name, url] = it.value
                                
           return fetch(url).then(res => {
               ctrl.enqueue({
                   name,
                   stream: () => {
                       r = res.body;
                       return r
                   }
               });
           })
       }
  }}).pipeThrough(progressStream)
     .pipeTo(writeStream)
     .catch( err => {
         console.log('failed to save zip');
         errorProgress();
         showError('something went wrong');
});

EDIT: I'll look more into AbortController/AbortSignal... That looks like it might be what I need.

@jat255
Copy link
Author

jat255 commented Mar 31, 2020

Fantastic! AbortController was exactly what I needed. MWE for those coming to this later:

var abortController = new AbortController();
var abortSignal = abortController.signal;

z = new ZIP({
    pull (ctrl) {
        const it = files.next()
        if (it.done) {
            ctrl.close()
        } else {
            const [name, url] = it.value

            return fetch(url, 
                            {signal: abortSignal})
                    .then(res => {
                        ctrl.enqueue({
                            name,
                            stream: () => {
                                r = res.body;
                                return r
                            }
                        });
                    })
        }
    }
}).pipeThrough(progressTransform)
    .pipeTo(writeStream,
            {signal: abortSignal})
    .catch( err => {
        if (abortSignal.aborted) {
            console.log('User clicked cancel');
        } else { 
            console.log('There was an error during the download:', err.message);
        }
});

An example of this might be good to add to the docs somewhere (maybe this is obvious to an actual JS developer, but as a hacker this took me a while to figure out).

@jat255 jat255 closed this as completed Mar 31, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants