diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/downloads.js')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/downloads.js | 46 |
1 files changed, 44 insertions, 2 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index 7f48fed..90f88b0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -192,7 +192,22 @@ async function serviceWorker() { if (!_swReady) { _swReady = navigator.serviceWorker.register(SW_PATH, { scope: '/' }) .then(() => navigator.serviceWorker.ready) - .then(reg => reg.active || navigator.serviceWorker.controller) + .then(async (reg) => { + // `reg.active` is not enough. A worker can be active while this page is + // still uncontrolled, and an uncontrolled page's requests are never + // handed to its fetch handler — so the worker would take our stream and + // then never be asked for it. The iframe would 404, nothing would read + // the stream, and the first write() would block for good: a download + // stuck at one chunk. + if (navigator.serviceWorker.controller) return navigator.serviceWorker.controller; + // sw.js claims clients on activate, so control usually arrives within a + // tick of registration. Wait briefly rather than give up at once. + return await new Promise((resolve) => { + const done = () => resolve(navigator.serviceWorker.controller || null); + navigator.serviceWorker.addEventListener('controllerchange', done, { once: true }); + setTimeout(done, 3000); + }); + }) .catch(err => { console.warn('[MeshBay] service worker unavailable:', err.message); return null; @@ -221,8 +236,20 @@ export async function openStreamedDownload(filename, size = 0) { const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; const { readable, writable } = new TransformStream(); + // The worker tells us when it actually answers the iframe. Without that + // confirmation this path fails silently: the write side blocks on + // backpressure that will never be relieved, which reads as a download frozen + // after one chunk rather than as an error. + const chan = new MessageChannel(); + const serving = new Promise((resolve) => { + chan.port1.onmessage = (e) => { + if (e.data && e.data.type === 'mbdl-serving') resolve(true); + }; + }); + try { - worker.postMessage({ type: 'mbdl', id, filename, size, readable }, [readable]); + worker.postMessage({ type: 'mbdl', id, filename, size, readable, port: chan.port2 }, + [readable, chan.port2]); } catch (err) { // Transferable streams are what makes the backpressure work; without them // this would be a memory buffer wearing a stream's clothes. @@ -235,6 +262,21 @@ export async function openStreamedDownload(filename, size = 0) { frame.src = `/_mbdl/${id}`; document.body.appendChild(frame); + const answered = await Promise.race([ + serving, + new Promise((r) => setTimeout(() => r(false), 8000)), + ]); + if (!answered) { + // Some browsers refuse a download started from a hidden iframe, and an + // uncontrolled page never reaches the worker at all. Say so and let the + // caller fall back rather than hand back a sink nothing drains. + console.warn('[MeshBay] the service worker never served the download; ' + + 'falling back'); + frame.remove(); + try { await writable.abort('not served'); } catch { /* already gone */ } + return null; + } + const writer = writable.getWriter(); return { name: filename, |