From aa8f4620587601a921f5bf00e373d8926a9d4047 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 14 Oct 2026 23:07:22 +0700 Subject: [PATCH] feat(library): ask the origin to keep the tiles, so the visit after opens painted --- docker/frontend/src/Library.tsx | 7 +++++ docker/frontend/src/engine/library.ts | 41 +++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/docker/frontend/src/Library.tsx b/docker/frontend/src/Library.tsx index 9bf8fad..e818f72 100644 --- a/docker/frontend/src/Library.tsx +++ b/docker/frontend/src/Library.tsx @@ -8,6 +8,7 @@ import { readGps, readSpecs, specsLine } from './engine/imageOps'; import { isRawName, isTiffName, rawThumbnail, tiffDevelop } from './engine/rawDevelop'; import { heicToJpeg, isHeicName } from './engine/heicDevelop'; import { + askPersistence, breathe, bySourceThenName, cachedFolders, @@ -563,6 +564,12 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) { return () => onLibraryBlocked(null); }, []); + // The tiles this screen spends its first seconds making are the ones the visit + // after reads: the origin is asked to keep them (see `askPersistence`). + useEffect(() => { + askPersistence(); + }, []); + // The folder the catalogue is copied into, and whether the browser is still // letting this page write to it — which it stops doing when the tab closes, so // the row below says so rather than letting a write fail quietly. The ref is diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts index eeb3ed7..266075b 100644 --- a/docker/frontend/src/engine/library.ts +++ b/docker/frontend/src/engine/library.ts @@ -2423,6 +2423,10 @@ async function tileFrom(photo: LibraryPhoto): Promise { // is drawn as it is rather than written to the store whole. const made = await makeTile(file); blob = made ?? file; + // Written before the tile is handed back. Held back (lightcraft's + // `put_deferred`) it measures the same — the decode is what the tile waits + // for, not the transaction — so the await stays and the tile is kept for + // certain by the time it is drawn. if (made) await writeThumb(photo.id, made, false); } return blob; @@ -2775,6 +2779,43 @@ export async function loadEdit(id: string): Promise { } } +// The tiles are the whole of what makes the library open painted: a frame whose +// tile is in the store is drawn off one read, and a frame whose tile the browser +// threw away is a decode, a draw and an encode paid again the first time it is +// looked at. A store the browser is free to evict under pressure is a library +// that opens grey and mints every tile a second time — the one thing a visit +// after a visit cannot pay for. So the origin is asked to be persistent, once, +// and the ask is a promise the browser may refuse: Safari grants it only off a +// gesture, and there is none behind a page that has just opened itself, so a +// refusal is asked again the first time the reader touches the screen. Either +// way the visit goes on — an evicted tile is a tile made again, which is what +// this whole screen is for. +let persistenceAsked = false; + +export function askPersistence(): void { + const storage = typeof navigator === 'undefined' ? undefined : navigator.storage; + if (persistenceAsked || !storage?.persist) return; + persistenceAsked = true; + const ask = async (): Promise => { + const est = await storage.estimate?.(); + const held = est?.usage && est.quota ? ` (${Math.round(est.usage / 1e6)} MB of ${Math.round(est.quota / 1e6)} MB)` : ''; + if (await storage.persisted?.()) { + console.info(`library: tiles are persistent${held}`); + return true; + } + const granted = await storage.persist(); + if (granted) console.info(`library: tiles are persistent${held}`); + else console.info(`library: tiles are not persistent — the browser may drop them under pressure${held}`); + return granted; + }; + void ask().then((granted) => { + if (granted) return; + // A gesture is the one thing Safari's ask is waiting for; one is enough. + const again = () => void storage.persist(); + window.addEventListener('pointerdown', again, { once: true }); + }).catch(() => {}); +} + export async function listEditedIds(): Promise> { try { const rows = await ask(EDITS, 'readonly', (s) => s.getAll());