fix(library): resolve main-thread UI freeze and process kill data loss via OPFS payload capping, yield throttling, and UI batching

This commit is contained in:
2026-10-05 12:13:45 +07:00
parent 1e93c8484b
commit 140b4f88b2
4 changed files with 24 additions and 9 deletions
+18 -5
View File
@@ -469,19 +469,32 @@ export function Library() {
fresh = 0;
}
};
// The reading hands its frames over the moment their batch is stored, so the
// strip is the roll arriving rather than the roll being found again — a read
// of the whole catalogue costs every thumbnail in it, and on a folder of RAW
// that read beside a quarter of a gigabyte of decoder is what took the tab.
let pendingRows: LibraryPhoto[] = [];
let updateTimer: ReturnType<typeof setTimeout> | null = null;
const flushPending = () => {
if (pendingRows.length > 0) {
const toAdd = pendingRows;
pendingRows = [];
setPhotos((prev) => [...prev, ...toAdd]);
}
updateTimer = null;
};
watchRows((rows) => {
fresh += rows.length;
setPhotos((prev) => [...prev, ...rows]);
pendingRows.push(...rows);
if (!updateTimer) {
updateTimer = setTimeout(flushPending, 500);
}
});
const unwatch = watchScan(watching);
// A scan already in flight when this screen opens is picked up where it is:
// the reader who left for the studio comes back to the same roll.
watching();
return () => {
if (updateTimer) clearTimeout(updateTimer);
flushPending();
unwatch();
watchRows(null);
};
+5 -3
View File
@@ -45,7 +45,7 @@ const LANES = 4;
// How often the walk's position is written down, in the same seconds as the
// catalogue is read back on: the position is the whole of what the walk has found
// and not read, and writing it is a string of a few megabytes on a long roll.
const WALK_MS = 2000;
const WALK_MS = 5000;
// How often the screen is told how far the reading has come: the column is
// rebuilt out of the count, and a rebuild a frame is a reading that spends its
// time drawing itself. Five times a second is a counter that moves to the eye
@@ -834,11 +834,12 @@ export async function scanFolder(
if (at - wroteAt < WALK_MS) return;
wroteAt = at;
const held = [...inHand, ...batch.map(pathOf)];
const savedEntries = entries.slice(0, 500).map((e) => e.rel);
await saveWalk(
folder.name,
[...tree.walked],
tree.pending.map((p) => p.rel),
[...held, ...entries.map((e) => e.rel)],
[...held, ...savedEntries],
held.length,
progress
);
@@ -962,11 +963,12 @@ export async function scanFolder(
// Micro-yield to the browser main thread event loop so scanning never blocks UI rendering or user interactions
processed++;
if (processed % 12 === 0) {
if (processed % 3 === 0) {
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
await Promise.all(lanes);
await flush();
};
// How many of the frames in hand were counted by the reading that handed them