react-native-onyx 3.0.85 → 3.0.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/OnyxCache.d.ts
CHANGED
|
@@ -122,8 +122,10 @@ declare class OnyxCache {
|
|
|
122
122
|
addEvictableKeysToRecentlyAccessedList(isCollectionKeyFn: (key: OnyxKey) => boolean, getAllKeysFn: () => Promise<Set<OnyxKey>>): Promise<void>;
|
|
123
123
|
/**
|
|
124
124
|
* Finds the least recently accessed key that can be safely evicted from storage.
|
|
125
|
+
* `excludeKeys` skips keys that must not be evicted (e.g. the in-flight write's own keys,
|
|
126
|
+
* whose cache value is the merge base the retry depends on).
|
|
125
127
|
*/
|
|
126
|
-
getKeyForEviction(): OnyxKey | undefined;
|
|
128
|
+
getKeyForEviction(excludeKeys?: Set<OnyxKey>): OnyxKey | undefined;
|
|
127
129
|
/**
|
|
128
130
|
* Set the collection keys for optimized storage
|
|
129
131
|
*/
|
package/dist/OnyxCache.js
CHANGED
|
@@ -265,11 +265,18 @@ class OnyxCache {
|
|
|
265
265
|
}
|
|
266
266
|
/**
|
|
267
267
|
* Finds the least recently accessed key that can be safely evicted from storage.
|
|
268
|
+
* `excludeKeys` skips keys that must not be evicted (e.g. the in-flight write's own keys,
|
|
269
|
+
* whose cache value is the merge base the retry depends on).
|
|
268
270
|
*/
|
|
269
|
-
getKeyForEviction() {
|
|
271
|
+
getKeyForEviction(excludeKeys) {
|
|
270
272
|
// recentlyAccessedKeys is ordered from least to most recently accessed,
|
|
271
|
-
// so the first
|
|
272
|
-
|
|
273
|
+
// so the first non-excluded key is the best candidate for eviction.
|
|
274
|
+
for (const key of this.recentlyAccessedKeys) {
|
|
275
|
+
if (!(excludeKeys === null || excludeKeys === void 0 ? void 0 : excludeKeys.has(key))) {
|
|
276
|
+
return key;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return undefined;
|
|
273
280
|
}
|
|
274
281
|
/**
|
|
275
282
|
* Set the collection keys for optimized storage
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -140,7 +140,7 @@ declare function reportStorageQuota(error?: Error): Promise<void>;
|
|
|
140
140
|
* - Non-retriable errors: logs an alert and resolves without retrying
|
|
141
141
|
* - Other errors: retries the operation
|
|
142
142
|
*/
|
|
143
|
-
declare function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, onyxMethod: TMethod, defaultParams: Parameters<TMethod>[0], retryAttempt: number | undefined): Promise<void>;
|
|
143
|
+
declare function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, onyxMethod: TMethod, defaultParams: Parameters<TMethod>[0], retryAttempt: number | undefined, inFlightKeys?: Set<OnyxKey>): Promise<void>;
|
|
144
144
|
/**
|
|
145
145
|
* Notifies subscribers and writes current value to cache
|
|
146
146
|
*/
|
package/dist/OnyxUtils.js
CHANGED
|
@@ -679,7 +679,7 @@ function reportStorageQuota(error) {
|
|
|
679
679
|
* - Non-retriable errors: logs an alert and resolves without retrying
|
|
680
680
|
* - Other errors: retries the operation
|
|
681
681
|
*/
|
|
682
|
-
function retryOperation(error, onyxMethod, defaultParams, retryAttempt) {
|
|
682
|
+
function retryOperation(error, onyxMethod, defaultParams, retryAttempt, inFlightKeys) {
|
|
683
683
|
var _a, _b, _c, _d;
|
|
684
684
|
const currentRetryAttempt = retryAttempt !== null && retryAttempt !== void 0 ? retryAttempt : 0;
|
|
685
685
|
const nextRetryAttempt = currentRetryAttempt + 1;
|
|
@@ -704,8 +704,10 @@ function retryOperation(error, onyxMethod, defaultParams, retryAttempt) {
|
|
|
704
704
|
// @ts-expect-error No overload matches this call.
|
|
705
705
|
return onyxMethod(defaultParams, nextRetryAttempt);
|
|
706
706
|
}
|
|
707
|
-
// Find the least recently accessed evictable key that we can remove
|
|
708
|
-
|
|
707
|
+
// Find the least recently accessed evictable key that we can remove. Never evict an in-flight
|
|
708
|
+
// key — its cache value is the merge base this retry depends on, so dropping it would truncate
|
|
709
|
+
// the write to just the delta and diverge cache from storage.
|
|
710
|
+
const keyForRemoval = OnyxCache_1.default.getKeyForEviction(inFlightKeys);
|
|
709
711
|
if (!keyForRemoval) {
|
|
710
712
|
// If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
|
|
711
713
|
// then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we
|
|
@@ -1177,21 +1179,29 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1177
1179
|
// so re-entrant callbacks (e.g. Onyx.set inside a callback) see consistent cache
|
|
1178
1180
|
// and subscriber state, matching the original per-key notification semantics.
|
|
1179
1181
|
OnyxCache_1.default.set(key, value);
|
|
1180
|
-
|
|
1182
|
+
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1183
|
+
// waitForCollectionCallback subscribers re-fire on every keyChanged by contract.
|
|
1184
|
+
if (!retryAttempt) {
|
|
1185
|
+
keyChanged(key, value);
|
|
1186
|
+
}
|
|
1181
1187
|
}
|
|
1182
1188
|
}
|
|
1183
1189
|
// One keysChanged() per collection — fires each collection-level subscriber once and lets
|
|
1184
1190
|
// keysChanged() internally decide which individual member subscribers need notification.
|
|
1185
|
-
|
|
1186
|
-
|
|
1191
|
+
// Skip on retry — already notified on attempt 0 (see same-reason comment above).
|
|
1192
|
+
if (!retryAttempt) {
|
|
1193
|
+
for (const [collectionKey, batch] of collectionBatches) {
|
|
1194
|
+
keysChanged(collectionKey, batch.partial, batch.previous);
|
|
1195
|
+
}
|
|
1187
1196
|
}
|
|
1188
1197
|
const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => {
|
|
1189
1198
|
const [key] = keyValuePair;
|
|
1190
1199
|
// Filter out the RAM-only key value pairs, as they should not be saved to storage
|
|
1191
1200
|
return !OnyxKeys_1.default.isRamOnlyKey(key);
|
|
1192
1201
|
});
|
|
1202
|
+
const inFlightKeys = new Set(keyValuePairsToSet.map(([key]) => key));
|
|
1193
1203
|
return storage_1.default.multiSet(keyValuePairsToStore)
|
|
1194
|
-
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt))
|
|
1204
|
+
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt, inFlightKeys))
|
|
1195
1205
|
.then(() => {
|
|
1196
1206
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
|
|
1197
1207
|
});
|
|
@@ -1247,14 +1257,19 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
|
|
|
1247
1257
|
const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
|
|
1248
1258
|
for (const [key, value] of keyValuePairs)
|
|
1249
1259
|
OnyxCache_1.default.set(key, value);
|
|
1250
|
-
|
|
1260
|
+
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1261
|
+
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
|
|
1262
|
+
if (!retryAttempt) {
|
|
1263
|
+
keysChanged(collectionKey, mutableCollection, previousCollection);
|
|
1264
|
+
}
|
|
1251
1265
|
// RAM-only keys are not supposed to be saved to storage
|
|
1252
1266
|
if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
|
|
1253
1267
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1254
1268
|
return;
|
|
1255
1269
|
}
|
|
1270
|
+
const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
|
|
1256
1271
|
return storage_1.default.multiSet(keyValuePairs)
|
|
1257
|
-
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt))
|
|
1272
|
+
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt, inFlightKeys))
|
|
1258
1273
|
.then(() => {
|
|
1259
1274
|
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1260
1275
|
});
|
|
@@ -1364,10 +1379,15 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1364
1379
|
// write fails.
|
|
1365
1380
|
const previousCollection = getCachedCollection(collectionKey, existingKeys);
|
|
1366
1381
|
OnyxCache_1.default.merge(finalMergedCollection);
|
|
1367
|
-
|
|
1382
|
+
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1383
|
+
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
|
|
1384
|
+
if (!retryAttempt) {
|
|
1385
|
+
keysChanged(collectionKey, finalMergedCollection, previousCollection);
|
|
1386
|
+
}
|
|
1368
1387
|
const promises = [];
|
|
1369
|
-
// New keys
|
|
1370
|
-
//
|
|
1388
|
+
// New keys go through multiSet and existing keys through multiMerge. multiMerge on a
|
|
1389
|
+
// missing key stores the value just like multiSet across all backends; splitting them lets
|
|
1390
|
+
// multiSet strip nested nulls (the merge layer keeps them to delete nested storage keys).
|
|
1371
1391
|
// We can skip this step for RAM-only keys as they should never be saved to storage
|
|
1372
1392
|
if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForExistingCollection.length > 0) {
|
|
1373
1393
|
promises.push(storage_1.default.multiMerge(keyValuePairsForExistingCollection));
|
|
@@ -1376,8 +1396,9 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1376
1396
|
if (!OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keyValuePairsForNewCollection.length > 0) {
|
|
1377
1397
|
promises.push(storage_1.default.multiSet(keyValuePairsForNewCollection));
|
|
1378
1398
|
}
|
|
1399
|
+
const inFlightKeys = new Set(Object.keys(finalMergedCollection));
|
|
1379
1400
|
return Promise.all(promises)
|
|
1380
|
-
.catch((error) => retryOperation(error, mergeCollectionWithPatches, { collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate }, retryAttempt))
|
|
1401
|
+
.catch((error) => retryOperation(error, mergeCollectionWithPatches, { collectionKey, collection: resultCollection, mergeReplaceNullPatches, isProcessingCollectionUpdate }, retryAttempt, inFlightKeys))
|
|
1381
1402
|
.then(() => {
|
|
1382
1403
|
sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
|
|
1383
1404
|
});
|
|
@@ -1427,13 +1448,18 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
|
|
|
1427
1448
|
const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
|
|
1428
1449
|
for (const [key, value] of keyValuePairs)
|
|
1429
1450
|
OnyxCache_1.default.set(key, value);
|
|
1430
|
-
|
|
1451
|
+
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1452
|
+
// waitForCollectionCallback subscribers re-fire on every keysChanged by contract.
|
|
1453
|
+
if (!retryAttempt) {
|
|
1454
|
+
keysChanged(collectionKey, mutableCollection, previousCollection);
|
|
1455
|
+
}
|
|
1431
1456
|
if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
|
|
1432
1457
|
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1433
1458
|
return;
|
|
1434
1459
|
}
|
|
1460
|
+
const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
|
|
1435
1461
|
return storage_1.default.multiSet(keyValuePairs)
|
|
1436
|
-
.catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt))
|
|
1462
|
+
.catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt, inFlightKeys))
|
|
1437
1463
|
.then(() => {
|
|
1438
1464
|
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1439
1465
|
});
|
|
@@ -69,6 +69,10 @@ function getBudgetedHealErrorLabel(error) {
|
|
|
69
69
|
return 'connection lost';
|
|
70
70
|
return 'unknown';
|
|
71
71
|
}
|
|
72
|
+
/** Union of all error types indicating a stale/dead IDB connection. Used by the visibilitychange probe. */
|
|
73
|
+
function isStaleConnectionError(error) {
|
|
74
|
+
return isInvalidStateError(error) || isBackingStoreError(error) || isConnectionLostError(error);
|
|
75
|
+
}
|
|
72
76
|
// This is a copy of the createStore function from idb-keyval, we need a custom implementation
|
|
73
77
|
// because we need to create the database manually in order to ensure that the store exists before we use it.
|
|
74
78
|
// If the store does not exist, idb-keyval will throw an error
|
|
@@ -146,6 +150,52 @@ function createStore(dbName, storeName) {
|
|
|
146
150
|
healAttemptsRemaining = HEAL_ATTEMPTS_MAX;
|
|
147
151
|
return result;
|
|
148
152
|
}
|
|
153
|
+
// Proactive IDB health check when tab returns to foreground.
|
|
154
|
+
// Safari kills IDB connections for backgrounded tabs. By probing as soon as
|
|
155
|
+
// the tab becomes visible, we drop the stale dbp early so the first real
|
|
156
|
+
// operation opens a fresh connection instead of failing.
|
|
157
|
+
document.addEventListener('visibilitychange', () => {
|
|
158
|
+
if (document.visibilityState !== 'visible' || !dbp) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
Logger.logInfo('IDB visibilitychange probe: tab became visible, checking connection health', { dbName, storeName });
|
|
162
|
+
const probePromise = dbp;
|
|
163
|
+
const dropCacheIfStale = (error) => {
|
|
164
|
+
if (dbp !== probePromise || !isStaleConnectionError(error)) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
Logger.logAlert('IDB visibilitychange probe: stale connection detected, dropping cached connection', {
|
|
168
|
+
dbName,
|
|
169
|
+
storeName,
|
|
170
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
171
|
+
});
|
|
172
|
+
dbp = undefined;
|
|
173
|
+
};
|
|
174
|
+
probePromise
|
|
175
|
+
.then((db) => {
|
|
176
|
+
if (dbp !== probePromise) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const tx = db.transaction(storeName, 'readonly');
|
|
181
|
+
const probeStore = tx.objectStore(storeName);
|
|
182
|
+
const req = probeStore.count();
|
|
183
|
+
req.onsuccess = () => {
|
|
184
|
+
Logger.logInfo('IDB visibilitychange probe: connection is healthy', { dbName, storeName });
|
|
185
|
+
};
|
|
186
|
+
req.onerror = () => {
|
|
187
|
+
dropCacheIfStale(req.error);
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
dropCacheIfStale(error);
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
.catch(() => {
|
|
195
|
+
// The cached open promise rejected; cacheOpenPromise already cleared dbp on its own
|
|
196
|
+
// branch. Swallow here so the probe's separate branch doesn't surface an unhandled rejection.
|
|
197
|
+
});
|
|
198
|
+
});
|
|
149
199
|
// Handles three recoverable error classes:
|
|
150
200
|
// 1. InvalidStateError — connection closed between getDB() resolving and db.transaction().
|
|
151
201
|
// Retry once with a fresh connection. No budget limit (transient, always worth one reopen).
|