-
-
Notifications
You must be signed in to change notification settings - Fork 420
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
download cancelled event? #13
Comments
There should be right about here Line 67 in 2f6c53e
But it doesn't get triggered... Think it's a bug or missing |
yeah, thanks. that's what i figured... i reported it here: w3c/ServiceWorker#957 (comment) |
Thanks for that 👍 |
Now with transferable stream there is a way to detect when the bucket strategy is full (Meaning the client paused the stream) you can also detect if user aborted the request. |
Just a heads up: Firefox already does notify the Stream passed to respondWith when the download is cancelled... This line is executed: Line 65 in da6218e
|
I consider the abort event as a minor issue and it would automatically be resolved once all browser start supporting transferable streams. However it would be nice to solve this abort event in Firefox |
Hey all, this was a "must" for me in Firefox, so here's my solution: #105 Would love feedback. |
Is it possible to access the |
Hello Guys, Is there any news about this point ? Thks |
sry, got some bad news. transferable streams is still only supported in Blink with an experimental flag. https://chromestatus.com/feature/5298733486964736 2nd issue is about cancelation... chrome never emits the cancel event (here) but it can fill up the bucket to the point where it stop stops calling 3th issue is that streamsaver lacks the concept about buckets when talking to the service worker over MessageChannel it don't use the pull system and just eagerly enqueues more data without any respect to a bucket or the pull request. - which can lead to memory issue if enqueue data faster than what you are able to write it to the disk. I have written a 2nd stream saving library based on native file system access that kind of acts like an adapter for different storages (such as writing to sandboxed fs, IndexedDB, cache storage and the memory) it too also comes with a adoption for writing data to the disk using same technique as StreamSaver with service worker. However my adapter dose it i slightly different and behaves more like a I think that in the feature native file system access will supersede FileSaver and my own StreamSaver lib when it gets more adoptions at which point i will maybe deprecate StreamSaver in favor of my 2nd file system adapter - but not yet Maybe you would like to try it out instead? One thing that native file system dose differently is that it can enqueue other types of data such as string, blobs and any typed array or arraybuffer - so saving a large blob/file is more beneficial since the browser don't need to read the blob. Oh, and give this issue a 👍 as well ;) |
Thanks for your detailed answer, I'll have a look on your file system lib, looks very interesting and may solve my problem. |
@jimmywarting Is there a minimal, verifiable, complete example of this issue? |
Hmm, i tried to create a minimal plunkr example here: https://plnkr.co/edit/I27Dl0chuMCuaoHD?open=lib%2Fscript.js&preview basically wait 2s until the iframe pops up and save the never ending file download. then cancel the download from the browser UI and expect the I'm 100% sure that this used to work in firefox but i can't get the cancel event to fire anymore in firefox. 😕 |
sw.js:11 Uncaught (in promise) TypeError: Failed to execute 'cancel' on 'ReadableStream': Cannot cancel a locked stream
At client side
See also the code at Is it possible to write to WebAssembly.Memory in PHP that is exported to and read in JavaScript in parallel? at background.js in an extension, where we stream raw PCM audio (without a definitive end) via
(BTW, created several screenshot workarounds, two of which are published at the linked repository https://gist.github.com/guest271314/13739f7b0343d6403058c3dbca4f5580) |
didn't know what to call it, it is kind of like an event that happens when it get aborted by the user... but whatever
I know service worker isn't the best solution but it's currently the only/best client side solution at the moment until native file system access becomes more wildly adapted in more browser without a experimental flag. it too comes with its drawback
I'm using service worker to mimic a normal download that occur from downloading something from a server so that i don't have to build a Blob in the memory and later download the hole file at once as it's wasteful use of memory when downloading large files. + there is better ways to solve that 4y old issue if he just did And as always use the server to download the file if it comes from the cloud if you can var a = document.createElement("a")
a.download = "citylots.json"
// mostly only work for same origin
a.href = "/citylots.json"
document.body.appendChild(a)
a.click() |
For this specific issue you can rearrage the placement of
I'm sure we could build a custom HTML element and implement progress events, either by estimation https://stackoverflow.com/a/41215449 or counting every byte How to read and echo file size of uploaded file being written at server in real time without blocking at both server and client?. |
OS froze at plnkr during tests. Registering and un-registering |
The plnkr throws error at Nightly 84
Some observations running the below code https://plnkr.co/edit/P2op0uo5YBA5eEEm?open=lib%2Fscript.js at Chromium 88, which might not be the exact requirement, though is a start and extensible.
Utilizing clinet-side code we can get progress of bytes enqueed, post messages containing download status to main thread using index.html
lib/script.js
sw.js
|
Once the
Also, its not clear what the signal on a Response would accomplish. If you want to abort the Response body you can just error the body stream, no?
Firefox does not support We Tested several hundred runs at Chromium 88 to derive the current working example that still requires independent verification. The main issue that encountered when testing is Running the code at Firefox or Nightly at
Have not yet successfully run the code at Mozilla browsers. The working Chromium version provides a template of how the code can work at Firefox, given similar implementations and support. From what can gather from the entirety of the issue this is resulting interpretation of a potential solution to handle both aborting the download and notifying the client of the state of the download. Kindly verify the code produces the expected output and handles the use cases described based on own interpreation of the issue, above. index.html <!DOCTYPE html>
<html>
<head>
<script src="lib/script.js"></script>
</head>
<body>
<button id="start">Start download</button>
<button id="abort">Abort download</button>
</body>
</html> lib/script.js const unregisterServiceWorkers = async (_) => {
const registrations = await navigator.serviceWorker.getRegistrations();
for (const registration of registrations) {
console.log(registration);
try {
await registration.unregister();
} catch (e) {
throw e;
}
}
return `ServiceWorker's unregistered`;
};
const bc = new BroadcastChannel('downloads');
bc.onmessage = (e) => {
console.log(e.data);
};
onload = async (_) => {
console.log(await unregisterServiceWorkers());
document.querySelector('#abort').onclick = (_) =>
bc.postMessage({ abort: true });
document.querySelector('#start').onclick = async (_) => {
console.log(await unregisterServiceWorkers());
console.log(
await navigator.serviceWorker.register('sw.js', { scope: './' })
);
let node = document.querySelector('iframe');
if (node) document.body.removeChild(node);
const iframe = document.createElement('iframe');
iframe.onload = async (e) => {
console.log(e);
};
document.body.append(iframe);
iframe.src = './ping';
};
}; sw.js // https://stackoverflow.com/a/34046299
self.addEventListener('install', (event) => {
// Bypass the waiting lifecycle stage,
// just in case there's an older version of this SW registration.
event.waitUntil(self.skipWaiting());
});
self.addEventListener('activate', (event) => {
// Take control of all pages under this SW's scope immediately,
// instead of waiting for reload/navigation.
event.waitUntil(self.clients.claim());
});
self.addEventListener('fetch', (event) => {
console.log(event.request);
if (event.request.url.endsWith('ping')) {
var encoder = new TextEncoder();
var bytes = 0;
var n = 0;
var abort = false;
let aborted = false;
var res;
const bc = new BroadcastChannel('downloads');
bc.onmessage = (e) => {
console.log(e.data);
if (e.data.abort) {
abort = true;
}
};
var controller = new AbortController();
var signal = controller.signal;
console.log(controller, signal);
signal.onabort = (e) => {
console.log(
`Event type:${e.type}\nEvent target:${e.target.constructor.name}`
);
};
var readable = new ReadableStream({
async pull(c) {
if (n === 10 && !abort) {
c.close();
return;
}
const data = encoder.encode(n + '\n');
bytes += data.buffer.byteLength;
c.enqueue(data);
bc.postMessage({ bytes, aborted });
await new Promise((r) => setTimeout(r, 1000));
++n;
},
cancel(reason) {
console.log(
`readable cancel(reason):${reason.join(
'\n'
)}\nreadable ReadableStream.locked:${readable.locked}\na locked:${
a.locked
}\nb.locked:${b.locked}`
);
},
});
var [a, b] = readable.tee();
console.log({ readable, a, b });
async function cancelable() {
if ('pipeTo' in b) {
var writeable = new WritableStream({
async write(v, c) {
console.log(v);
if (abort) {
controller.abort();
try {
console.log(await a.cancel('Download aborted!'));
} catch (e) {
console.error(e);
}
}
},
abort(reason) {
console.log(
`abort(reason):${reason}\nWritableStream.locked:${writeable.locked}`
);
},
});
return b
.pipeTo(writeable, { preventCancel: false, signal })
.catch((e) => {
console.log(
`catch(e):${e}\nReadableStream.locked:${readable.locked}\nWritableStream.locked:${writeable.locked}`
);
bc.postMessage({ aborted: true });
return 'Download aborted.';
});
} else {
var reader = b.getReader();
return reader.read().then(async function process({ value, done }) {
if (done) {
if (abort) {
reader.releaseLock();
reader.cancel();
console.log(await a.cancel('Download aborted!'));
bc.postMessage({ aborted: true });
}
return reader.closed.then((_) => 'Download aborted.');
}
return reader.read().then(process).catch(console.error);
});
}
}
var downloadable = cancelable().then((result) => {
console.log({ result });
const headers = {
'content-disposition': 'attachment; filename="filename.txt"',
};
try {
bc.postMessage({ done: true });
bc.close();
res = new Response(a, { headers, cache: 'no-store' });
console.log(res);
return res;
} catch (e) {
console.error(e);
} finally {
console.assert(res, { res });
}
});
evt.respondWith(downloadable);
}
});
console.log('que?'); Updated plnkr https://plnkr.co/edit/P2op0uo5YBA5eEEm |
I have another older (original) idea in mind also that didn't work earlier. Blink (v85) have recently gotten support for streaming upload. Example: await fetch('https://httpbin.org/post', {
method: 'POST',
body: new ReadableStream({
start(ctrl) {
ctrl.enqueue(new Uint8Array([97])) // a
ctrl.close()
}
})
}).then(r => r.json()).then(j=>j.data) // "a" None of the other browser supports it yet. but it can simplify stuff quite a bit. // oversimplified (you need two fetch events for this to work)
// canceling the ajax with a AbortSignal or interrupt the readableStream from the main thread can abort the download (aka: iframeEvent).
// canceling the downloading iframe body (from browser UI) can abort the ajax (aka: ajaxEvent)
iframeEvent.respondWith(new Response(ajaxEvent.request.body, { headers: ajaxEvent.request.headers }))
ajaxEvent.respondWith(new Response(iframeEvent.request.body))
|
i think at this point it may be worth moving this discussion to another issue, as whatever problem you're solving here isn't the one that was originally reported. |
I suggest breaking the issue into parts, as I have done. The entire problem statement should be in the OP of each and every issue or bug filed. Instead of appending additional core requirements in subsequent posts. Yes, the problem is solved, here. At Chromium you have the ability to transfer the stream to the service worker, for the sole purpose of initiating a download via iframe. it is not necessary to commence the stream in the service worker. Again, your Chromium issue is valid, but this repository cannot solve that problem or fix that bug, either chrome://downloads aspect or cancel(){} being fired in the service worker. That is the purpose of the Chromium bug. My sole intent is to Fix WontFix, to provide workarounds until implementers actually fix the bug. If that is not viable for you, right now, in order to implement your own application, then I suggest the proper venue is the Chrimium bug - as again, whatever fixes for this issue, in this repository, AFAIK, are not binding on Chromium authors to implement. |
I meet similar situation today. And I try to change diff --git a/node_modules/streamsaver/StreamSaver.js b/node_modules/streamsaver/StreamSaver.js
index 018ddc3..acc9288 100644
--- a/node_modules/streamsaver/StreamSaver.js
+++ b/node_modules/streamsaver/StreamSaver.js
@@ -154,6 +154,9 @@
} else {
opts = options || {}
}
+
+ let stream
+
if (!useBlobFallback) {
loadTransporter()
@@ -210,7 +213,23 @@
channel.port1.postMessage({ readableStream }, [ readableStream ])
}
+ let aborted
+
channel.port1.onmessage = evt => {
+ if (aborted) {
+ return
+ }
+
+ if (evt.data.aborted) {
+ channel.port1.onmessage = null
+ aborted = true
+ if (stream._writer) {
+ stream._writer.abort()
+ stream._writer = undefined
+ }
+ return stream.abort()
+ }
+
// Service worker sent us a link that we should open.
if (evt.data.download) {
// Special treatment for popup...
@@ -249,7 +268,7 @@
let chunks = []
- return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
+ stream = (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
write (chunk) {
if (!(chunk instanceof Uint8Array)) {
throw new TypeError('Can only wirte Uint8Arrays')
@@ -302,6 +321,8 @@
channel = null
}
}, opts.writableStrategy)
+
+ return stream
}
return streamSaver And add the following into cancel() {
port.postMessage({ aborted: true })
console.log('user aborted')
} It seems working on Firefox for most cases to me, while there are still two problems:
|
My related source codes: import * as streamSaver from 'streamsaver'
import { WritableStream } from 'web-streams-polyfill/ponyfill'
export const pipeStream = async <T = unknown>(
reader: ReadableStreamDefaultReader<T>,
writer: WritableStreamDefaultWriter<T>,
signal?: AbortSignal,
) => {
let chunkResult: ReadableStreamDefaultReadResult<T>
let aborted: boolean | undefined
while (!signal?.aborted && !(chunkResult = await reader.read()).done) {
try {
await writer.write(chunkResult.value)
} catch (err) {
if (signal?.aborted) {
break
}
if (!err) {
aborted = true
break
}
throw err
}
}
if (signal?.aborted || aborted) {
await Promise.all([reader.cancel(), writer.abort()])
throw new DOMException('aborted', 'AbortError')
}
return writer.close()
}
export const downloadFile = async <T = unknown>(
readStream: ReadableStream<T>,
fileName: string,
signal?: AbortSignal,
) => {
if (
(__DEV__ || location.protocol === 'https:') &&
window.showSaveFilePicker
) {
const handle = await window.showSaveFilePicker({
suggestedName: fileName,
})
return pipeStream(
readStream.getReader(),
await handle.createWritable<T>(),
signal,
)
}
if (streamSaver.mitm !== '/streamsaver/mitm.html') {
Object.assign(streamSaver, {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
WritableStream: streamSaver.WritableStream || WritableStream,
mitm: '/streamsaver/mitm.html',
})
}
const writeStream = streamSaver.createWriteStream(fileName)
// Safari
if (typeof readStream.pipeTo === 'function') {
return readStream.pipeTo(writeStream, { signal })
}
// Firefox
return pipeStream(readStream.getReader(), writeStream.getWriter(), signal)
} |
Chrome issue (#638494) got merged feel days ago into upstream (https://chromium-review.googlesource.com/c/chromium/src/+/3347484).. Getting a different behaviour on Canary, |
Okay, below are my findings: For Chrome, Canary version const streamSaver = StreamSaver.createWriteStream(`abc.zip`);
this.fileStream = streamSaver.getWriter();
readable.on('data', (d) => {
if (this.cancelled) return;
this.fileStream.write(d)
.catch((e) => { this.cancelled = true; this.abort(); });
}); However, testing on Firefox, webworker does print @jimmywarting do you believe this commit could be merged or there are any other cases I'm not handling properly? |
@gwdp hmm, yea maybe. However I would like to rewrite the hole When readable streams are not transferable......Then we use So rather than pushing data from main thread to the service worker I instead built a The source is inspired by @MattiasBuelens remote-web-streams edit: i wish he would have named it transferable-stream-shim or polyfill so it could be easier to google :P |
Wow 🤯. If I understood properly, that would involve a major refactor on the code (simplification as well), sounds like the next release of StreamSaver? 🤓 I have been using StreamSaver for a while now and I found about For the download cancelled event issue I believe this will need to be handled anyways; Opening a PR for that so other folks can have this fixed in the current version :) |
I know, I know. 😛 Granted, I made that library before transferable streams were defined in the spec, so I didn't know what the spec would end up calling it. Also, I'm hesitant to call it a "polyfill". For it to be a proper polyfill, it would need to patch |
One way to check when
if necessary handle cannot write to a closing writable stream error(s). |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
@gwdp For completeness when closing the writable side in the pattern release the lock as well for the
If a handler is attached where |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
Native Messaging works in Firefox and Chrome. The user installs the extension locally. On Chromium simply select "Developer mode" at chrome://extensions then click "Load unpacked". No external resources or requests or Chrome Web Store are required. Then you can use the same loading of That allows you to use If you are talking about insecure pages, and requesting CDN's, all of those concerns go away when you are only running code already on your own machine. Alternatively, use the Native Messaging host to download directly to the users' local file system. I already filed a PR for this capability. You appeared to think unpacking a local extension involves external code or requests, it does not, and is at least as secure as the MITM code and requesting CDN's you are already using. |
No visitor on your web site is ever going to do this... and you shouldn't force the user do this either |
I think you misunderstand what I am conveying. I am not talking about visiting websites, or StreamSaver used by websites. I am talking about the end user that wants to use your StreamSaver code on their own machine. Native Messaging is useful for me and other users to, for example, stream speech synthesis engine output from local speech synthesis engine as a From my perspetive StreamSaver concept is based on streaming download of files on any browser. So you can basically do the opposite of what I do here https://github.com/guest271314/captureSystemAudio/tree/master/native_messaging/capture_system_audio (using
Native Messaging allows you to deploy your concept locally, to your precise specification, without reliance on any extrnal code - for end users. Perhaps I misunderstand and your code is written primarily for web site developers exlusively
not individual users that want to run your gear locally on any site they happen to be visting. Again, good luck. |
is there a way to know if the download has been cancelled by the user?
The text was updated successfully, but these errors were encountered: