web: a reload in the middle of a reading goes on, it does not start over
The reader opens a roll of a few thousand frames and holds Ctrl+Shift+R while
it is being read — over a reading that takes minutes, that is the one thing
they do. What came back was a reading that started again at the top: every
folder listed a second time, the toolbar counter back at zero, the frames that
were already in the catalogue walked over so that their size and their write
time could say they had not moved.
None of that was in the catalogue, because the catalogue only holds the frames
that were read. What was lost with the page was the reading's position: the
folders the walk had been through, the folders still in its queue, and the
frames it had found and not yet read. The catalogue alone can never answer
where a reading was.
The position is now written to session storage as the reading goes — walked,
pending and the frames in hand — and read back on the way in. Session storage
and not local storage, because a position belongs to the tab: the tab that
reloads carries on from the frame it stopped at, and a tab opened beside it
starts a reading of its own. The key goes when the reading finishes, and goes
with the folder when the folder is removed.
The frames in hand are the part worth spelling out. A frame a lane is in the
middle of, and a frame whose row is in the batch and not yet in the catalogue,
are on neither side of the line the position is written on: the catalogue does
not hold them and the walk will not find them again, so both are written into
the queue and read again. They also were counted when they left the queue, and
the count is written with them — else the reloaded reading would count them a
second time. The counter now carries on from where it was instead of restarting
from zero.
One thing was hidden behind the other: the stand-in folder the check hands the
page had no `getFileHandle` and no `getDirectoryHandle`, so the path that asks
the root for the frames a position names threw and answered nothing — and the
reading then walked the roll from the top, exactly the behaviour the check was
meant to catch. The check's folder answers both now, the way the browser's does.
Verified:
library-check.mjs — 45 steps, all passed, two of them new. The reading is
stopped on the frame it is reading (`check.hold`), the tab is reloaded, and
it comes back at "Scanning 3/3 — 0 new…" — the same line it was stopped at,
not a reading starting over — with the position still holding 4 folders
walked, an empty queue and 3 frames in hand. Listing 3 tiles and letting the
key go, the reading finishes the roll: 3 tiles in the strip, the catalogue
written, the position cleared, and every folder listed once
({"2026":1,"CheckRoll":1,"Empty":1,"04":1}) where a reading that started
over lists each of them twice.
scan-nav-check.mjs, roll-walk-check.mjs — all passed. frontend tsc --noEmit
clean, vite build clean.
ponytail: the position is the tab's, so a reload keeps it and a new tab does
not — the tab that reloaded is the one the reader is looking at, and a second
tab reading the same roll from the top costs a walk and no bytes, because a
frame that has not moved is dropped on its size and its time. The position is
written at the batch beat, so a reload reads at most one batch again, and the
frames in hand are read again on purpose: their rows were never stored.
Co-authored-by: PenguinHarness <noreply@penguin.local>
This commit is contained in:
@@ -133,14 +133,22 @@ await context.addInitScript(
|
||||
const fileHandle = (name) => {
|
||||
const handle = { kind: 'file', name, __fake: true };
|
||||
Object.defineProperty(handle, 'getFile', {
|
||||
value: async () =>
|
||||
new File([await load(name)], name, {
|
||||
value: async () => {
|
||||
const gets = JSON.parse(sessionStorage.getItem('check.gets') ?? '{}');
|
||||
gets[name] = (gets[name] ?? 0) + 1;
|
||||
sessionStorage.setItem('check.gets', JSON.stringify(gets));
|
||||
// A reading caught in the middle of a frame: the check sets the key, the
|
||||
// scan stops on the frame it was reading, and the next visit — with the
|
||||
// key cleared — is the one that goes on.
|
||||
while (localStorage.getItem('check.hold') === '1') await new Promise((r) => setTimeout(r, 25));
|
||||
return new File([await load(name)], name, {
|
||||
type: name.endsWith('.JPG') ? 'image/jpeg' : 'application/octet-stream',
|
||||
// A real handle reports one write time for one file, and the same one
|
||||
// on every read; a fresh `Date.now()` per call would make every frame
|
||||
// look touched and hide what is being checked.
|
||||
lastModified: Date.UTC(2026, 0, 1),
|
||||
}),
|
||||
});
|
||||
},
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
@@ -154,14 +162,37 @@ await context.addInitScript(
|
||||
const handle = { kind: 'directory', name: label, __fake: true };
|
||||
Object.defineProperties(handle, {
|
||||
values: {
|
||||
value: () =>
|
||||
[
|
||||
value: () => {
|
||||
// Every listing a reading asks for, counted across reloads: a reading
|
||||
// that starts over lists the same folder twice, and that is the one
|
||||
// thing on screen a reader reads as "it threw the catalogue away".
|
||||
const lists = JSON.parse(sessionStorage.getItem('check.lists') ?? '{}');
|
||||
lists[label] = (lists[label] ?? 0) + 1;
|
||||
sessionStorage.setItem('check.lists', JSON.stringify(lists));
|
||||
return [
|
||||
...(node.files ?? []).map(fileHandle),
|
||||
...Object.entries(node.dirs ?? {}).map(([name, kid]) => dirHandle(name, kid)),
|
||||
][Symbol.iterator](),
|
||||
][Symbol.iterator]();
|
||||
},
|
||||
},
|
||||
queryPermission: { value: async () => (window.__deny ? 'denied' : 'granted') },
|
||||
requestPermission: { value: async () => (window.__deny ? 'denied' : 'granted') },
|
||||
// A folder is asked for a child by name as often as it is listed: the
|
||||
// reading spells its queue as paths, and a path comes back as a handle
|
||||
// this way. A name that is not there is the error the browser gives.
|
||||
getDirectoryHandle: {
|
||||
value: async (name) => {
|
||||
const kid = node.dirs?.[name];
|
||||
if (!kid) throw new Error(`no folder named ${name}`);
|
||||
return dirHandle(name, kid);
|
||||
},
|
||||
},
|
||||
getFileHandle: {
|
||||
value: async (name) => {
|
||||
if (!(node.files ?? []).includes(name)) throw new Error(`no frame named ${name}`);
|
||||
return fileHandle(name);
|
||||
},
|
||||
},
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
@@ -678,6 +709,73 @@ await page
|
||||
.catch(() => {});
|
||||
step('and taking it reads the folder again', (await page.$$eval(thumbs, (els) => els.length)) === 3);
|
||||
|
||||
// A reload in the middle of a reading — the reader holding Ctrl+Shift+R over a
|
||||
// roll of a few thousand frames, which is the one thing they do while a reading
|
||||
// runs. The catalogue holds the frames the reading got through, but the reading's
|
||||
// own position is not in the catalogue: without it the walk starts at the top of
|
||||
// the roll again, every folder is listed a second time, and the counter starts
|
||||
// from zero while the frames it already holds are read again to be skipped.
|
||||
//
|
||||
// The position belongs to the tab now, so the reading carries on instead of
|
||||
// starting over. `check.hold` stops the reading on the frame it is reading, which
|
||||
// is where a reload lands — and the reading it left behind is still there when the
|
||||
// key goes, so the same reading finishes the roll. Everything is taken out of the
|
||||
// catalogue first, so the reading has frames on both sides of the reload.
|
||||
await page.evaluate(async () => {
|
||||
sessionStorage.clear();
|
||||
localStorage.setItem('check.hold', '1');
|
||||
const db = await new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open('recipescam-library');
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error);
|
||||
});
|
||||
await new Promise((resolve, reject) => {
|
||||
const tx = db.transaction('photos', 'readwrite');
|
||||
tx.objectStore('photos').clear();
|
||||
tx.oncomplete = () => resolve();
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
});
|
||||
// The position the tab is holding, the line the toolbar reads out, and the wait
|
||||
// for it: the line is how far the reading has come, and it is the one thing on
|
||||
// screen that says whether a reading started over.
|
||||
const position = () => page.evaluate(() => JSON.parse(sessionStorage.getItem('recipescam.library.walk.CheckRoll') ?? 'null'));
|
||||
const readout = () => page.$eval('[data-key="lib-progress"]', (e) => e.textContent ?? '').catch(() => '');
|
||||
const counting = () =>
|
||||
page.waitForFunction(
|
||||
() => /\d+\/\d+/.test(document.querySelector('[data-key="lib-progress"]')?.textContent ?? ''),
|
||||
null,
|
||||
{ timeout: 30_000, polling: 50 }
|
||||
);
|
||||
await page.reload();
|
||||
await counting().catch(() => {});
|
||||
const stopped = await readout();
|
||||
await page.reload();
|
||||
await counting().catch(() => {});
|
||||
const resumed = await readout();
|
||||
const at = await position();
|
||||
step(
|
||||
'a reading a reload cut in half goes on from where it was',
|
||||
/\d+\/\d+/.test(stopped) && resumed === stopped && at?.walked.length === 4 && at.pending.length === 0 && at.frames.length === 3,
|
||||
`stopped at "${stopped.trim()}", back at "${resumed.trim()}", position ${JSON.stringify(at && { walked: at.walked.length, pending: at.pending.length, frames: at.frames.length })}`
|
||||
);
|
||||
|
||||
// The key goes, and the reading the reload interrupted finishes the roll on its
|
||||
// own: the frames it had found are read, the folders are not listed again — the
|
||||
// check counts every listing a reading asks for, across reloads — and the position
|
||||
// is dropped once there is nothing left to carry on from.
|
||||
await page.evaluate(() => localStorage.removeItem('check.hold'));
|
||||
await page
|
||||
.waitForFunction(() => document.querySelectorAll('[data-key^="lib-thumb-"]').length === 3, null, { timeout: 60_000, polling: 50 })
|
||||
.catch(() => {});
|
||||
const carried = await page.$$eval(thumbs, (els) => els.length).catch(() => 0);
|
||||
const again = await page.evaluate(() => JSON.parse(sessionStorage.getItem('check.lists') ?? '{}'));
|
||||
step(
|
||||
'and it finishes the roll without listing a folder twice',
|
||||
carried === 3 && again.CheckRoll === 1 && again['2026'] === 1 && again.Empty === 1 && again['04'] === 1 && (await position()) === null,
|
||||
`${carried} tiles, folders listed ${JSON.stringify(again)}`
|
||||
);
|
||||
|
||||
await browser.close();
|
||||
sampleServer.close();
|
||||
app.kill('SIGTERM');
|
||||
|
||||
Reference in New Issue
Block a user