feat: add visibilitychange probe for proactive IDB health check#788
Draft
leshniak wants to merge 10 commits into
Draft
feat: add visibilitychange probe for proactive IDB health check#788leshniak wants to merge 10 commits into
leshniak wants to merge 10 commits into
Conversation
Adds a Dexie-style heal pattern to createStore for Chromium's Internal error opening backing store error (884K errors/month). - isBackingStoreError() detects the Chromium-specific corruption - Shared healAttemptsRemaining counter (3, reset on success) - On backing store error: clear cached connection, retry once - Clear dbp on rejection so retries get fresh indexedDB.open() - 5 new tests: mid-session heal, init heal, budget exhaustion, budget reset, error classification No deleteDatabase(), no provider swap, no UI changes. Scoped to IDBKeyValProvider only -- SQLite provider untouched. Ref: Expensify/App#90636 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Capture dbp reference before attaching reject handler; only clear if dbp hasn't been replaced by a concurrent heal/retry (prevents stale rejection handler from clearing a newer promise) - Add comment documenting concurrent store() budget drain behavior - Fix test formatting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ose) The heal path clears the cached dbp and reopens via indexedDB.open(), but does not call db.close() on the old IDBDatabase. Updated comments and log messages from 'close + reopen' to 'drop cached connection and reopen' to match what the code actually does. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- isBackingStoreError: use Error instead of DOMException, drop .name check - InvalidStateError catch: same simplification - Remove issue link from JSDoc Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add isConnectionLostError() to detect 'Connection to Indexed Database server lost' and 'Connection is closing' — Safari/WebKit errors that fire when the browser terminates IDB connections for backgrounded tabs. Uses the same heal-and-retry mechanism as backing store corruption: drop cached dbp, retry once with fresh indexedDB.open(), shared budget. Addresses Expensify/App#87864. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract module-level helpers: isInvalidStateError, isBudgetedHealError, getBudgetedHealErrorLabel. Extract cacheOpenPromise to deduplicate rejected-promise cleanup in getDB and verifyStoreExists. Pure refactor — no behavior change. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Register a visibilitychange listener inside createStore() that runs a lightweight readonly probe when the tab returns to foreground. If the probe detects a dead IDB connection (connection lost, backing store error, or InvalidStateError), it drops the cached dbp so the next real operation opens a fresh connection instead of failing. This prevents the ReconnectApp write storm from hitting a dead IDB connection after Safari backgrounds a tab. Addresses Expensify/App#87864. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Probe start: logInfo when tab becomes visible and probe begins - Probe healthy: logInfo confirming connection is healthy - Probe stale: logAlert with error details when stale connection detected - Heal attempts/success/exhaustion/non-recoverable: same as Expensify#780 - Updated test assertions to match new log messages and levels Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The "connection is healthy" log was emitted synchronously after count(), before the IDB request completed. If the request later failed via onerror, both healthy and stale logs would fire for the same visibility event. Now only logs on actual success. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Details
Adds a proactive
visibilitychangeprobe to the IDB connection manager, building on the reactive heal mechanism from #780.Problem: Safari kills IDB connections for backgrounded tabs (WebKit #197050, #201483). When the user returns to the Expensify tab, the app fires a ReconnectApp write storm that hits the dead cached
dbp— every write fails before the reactive heal can kick in.Solution: Register a
visibilitychangelistener insidecreateStore()that runs a lightweight readonlycount()probe when the tab becomes visible. If the probe detects a dead IDB connection, it drops the staledbpbefore the write storm arrives, so the first real operation opens a fresh connection.What it does:
isStaleConnectionError()— union detector for all three stale connection error types (InvalidStateError, backing store corruption, connection lost)visibilitychangelistener withprobePromiseguard — prevents stale probe from clearing adbpthat was already replaced by a concurrent heal/retryreq.onerror— only dropsdbpfor actual stale connection errors, not unrelated IDB errorstypeof document !== 'undefined'for SSR/Node safetyDepends on: #780 (reactive heal mechanism)
Related Issues
Expensify/App#87864
Automated Tests
4 new tests in
tests/unit/storage/providers/createStoreTest.ts:Visibilitychange probe (4): probe detects dead connection + drops dbp, skipped when no dbp, healthy connection preserved, InvalidStateError sync throw handled
All 456 tests pass.
Manual Test Steps
Simulating Safari connection lost:
IDB visibilitychange probe: connection lost, dropping cached connectionappears (if Safari killed the connection)Author Checklist
### Related Issuessection aboveTestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)Screenshots/Videos
Android: Native
N/A — library-level change, IDB is web-only. No UI, no native code touched.
Android: mWeb Chrome
N/A — library-level change, IDB is web-only. No UI, no native code touched.
iOS: Native
N/A — library-level change, IDB is web-only. No UI, no native code touched.
iOS: mWeb Safari
N/A — library-level change, IDB is web-only. No UI, no native code touched.
MacOS: Chrome / Safari
N/A — library-level change, no UI. Verified via 456/456 unit tests passing.