// The walk that reads a picked folder (src/engine/rollWalk.ts) is the part of a // scan that can run for minutes, and its order is what the reader sees: parents // before children, shallower layers before deeper ones, and the folder they just // clicked in front of everything. None of that can be watched in a browser, so // it is walked here over a stand-in tree. // // node scripts/roll-walk-check.mjs import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import ts from 'typescript'; const src = readFileSync(new URL('../src/engine/rollWalk.ts', import.meta.url), 'utf8'); const js = ts.transpileModule(src, { compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 }, }).outputText; const { DIRS_PER_PASS, jumpQueue, walkPass } = await import(`data:text/javascript,${encodeURIComponent(js)}`); // A directory handle is only ever read for `values()`, so a stand-in tree needs // nothing else: `{ name: { … } }` for folders and `name: null` for a file. // `@eaDir` and `.thumbnails` are the folders a real roll drags along. const dir = (name, children) => ({ kind: 'directory', name, values: async function* () { for (const [child, kids] of Object.entries(children)) { yield kids === null ? { kind: 'file', name: child } : dir(child, kids); } }, }); const keep = (name) => /\.(jpe?g|rw2)$/i.test(name); const roll = dir('CheckRoll', { '.thumbnails': { hidden: null }, '@eaDir': { junk: null }, 'P1.JPG': null, '2026': { 'P2.JPG': null, '04': { 'P3.RW2': null, 'notes.txt': null } }, '2025': { 'P4.JPG': null }, }); // Walks to the end, one click per pass, which is what the browser does between // passes: whatever the reader clicked last is what `jump` names. const walk = async (jumps = []) => { const pending = [{ dir: roll, rel: '' }]; const out = []; let pass = 0; let walked = false; while (!walked) walked = await walkPass(pending, out, keep, () => jumps[pass++] ?? null); return { out, passes: pass }; }; // Nothing clicked: the picked folder's own frames, then a layer down, then the // deepest — a folder is never walked before the folder that holds it. const { out, passes } = await walk(); const rels = out.map((f) => f.rel); assert.deepEqual(rels, ['P1.JPG', '2025/P4.JPG', '2026/P2.JPG', '2026/04/P3.RW2'], 'layer by layer from the parent down'); assert.ok( rels.every((rel, i) => i === 0 || rel.split('/').length >= rels[i - 1].split('/').length), 'no deeper frame is read before a shallower one' ); assert.ok(!rels.some((rel) => /\.thumbnails|@eaDir|notes\.txt/.test(rel)), 'hidden and unsupported names are skipped'); assert.equal(passes, 4, 'one folder a pass — the thread goes back to the page between them'); // A click on one of the two folders a layer down while its layer is still to be // walked puts that folder first: the frames the reader is looking at are read // before the branch beside them. const clicked = await walk([null, '2026/']); assert.deepEqual( clicked.out.map((f) => f.rel), ['P1.JPG', '2026/P2.JPG', '2025/P4.JPG', '2026/04/P3.RW2'], 'the clicked folder jumps the queue' ); // And the same rule without a browser around it: a pass reads the queue in its // own order unless the click says otherwise, and a name the queue does not hold // changes nothing. const wide = dir('Wide', { a: { 'A.JPG': null }, b: { 'B.JPG': null } }); const before = []; await walkPass([{ dir: wide, rel: '' }], before, keep, () => null); assert.deepEqual(before.map((f) => f.rel), ['a/A.JPG', 'b/B.JPG'], 'untouched, the folders are read in the order they were found'); const after = []; await walkPass([{ dir: wide, rel: '' }], after, keep, () => 'b/'); assert.deepEqual( after.map((f) => f.rel), ['b/B.JPG', 'a/A.JPG'], 'the folder the reader clicked is read first' ); assert.equal(jumpQueue([{ rel: 'a/' }], 'nowhere/'), undefined, 'a folder already walked, or never seen, is left alone'); // A pass is bounded: a library wider than the budget is read over several passes, // and the rest waits in the queue rather than in a loop that never yields. const children = {}; for (let i = 0; i < DIRS_PER_PASS + 5; i++) children[`d${String(i).padStart(3, '0')}`] = { 'x.jpg': null }; const many = [{ dir: dir('Many', children), rel: '' }]; const found = []; assert.equal(await walkPass(many, found, keep, () => null), false, 'the queue is not empty after one pass'); assert.equal(found.length, DIRS_PER_PASS - 1, `one pass walked the picked folder and ${DIRS_PER_PASS - 1} below it`); assert.equal(many.length, 6, 'the rest is left in the queue, for the next pass'); console.log(`ok roll walk — ${rels.length} frames layer by layer, a click jumps the queue, ${DIRS_PER_PASS} folders a pass`);