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 | 81 |
1 files changed, 78 insertions, 3 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js index a71f289..7f48fed 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/downloads.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/downloads.js @@ -171,8 +171,83 @@ export async function openTarget(filename) { } /** - * Below this, a download with no granted folder is collected in memory and - * handed to the browser, which saves it without asking. Above it that would - * mean holding gigabytes in a tab, so it is worth one Save As dialog instead. + * Below this, a download with no granted folder and no service worker is + * collected in memory and handed to the browser. Above it that would mean + * holding gigabytes in a tab, so it is worth one Save As dialog instead. */ export const BLOB_LIMIT = 512 * 1024 * 1024; + +// ── Streaming to disk without the File System Access API ──────────────────── + +const SW_PATH = '/sw.js'; +let _swReady = null; + +export const STREAMS_VIA_SW = typeof window !== 'undefined' + && 'serviceWorker' in navigator + && typeof TransformStream === 'function' + && window.isSecureContext; + +async function serviceWorker() { + if (!STREAMS_VIA_SW) return null; + if (!_swReady) { + _swReady = navigator.serviceWorker.register(SW_PATH, { scope: '/' }) + .then(() => navigator.serviceWorker.ready) + .then(reg => reg.active || navigator.serviceWorker.controller) + .catch(err => { + console.warn('[MeshBay] service worker unavailable:', err.message); + return null; + }); + } + return _swReady; +} + +/** + * A sink the browser writes to disk, for Firefox and anything else without the + * File System Access API. + * + * The page keeps the writable half of a stream and gives the readable half to + * the service worker, which answers a made-up URL with it. Navigating a hidden + * iframe there turns it into an ordinary download: written as it arrives, with + * the browser's own progress, and nothing held in the tab. Backpressure is + * real — `writer.write()` waits when the browser is behind. + * + * Returns {writable, name} shaped like the File System Access one, or null if + * this browser cannot do it either. + */ +export async function openStreamedDownload(filename, size = 0) { + const worker = await serviceWorker(); + if (!worker) return null; + + const id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`; + const { readable, writable } = new TransformStream(); + + try { + worker.postMessage({ type: 'mbdl', id, filename, size, readable }, [readable]); + } catch (err) { + // Transferable streams are what makes the backpressure work; without them + // this would be a memory buffer wearing a stream's clothes. + console.warn('[MeshBay] streams cannot be transferred here:', err.message); + return null; + } + + const frame = document.createElement('iframe'); + frame.hidden = true; + frame.src = `/_mbdl/${id}`; + document.body.appendChild(frame); + + const writer = writable.getWriter(); + return { + name: filename, + writable: { + write: (bytes) => writer.write(bytes), + close: async () => { + await writer.close(); + setTimeout(() => frame.remove(), 2000); + }, + abort: async (reason) => { + try { await writer.abort(reason); } catch { /* already gone */ } + frame.remove(); + }, + }, + }; +} |