Repository navigation
Conversation
…lback - Add `onPersistenceError(error)`, wired to Loki's `autosaveCallback`: called once per save failure (not on every autosave retry), and again after a successful save. Raw IDB events are unwrapped, quota errors also go to `onQuotaExceededError`, and save failures are always logged. - Add `allowInMemoryFallback` (default `true`): with `false`, unusable IndexedDB (missing, failing to open, or throwing on access/open) fails setup, so `onSetUpError` is called, with the original error as `cause`. - Add `onInMemoryFallback(reason)`, called once per adapter. - On `versionchange` (database deleted or upgraded in another tab), stop saving instead of re-creating the database from memory, break the driver so changes (`batch`, local storage, `unsafeExecute`, `unsafeResetDatabase`) are rejected, and report it via `onPersistenceError` before calling the app's `onversionchange`. - Errors thrown by app callbacks on these paths are logged, so one failing callback can't skip the others or change the fallback policy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Hi @joker-777, thank you for such a careful investigation, especially the multi-tab browser tests! I maintain NitromelonDB, a maintained fork of WatermelonDB (New Architecture, Nitro Modules). Something that might interest you: on the web, our
More details are in the wa-sqlite adapter docs. One caveat: existing LokiJS data in the browser is not migrated automatically. If you need to stay on LokiJS for now, the fork still ships the LokiJS adapter with the same code, so a PR there would be very welcome too. Our fork was rewritten in TypeScript (strict mode) to reduce the chance of bugs like this one slipping through, so I think the community would really benefit if you opened your PRs against it too. If you don't have the time or would rather not, no worries at all, I'm happy to port it myself. Just let me know. Thanks a lot! |
Problem
On the web,
LokiJSAdaptercan lose data without the app ever finding out:onQuotaExceededErroronly fires when the pre-open checker database fails, which a full disk does not trigger.database.write()resolving therefore does not mean the data is on disk (see Save database from in-memory loki db to IndexedDB manually on web #1329).LokiMemoryAdapterwith only a log line. Setup succeeds,onSetUpErroris not called, and the app cannot tell that everything will be lost when the page is closed.IncrementalIndexedDBAdaptercloses its connection onversionchange. The next autosave in this tab silently re-opens and re-creates the database from this tab's memory. Related multi-tab issue: Schema migration issue - multiple tabs #1865.Changes
All new options are additive. Defaults keep today's behaviour, except for the deleted-database case.
onPersistenceError(error)is wired to Loki'sautosaveCallback. It is called once per failure, not on every 500 ms autosave retry, and again if saving fails after it succeeded in the meantime. Raw IDBEvents are unwrapped toevent.target.error.QuotaExceededErrors are also passed toonQuotaExceededError. Save failures are now always logged, not only in the adapter's debug mode.allowInMemoryFallbackdefaults totrue. Withfalse, a missing or unusable IndexedDB fails setup, soonSetUpErroris called. The error'scauseis the original IDB error.onInMemoryFallback(reason)lets an app that keeps the default learn about the fallback, for example to warn the user. It is called once per adapter, even though setting up an empty database opens it twice.versionchange: Loki saving is disabled through the existinglokiFatalErrorpath instead of re-creating the database. The driver is put in the broken state, so further changes are rejected while reads still work.onPersistenceErroris called with aVersionChangeError, and the app's ownonversionchangeis still called afterwards. Breaking the driver was factored out ofDatabaseDriver._fatalErrorinto_breakDriver, which does not depend onexperimentalAllowsFatalError.indexedDBor callingindexedDB.openthrows (e.g.SecurityErrorwhen storage access is denied, or in a sandboxed iframe), it is handled like other open failures. It falls back to memory by default, where setup used to fail, or fails setup withallowInMemoryFallback: false.index.d.ts, the web adapter section of the Setup docs, and the changelog are updated.Remaining multi-tab caveats (documented, not addressed here)
onDidOverwriteis called. Coordinating writers, for example with Web Locks, is out of scope.versionchange, the tab must be reloaded. There is no in-place recovery.LokiIndexedAdapterhas noversionchangehandling.useWebWorker: false, like the existing ones.Usage
Tests
src/adapters/lokijs/persistence.test.jsuses a fake incremental persistence adapter through_testLokiAdapterand a stubbedindexedDBglobal. It covers:versionchangestopping saves and rejecting writes;allowInMemoryFallback, and the checker open failure.Jest for
src/adapters/lokijsandsrc/adapters/__tests__passes, with 150 tests. ESLint, TSLint and Flow are clean.Validation against a real IndexedDB implementation
The unit tests use a fake persistence adapter. In addition, the cross-tab deletion path was checked once with the real
LokiJSAdapterand lokijsIncrementalIndexedDBAdapteron top offake-indexeddb6.0.1, a spec-compliant IndexedDB implementation. That check is not committed, because adding a dev dependency would grow the diff.indexedDB.deleteDatabase(). The deletion is not blocked,onversionchangeandonPersistenceErrorare each called once, later writes are rejected, and after several autosave intervals the database still does not exist.versionchangehandling disabled, the same scenario re-creates the deleted database.Jest's sandbox can't simulate a global
indexedDBgetter that throws. That case was checked with a plain Node script that loads the adapter through Babel. With the fix, the default setup falls back to memory andallowInMemoryFallback: falsefails setup, both with the original error ascause. Before the fix, setup failed in both modes.Validation in a real browser with multiple tabs
This PR's code was transpiled with the repo's Babel config, bundled with esbuild into a test page, and served from localhost. Each tab of Chromium 152 ran its own
LokiJSAdapteron real IndexedDB, withuseIncrementalIndexedDB: true,useWebWorker: falseandallowInMemoryFallback: false. The test page is not committed.onPersistenceErrorwith aVersionChangeError, thenonversionchange. Tab A rejectsbatchandunsafeExecute, and 3 s later the database still does not exist.versionchangemid-save and reports once, with no second error for the aborted save. The database is still gone 4 s later.versionchangehandling disabledonSetUpErrorafter aVersionChangeError. From 10 to 80 ms, setup succeeds, then the driver is broken. In every case where the database existed, it stayed deleted.The result does not depend on event order. Saving is disabled synchronously in the
versionchangehandler, and closing the connection lets an in-flight transaction finish before the deletion proceeds. So whichever finishes first, nothing writes to the database after it has been deleted.Independent of #1980 (
unsafeResetDatabase()during an autosave); the two touchlokiExtensions.jsin different places and merge cleanly in either order.🤖 Generated with Claude Code