test #21
Loading…
Reference in a new issue
No description provided.
Delete branch "test"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The tablet had been killing itself with an OutOfMemoryError, in the foreground, while being written in. The pull asked each notebook for every page it had, content and all, and a page's content is every stroke on it. Retrofit parses a response whole before any of it reaches the database, and kotlinx inflates the content field into a JsonObject tree on the way — a boxed JsonPrimitive per coordinate — which SyncEngine then immediately re-encoded straight back to a string for Room. Text in, tree built, text out, the tree pure waste. So peak memory during a sync tracked the size of the journal. It crossed a 256MB heap somewhere around the middle of August and the app started dying several times a night. Nothing about it was a regression: exit-info has the same illness on the 17th as a LOW_MEMORY kill at 1.1GB resident, four days before the release that was first blamed for it. It was always going to happen eventually, it gets worse the more you write, and it cannot be fixed by using the app less. The pull is now driven by an index. GET /notebooks/{id}/page-index returns every page without its content, so the response tracks how many pages there are rather than how much has been written on them, and the client compares updated_at against what it holds to find the stale ones. Those are fetched by explicit id through GET /pages?ids=, at most PAGE_FETCH_CHUNK of them per call, which is what bounds a response — and therefore the peak — independently of how big the journal gets. The server refuses a larger ask rather than trusting the client to keep to it. Asking by id rather than for everything-since-a-timestamp is deliberate. There is no ordering for the two ends to agree on, no tie to break when two pages share a timestamp, and no stored cursor that can go stale or skip a record after a pass that half-failed. The client's own copy of a page is the cursor, which means a page missed for any reason is simply picked up next time. Position, title, day and paper all travel in the index, so a page that was renamed or re-filed — the copy/move case from #19 — is applied without fetching anything. That is also a fix: it used to sit unapplied until something happened to rewrite the ink. The old unfiltered endpoint stays exactly as it was. The web client still calls it, and so does the build currently on the tablet, so the server can deploy ahead of the app without either of them noticing. Two things found on the way, both in the code being replaced. A failed page listing deleted every clean page in that notebook. The comment said it skipped the prune to avoid precisely that; nothing did. The ids simply never reached the set prune consults, and absent from a list we never read is indistinguishable there from deleted on the server. Only a notebook whose index we actually read may now have its pages pruned. The test for it fails against the old prune — checked, not assumed. And that failure went to Log.w and nowhere else, which is the thing CLAUDE.md says not to do and for the reason it gives: half a pull leaves no trace at all, the screen goes on showing yesterday's pages, and nothing says it is behind. Read failures now collect like push failures do and reach the badge. Ten new tests in SyncEngineTest, all by running a real sync against fakes: that a steady state fetches no content, that a changed page and an unseen page both do, that a large notebook comes in bounded chunks, that a move costs no fetch, that a failed index neither prunes nor stays quiet, and that a failed content fetch keeps what we have. integration_page_sync.py drives both new endpoints against a real Postgres, including that neither leaks another account's pages. Backend suite 54 pass, frontend 5 pass, Android 323 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>