feat(library): update, rescan and stop on the folder menu, queued per roll
A folder row now asks for the rest of a roll, the whole of it from the top, or takes its request back. Requests queue behind the reading in flight and run one at a time; a row waiting its turn draws a ring that does not turn.
This commit is contained in:
@@ -1027,6 +1027,12 @@ export async function unfinished(folder: string): Promise<boolean> {
|
||||
}
|
||||
|
||||
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 = [];
|
||||
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.
|
||||
if (!live) {
|
||||
@@ -1037,6 +1043,22 @@ export function stopScan(): void {
|
||||
tell();
|
||||
}
|
||||
|
||||
// The row's own stop: the request for this row that is still waiting its turn,
|
||||
// and the reading of this roll, which is one at a time — a row under the roll
|
||||
// stops the reading the same way the roll's own row does, because the reading it
|
||||
// stops is the roll's. The other rolls' requests are other readings and run on.
|
||||
export function stopScanAt(folder: string, rel: string): void {
|
||||
for (const job of waiting) {
|
||||
// The roll's own row stops the whole roll — the requests under it are readings
|
||||
// of that one roll, a folder at a time. A subfolder's row stops that folder.
|
||||
if (job.folder === folder && (rel === '' || job.rel === rel)) drop(job);
|
||||
}
|
||||
waiting = waiting.filter((j) => !j.dropped);
|
||||
if (live?.folder === folder) live.stop = true;
|
||||
else if (peer?.folder === folder) say({ k: 'stop', folder });
|
||||
tell();
|
||||
}
|
||||
|
||||
export function jumpScan(rel: string | null): void {
|
||||
if (live) {
|
||||
live.jump = rel;
|
||||
@@ -1048,8 +1070,20 @@ export function jumpScan(rel: string | null): void {
|
||||
// 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, from = ''): Promise<ScanProgress> {
|
||||
//
|
||||
// `update` is the reading that picks up where the last one left off: the position
|
||||
// on the disk if there is one, the whole roll from the top if there is not, with
|
||||
// every frame the catalogue already holds skipped on its size and its time.
|
||||
// `rescan` is the same reading refusing that position — the roll walked again
|
||||
// from the top — which is what finds a folder that was added while a reading was
|
||||
// cut off, and what a frame written over is found by.
|
||||
export function startScan(folder: LibraryFolder, from = '', mode: ScanMode = 'update'): Promise<ScanProgress> {
|
||||
if (live) return Promise.reject(new Error('a scan is already running'));
|
||||
// A reading kept to one folder under the roll keeps no position of its own and
|
||||
// reads that folder however it was asked for, so only a roll's own rescan has a
|
||||
// position to throw away — and throwing away a position this reading is not
|
||||
// going to use would cost the next update the rest of the roll.
|
||||
const go = mode === 'rescan' && !from ? clearWalk(folder.name) : Promise.resolve();
|
||||
const session: ScanSession = {
|
||||
folder: folder.name,
|
||||
progress: { folder: folder.name, from, total: 0, done: 0, added: 0, written: 0, dirs: [], counts: {}, at: '' },
|
||||
@@ -1063,16 +1097,18 @@ export function startScan(folder: LibraryFolder, from = ''): Promise<ScanProgres
|
||||
// silence long enough would take the reading for abandoned and start its own.
|
||||
if (heart) clearInterval(heart);
|
||||
heart = setInterval(announce, HEART_MS);
|
||||
return scanFolder(
|
||||
folder,
|
||||
(p) => {
|
||||
session.progress = p;
|
||||
tell();
|
||||
announce();
|
||||
},
|
||||
() => session.stop,
|
||||
() => session.jump,
|
||||
from
|
||||
return go.then(() =>
|
||||
scanFolder(
|
||||
folder,
|
||||
(p) => {
|
||||
session.progress = p;
|
||||
tell();
|
||||
announce();
|
||||
},
|
||||
() => session.stop,
|
||||
() => session.jump,
|
||||
from
|
||||
)
|
||||
).finally(() => {
|
||||
if (heart) {
|
||||
clearInterval(heart);
|
||||
@@ -1084,6 +1120,95 @@ export function startScan(folder: LibraryFolder, from = ''): Promise<ScanProgres
|
||||
});
|
||||
}
|
||||
|
||||
// --- what the reader asked for, in the order they asked ---------------------
|
||||
|
||||
// One reading at a time is not one request at a time: a reader who right-clicks
|
||||
// four folders means four readings, and the walk is one thread — a second reading
|
||||
// of the same disk only slows the first. So the ones that cannot run yet wait
|
||||
// here, in the order they were asked for. The folder asked for while nothing runs
|
||||
// starts at once; the one asked for during a reading starts when that reading
|
||||
// ends, whether it ran to its end or was stopped; and a request taken out of the
|
||||
// queue is a reading that never happens.
|
||||
export type ScanMode = 'update' | 'rescan';
|
||||
|
||||
export interface ScanJob {
|
||||
// The picked folder the reading belongs to, by name.
|
||||
folder: string;
|
||||
// The row it was asked of, spelled the way the walk spells a path: '' for the
|
||||
// picked folder itself, `2026/04/` for a folder under it.
|
||||
rel: string;
|
||||
mode: ScanMode;
|
||||
}
|
||||
|
||||
interface Waiting extends ScanJob {
|
||||
// The picker's own handle, which is what a reading is started from: a name is
|
||||
// not a folder that can be walked.
|
||||
handle: FileSystemDirectoryHandle;
|
||||
dropped: boolean;
|
||||
settle: (p: ScanProgress | null) => void;
|
||||
fail: (err: unknown) => void;
|
||||
// The same request asked twice is one reading, and this is the answer both
|
||||
// askers are waiting for.
|
||||
answer: Promise<ScanProgress | null>;
|
||||
}
|
||||
|
||||
let waiting: Waiting[] = [];
|
||||
|
||||
const drop = (job: Waiting): void => {
|
||||
if (job.dropped) return;
|
||||
job.dropped = true;
|
||||
job.settle(null);
|
||||
};
|
||||
|
||||
// What is waiting its turn, in the order it will be read: what a row draws its
|
||||
// mark from, and the whole of what a screen knows about the queue.
|
||||
export function scanQueue(): ScanJob[] {
|
||||
return waiting.filter((j) => !j.dropped).map(({ folder, rel, mode }) => ({ folder, rel, mode }));
|
||||
}
|
||||
|
||||
// Ask for a reading of one folder: the row that was right-clicked, and whether
|
||||
// what is wanted is what is new under it or the whole of it again. A request is
|
||||
// never refused and never starts a second reading — it waits its turn and answers
|
||||
// with the reading it asked for, or with null when it was taken out of the queue
|
||||
// before it ran.
|
||||
export function scanAsked(folder: LibraryFolder, rel = '', mode: ScanMode = 'update'): Promise<ScanProgress | null> {
|
||||
const same = waiting.find((j) => !j.dropped && j.folder === folder.name && j.rel === rel && j.mode === mode);
|
||||
if (same) return same.answer;
|
||||
let settle!: (p: ScanProgress | null) => void;
|
||||
let fail!: (err: unknown) => void;
|
||||
const answer = new Promise<ScanProgress | null>((resolve, reject) => {
|
||||
settle = resolve;
|
||||
fail = reject;
|
||||
});
|
||||
waiting.push({ folder: folder.name, rel, mode, handle: folder.handle, dropped: false, settle, fail, answer });
|
||||
tell();
|
||||
pump();
|
||||
return answer;
|
||||
}
|
||||
|
||||
// Hand the next request its turn. The reading that just ended is the one thing
|
||||
// that can make room for it, and the window that holds the reading is the one
|
||||
// that hears when it ends.
|
||||
function pump(): void {
|
||||
if (live) return;
|
||||
const job = waiting.find((j) => !j.dropped);
|
||||
if (!job) return;
|
||||
// A reading another window holds is waited out rather than raced: the disk is
|
||||
// one disk, and two readers of one roll is the whole of what this is for. The
|
||||
// turn is taken when that window says it is through, which is the word the
|
||||
// screens draw their progress from.
|
||||
if (peer) {
|
||||
const stop = watchScan(() => {
|
||||
if (peer) return;
|
||||
stop();
|
||||
pump();
|
||||
});
|
||||
return;
|
||||
}
|
||||
waiting = waiting.filter((j) => j !== job);
|
||||
startScan({ name: job.folder, handle: job.handle }, job.rel, job.mode).then(job.settle, job.fail).finally(pump);
|
||||
}
|
||||
|
||||
// --- reading ---------------------------------------------------------------
|
||||
|
||||
// The catalogue, read once and kept. STUDIO and LIBRARY are two screens of one
|
||||
|
||||
Reference in New Issue
Block a user