fix(library): keep the shell the deploy replaced out of a failed navigation, and say when another tab holds the library open
This commit is contained in:
@@ -37,9 +37,15 @@ const cachedFetch = (request) =>
|
||||
return response;
|
||||
});
|
||||
|
||||
// Offline, the studio the app is for; the landing page for a build without /app.
|
||||
const offlineShell = async () =>
|
||||
(await caches.match('/app')) ?? (await caches.match('/')) ?? Response.error();
|
||||
// Offline, the route that was asked for if this browser has ever been on it, then
|
||||
// the studio the app is for, then the landing page for a build without /app. The
|
||||
// route first because that is the copy a visit keeps up to date: the other two may
|
||||
// be shells of a build the deploy has replaced.
|
||||
const offlineShell = async (request) =>
|
||||
(await caches.match(new Request(new URL(request.url).pathname))) ??
|
||||
(await caches.match('/app')) ??
|
||||
(await caches.match('/')) ??
|
||||
Response.error();
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// Best effort, one route at a time: a deploy caught mid-flight must not leave the
|
||||
@@ -73,12 +79,28 @@ self.addEventListener('fetch', (event) => {
|
||||
// `no-store` because a plain fetch is not the network: it may be answered by the
|
||||
// browser's own cache, which is the other place a stale shell hides.
|
||||
if (request.mode === 'navigate') {
|
||||
event.respondWith(fetch(request, { cache: 'no-store' }).catch(offlineShell));
|
||||
event.respondWith(
|
||||
fetch(request, { cache: 'no-store' })
|
||||
.then((response) => {
|
||||
// The shell kept for offline is the one the server handed over last, not
|
||||
// the one this worker installed with: a navigation that fails — a deploy
|
||||
// recreating the container is the ordinary way one does — otherwise
|
||||
// answers with the build of the first visit, `activate` never runs to
|
||||
// replace it while this worker's bytes stay the same, and the browser
|
||||
// runs an old build until a reload happens to land after the server is
|
||||
// back. Stored under the route the navigation asked for, which is one of
|
||||
// the three SHELL keys — a navigation request is not a key a cache takes.
|
||||
if (response.ok) {
|
||||
const copy = response.clone();
|
||||
const key = new Request(new URL(request.url).pathname);
|
||||
caches.open(VERSION).then((cache) => cache.put(key, copy)).catch(() => {});
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.catch(() => offlineShell(request))
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// ponytail: a navigation is never written back, the precached entry is what
|
||||
// answers offline. Add a put() here once a route must survive on its own.
|
||||
if (STATIC.test(url.pathname)) {
|
||||
event.respondWith(caches.match(request).then((hit) => hit ?? cachedFetch(request)));
|
||||
return;
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
listEditedIds,
|
||||
listFolders,
|
||||
normPath,
|
||||
onLibraryBlocked,
|
||||
pickFolder,
|
||||
readPhotoFile,
|
||||
readPhotos,
|
||||
@@ -376,6 +377,15 @@ function Catalogue() {
|
||||
// the folders and cannot tell whether the remembered subfolder still exists.
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
|
||||
// Whether another tab still holds the library database at the version it was
|
||||
// opened with — a tab left open across a build. Nothing can be read until it
|
||||
// goes, and the screen would otherwise sit blank with no reason on it.
|
||||
const [libraryBlocked, setLibraryBlocked] = useState(false);
|
||||
useEffect(() => {
|
||||
onLibraryBlocked(setLibraryBlocked);
|
||||
return () => onLibraryBlocked(null);
|
||||
}, []);
|
||||
|
||||
// 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
|
||||
@@ -1614,6 +1624,12 @@ function Catalogue() {
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{libraryBlocked ? (
|
||||
<p className="hint adm-note" data-key="lib-blocked">
|
||||
{t('lib.blocked')}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{/* The folders are on their way in for as long as the catalogue behind
|
||||
them is being read back, and a screen that says there is no folder
|
||||
before it has looked is a screen that says it every visit. */}
|
||||
|
||||
@@ -213,9 +213,21 @@ async function migrateLegacyDatabase(db: IDBDatabase): Promise<void> {
|
||||
|
||||
let dbPromise: Promise<IDBDatabase> | null = null;
|
||||
|
||||
// One library database per browser, so every tab shares it — and a tab left open
|
||||
// across a build holds it at the version it was opened with. The browser answers
|
||||
// the newer tab's open with silence until that tab goes: no error, no rows, no
|
||||
// note, a LIBRARY that has simply not loaded and never says why. The wait is
|
||||
// reported instead, and it ends by itself when the other tab is closed or reloaded.
|
||||
let blockedSink: ((blocked: boolean) => void) | null = null;
|
||||
|
||||
export function onLibraryBlocked(fn: ((blocked: boolean) => void) | null): void {
|
||||
blockedSink = fn;
|
||||
}
|
||||
|
||||
function openDb(): Promise<IDBDatabase> {
|
||||
dbPromise ??= new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onblocked = () => blockedSink?.(true);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
if (!db.objectStoreNames.contains(FOLDERS)) db.createObjectStore(FOLDERS, { keyPath: 'name' });
|
||||
@@ -230,6 +242,7 @@ function openDb(): Promise<IDBDatabase> {
|
||||
if (!db.objectStoreNames.contains(EDITS)) db.createObjectStore(EDITS, { keyPath: 'photoId' });
|
||||
};
|
||||
req.onsuccess = () => {
|
||||
blockedSink?.(false);
|
||||
const db = req.result;
|
||||
void migrateLegacyDatabase(db).then(() => resolve(db));
|
||||
};
|
||||
|
||||
@@ -296,6 +296,8 @@ export const en: Dict = {
|
||||
'lib.scanned': 'Scanned {folder}: {added}/{total} new frames.',
|
||||
'lib.empty': 'Nothing in this folder yet.',
|
||||
'lib.noFolders': 'No folder yet. Hit ADD FOLDER and pick a photo folder on this machine.',
|
||||
'lib.blocked':
|
||||
'Another tab of this app is holding the library open from an older build. Close that tab, or reload it, and this one picks the library up.',
|
||||
'lib.missing': 'Could not open {name} — the file has been moved or deleted.',
|
||||
'lib.failed': 'That folder could not be read.',
|
||||
'lib.unsupported':
|
||||
|
||||
@@ -309,6 +309,8 @@ export const vi = {
|
||||
'lib.scanned': 'Đã quét {folder}: {added}/{total} ảnh mới.',
|
||||
'lib.empty': 'Chưa có ảnh nào trong thư mục này.',
|
||||
'lib.noFolders': 'Chưa có thư mục nào. Bấm THÊM THƯ MỤC để chọn một thư mục ảnh trên máy.',
|
||||
'lib.blocked':
|
||||
'Một tab khác của ứng dụng đang giữ thư viện ở bản cũ hơn. Hãy đóng tab đó, hoặc tải lại nó, để tab này mở được thư viện.',
|
||||
'lib.missing': 'Không mở được {name} — tệp đã bị di chuyển hoặc xoá.',
|
||||
'lib.failed': 'Không đọc được thư mục này.',
|
||||
'lib.unsupported':
|
||||
|
||||
Reference in New Issue
Block a user