summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--src/lib/cache/imageCache.ts27
-rw-r--r--src/lib/components/CacheDebug.svelte59
2 files changed, 83 insertions, 3 deletions
diff --git a/src/lib/cache/imageCache.ts b/src/lib/cache/imageCache.ts
index 2326974..5ff7393 100644
--- a/src/lib/cache/imageCache.ts
+++ b/src/lib/cache/imageCache.ts
@@ -109,6 +109,10 @@ async function idbGet(key: CacheKey): Promise<StoredRecord | undefined> {
});
}
+/** Last write error (if any) — exposed via getStats() for diagnostics. */
+let lastWriteError: string | undefined;
+let writeErrorCount = 0;
+
/** Write a record to IndexedDB (best-effort). */
async function idbPut(record: StoredRecord): Promise<void> {
const db = await openDb();
@@ -119,9 +123,22 @@ async function idbPut(record: StoredRecord): Promise<void> {
const tx = db.transaction(STORE_NAME, 'readwrite');
const store = tx.objectStore(STORE_NAME);
store.put(record);
- tx.oncomplete = () => resolve();
- tx.onerror = () => resolve();
- } catch {
+ tx.oncomplete = () => {
+ lastWriteError = undefined;
+ resolve();
+ };
+ tx.onerror = () => {
+ const msg = tx.error?.message ?? 'unknown write error';
+ lastWriteError = msg;
+ writeErrorCount++;
+ console.warn(`[imageCache] IDB write failed: ${msg}`);
+ resolve();
+ };
+ } catch (e) {
+ const msg = e instanceof Error ? e.message : String(e);
+ lastWriteError = msg;
+ writeErrorCount++;
+ console.warn(`[imageCache] IDB write exception: ${msg}`);
resolve();
}
});
@@ -280,6 +297,8 @@ export const imageCache = {
idbEntries: number;
idbBytes: number;
idbError: string | undefined;
+ idbLastWriteError: string | undefined;
+ idbWriteErrorCount: number;
}> {
let fullCount = 0;
let thumbCount = 0;
@@ -332,6 +351,8 @@ export const imageCache = {
idbEntries,
idbBytes,
idbError,
+ idbLastWriteError: lastWriteError,
+ idbWriteErrorCount: writeErrorCount,
};
},
diff --git a/src/lib/components/CacheDebug.svelte b/src/lib/components/CacheDebug.svelte
index 293ff62..09e859d 100644
--- a/src/lib/components/CacheDebug.svelte
+++ b/src/lib/components/CacheDebug.svelte
@@ -9,6 +9,8 @@
idbEntries: number;
idbBytes: number;
idbError: string | undefined;
+ idbLastWriteError: string | undefined;
+ idbWriteErrorCount: number;
}
let stats = $state<CacheStats | undefined>(undefined);
@@ -24,11 +26,44 @@
return `${val.toFixed(1)} ${sizes[i]}`;
}
+ let storageEstimate = $state<{ usage: number; quota: number } | undefined>(undefined);
+ let storageError = $state<string | undefined>(undefined);
+
async function refresh() {
refreshing = true;
try {
stats = await imageCache.getStats();
} catch { /* ignore */ }
+
+ // Try standard Storage API first, then webkit fallback
+ storageEstimate = undefined;
+ storageError = undefined;
+ try {
+ if (navigator.storage && navigator.storage.estimate) {
+ const est = await navigator.storage.estimate();
+ storageEstimate = { usage: est.usage ?? 0, quota: est.quota ?? 0 };
+ } else if ('webkitTemporaryStorage' in navigator) {
+ // Chrome HTTP fallback
+ const wk = (navigator as Record<string, unknown>)['webkitTemporaryStorage'] as
+ { queryUsageAndQuota: (ok: (u: number, q: number) => void, err: (e: unknown) => void) => void } | undefined;
+ if (wk !== undefined) {
+ const result = await new Promise<{ usage: number; quota: number }>((resolve, reject) => {
+ wk.queryUsageAndQuota(
+ (usage, quota) => resolve({ usage, quota }),
+ (err) => reject(err),
+ );
+ });
+ storageEstimate = result;
+ } else {
+ storageError = 'Storage API not available (HTTP origin)';
+ }
+ } else {
+ storageError = 'Storage API not available';
+ }
+ } catch (e) {
+ storageError = e instanceof Error ? e.message : String(e);
+ }
+
refreshing = false;
}
@@ -65,6 +100,12 @@
{:else}
<div>Entries: <span class="text-info">{stats.idbEntries}</span></div>
<div>Size: <span class="text-info">{formatBytes(stats.idbBytes)}</span></div>
+ {#if stats.idbWriteErrorCount > 0}
+ <div class="text-error mt-1">❌ {stats.idbWriteErrorCount} write errors</div>
+ {#if stats.idbLastWriteError !== undefined}
+ <div class="text-error text-[10px] break-all">{stats.idbLastWriteError}</div>
+ {/if}
+ {/if}
<div class="mt-1">
{#if stats.idbEntries === stats.entries}
<span class="text-success">✅ In sync with memory</span>
@@ -76,6 +117,24 @@
</div>
{/if}
</div>
+ <!-- Storage Quota -->
+ <div class="border-t border-base-300 pt-2">
+ <div class="font-semibold text-accent">Storage Quota</div>
+ {#if storageEstimate !== undefined}
+ <div>Used: <span class="text-info">{formatBytes(storageEstimate.usage)}</span> / {formatBytes(storageEstimate.quota)}</div>
+ {#if storageEstimate.quota > 0}
+ {@const pct = (storageEstimate.usage / storageEstimate.quota * 100)}
+ <div>
+ <progress class="progress progress-primary w-full h-2" value={pct} max="100"></progress>
+ <span class:text-error={pct > 90} class:text-warning={pct > 70 && pct <= 90}>{pct.toFixed(1)}%</span>
+ </div>
+ {/if}
+ {:else if storageError !== undefined}
+ <div class="text-warning">{storageError}</div>
+ {:else}
+ <div class="text-base-content/50">Querying…</div>
+ {/if}
+ </div>
</div>
{:else if !collapsed}
<div class="text-base-content/50">