Skip to content

Commit dcd8abc

Browse files
committed
Merge branch 'main' into fix/idb-corruption-detect-and-heal
2 parents d98d169 + 7b1f808 commit dcd8abc

14 files changed

Lines changed: 1313 additions & 60 deletions

File tree

jestSetup.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ jest.mock('./lib/storage/platforms/index', () => require('./lib/storage/__mocks_
55
jest.mock('react-native-device-info', () => ({getFreeDiskStorage: () => {}}));
66
jest.mock('react-native-nitro-sqlite', () => ({
77
open: () => ({execute: () => {}}),
8-
enableSimpleNullHandling: () => undefined,
98
}));
109

1110
jest.useRealTimers();

lib/OnyxSnapshotCache.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,9 @@ class OnyxSnapshotCache {
6262
* Other options like `reuseConnection` don't affect the data transformation
6363
* or timing behavior of getSnapshot, so they're excluded from the cache key for better cache hit rates.
6464
*/
65-
registerConsumer<TKey extends OnyxKey, TReturnValue>(options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector'>): string {
65+
registerConsumer<TKey extends OnyxKey, TReturnValue>(key: TKey, options: Pick<UseOnyxOptions<TKey, TReturnValue>, 'selector'>): string {
6666
const selectorID = options?.selector ? this.getSelectorID(options.selector) : 'no_selector';
67-
const cacheKey = String(selectorID);
67+
const cacheKey = `${key}_${selectorID}`;
6868

6969
// Increment reference count for this cache key
7070
const currentCount = this.cacheKeyRefCounts.get(cacheKey) || 0;

lib/OnyxUtils.ts

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -347,9 +347,10 @@ function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<
347347
continue;
348348
}
349349

350-
const cacheValue = cache.get(key) as OnyxValue<TKey>;
351-
if (cacheValue) {
352-
dataMap.set(key, cacheValue);
350+
// hasCacheForKey catches cached falsy values (0, '', false, null) as cache hits, which
351+
// a truthy check on the value would miss.
352+
if (cache.hasCacheForKey(key)) {
353+
dataMap.set(key, cache.get(key) as OnyxValue<TKey>);
353354
continue;
354355
}
355356

@@ -401,6 +402,13 @@ function multiGet<TKey extends OnyxKey>(keys: CollectionKeyBase[]): Promise<Map<
401402
}
402403
}
403404

405+
// Prefer cache over stale storage if a concurrent write populated it during
406+
// the read — otherwise cache.merge(temp) below would resurrect dropped fields.
407+
if (cache.hasCacheForKey(key)) {
408+
dataMap.set(key, cache.get(key) as OnyxValue<TKey>);
409+
continue;
410+
}
411+
404412
dataMap.set(key, value as OnyxValue<TKey>);
405413
temp[key] = value as OnyxValue<TKey>;
406414
}
@@ -782,8 +790,12 @@ function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?:
782790

783791
function reportStorageQuota(error?: Error): Promise<void> {
784792
return Storage.getDatabaseSize()
785-
.then(({bytesUsed, bytesRemaining}) => {
786-
Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}. Original error: ${error}`);
793+
.then(({bytesUsed, bytesRemaining, usageDetails}) => {
794+
Logger.logInfo(
795+
`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}${
796+
usageDetails ? ` usageDetails: ${JSON.stringify(usageDetails)}` : ''
797+
}. Original error: ${error}`,
798+
);
787799
})
788800
.catch((dbSizeError) => {
789801
Logger.logAlert(`Unable to get database size. getDatabaseSize error: ${dbSizeError}. Original error: ${error}`);
@@ -1597,12 +1609,17 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
15971609
// finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
15981610
const finalMergedCollection = {...existingKeyCollection, ...newCollection};
15991611

1600-
// Pre-warm cache for any existing storage keys that aren't yet in cache. get() is a no-op
1601-
// (sync-resolved) for cache hits, and on a cache miss it reads from storage and writes the
1602-
// value back to cache. This is required so the subsequent cache.merge() merges the new delta
1603-
// into the real previous storage value (rather than starting from `undefined` and dropping
1604-
// the existing keys).
1605-
return Promise.all(existingKeys.map((key) => get(key))).then(() => {
1612+
// Pre-warm cache for cache-miss existingKeys so cache.merge() merges the new delta into
1613+
// the real previous storage value. Fast path (all warm) skips the pre-warm to preserve
1614+
// promise-chain depth; slow path batches the misses into one Storage.multiGet.
1615+
const hasColdExistingKey = existingKeys.some((key) => !cache.hasCacheForKey(key));
1616+
// Swallow pre-warm read failures so a transient Storage.multiGet rejection doesn't
1617+
// skip the cache.merge() + keysChanged() below. Subscribers still see the merge even
1618+
// when storage reads fail.
1619+
const prewarmPromise = hasColdExistingKey
1620+
? multiGet(existingKeys).catch((err) => Logger.logInfo(`mergeCollectionWithPatches pre-warm failed; proceeding with cache-only merge. Error: ${err}`))
1621+
: Promise.resolve();
1622+
return prewarmPromise.then(() => {
16061623
// Snapshot previous values from the (now-warm) cache for keysChanged's diff, then update
16071624
// cache and notify subscribers synchronously BEFORE issuing storage writes. This matches
16081625
// the cache-first / storage-second invariant followed by every other Onyx write method

lib/storage/providers/IDBKeyValProvider/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ const provider: StorageProvider<UseStore | undefined> = {
156156
.then((value) => ({
157157
bytesUsed: value.usage ?? 0,
158158
bytesRemaining: (value.quota ?? 0) - (value.usage ?? 0),
159+
usageDetails: (value as StorageEstimate & {usageDetails?: Record<string, number>}).usageDetails,
159160
}))
160161
.catch((error) => {
161162
throw new Error(`Unable to estimate web storage quota. Original error: ${error}`);

lib/storage/providers/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ type StorageKeyList = OnyxKey[];
77
type DatabaseSize = {
88
bytesUsed: number;
99
bytesRemaining: number;
10+
usageDetails?: Record<string, number>;
1011
};
1112

1213
type OnStorageKeyChanged = <TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>) => void;

lib/useOnyx.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,10 @@ function useOnyx<TKey extends OnyxKey, TReturnValue = OnyxValue<TKey>>(
124124
// Cache the options key to avoid regenerating it every getSnapshot call
125125
const cacheKey = useMemo(
126126
() =>
127-
onyxSnapshotCache.registerConsumer({
127+
onyxSnapshotCache.registerConsumer(key, {
128128
selector: options?.selector,
129129
}),
130-
[options?.selector],
130+
[key, options?.selector],
131131
);
132132

133133
useEffect(() => () => onyxSnapshotCache.deregisterConsumer(key, cacheKey), [key, cacheKey]);

0 commit comments

Comments
 (0)