feat(sqlite): ask the browser not to evict a browser database - #658
Merged
Conversation
Rask.SQLite.Browser keeps its snapshots in IndexedDB, and IndexedDB is evictable. Under storage pressure a browser may discard them, and the database comes back empty on the next load with nothing to indicate why — the app looks like it lost the user's data, and nothing in it can tell them otherwise. #645 landed IStorageEstimator.RequestPersistAsync, so the owning tab can now ask. IsPersistedAsync is checked first, so an origin that is already exempt is never asked a second time — which matters on the browsers that prompt. A refusal is logged and changes nothing else. The app runs exactly as it did before; the risk simply stops being silent. One log branch, not two: RequestPersistAsync resolves false both when the browser declines and when it has no such API, and from the caller's side those have the same consequence. Only the owning tab asks. The others persist nothing, so a prompt there would buy the user nothing. RequestPersistentStorage defaults to true because a silently evictable database is a worse default than a request, but it is an option for a real reason: Chromium decides from engagement heuristics without prompting, whereas Firefox shows a permission prompt — and this is asked during boot rather than from a click. An app that would rather choose its moment sets it to false and calls IStorageEstimator.RequestPersistAsync() from a user-gesture handler instead.
A second tab was told to close the other one and then left to guess when that had happened. Now BrowserSqliteOwnership.Available completes once the owner goes away, so an app can offer a reload. Reloading is what takes ownership, and that is a constraint rather than a shortcut. A waiting tab already opened its own empty database at boot and the app holds live connections to it: the file cannot be swapped underneath them, and a tab that started persisting its empty database would overwrite the previous owner's good snapshot with nothing. So this reports availability; it does not transfer ownership. The watcher polls with TryRequestAsync, which acquires and releases inside the call, rather than waiting on RequestAsync. Waiting would mean HOLDING the lock the instant it frees — making this tab an owner it must never be, and blocking a tab that could actually use it. Which is also why the signal is advisory: another tab may win between the poll and the reload, and the reloaded page runs the normal election and finds out. The watch is started unawaited so it cannot delay the boot, and StopAsync cancels and awaits it so a poll cannot outlive the page. Covered by an E2E that opens two real pages in one browser context, closes the owner, and asserts the second flips from "another tab has this" to "your data is ready".
pal-tamas
force-pushed
the
worktree-browser-sqlite-persist
branch
from
August 10, 2026 04:44
e37685c to
ff8ee98
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Rask.SQLite.Browserkeeps its snapshots in IndexedDB, and IndexedDB is evictable. Under storagepressure a browser may discard them, and the database comes back empty on the next load with nothing to
indicate why — the app looks like it lost the user's data, and nothing in it can say otherwise.
#645 landed
IStorageEstimator.RequestPersistAsync(), so theowning tab can now ask for the origin to be exempted from eviction.
Decisions worth reviewing
IsPersistedAsync()is checked before asking, so an origin that is already exempt is never asked asecond time — which matters on the browsers that prompt.
before, the risk simply stops being silent. One log branch, not two:
RequestPersistAsyncresolvesfalseboth when the browser declines and when it has no such API, and from the caller's side those havethe same consequence.
RequestPersistentStoragedefaults totrue, because a silently evictable database is a worsedefault than a request. It is an option for a real reason though: Chromium decides from engagement
heuristics without prompting, whereas Firefox shows a permission prompt — and this is asked during
boot rather than from a click. An app that would rather choose its moment sets it to
falseand callsIStorageEstimator.RequestPersistAsync()from a user-gesture handler.Testing
Rask.SQLite.Browser.Tests(6 new): asks once; never asks when already persisted; never asksin a non-owner tab; never asks when disabled; still boots and restores when declined; still boots when
the interop throws.
dotnet formatclean,dotnet build Rask.slnx -warnaserrorclean.Follow-ups (not in this PR)
IOriginPrivateFileSystem.WriteAsyncnow exists, so an offset+length interfacecan be designed against something real, with per-page hashes as the diff baseline rather than a shadow
copy.