Skip to content

Commit af905f9

Browse files
committed
Merge branch 'main' into fix/idb-visibilitychange-probe
2 parents b63de8b + 14be5b8 commit af905f9

15 files changed

Lines changed: 1321 additions & 66 deletions

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/createStore.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const HEAL_ATTEMPTS_MAX = 3;
1010
* internal recovery (RepairDB -> delete -> recreate) also fails.
1111
*/
1212
function isBackingStoreError(error: unknown): boolean {
13-
return error instanceof Error && error.message.includes('Internal error opening backing store');
13+
return (error instanceof Error || error instanceof DOMException) && (error as Error).message.includes('Internal error opening backing store');
1414
}
1515

1616
/**
@@ -19,22 +19,24 @@ function isBackingStoreError(error: unknown): boolean {
1919
* WebKit bugs: https://bugs.webkit.org/show_bug.cgi?id=197050, https://bugs.webkit.org/show_bug.cgi?id=201483
2020
*/
2121
function isConnectionLostError(error: unknown): boolean {
22-
if (!(error instanceof Error)) return false;
23-
const msg = error.message.toLowerCase();
22+
if (!(error instanceof Error || error instanceof DOMException)) return false;
23+
const msg = (error as Error).message.toLowerCase();
2424
return msg.includes('connection to indexed database server lost') || msg.includes('connection is closing');
2525
}
2626

2727
function isInvalidStateError(error: unknown): boolean {
28-
return error instanceof Error && error.name === 'InvalidStateError';
28+
return (error instanceof Error || error instanceof DOMException) && (error as Error).name === 'InvalidStateError';
2929
}
3030

3131
/** Errors that trigger a budgeted heal-and-retry in store(). */
3232
function isBudgetedHealError(error: unknown): boolean {
3333
return isBackingStoreError(error) || isConnectionLostError(error);
3434
}
3535

36-
function getBudgetedHealErrorLabel(error: unknown): 'backing store' | 'connection lost' {
37-
return isBackingStoreError(error) ? 'backing store' : 'connection lost';
36+
function getBudgetedHealErrorLabel(error: unknown): string {
37+
if (isBackingStoreError(error)) return 'backing store';
38+
if (isConnectionLostError(error)) return 'connection lost';
39+
return 'unknown';
3840
}
3941

4042
/** Union of all error types indicating a stale/dead IDB connection. Used by the visibilitychange probe. */

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)