From 3c42834d4776d71ebac2eb52f652bcf6da996369 Mon Sep 17 00:00:00 2001 From: apt-get Date: Sun, 6 Sep 2026 02:23:06 +0200 Subject: [PATCH] display cache usage and warn if not enough disk space available --- src/lib/streamCache.svelte.ts | 39 +++++++++++++++++++++++++++++++ src/routes/streams/Sidebar.svelte | 23 +++++++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/src/lib/streamCache.svelte.ts b/src/lib/streamCache.svelte.ts index d9be581..a3a698c 100644 --- a/src/lib/streamCache.svelte.ts +++ b/src/lib/streamCache.svelte.ts @@ -17,6 +17,15 @@ const CHUNK_SIZE = 4 * 1024 * 1024; // as the intent is to catch socket breakage during cellular network changes for example const CHUNK_TIMEOUT = 60_000; +class QuotaError extends Error { + constructor(readonly missing: number) { + super(); + } +} + +// total bytes used by our OPFS origin +export const usage = $state<{ bytes?: number }>({}); + export const cached = new SvelteSet(readStored(STORAGE_KEY, [])); export const downloading = new SvelteMap(); @@ -26,6 +35,7 @@ export const partials = new SvelteMap( ); if (browser) { + refreshUsage(); $effect.root(() => { $effect(() => { writeStored(STORAGE_KEY, [...cached]); @@ -52,6 +62,12 @@ export async function download(stream: StreamSummary) { cached.add(stream.id); return; } catch (err) { + if (err instanceof QuotaError) { + alert( + `not enough disk space ;_;, need ${Math.ceil(err.missing / 1_000_000)} MB more.` + ); + return; + } console.error(`[streamCache] ${stream.id} attempt failed`, err); // reset attempts if download of at least one chunk successful attempt = (partials.get(stream.id)?.received ?? 0) > before ? 0 : attempt + 1; @@ -84,16 +100,33 @@ export async function getUrl(streamId: string): Promise { async function attemptDownload(stream: StreamSummary) { let state = partials.get(stream.id); + let checkedSpace = false; while (!state || state.received < state.total) { state = await fetchChunk(stream, state); partials.set(stream.id, state); downloading.set(stream.id, percent(state.received, state.total)); + + // first chunk's HTTP response carries the total stream size + if (!checkedSpace) { + await checkSpace(stream.id, state); + checkedSpace = true; + } } console.log(`[streamCache] ${stream.id} assembling ${state.total} bytes`); await assemble(stream.id, state.total); console.log(`[streamCache] ${stream.id} assembled`); } +/** check available space below what's needed to download the stream */ +async function checkSpace(streamId: string, state: PartialDownload) { + const { usage: used, quota } = await navigator.storage.estimate(); + if (used === undefined || quota === undefined) return; + + // extra CHUNK_SIZE is due to the delta in `assemble()` merging one chunk in (+1) then deleting it + const peak = used + (state.total - state.received) + CHUNK_SIZE; + if (peak > quota) throw new QuotaError(peak - quota); +} + /** fetch chunk starting from the last valid one, store in OPFS, and return updated state */ async function fetchChunk(stream: StreamSummary, state?: PartialDownload) { const start = state?.received ?? 0; @@ -194,6 +227,12 @@ async function sweep(force?: string) { orphans.push(name); } for (const name of orphans) await parts.removeEntry(name, { recursive: true }).catch(() => {}); + + await refreshUsage(); +} + +async function refreshUsage() { + usage.bytes = (await navigator.storage.estimate()).usage; } /** exponential backoff, cut short if the connection comes back before it elapses */ diff --git a/src/routes/streams/Sidebar.svelte b/src/routes/streams/Sidebar.svelte index 34426d4..e147a19 100644 --- a/src/routes/streams/Sidebar.svelte +++ b/src/routes/streams/Sidebar.svelte @@ -15,6 +15,7 @@ let listOpen = $state(); let displayedStreams = $derived.by(streamsToDisplay); let remainingTags = $derived.by(getRemainingTags); + let cacheUsage = $derived(streamCache.usage.bytes); $effect(() => { if (displayedStreams.length == 1) { @@ -68,11 +69,12 @@ {#each ctx.streams as stream} {@const favorited = favoritedStreams.has(stream.id)} {@const isCached = streamCache.cached.has(stream.id)} + {@const isPartial = streamCache.partials.has(stream.id)} {@const current = ctx.current?.id === stream.id}