web: keep reading a roll while the studio is up
The catalogue hands the visitor to the studio with a plain `<a href>`, and the app has no router: following it threw the page away, so a scan that was halfway through a roll died with it. A scan belongs to the tab, not to the screen that started it. The scan now lives outside the component (`scanSession`/`startScan`/`stopScan`, watched by whoever is up), so the catalogue can unmount and come back to a reading that never stopped; a screen that returns joins it and sees the toolbar line, the stop button and the rows filling in. The internal links are taken over for a history push *only while a scan is in flight* — everywhere else the browser navigates exactly as before, so the landing-to-studio flow is untouched. `scripts/scan-nav-check.mjs` is the regression: a roll read one frame at a time off a slow server, handed to the studio mid-scan, back to the catalogue, and the whole roll read to the end with the studio up, counting documents along the way.
This commit is contained in:
@@ -368,6 +368,81 @@ export async function scanFolder(
|
||||
return progress;
|
||||
}
|
||||
|
||||
// --- the scan in flight ----------------------------------------------------
|
||||
|
||||
// A scan belongs to the tab, not to the screen it was started from. A roll takes
|
||||
// minutes, and the reader who started it has every reason to go to the studio and
|
||||
// work on a frame while it runs — so the screen that started it may well be gone
|
||||
// before the scan is. What that screen would have held lives here instead: one
|
||||
// scan at a time, watched by whichever screen is up.
|
||||
export interface ScanSession {
|
||||
// The folder being read, by name.
|
||||
folder: string;
|
||||
progress: ScanProgress;
|
||||
// What the reader has asked for since: stop, or read this folder next (the path
|
||||
// as `rollWalk` spells it — `''` for the picked folder itself).
|
||||
stop: boolean;
|
||||
jump: string | null;
|
||||
}
|
||||
|
||||
let live: ScanSession | null = null;
|
||||
const watchers = new Set<() => void>();
|
||||
|
||||
const tell = () => {
|
||||
for (const fn of watchers) fn();
|
||||
};
|
||||
|
||||
// The scan in flight, or null. The same object for the whole of the scan, with
|
||||
// `progress` replaced on every pass — watchers are told, so they re-read it.
|
||||
export function scanSession(): ScanSession | null {
|
||||
return live;
|
||||
}
|
||||
|
||||
// Watch, and get the unsubscribe back: the shape an effect already has.
|
||||
export function watchScan(fn: () => void): () => void {
|
||||
watchers.add(fn);
|
||||
return () => {
|
||||
watchers.delete(fn);
|
||||
};
|
||||
}
|
||||
|
||||
export function stopScan(): void {
|
||||
if (!live) return;
|
||||
live.stop = true;
|
||||
tell();
|
||||
}
|
||||
|
||||
export function jumpScan(rel: string | null): void {
|
||||
if (live) live.jump = rel;
|
||||
}
|
||||
|
||||
// Start reading a roll. Only one at a time: the walk reads one frame at a time on
|
||||
// this thread, so a second scan would only slow the first one down. The promise
|
||||
// settles when the scan does — the caller that started it may be long gone.
|
||||
export function startScan(folder: LibraryFolder): Promise<ScanProgress> {
|
||||
if (live) return Promise.reject(new Error('a scan is already running'));
|
||||
const session: ScanSession = {
|
||||
folder: folder.name,
|
||||
progress: { folder: folder.name, total: 0, done: 0, added: 0, dirs: [] },
|
||||
stop: false,
|
||||
jump: null,
|
||||
};
|
||||
live = session;
|
||||
tell();
|
||||
return scanFolder(
|
||||
folder,
|
||||
(p) => {
|
||||
session.progress = p;
|
||||
tell();
|
||||
},
|
||||
() => session.stop,
|
||||
() => session.jump
|
||||
).finally(() => {
|
||||
live = null;
|
||||
tell();
|
||||
});
|
||||
}
|
||||
|
||||
// --- reading ---------------------------------------------------------------
|
||||
|
||||
export async function listPhotos(folder?: string): Promise<LibraryPhoto[]> {
|
||||
|
||||
Reference in New Issue
Block a user