darepod: persist wasm seed in OPFS instead of localStorage - #811
Conversation
There was a problem hiding this comment.
Code Review
This pull request migrates the WebAssembly encrypted seed storage from browser localStorage to the Origin-Private File System (OPFS), enabling the daemon to run inside a Web Worker. It introduces several helper functions to handle the asynchronous JS OPFS API synchronously in Go. Feedback on the changes highlights a potential file lock leak in SaveEncryptedSeed where the writable stream is not closed if the write operation fails; a suggestion is provided to ensure close() is always called.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
localStorage is a Window-only API that does not exist in Web Workers, so the wasm daemon could not initialize a wallet when run off the main thread; it failed with "browser localStorage is unavailable". That blocked running the runtime in a Worker to keep host UIs responsive. Rewrite the js+wasm seed storage to read, write, and stat a single OPFS file (the analog of the native file backend) via the async OPFS API, which is available on both the window and worker globals. The functions block the calling goroutine on the OPFS promises to preserve their synchronous signatures, which is safe because each verb runs on its own goroutine. Call sites and the native backend are unchanged.
5c99ae0 to
7badbc9
Compare
|
@claude review this |
|
Claude finished @Roasbeef's task in 2m 4s —— View job Review:
|
SeedFileExists previously returned err == nil, collapsing every OPFS failure into "no seed". A non-NotFound rejection (transient I/O, quota, or a handle locked by a concurrent writer) would then make the daemon enter WalletStateNone, where a subsequent InitWallet passes the None->Locked CompareAndSwap guard and SaveEncryptedSeed overwrites a seed that may in fact still exist, losing funds. Preserve the DOMException name on rejected OPFS promises so callers can tell a genuinely-absent file (NotFoundError) and an unavailable OPFS API apart from an ambiguous runtime error. SeedFileExists now reports absence only for those two confirmed cases and otherwise reports the seed as present, keeping the daemon out of the clobber path.
The seed file is walked as a nested OPFS directory path, but the daemon's SQLite data is stored under hashed flat names at the OPFS root, so the two are not co-located. Reword the package comment, which claimed the seed lands "alongside the daemon's other OPFS data", to describe the actual layout. Also note that awaitJSPromise requires a thenable argument, since it is now the shared chokepoint for all OPFS access.
Summary
The wasm daemon persists the encrypted wallet seed to browser
localStorage. ButlocalStorageis aWindow-only API that doesn't exist in Web Workers, so when the daemon runs off the main thread, wallet create/unlock fails withbrowser localStorage is unavailable. That blocks running the runtime in a Worker, which is what we want so host UIs stay responsive during boot and heavy operations. Everything else, including the OPFS-backed SQLite stack, already works in a Worker.This rewrites the js+wasm seed storage to use the origin-private file system (OPFS) instead. OPFS is reachable from both the window and worker globals, so the same code path works in either context.
Changes
SaveEncryptedSeed/LoadEncryptedSeed/SeedFileExistsindarepod/seed_storage_wasm.goto read, write, and stat a single OPFS file, the async-OPFS analog of the native file backend.SeedFilePathstring as the OPFS path, walking it as nested directory handles so the seed lands alongside the daemon's other OPFS data.wasmsqlitedriver already uses: it blocks the goroutine, never the JS event loop).!js) backend are untouched.Testing