Repository navigation
Conversation
|
@fabioh8010 I've aligned the code to your feedback. I've refactored the |
| @@ -0,0 +1,7 @@ | |||
| /** Messages of errors thrown by the IndexedDB provider itself, matched by `classifyIDBError` as UNAVAILABLE. */ | |||
There was a problem hiding this comment.
"matched by classifyIDBError as UNAVAILABLE." is this right?
There was a problem hiding this comment.
Yes, both messages are classified as StorageErrorClass.UNAVAILABLE (missing engine and spent heal budget both mean the store is gone for the session). I reworded the comment to name the class explicitly so it doesn't read as the key is unavailable
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
mountiny
left a comment
There was a problem hiding this comment.
Changes look good but waiting for testing and checklist from @mkhutornyi
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeiOS: HybridAppiOS: mWeb SafariMacOS: Chrome / Safaritest1.movtest2.mov |
| return provider; | ||
| }, | ||
|
|
||
| degradeToMemoryOnly: (error) => degradePerformance(error), |
There was a problem hiding this comment.
degradePerformance can run many times
Consider adding if (provider === MemoryOnlyProvider) return; at the start of this function
There was a problem hiding this comment.
I made the fix you suggested
| Logger.logAlert(`Failed to load data from storage during init. The app will boot with default key states only. Error: ${error}`); | ||
|
|
||
| if (Storage.classifyError(error) === StorageErrorClass.FATAL) { | ||
| Storage.degradeToMemoryOnly(error); |
There was a problem hiding this comment.
degradeToMemoryOnly can receive something that isn't an Error
The value caught at startup is passed in as is.
A string rejection that is classified as FATAL from its text produces the log line Error: undefined Stack: undefined, which loses the only diagnostic for the switch.
There was a problem hiding this comment.
I made a fix like you suggested
| errorMessage, | ||
| }); | ||
| } else if (errorClass === StorageErrorClass.UNKNOWN) { | ||
| throw new Error(`${IDBErrorMessage.HEAL_EXHAUSTED}: ${errorMessage}`, {cause: error}); |
There was a problem hiding this comment.
HEAL_EXHAUSTED is detected by matching text in the error message
Consider using dedicated name such as IDBHealExhaustedError, checked by name
There was a problem hiding this comment.
Fixed like you suggested
A tab on memory-only storage loses data when another tab writesAfter a tab switches to memory-only storage, cross-tab sync still reads changed keys from that tab's own storage. That storage is now the empty in-memory store, so it returns On Example: two tabs both fall back to memory. Send a message in tab B, and in tab A that chat's messages vanish until reload. Suggested fix: ignore sync events (or skip |
mkhutornyi
left a comment
There was a problem hiding this comment.
No App issues found.
Worth checking #854 (comment) before merge
|
@fabioh8010 @sosek108 can you check the review comment, please? |
|
@mkhutornyi for I've applied your suggested fixes (alongside with #854 (comment)). Ready for review |
Details
Follow-up to #853. That PR made
UnknownError: Internal error.go through the connection layer'sFATALheal. A heal reopens the connection, which is the same thing a reload does. For users whose database stays broken across connections (the user in the linked issue failed init on 5 reloads in a row), the heal budget runs out. After that, Onyx keeps sending every read and write to a dead IndexedDB for the rest of the session.With this PR, once the budget is exhausted,
createStorerethrows the error asIndexedDB heal budget exhausted: <original message>, with the original error ascause.classifyIDBErrormaps that wording toUNAVAILABLE. That class already does what we need:lib/storage/index.ts) swaps inMemoryOnlyProvider, andretryOperationskips the retry quietly.The app keeps working for the session with in-memory storage, so data from
OpenAppand later writes can be read back. Nothing persists across reloads, but persistence was already broken for these users.The switch also happens earlier, at init. When
Storage.getAllininitializeWithDefaultKeyStatesfailsFATAL, the connection layer has already reopened once and the read still failed. OnyxUtils boots with defaults as before, and now also calls the newStorage.degradeToMemoryOnly(error). Without this, the first writes after boot (the OpenApp burst) would each fail against IndexedDB until the budget ran out, and get dropped. The session already runs on defaults at that point, so writing it into a database we could not read gains nothing.The budget-exhausted path still covers a database that breaks after a good init. It only fires after 3 failed reopen attempts with no successful operation in between. A one-off
FATALstill heals as before and keeps IndexedDB.The
heal budget exhaustedalert now carrieserrorMessage. Together with theFalling back to only using cachelog, it shows in VictoriaLogs how many sessions end up on memory-only, and for which error.Related Issues
Expensify/App#102272
Linked E/App PR
Expensify/App#103208
Automated Tests
classifyErrorTest: the heal-exhausted wording classifies asUNAVAILABLE.createStoreTest: once the budget is spent, the rejection carries the new message and the originalDOMExceptionascause, and classifies asUNAVAILABLE.tryOrDegradePerformanceTest: a heal-exhausted rejection swaps the provider toMemoryOnlyProvider, and a later write and read round-trip through it.tryOrDegradePerformanceTest:degradeToMemoryOnlyswaps the provider toMemoryOnlyProvider.onyxUtilsTest: an init read failing withUnknownError: Internal error.callsdegradeToMemoryOnly, and one failing with an unclassified error does not.Manual Tests
To be verified against the linked E/App PR.
Author Checklist
### Related Issuessection above### Linked E/App PRsection above, and verified this change against it (E/App CI passed and manual testing completed)TestssectiontoggleReportand notonIconClick)myBool && <MyComponent />.STYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)/** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Avataris modified, I verified thatAvataris working as expected in all cases)mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
Nagranie.z.ekranu.2026-10-8.o.10.33.15.mov
iOS: mWeb Safari
MacOS: Chrome / Safari
Nagranie.z.ekranu.2026-10-8.o.11.59.18.mov