Repository navigation
Description
Problem
BrowserDb keeps every page it has ever written, both in the WASM heap and in IndexedDB.
apply_writeruns a fullPersistentFactStorage::save()on everytransact/retract(src/browser/mod.rs). Under v7 each save rebuilds the B+trees and abandons the old index pages.BrowserBufferBackend(HashMap<u64, Vec<u8>>) holds every page, live or dead.BrowserDb.open()loads all of them from IndexedDB into memory.IndexedDbBackendhas no delete path, so dead page keys stay in the object store forever.
So both heap memory and IndexedDB size grow with write history rather than live data. Open time grows with them too, because every dead page is read back at open.
Approach: reuse, as v8 does
No browser-specific reclamation or vacuum. BrowserDb sits on the same PersistentFactStorage, so the v8 copy-on-write checkpoint with a free list (#374, #434; spec docs/superpowers/specs/2026-10-05-v8-storage-format-design.md) brings page reuse to the browser automatically. Each transact's checkpoint reuses the pages the previous checkpoint freed.
Facts are append-only (a retraction adds pages, it frees none) and the trees only grow. Steady-state storage is therefore about the live pages plus one checkpoint's worth of freed pages, which is enough without shrinking.
This issue covers making sure the browser actually gets that behaviour, and covering the one case reuse leaves open.
1. Verify reuse on the browser path
BrowserBufferBackenddirty sets are O(change), so a transact flushes only the pages it touched (already noted in the v8 spec, §12).- The IndexedDB flush writes the meta page in the same IDB transaction as the data pages (v8 spec, §12).
2. v7→v8 migration bloat
The v8 migration leaves the whole old v7 region as free pages, about one extra copy of the database. Natively that is only disk space that later growth reuses. In the browser it is permanent heap, because open() loads every page, plus an equal amount of IndexedDB storage. Fix it with one of:
- Don't store free-page contents: keep free-page ids on the free list, but drop their bytes from the buffer and delete their IndexedDB keys, in the same IDB transaction as the meta page. An allocated page is always written before it is read, so its old contents are never needed. Reads of an absent page stay an error for any page that is not on the free list.
- Migrate into a fresh object store: write the v8 database to a new store, switch to it, then drop the v7 store.
This also needs #411 (migrated pages are not written back to IndexedDB).
Acceptance criteria
- After N transacts, the IndexedDB key count and the
BrowserBufferBackendpage count are O(live pages), not O(N). Add a test that asserts a bound. - After opening a migrated v7 database, buffer and IndexedDB size are about one copy of the v8 database, not v7 + v8.
- Fault injection (a crash between page writes and the meta commit) always reopens to a consistent meta page and free list.
exportGraph()output opens natively withMinigraf::open().
Activity
[-]BrowserDb: reclaim dead/free pages from the in-memory buffer and IndexedDB[/-][+]BrowserDb: bound page growth via v8 free-list reuse; avoid v7→v8 migration bloat[/+]Merge pull request #474 from project-minigraf/fix/411-440-browser-pages
adityamukho commentedon Oct 7, 2026
Fixed on
v3by #474 (merge deb82a4); ships in v3.0.0.fix(storage): skip the page-count check on the sparse browser store (#…