display cache usage and warn if not enough disk space available

This commit is contained in:
2026-09-06 02:23:06 +02:00
parent 343447c37c
commit 3c42834d47
2 changed files with 61 additions and 1 deletions
+39
View File
@@ -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<string>(readStored<string[]>(STORAGE_KEY, []));
export const downloading = new SvelteMap<string, number>();
@@ -26,6 +35,7 @@ export const partials = new SvelteMap<string, PartialDownload>(
);
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<string | null> {
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 */
+22 -1
View File
@@ -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}
<li
hidden={!displayedStreams.includes(stream) ||
(favoritesOnly && !favorited) ||
(cachedOnly && !isCached)}
(cachedOnly && !(isCached || isPartial))}
class="stream-item {current ? 'current-stream' : ''}"
id="stream-{stream.id}"
>
@@ -131,6 +133,18 @@
{/each}
</ul>
{#if cachedOnly && (streamCache.cached.size || streamCache.partials.size)}
<p class="cache-usage">
{#if cacheUsage !== undefined}
{cacheUsage > 1_000_000_000
? (cacheUsage / 1_000_000_000).toFixed(1) + ' GB'
: Math.round(cacheUsage / 1_000_000) + ' MB'} of cached streams
{:else}
Calculating…
{/if}
</p>
{/if}
<style>
.stream-list {
list-style-type: none;
@@ -192,6 +206,13 @@
margin-bottom: 1px;
}
.cache-usage {
margin: 0;
padding: 4px 5px;
font-family: Tahoma;
font-size: smaller;
}
.material-icons {
margin-bottom: 0px;
color: black;