Paint the last-known library on cold start #2

Merged
laoc merged 2 commits from feat/local-cache into main 2026-08-05 18:51:39 +00:00
Owner

Item 2 of the speed lane. Stacked on #1.

Bookmarks, highlights and lists were memory onlyStashPreferences persisted identity, relays, last shared page and recent hashtags and nothing else. So launching the app showed an empty screen until a relay answered, and offline showed nothing ever.

Design

EventCache stores raw event JSON, not parsed bookmarks, and reads it back through exactly the same map/finish a fetch uses. A cached row therefore cannot drift from a fetched one, and the cache never has to learn what a bookmark is — which is also why it is testable on the JVM with no Android and no rust-nostr.

  • one file per key, newline-delimited; write refuses an entry containing a raw newline rather than let one event silently become two broken rows
  • keys carry the pubkey, so signing in as someone else cannot paint the previous user's library
  • a corrupt file reads as empty rather than throwing: losing the cache is a slow start, crashing on launch is an unusable app
  • written only at the end of a completed stream, never from a partial paint — caching a half-answer would persist it as the user's whole library
  • filesDir, not cacheDir: Android may evict cacheDir under storage pressure, and the entire point is that it survives to the next launch
  • logout clears it

One thing deliberately left alone: bookmarkEvidence/CompanionGate is not satisfied by a cache read. The gate exists because an unloaded list is indistinguishable from an empty one, and a cached list is exactly that — so it still waits for a real fetch.

Testing

EventCacheTest        8 JVM tests, 5/5 mutations caught (each patch verified landed)
EventCacheE2ETest     4/4 on device
ColdStartCacheE2ETest 2/2 on device

ColdStartCacheE2ETest drives the real Activity: first launch against a healthy relay, second launch with only a relay that answers nothing. Anything on screen came off the disk, asserted within 4s — FETCH_TIMEOUT alone is 12s.

That test was written after the wiring, so I proved it can fail rather than assuming: removing the cold-start paint (1+/7−, verified landed) fails it, while the empty-state control correctly keeps passing.

Item 2 of the speed lane. Stacked on #1. Bookmarks, highlights and lists were **memory only** — `StashPreferences` persisted identity, relays, last shared page and recent hashtags and nothing else. So launching the app showed an empty screen until a relay answered, and offline showed nothing ever. ### Design `EventCache` stores **raw event JSON**, not parsed bookmarks, and reads it back through exactly the same `map`/`finish` a fetch uses. A cached row therefore cannot drift from a fetched one, and the cache never has to learn what a bookmark is — which is also why it is testable on the JVM with no Android and no rust-nostr. - one file per key, newline-delimited; `write` refuses an entry containing a raw newline rather than let one event silently become two broken rows - keys carry the pubkey, so signing in as someone else cannot paint the previous user's library - a corrupt file reads as empty rather than throwing: losing the cache is a slow start, crashing on launch is an unusable app - written only at the end of a **completed** stream, never from a partial paint — caching a half-answer would persist it as the user's whole library - `filesDir`, not `cacheDir`: Android may evict `cacheDir` under storage pressure, and the entire point is that it survives to the next launch - logout clears it One thing deliberately left alone: `bookmarkEvidence`/`CompanionGate` is **not** satisfied by a cache read. The gate exists because an unloaded list is indistinguishable from an empty one, and a cached list is exactly that — so it still waits for a real fetch. ### Testing ``` EventCacheTest 8 JVM tests, 5/5 mutations caught (each patch verified landed) EventCacheE2ETest 4/4 on device ColdStartCacheE2ETest 2/2 on device ``` `ColdStartCacheE2ETest` drives the real Activity: first launch against a healthy relay, second launch with only a relay that answers nothing. Anything on screen came off the disk, asserted within 4s — `FETCH_TIMEOUT` alone is 12s. That test was written after the wiring, so I proved it can fail rather than assuming: removing the cold-start paint (1+/7−, verified landed) fails it, while the empty-state control correctly keeps passing.
laoc changed target branch from feat/save-latency to main 2026-08-05 18:51:31 +00:00
laoc merged commit a64f11dae2 into main 2026-08-05 18:51:39 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
laoc/stash-bookmark!2
No description provided.