A series page took about a second to show its seasons on a phone, every
visit, although they were cached. Three things stacked up:
- One SQLite connection behind one mutex served the whole app, so every
read queued behind every write. The database now has one owner: a
writer thread for writes and a pool of read-only WAL connections for
reads. synchronous = NORMAL and a busy timeout on every connection.
- The listing query built the set of every available item in the
database before filtering to the parent (~80 ms on a desktop for a
100k-item cache), then fetched user data one row at a time. It now
checks availability per row, uses the hierarchy indexes (1.5 ms on
the same benchmark) and batches the user-data lookup.
- A cache read that missed the 100 ms fast path was set aside until the
server answered. It is now raced against the server; whichever answers
first with content wins.
On the Fairphone, Frasier's season and episode lists now come from
cache in 34-133 ms (was 600-1030 ms waiting on the server).
Fixes found on the way, each with a test that failed first:
- sync_queue_mutation could return another mutation's row id: the id
came from a second trip to the shared connection. insert() reads it in
the same job.
- save_to_cache switched foreign keys off on the shared connection
across its awaits, so concurrent writes ran unchecked. The toggle now
lives inside one writer job, and a page is one transaction instead of
one commit per row.
Also: thumbnail LRU touches no longer block the lookup; unused
tokio-rusqlite dropped. Design and invariants in
docs/architecture/08-database-design.md (Connection ownership, Listing
query shape) and 03-data-flow.md.
Three defects made "offline" depend on a server it could not reach.
A downloaded film would not play offline. The player found the file on disk,
then asked the server for the item's PlaybackInfo only to read its
media-source id; with no network that retried for seven seconds and failed,
and the file was never opened. A completed download now answers playback
info from its download row — local path, direct play, item id as media
source — and the hybrid repository consults it before the network.
"More info" on a downloaded show failed with "Failed to load item". The
cache is one SQLite connection behind one mutex, so any write in progress
(the catalog sync at every launch, a download finishing) pushes a read past
the 100 ms fast path — and get_items, the library list, genres and playlist
items discarded such a read, waited on the server, and returned its error
over data sitting on disk. They now keep the read running and wait for it
when the server fails; the cache-only reads (search, favourites) simply
await the cache, having no server to fall back from.
Next Up went only to the server, and the TV landing page loads it in one
Promise.all with its other rows, so offline it blanked the whole page. It now
falls back to the cache.
Each fix has a test that failed first against an unreachable server (and, for
the cache, a database held past the fast path).
DR-294, UT-260, UT-261, UT-263.
A spec was a promise; sixteen of them had become descriptions of code that
already shipped, sitting beside four that describe work still outstanding, with
nothing in the file telling the two apart. Half the statuses were also wrong —
audio-equalizer read "Accepted" with the EQ live on both platforms, the native
video spec said the flag stays off after the default was flipped on.
The shipped designs move into docs/architecture, which is the maintained
description of the build, and the spec files go. Git history keeps the
originals; what a future change still needs is carried across:
- 01-rust-backend: favourites rewritten (the old section named a file that no
longer exists and called shipped buttons "planned"), domain vocabulary owned
by Rust (SearchScope, exclusions, the bitrate ladder), background workers
- 02-svelte-frontend: app shell and chrome, library mosaic, series/episode
navigation, downloaded browse, safe-area insets, native-video store, logging
- 03-data-flow: locally-indexed search
- 05-platform-backends: audio settings on ExoPlayer, the equalizer's band
vocabulary, native video compositing, the background-audio handoff
- 06-downloads-and-offline: one storage model, offline catalog visibility
- 09-security: path confinement and input binding
docs/specs/README.md now says what the directory is for and where each shipped
design went. Deferred work the specs recorded is kept beside the code it
concerns rather than lost: season-bounded autoplay, the two dead search
commands, why indexing is a full crawl.
requirements.md had fourteen stale statuses — Android audio parity still read
"Linux only", DR-150 still said the native-video default was off, DR-190 was
Proposed after DR-196 implemented it, and five tooling requirements were
Proposed after landing. Three unbuilt specs suggested requirement ids that have
since been allocated to other work; each now carries a warning.