The index pass belongs to the view model, not to the keystroke that started it

Reported: the index looked recreated on every keystroke, and switching library
started it over. Both are the same bug, and it is in the ownership of the pass.

searchEverywhere cancels and re-creates searchJob on every keystroke, and
loadLibrary does the same with libraryJob. bringIndexCurrent was a suspend
function called from inside those jobs, so the walk ran in the job that the next
interaction cancelled. A cancelled pass never reaches endVolume, so the volume's
stamp stayed old, so the next interaction found the index stale and started
again from the first folder. With a 20 000-file volume on a phone, where a walk
takes tens of seconds and typing takes milliseconds, the index could not finish
while anyone was typing - and the progress counter restarting with every
character was the visible half of it.

The pass now has its own job on the view model: one at a time, shared by
whoever asks, not cancelled by the screen that asked. Search does not wait for
it once there is an answer to show (it did before, which queued the UI behind
the walk), and when the pass lands it re-answers whatever is on screen - the
query or the library - so "I just downloaded it" still works. A rebuild cancels
any pass in flight and takes over: it is the one action that recovers from the
index being wrong rather than old.

Three tests, each standing inside a walk through a gated StorageSource: typing
does not start a second pass, switching library does not start a second pass, and
a finished pass answers the search again when it finds the file. 430 tests total.
This commit is contained in:
2026-10-18 10:50:39 +02:00
parent 14057ca4ce
commit 7dcd2bc0f0
4 changed files with 176 additions and 14 deletions
+9
View File
@@ -242,6 +242,15 @@ upload needs a **higher `versionCode`** than every previous upload.
### Fixed
- **Typing in the search field no longer restarts the index, and neither does opening another library.**
The pass that keeps the index current was started from inside the search's own job, and that job is
cancelled and replaced on every keystroke — so each letter cancelled the walk that had just started,
before it could record that it had finished, and the next letter began again at the first folder. The
progress counter restarting with every character was the visible half; the hidden half was that on a
full volume the index could not finish while anyone was typing. The same thing happened when switching
libraries, which restarts its own job too. The pass now belongs to the view model: one at a time,
whoever asks, not cancelled by the screen that asked, re-answering what is on screen when it lands.
- **The search field told the truth everywhere except inside a library.** Opening the field from a
library left the scope on "this folder" — the hint read *Search this folder* and a folder toggle
sat beside it — while the search actually ran everywhere, because the view model has always known