diff --git a/docker/frontend/src/engine/library.ts b/docker/frontend/src/engine/library.ts index 128b74f..6925c55 100644 --- a/docker/frontend/src/engine/library.ts +++ b/docker/frontend/src/engine/library.ts @@ -270,6 +270,44 @@ export function onLibraryBlocked(fn: ((blocked: boolean) => void) | null): void blockedSink = fn; } +// The stores this build reads and writes. A catalogue that has been through the +// newer build has all of them — it is only a version number that separates the +// two. +const STORES = [FOLDERS, PHOTOS, THUMBS, EDITS, DIRS]; + +// The catalogue as the disk already holds it, whatever version that is. Asked for +// with no version on purpose: a build that names a version older than the one on +// the disk — which is what a test server and a release server behind one domain +// are, two builds against the one database the origin owns, with the reader moving +// between them — is answered with `VersionError` and nothing else, every read of +// the catalogue falls back on an empty list, and the LIBRARY draws the same empty +// column a folder-less visit draws. No note, no error, nothing to read off the +// screen. Taking the database as it stands is what lets the older build read what +// the newer one wrote; a store it needs and does not find is the one case that is +// still refused, and it is refused by name. +function openAsIs(): Promise { + return new Promise((resolve, reject) => { + const req = indexedDB.open(DB_NAME); + req.onblocked = () => blockedSink?.(true); + req.onsuccess = () => { + blockedSink?.(false); + const db = req.result; + const missing = STORES.filter((s) => !db.objectStoreNames.contains(s)); + if (missing.length) { + // A catalogue that is missing a store is not one this build can read, and + // guessing at it is how a write lands on the wrong shape. The name goes to + // the console; the screen keeps its own empty column, as before. + console.error('catalogue: the database is missing', missing.join(', ')); + db.close(); + reject(new Error('catalogue-incomplete')); + return; + } + resolve(db); + }; + req.onerror = () => reject(req.error); + }); +} + function openDb(): Promise { dbPromise ??= new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); @@ -300,7 +338,24 @@ function openDb(): Promise { const db = req.result; void migrateLegacyDatabase(db).then(() => resolve(db)); }; - req.onerror = () => reject(req.error); + req.onerror = () => { + const err = req.error; + // The one refusal that is not a failure: the catalogue on the disk was + // written by a newer build of this app and this build is asking for an older + // version of it. Every other way in is closed and this one is open. + if (err && err.name === 'VersionError') { + void openAsIs().then(resolve, reject); + return; + } + reject(err); + }; + }).catch((err: unknown) => { + // A failed open is a fact about the ask, not about the visit: put nothing in + // its place, so the read that follows the reader closing the other tab — or + // reloading the build that holds the pen — opens the database again instead of + // being answered forever by the one rejection. + dbPromise = null; + throw err; }); return dbPromise; }