From 03b74089a0c475a04759f87e0c1f9f57bc59db21 Mon Sep 17 00:00:00 2001 From: Josh Creek <8179928+jcreek@users.noreply.github.com> Date: Tue, 15 Sep 2026 17:48:57 +0100 Subject: [PATCH] feat(offline): open entry details from the saved snapshot Opening an entry while offline needed a request that could not be made. Read it from the snapshot claimed by the signed-in account instead, re-checking ownership after the read so a sign-in mid-read cannot surface another account's data, and let controls marked data-offline-action stay usable in read-only mode. --- src/lib/stores/offlineSync.ts | 24 ++++++++++++++ src/routes/+layout.svelte | 9 ++--- tests/unit/offlineEntry.test.ts | 58 +++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 tests/unit/offlineEntry.test.ts diff --git a/src/lib/stores/offlineSync.ts b/src/lib/stores/offlineSync.ts index 271a3b9..1ff4f09 100644 --- a/src/lib/stores/offlineSync.ts +++ b/src/lib/stores/offlineSync.ts @@ -265,3 +265,27 @@ export function startOfflineSync(getUserId: () => string | null): () => void { window.removeEventListener('online', schedule); }; } + +/** Read details only from the snapshot currently claimed by this account. */ +export async function readOfflineEntry(userId: string, pokedexId: string, entryId: string) { + if (typeof window === 'undefined' || !('caches' in window)) return null; + const meta = (await readOfflineMeta()) as (OfflineMeta & { dataCache?: string }) | null; + if ( + meta?.userId !== userId || + meta.format !== OFFLINE_META_FORMAT || + !meta.dataCache?.startsWith(`${OFFLINE_CACHE_PREFIX}data-v1-${userId}-`) + ) + return null; + const response = await ( + await caches.open(meta.dataCache) + ).match(`/__offline/snapshot/${encodeURIComponent(userId)}`); + const snapshot = (await response?.json()) as OfflineSnapshot | undefined; + const current = await readOfflineMeta(); + if (current?.userId !== userId || snapshot?.userId !== userId || snapshot.version !== 1) + return null; + return ( + snapshot.pokedexes + .find((dex) => dex.pokedex._id === pokedexId) + ?.entries.find((row) => row.pokedexEntry._id === entryId) ?? null + ); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index d4217fa..e916cc7 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -52,6 +52,7 @@ if (navigator.onLine) return; const target = event.target instanceof Element ? event.target : null; if (!target?.closest('button, input, textarea, select, form')) return; + if (target.closest('[data-offline-action]')) return; event.preventDefault(); event.stopImmediatePropagation(); }; @@ -313,10 +314,10 @@