summaryrefslogtreecommitdiffhomepage
path: root/src/lib/components
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-04-09 23:40:59 +0900
committerAdam Malczewski <[email protected]>2026-04-09 23:40:59 +0900
commitb650a2ede779c5f2e84412bf30d9c182d92e83f6 (patch)
tree414b59c77bc56f894fde4c59fa0c3012fcf54233 /src/lib/components
parent1c64cfc8feb2aee5d622a50f2f4878281b9dcc05 (diff)
downloadflashair-speedsync-b650a2ede779c5f2e84412bf30d9c182d92e83f6.tar.gz
flashair-speedsync-b650a2ede779c5f2e84412bf30d9c182d92e83f6.zip
debug issue with IndexedDB
Diffstat (limited to 'src/lib/components')
-rw-r--r--src/lib/components/CacheDebug.svelte85
-rw-r--r--src/lib/components/ImagePreview.svelte46
2 files changed, 121 insertions, 10 deletions
diff --git a/src/lib/components/CacheDebug.svelte b/src/lib/components/CacheDebug.svelte
new file mode 100644
index 0000000..293ff62
--- /dev/null
+++ b/src/lib/components/CacheDebug.svelte
@@ -0,0 +1,85 @@
+<script lang="ts">
+ import { imageCache } from '../cache';
+
+ interface CacheStats {
+ entries: number;
+ fullCount: number;
+ thumbCount: number;
+ totalBytes: number;
+ idbEntries: number;
+ idbBytes: number;
+ idbError: string | undefined;
+ }
+
+ let stats = $state<CacheStats | undefined>(undefined);
+ let refreshing = $state(false);
+ let collapsed = $state(false);
+
+ function formatBytes(bytes: number): string {
+ if (bytes === 0) return '0 B';
+ const k = 1024;
+ const sizes = ['B', 'KB', 'MB', 'GB'];
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
+ const val = bytes / Math.pow(k, i);
+ return `${val.toFixed(1)} ${sizes[i]}`;
+ }
+
+ async function refresh() {
+ refreshing = true;
+ try {
+ stats = await imageCache.getStats();
+ } catch { /* ignore */ }
+ refreshing = false;
+ }
+
+ $effect(() => {
+ void refresh();
+ const id = setInterval(() => { void refresh(); }, 2000);
+ return () => clearInterval(id);
+ });
+</script>
+
+<div class="fixed top-2 left-2 z-50 bg-base-100/95 backdrop-blur-sm rounded-box shadow-lg border border-base-300 p-3 text-xs font-mono max-w-80 pointer-events-auto">
+ <button
+ class="flex items-center justify-between w-full mb-1"
+ onclick={() => collapsed = !collapsed}
+ >
+ <span class="font-bold text-sm">Cache Debug</span>
+ <span class="text-base-content/50">{collapsed ? '▶' : '▼'}</span>
+ </button>
+
+ {#if !collapsed && stats !== undefined}
+ <div class="space-y-2">
+ <!-- Memory Cache -->
+ <div>
+ <div class="font-semibold text-primary">Memory Cache</div>
+ <div>Entries: <span class="text-info">{stats.entries}</span> ({stats.fullCount} full, {stats.thumbCount} thumb)</div>
+ <div>Size: <span class="text-info">{formatBytes(stats.totalBytes)}</span></div>
+ </div>
+
+ <!-- IndexedDB -->
+ <div class="border-t border-base-300 pt-2">
+ <div class="font-semibold text-secondary">IndexedDB</div>
+ {#if stats.idbError !== undefined}
+ <div class="text-error">Error: {stats.idbError}</div>
+ {:else}
+ <div>Entries: <span class="text-info">{stats.idbEntries}</span></div>
+ <div>Size: <span class="text-info">{formatBytes(stats.idbBytes)}</span></div>
+ <div class="mt-1">
+ {#if stats.idbEntries === stats.entries}
+ <span class="text-success">✅ In sync with memory</span>
+ {:else if stats.idbEntries < stats.entries}
+ <span class="text-warning">⏳ IDB behind ({stats.entries - stats.idbEntries} pending)</span>
+ {:else}
+ <span class="text-info">IDB has {stats.idbEntries - stats.entries} extra (pre-loaded)</span>
+ {/if}
+ </div>
+ {/if}
+ </div>
+ </div>
+ {:else if !collapsed}
+ <div class="text-base-content/50">
+ {#if refreshing}Loading…{:else}No data{/if}
+ </div>
+ {/if}
+</div>
diff --git a/src/lib/components/ImagePreview.svelte b/src/lib/components/ImagePreview.svelte
index 20a9937..914f9a1 100644
--- a/src/lib/components/ImagePreview.svelte
+++ b/src/lib/components/ImagePreview.svelte
@@ -224,26 +224,36 @@
};
});
+ /**
+ * The path currently being loaded. Used to detect stale async results
+ * without relying on the AbortController (which the $effect cleanup
+ * may fire prematurely if Svelte re-schedules the effect).
+ */
+ let activePath: string | undefined;
+
$effect(() => {
const currentFile = file;
if (currentFile === undefined) {
+ activePath = undefined;
cleanup();
return;
}
+ // Track which path we are loading so async callbacks can detect staleness.
+ activePath = currentFile.path;
+
// Reset zoom on file change
resetZoom();
- // Clear stale state synchronously before async loads
- imgNaturalW = 0;
- imgNaturalH = 0;
+ // Revoke previous thumbnail blob URL (plain let, not $state)
if (rawThumbnailUrl !== undefined) {
URL.revokeObjectURL(rawThumbnailUrl);
rawThumbnailUrl = undefined;
}
- thumbnailBlobUrl = undefined;
- imageAspectRatio = '3 / 2';
+ // Kick off async loaders — they handle most $state writes internally.
+ // resetZoom() above writes to $state (zoomLevel, panX, panY) but those
+ // are never read in this $effect body, so they don't add dependencies.
loadThumbnail(currentFile);
loadFullImage(currentFile);
@@ -282,11 +292,18 @@
}
async function loadThumbnail(entry: FlashAirFileEntry) {
+ // Reset thumbnail state at the start of each load
+ thumbnailBlobUrl = undefined;
+ imageAspectRatio = '3 / 2';
+
const url = flashair.thumbnailUrl(entry.path);
if (url === undefined) return;
// Try cache first
const cached = await imageCache.get('thumbnail', entry.path);
+ // Check staleness after await
+ if (activePath !== entry.path) return;
+
if (cached !== undefined) {
const blobUrl = URL.createObjectURL(cached.blob);
rawThumbnailUrl = blobUrl;
@@ -300,6 +317,8 @@
try {
const { blob, meta } = await flashair.fetchThumbnail(entry.path);
+ // Check staleness after await
+ if (activePath !== entry.path) return;
// Store in cache (fire-and-forget)
void imageCache.put('thumbnail', entry.path, blob, meta);
const blobUrl = URL.createObjectURL(blob);
@@ -315,11 +334,14 @@
}
async function loadFullImage(entry: FlashAirFileEntry) {
+ // Reset state at the start of each load
if (rawObjectUrl !== undefined) {
URL.revokeObjectURL(rawObjectUrl);
rawObjectUrl = undefined;
}
fullObjectUrl = undefined;
+ imgNaturalW = 0;
+ imgNaturalH = 0;
progress = 0;
loadError = undefined;
@@ -331,7 +353,11 @@
// Try cache first — before setting downloading=true to avoid flicker
const cached = await imageCache.get('full', entry.path);
- if (cached !== undefined && !abort.signal.aborted) {
+ // Use activePath for staleness: the abort signal may have been tripped by
+ // a Svelte effect re-schedule even though the user didn't change images.
+ if (activePath !== entry.path) return;
+
+ if (cached !== undefined) {
const objectUrl = URL.createObjectURL(cached.blob);
rawObjectUrl = objectUrl;
fullObjectUrl = objectUrl;
@@ -358,7 +384,7 @@
const reader = res.body?.getReader();
if (reader === undefined) {
const blob = await res.blob();
- if (abort.signal.aborted) return;
+ if (abort.signal.aborted || activePath !== entry.path) return;
// Store in cache (fire-and-forget)
void imageCache.put('full', entry.path, blob);
autoCacheService.markCached(entry.path);
@@ -382,7 +408,7 @@
progress = totalBytes > 0 ? received / totalBytes : 0;
}
- if (abort.signal.aborted) return;
+ if (abort.signal.aborted || activePath !== entry.path) return;
const blob = new Blob(chunks);
// Store in cache (fire-and-forget)
@@ -393,10 +419,10 @@
fullObjectUrl = objectUrl;
progress = 1;
} catch (e) {
- if (abort.signal.aborted) return;
+ if (abort.signal.aborted || activePath !== entry.path) return;
loadError = e instanceof Error ? e.message : String(e);
} finally {
- if (!abort.signal.aborted) {
+ if (!abort.signal.aborted && activePath === entry.path) {
downloading = false;
autoCacheService.resumeAfterUserDownload();
}