fix(app): keep the app on its feet — a boundary, one window, and a reading that ends
- boundary: src/ui/ErrorBoundary.tsx, around the whole app rather than around a screen. A throw on the way to the first paint used to be a white page with nothing in it to report; it is a sentence, the message and the stack now, with a reload beside them, so the next report of a crash carries what went wrong. - one window: src/ui/SingleTab.tsx holds the app to one tab over localStorage and a BroadcastChannel. A second tab on /app or /library is told so and offered the roll — "use this tab" takes it, and the tab that held it gets a bar and the offer back. A tab holding a reading is a tab the machine has already spent its memory on, and a second one beside it is the memory the first will not get. - mount: the studio and the library are mounted the first time their route is asked for and stay mounted after that, not on every visit. Booting CanvasKit (~20MB) and reading the whole catalogue on a page that shows neither is the read and the memory a machine short of either takes for a crash; a trip back from the studio is a screen opening rather than a page loading. - reading: READ_MS bounds one touch of the disk at 30s. getFile and arrayBuffer on a drive that has gone away do not fail, they never settle — the lane waits for ever, the reading never ends, and STOP is a button with nothing left to read it. A frame given up on is a frame the next reading takes up again. - reading: the rows of a walk go down a pass at a time, so a tab that never comes back leaves the column with the folders this reading walked rather than the ones the last reading did. - reading: a stopped reading hands over the numbers it counted instead of null. Handed null it threw its own count away and had noughts written for every folder it walked that the file did not already hold, so a folder added to a drive read nought until the next scan. - repair: STOP ends the afternoon. A repair is a loop the button has no handle on, so it holds the count of stops it started at and compares between rolls, and says it was cut short rather than reporting a run it did not make. - add: a folder the browser could not name — every drive root on Windows is "\" — is asked for one, and a folder whose name is already another folder's is refused rather than folded in with it. The record, the frame ids, the walk's position and the number the column draws are all keyed by that name. - wall: four hundred tiles queued ahead is a reader waiting on frames three screens below the one they are looking at; sixty is a screenful and the wall mints the rest as it scrolls. The catalogue is read back every 30s, not every 5s — reading it back costs every thumbnail in it. - library: the note is drawn under the frame rather than above it. A line that comes and goes in the flow moved the frame, the column and the strip by its own height every time a scan started or ended. - manifest: "id": "/", so the installed app and the tab are one app. - checks: tree-scan-check waited for a count of exactly four rows — a state the screen does not have. The wait ran out its clock and then measured whatever the reading had done since; it waits for the whole tree now.
This commit is contained in:
@@ -725,6 +725,30 @@ export const HEAD_BYTES = 256 * 1024;
|
||||
// the first two hundred kilobytes is one read once more with this much of itself
|
||||
// rather than one filed under the clock on the file.
|
||||
const DEEP_HEAD_BYTES = 4 * 1024 * 1024;
|
||||
// How long one touch of the disk may take before the reading gives up on that
|
||||
// frame. `getFile` and `arrayBuffer` on a drive that has gone away — a network
|
||||
// share that dropped, a handle the browser took back — do not fail, they never
|
||||
// settle: the lane waits for ever, the reading never ends, and STOP is a button
|
||||
// that does nothing because there is nothing left to read it. A frame given up on
|
||||
// is a frame the next reading takes up again, which is what the position on the
|
||||
// disk is.
|
||||
const READ_MS = 30000;
|
||||
|
||||
function withTimeout<T>(work: Promise<T>, ms: number): Promise<T> {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const bell = setTimeout(() => reject(new Error('read-timeout')), ms);
|
||||
work.then(
|
||||
(v) => {
|
||||
clearTimeout(bell);
|
||||
resolve(v);
|
||||
},
|
||||
(e) => {
|
||||
clearTimeout(bell);
|
||||
reject(e);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
// The probe is decoded at this width, purely to learn which edge of the frame is
|
||||
// the long one; the decoder scales the real one off that. A JPEG does not need
|
||||
// it: its own header says how big the frame is, in the first few hundred bytes.
|
||||
@@ -1277,7 +1301,14 @@ export async function scanFolder(
|
||||
const due = () => batch.length > 0 && (batch.length >= BATCH || Date.now() - flushedAt >= FLUSH_MS);
|
||||
|
||||
const readOne = async (handle: FileSystemFileHandle, rel: string): Promise<void> => {
|
||||
const file = await handle.getFile();
|
||||
// The flag is read here and not only at the top of the frame: a lane already
|
||||
// handed a frame is a lane on its way to the disk, and a frame it opens after
|
||||
// STOP is a frame the reading was told not to want.
|
||||
if (shouldStop?.()) {
|
||||
stop = true;
|
||||
return;
|
||||
}
|
||||
const file = await withTimeout(handle.getFile(), READ_MS);
|
||||
const id = photoId(folder.name, rel);
|
||||
const seen = known.get(id);
|
||||
if (seen && seen.size === file.size && seen.mtime === file.lastModified) {
|
||||
@@ -1297,10 +1328,10 @@ export async function scanFolder(
|
||||
// has to do. The tile is the wall's business — one is made when one is drawn
|
||||
// and not before — see `makeTile`.
|
||||
const rawish = isRawName(file.name) || isTiffName(file.name) || isHeicName(file.name);
|
||||
const bytes = new Uint8Array(await file.slice(0, Math.min(file.size, HEAD_BYTES)).arrayBuffer());
|
||||
const bytes = new Uint8Array(await withTimeout(file.slice(0, Math.min(file.size, HEAD_BYTES)).arrayBuffer(), READ_MS));
|
||||
let taken = await readCapturedAt(bytes);
|
||||
if (taken === null && rawish && file.size > HEAD_BYTES) {
|
||||
const deeper = await file.slice(0, Math.min(file.size, DEEP_HEAD_BYTES)).arrayBuffer();
|
||||
const deeper = await withTimeout(file.slice(0, Math.min(file.size, DEEP_HEAD_BYTES)).arrayBuffer(), READ_MS);
|
||||
taken = await readCapturedAt(new Uint8Array(deeper));
|
||||
}
|
||||
const relNorm = rel.replace(/\\/g, '/');
|
||||
@@ -1446,6 +1477,15 @@ export async function scanFolder(
|
||||
publish(true);
|
||||
}
|
||||
await drain();
|
||||
// A roll takes minutes and the tab can go at any point in them; what the
|
||||
// reading has found so far is written down a pass at a time rather than once
|
||||
// at the end, so a tab that never comes back leaves the column with the
|
||||
// folders this reading walked instead of the ones the last reading did.
|
||||
// ponytail: one IDB read and write of the whole dir list per pass — a pass is
|
||||
// a layer of the walk, tens of them for a roll of a hundred thousand, and the
|
||||
// write is small next to the frames it is about. Batch it only if a roll ever
|
||||
// grows passes into the thousands.
|
||||
await saveWalkedDirs(folder.name, dirs, counts, false, bounded ? from : '', failed);
|
||||
// Yield between pass iterations to maintain UI fluidness
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
} while (!walked && !stop);
|
||||
@@ -1472,9 +1512,15 @@ export async function scanFolder(
|
||||
// reading of the whole roll that reached its end is the roll and replaces what
|
||||
// was there — a folder it did not meet is a folder that is gone, and its number
|
||||
// with it. A reading kept to one folder knows one branch of the roll and is
|
||||
// folded in; a reading that was stopped keeps its numbers for the folders it
|
||||
// walked and none for the rest, which is what it counted. See `saveWalkedDirs`.
|
||||
await saveWalkedDirs(folder.name, dirs, stop ? null : counts, !stop && !bounded, bounded ? from : '', failed);
|
||||
// folded in. A reading that was stopped hands over the numbers it counted and
|
||||
// says nothing of the folders it never reached: those are not in `dirs` either,
|
||||
// so they keep the number the last reading left. A nought beside a walked
|
||||
// folder comes from there being no frame under it, not from the reading being
|
||||
// cut off — handed `null` instead, a stopped reading threw away its own count
|
||||
// and had noughts written for every folder it walked that the file did not
|
||||
// already hold, and a folder added to a drive read nought until the next scan.
|
||||
// See `saveWalkedDirs`.
|
||||
await saveWalkedDirs(folder.name, dirs, counts, !stop && !bounded, bounded ? from : '', failed);
|
||||
// The last word, whatever the clock says: the frames the reading got to are
|
||||
// the frames the column is about to be told about one last time.
|
||||
publish(true);
|
||||
@@ -1648,12 +1694,23 @@ export async function unfinished(folder: string): Promise<boolean> {
|
||||
return (await loadWalk(folder)) !== null;
|
||||
}
|
||||
|
||||
// How many times a reading has been called off since the page opened. A reading
|
||||
// of several rolls in a row — the repair — is one loop the stop button has no
|
||||
// handle on: it holds the count it started at and compares, so a press of STOP
|
||||
// between two rolls ends the loop instead of moving on to the next one.
|
||||
let stops = 0;
|
||||
|
||||
export function stopCount(): number {
|
||||
return stops;
|
||||
}
|
||||
|
||||
export function stopScan(): void {
|
||||
// Everything waiting is a reading that is not going to happen: the button that
|
||||
// stops the walk stops the afternoon, and a queue that ran on after it would be
|
||||
// a stop that stopped one folder and then read the next three anyway.
|
||||
waiting.forEach(drop);
|
||||
waiting = [];
|
||||
stops++;
|
||||
tell();
|
||||
// A reading another window holds is stopped by telling it so: the button this
|
||||
// window drew is the same button, on the window that can act on it.
|
||||
@@ -1676,6 +1733,7 @@ export function stopScanAt(folder: string, rel: string): void {
|
||||
if (job.folder === folder && (rel === '' || job.rel === rel)) drop(job);
|
||||
}
|
||||
waiting = waiting.filter((j) => !j.dropped);
|
||||
stops++;
|
||||
if (live?.folder === folder) live.stop = true;
|
||||
else if (peer?.folder === folder) say({ k: 'stop', folder });
|
||||
tell();
|
||||
|
||||
Reference in New Issue
Block a user