feat(library): ask the origin to keep the tiles, so the visit after opens painted

This commit is contained in:
2026-10-14 23:07:22 +07:00
parent df44200890
commit aa8f462058
2 changed files with 48 additions and 0 deletions
+7
View File
@@ -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
+41
View File
@@ -2423,6 +2423,10 @@ async function tileFrom(photo: LibraryPhoto): Promise<Blob | null> {
// 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<LibraryEdit | null> {
}
}
// 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<boolean> => {
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<Set<string>> {
try {
const rows = await ask<LibraryEdit[]>(EDITS, 'readonly', (s) => s.getAll());