react-native-onyx 3.0.99 → 3.0.101
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/Onyx.js +2 -3
- package/dist/OnyxCache.d.ts +7 -0
- package/dist/OnyxCache.js +78 -6
- package/dist/OnyxUtils.d.ts +11 -8
- package/dist/OnyxUtils.js +108 -33
- package/dist/storage/InstanceSync/index.web.js +15 -1
- package/dist/types.d.ts +0 -1
- package/dist/utils.d.ts +6 -0
- package/dist/utils.js +25 -0
- package/package.json +1 -1
package/dist/Onyx.js
CHANGED
|
@@ -103,7 +103,7 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
|
|
|
103
103
|
OnyxUtils_1.default.initStoreValues(keys, initialKeyStates, evictableKeys);
|
|
104
104
|
// Initialize all of our keys with data provided then give green light to any pending connections.
|
|
105
105
|
// addEvictableKeysToRecentlyAccessedList must run after initializeWithDefaultKeyStates because
|
|
106
|
-
// eager cache loading populates the key index (cache.
|
|
106
|
+
// eager cache loading populates the key index (cache.hydrate) inside initializeWithDefaultKeyStates,
|
|
107
107
|
// and the evictable keys list depends on that index being populated.
|
|
108
108
|
OnyxUtils_1.default.initializeWithDefaultKeyStates()
|
|
109
109
|
.then(() => OnyxCache_1.default.addEvictableKeysToRecentlyAccessedList(OnyxKeys_1.default.isCollectionKey, OnyxUtils_1.default.getAllKeys))
|
|
@@ -302,7 +302,7 @@ function merge(key, changes) {
|
|
|
302
302
|
* @param collection Object collection keyed by individual collection member keys and values
|
|
303
303
|
*/
|
|
304
304
|
function mergeCollection(collectionKey, collection) {
|
|
305
|
-
return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection
|
|
305
|
+
return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection }));
|
|
306
306
|
}
|
|
307
307
|
/**
|
|
308
308
|
* Clear out all the data in the store
|
|
@@ -516,7 +516,6 @@ function update(data) {
|
|
|
516
516
|
collectionKey,
|
|
517
517
|
collection: batchedCollectionUpdates.merge,
|
|
518
518
|
mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
|
|
519
|
-
isProcessingCollectionUpdate: true,
|
|
520
519
|
}));
|
|
521
520
|
}
|
|
522
521
|
if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
|
package/dist/OnyxCache.d.ts
CHANGED
|
@@ -66,6 +66,13 @@ declare class OnyxCache {
|
|
|
66
66
|
set(key: OnyxKey, value: OnyxValue<OnyxKey>): OnyxValue<OnyxKey>;
|
|
67
67
|
/** Forget the cached value for the given key */
|
|
68
68
|
drop(key: OnyxKey): void;
|
|
69
|
+
/**
|
|
70
|
+
* Bulk-loads values into a cache that's expected to be empty, skipping merge()'s per-key clone when
|
|
71
|
+
* safe. Falls back to a real merge for any key that already has a value, in case the cache wasn't
|
|
72
|
+
* empty after all. Used only by `Onyx.init()`.
|
|
73
|
+
* @param data - a map of (cache) key - values
|
|
74
|
+
*/
|
|
75
|
+
hydrate(data: Record<OnyxKey, OnyxValue<OnyxKey>>): void;
|
|
69
76
|
/**
|
|
70
77
|
* Deep merge data to cache, any non existing keys will be created
|
|
71
78
|
* @param data - a map of (cache) key - values
|
package/dist/OnyxCache.js
CHANGED
|
@@ -14,6 +14,15 @@ const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
|
|
|
14
14
|
* which relies on === equality to detect changes.
|
|
15
15
|
*/
|
|
16
16
|
const FROZEN_EMPTY_COLLECTION = Object.freeze({});
|
|
17
|
+
/**
|
|
18
|
+
* Merge options shared by every cache write path (`merge()` and `hydrate()`'s fallback), so the
|
|
19
|
+
* three call sites can't drift apart. Cached values must never hold nested nulls, and a source
|
|
20
|
+
* object carrying the replace mark must replace the target object rather than merge into it.
|
|
21
|
+
*/
|
|
22
|
+
const CACHE_MERGE_OPTIONS = {
|
|
23
|
+
shouldRemoveNestedNulls: true,
|
|
24
|
+
objectRemovalMode: 'replace',
|
|
25
|
+
};
|
|
17
26
|
// Task constants
|
|
18
27
|
const TASK = {
|
|
19
28
|
GET: 'get',
|
|
@@ -38,7 +47,7 @@ class OnyxCache {
|
|
|
38
47
|
this.collectionSnapshots = new Map();
|
|
39
48
|
this.dirtyCollections = new Set();
|
|
40
49
|
// bind all public methods to prevent problems with `this`
|
|
41
|
-
(0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'hasValueChanged', 'getCollectionData');
|
|
50
|
+
(0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hydrate', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'hasValueChanged', 'getCollectionData');
|
|
42
51
|
}
|
|
43
52
|
/** Get all the storage keys */
|
|
44
53
|
getAllKeys() {
|
|
@@ -126,6 +135,69 @@ class OnyxCache {
|
|
|
126
135
|
this.storageKeys.delete(key);
|
|
127
136
|
OnyxKeys_1.default.deregisterMemberKey(key);
|
|
128
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Bulk-loads values into a cache that's expected to be empty, skipping merge()'s per-key clone when
|
|
140
|
+
* safe. Falls back to a real merge for any key that already has a value, in case the cache wasn't
|
|
141
|
+
* empty after all. Used only by `Onyx.init()`.
|
|
142
|
+
* @param data - a map of (cache) key - values
|
|
143
|
+
*/
|
|
144
|
+
hydrate(data) {
|
|
145
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) {
|
|
146
|
+
throw new Error('data passed to cache.hydrate() must be an Object of onyx key/value pairs');
|
|
147
|
+
}
|
|
148
|
+
const affectedCollections = new Set();
|
|
149
|
+
// Use for-in loop to avoid an unnecessary array allocation from Object.keys()
|
|
150
|
+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
151
|
+
for (const key in data) {
|
|
152
|
+
const value = data[key];
|
|
153
|
+
this.addKey(key);
|
|
154
|
+
if (value === undefined) {
|
|
155
|
+
this.addNullishStorageKey(key);
|
|
156
|
+
// undefined means "no change" — skip storageMap modification
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
160
|
+
if (value === null) {
|
|
161
|
+
this.addNullishStorageKey(key);
|
|
162
|
+
delete this.storageMap[key];
|
|
163
|
+
if (collectionKey) {
|
|
164
|
+
affectedCollections.add(collectionKey);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
this.nullishStorageKeys.delete(key);
|
|
169
|
+
const existing = this.storageMap[key];
|
|
170
|
+
if (existing !== undefined) {
|
|
171
|
+
// Key already has a value, so the empty-cache assumption doesn't hold here (e.g. a write
|
|
172
|
+
// landed while storage was still being read). Fall back to a real merge, which has exactly
|
|
173
|
+
// the same semantics as the old `cache.merge(allDataFromStorage)` init path: the value
|
|
174
|
+
// loaded from disk is the merge source, so it wins on any overlapping leaf key.
|
|
175
|
+
// Note: this only covers non-null writes. A `cache.set(key, null)` that lands before hydrate
|
|
176
|
+
// clears the nullish marker too, so a deletion racing init is still undone - same as before.
|
|
177
|
+
const merged = utils_1.default.fastMerge(existing, value, CACHE_MERGE_OPTIONS).result;
|
|
178
|
+
// fastMerge is reference-stable: returns the original target when
|
|
179
|
+
// nothing changed, so a simple === check detects no-ops.
|
|
180
|
+
if (merged === existing) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
this.storageMap[key] = merged;
|
|
184
|
+
}
|
|
185
|
+
else if (utils_1.default.needsNormalization(value)) {
|
|
186
|
+
this.storageMap[key] = utils_1.default.fastMerge(undefined, value, CACHE_MERGE_OPTIONS).result;
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this.storageMap[key] = value;
|
|
190
|
+
}
|
|
191
|
+
if (collectionKey) {
|
|
192
|
+
affectedCollections.add(collectionKey);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// Mark affected collections as dirty — snapshots will be lazily rebuilt on next read
|
|
197
|
+
for (const collectionKey of affectedCollections) {
|
|
198
|
+
this.dirtyCollections.add(collectionKey);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
129
201
|
/**
|
|
130
202
|
* Deep merge data to cache, any non existing keys will be created
|
|
131
203
|
* @param data - a map of (cache) key - values
|
|
@@ -135,7 +207,10 @@ class OnyxCache {
|
|
|
135
207
|
throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
|
|
136
208
|
}
|
|
137
209
|
const affectedCollections = new Set();
|
|
138
|
-
for
|
|
210
|
+
// Use for-in loop to avoid an unnecessary array allocation from Object.entries()
|
|
211
|
+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
212
|
+
for (const key in data) {
|
|
213
|
+
const value = data[key];
|
|
139
214
|
this.addKey(key);
|
|
140
215
|
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
141
216
|
if (value === undefined) {
|
|
@@ -154,10 +229,7 @@ class OnyxCache {
|
|
|
154
229
|
this.nullishStorageKeys.delete(key);
|
|
155
230
|
// Per-key merge instead of spreading the entire storageMap
|
|
156
231
|
const existing = this.storageMap[key];
|
|
157
|
-
const merged = utils_1.default.fastMerge(existing, value,
|
|
158
|
-
shouldRemoveNestedNulls: true,
|
|
159
|
-
objectRemovalMode: 'replace',
|
|
160
|
-
}).result;
|
|
232
|
+
const merged = utils_1.default.fastMerge(existing, value, CACHE_MERGE_OPTIONS).result;
|
|
161
233
|
// fastMerge is reference-stable: returns the original target when
|
|
162
234
|
// nothing changed, so a simple === check detects no-ops.
|
|
163
235
|
if (merged === existing) {
|
package/dist/OnyxUtils.d.ts
CHANGED
|
@@ -15,6 +15,11 @@ declare const METHOD: {
|
|
|
15
15
|
/** Test-only: clears the disk-pressure log throttle so each test observes its own alert. */
|
|
16
16
|
declare function resetDiskPressureLogThrottle(): void;
|
|
17
17
|
type OnyxMethod = ValueOf<typeof METHOD>;
|
|
18
|
+
/** Result of `prepareKeyValuePairsForStorage`: pairs to write and keys whose `null` value marks them for removal. */
|
|
19
|
+
type PreparedKeyValuePairs = {
|
|
20
|
+
pairs: StorageKeyValuePair[];
|
|
21
|
+
keysToRemove: OnyxKey[];
|
|
22
|
+
};
|
|
18
23
|
declare function getSnapshotKey(): OnyxKey | null;
|
|
19
24
|
/**
|
|
20
25
|
* Getter - returns the merge queue.
|
|
@@ -121,7 +126,7 @@ declare function keysChanged<TKey extends CollectionKeyBase>(collectionKey: TKey
|
|
|
121
126
|
/**
|
|
122
127
|
* When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
|
|
123
128
|
*/
|
|
124
|
-
declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean
|
|
129
|
+
declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean): void;
|
|
125
130
|
/**
|
|
126
131
|
* Sends the data obtained from the keys to the connection.
|
|
127
132
|
*/
|
|
@@ -133,7 +138,7 @@ declare function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matching
|
|
|
133
138
|
/**
|
|
134
139
|
* Remove a key from Onyx and update the subscribers
|
|
135
140
|
*/
|
|
136
|
-
declare function remove<TKey extends OnyxKey>(key: TKey
|
|
141
|
+
declare function remove<TKey extends OnyxKey>(key: TKey): Promise<void>;
|
|
137
142
|
declare function reportStorageQuota(error?: Error): Promise<void>;
|
|
138
143
|
/**
|
|
139
144
|
* Handles storage operation failures based on the error class (see lib/storage/errors.ts).
|
|
@@ -160,11 +165,10 @@ declare function hasPendingMergeForKey(key: OnyxKey): boolean;
|
|
|
160
165
|
/**
|
|
161
166
|
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
|
|
162
167
|
* This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
|
|
163
|
-
* to an array of key-value pairs in the above format and
|
|
164
|
-
*
|
|
165
|
-
* @return an array of key - value pairs <[key, value]>
|
|
168
|
+
* to an array of key-value pairs in the above format, and collects the keys of null values into
|
|
169
|
+
* `keysToRemove` for the caller to delete as one batch (cache drop + notification + batched storage removal).
|
|
166
170
|
*/
|
|
167
|
-
declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches
|
|
171
|
+
declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches): PreparedKeyValuePairs;
|
|
168
172
|
/**
|
|
169
173
|
* Merges an array of changes with an existing value or creates a single change.
|
|
170
174
|
*
|
|
@@ -249,10 +253,9 @@ declare function setCollectionWithRetry<TKey extends CollectionKeyBase>({ collec
|
|
|
249
253
|
* @param params.collection Object collection keyed by individual collection member keys and values
|
|
250
254
|
* @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
|
|
251
255
|
* tuples that we'll use to replace the nested objects of that collection member record with something else.
|
|
252
|
-
* @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
|
|
253
256
|
* @param retryAttempt retry attempt
|
|
254
257
|
*/
|
|
255
|
-
declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>({ collectionKey, collection, mergeReplaceNullPatches
|
|
258
|
+
declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase>({ collectionKey, collection, mergeReplaceNullPatches }: MergeCollectionWithPatchesParams<TKey>, retryAttempt?: number): Promise<void>;
|
|
256
259
|
/**
|
|
257
260
|
* Sets keys in a collection by replacing all targeted collection members with new values.
|
|
258
261
|
* Any existing collection members not included in the new data will not be removed.
|
package/dist/OnyxUtils.js
CHANGED
|
@@ -522,7 +522,7 @@ function keysChanged(collectionKey, partialCollection, partialPreviousCollection
|
|
|
522
522
|
/**
|
|
523
523
|
* When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
|
|
524
524
|
*/
|
|
525
|
-
function keyChanged(key, value, canUpdateSubscriber = () => true
|
|
525
|
+
function keyChanged(key, value, canUpdateSubscriber = () => true) {
|
|
526
526
|
var _a, _b;
|
|
527
527
|
// Add or remove this key from the recentlyAccessedKeys list
|
|
528
528
|
if (value !== null && value !== undefined) {
|
|
@@ -562,11 +562,6 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, isProcessingCo
|
|
|
562
562
|
continue;
|
|
563
563
|
}
|
|
564
564
|
if (OnyxKeys_1.default.isCollectionKey(subscriber.key)) {
|
|
565
|
-
// Skip individual key changes during collection updates to prevent duplicate
|
|
566
|
-
// callbacks - the collection update will handle this properly.
|
|
567
|
-
if (isProcessingCollectionUpdate) {
|
|
568
|
-
continue;
|
|
569
|
-
}
|
|
570
565
|
// Cache once per dispatch to ensure all subscribers see a consistent snapshot
|
|
571
566
|
// even if a previous callback synchronously wrote to the same collection.
|
|
572
567
|
let cachedCollection = cachedCollections[subscriber.key];
|
|
@@ -637,9 +632,9 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
|
|
|
637
632
|
/**
|
|
638
633
|
* Remove a key from Onyx and update the subscribers
|
|
639
634
|
*/
|
|
640
|
-
function remove(key
|
|
635
|
+
function remove(key) {
|
|
641
636
|
OnyxCache_1.default.drop(key);
|
|
642
|
-
keyChanged(key, undefined
|
|
637
|
+
keyChanged(key, undefined);
|
|
643
638
|
if (OnyxKeys_1.default.isRamOnlyKey(key)) {
|
|
644
639
|
return Promise.resolve();
|
|
645
640
|
}
|
|
@@ -770,15 +765,15 @@ function hasPendingMergeForKey(key) {
|
|
|
770
765
|
/**
|
|
771
766
|
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
|
|
772
767
|
* This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
|
|
773
|
-
* to an array of key-value pairs in the above format and
|
|
774
|
-
*
|
|
775
|
-
* @return an array of key - value pairs <[key, value]>
|
|
768
|
+
* to an array of key-value pairs in the above format, and collects the keys of null values into
|
|
769
|
+
* `keysToRemove` for the caller to delete as one batch (cache drop + notification + batched storage removal).
|
|
776
770
|
*/
|
|
777
|
-
function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches
|
|
771
|
+
function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches) {
|
|
778
772
|
const pairs = [];
|
|
773
|
+
const keysToRemove = [];
|
|
779
774
|
for (const [key, value] of Object.entries(data)) {
|
|
780
775
|
if (value === null) {
|
|
781
|
-
|
|
776
|
+
keysToRemove.push(key);
|
|
782
777
|
continue;
|
|
783
778
|
}
|
|
784
779
|
const valueWithoutNestedNullValues = (shouldRemoveNestedNulls !== null && shouldRemoveNestedNulls !== void 0 ? shouldRemoveNestedNulls : true) ? utils_1.default.removeNestedNullValues(value) : value;
|
|
@@ -786,7 +781,7 @@ function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNu
|
|
|
786
781
|
pairs.push([key, valueWithoutNestedNullValues, replaceNullPatches === null || replaceNullPatches === void 0 ? void 0 : replaceNullPatches[key]]);
|
|
787
782
|
}
|
|
788
783
|
}
|
|
789
|
-
return pairs;
|
|
784
|
+
return { pairs, keysToRemove };
|
|
790
785
|
}
|
|
791
786
|
/**
|
|
792
787
|
* Merges an array of changes with an existing value or creates a single change.
|
|
@@ -863,9 +858,12 @@ function initializeWithDefaultKeyStates() {
|
|
|
863
858
|
}
|
|
864
859
|
allDataFromStorage[key] = value;
|
|
865
860
|
}
|
|
866
|
-
// Load all storage data into cache silently (no subscriber notifications)
|
|
867
|
-
|
|
868
|
-
|
|
861
|
+
// Load all storage data into cache silently (no subscriber notifications).
|
|
862
|
+
// hydrate() rather than merge(): the cache is empty at this point, so a per-key fastMerge
|
|
863
|
+
// would only deep-clone every row it was handed.
|
|
864
|
+
// No setAllKeys() call is needed: hydrate() calls addKey() for every key, which populates the
|
|
865
|
+
// key index and registers collection member keys itself.
|
|
866
|
+
OnyxCache_1.default.hydrate(allDataFromStorage);
|
|
869
867
|
// For keys that have a developer-defined default (via `initialKeyStates`), merge the
|
|
870
868
|
// persisted value with the default so new properties added in code updates are applied
|
|
871
869
|
// without wiping user data that already exists in storage.
|
|
@@ -1172,7 +1170,11 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1172
1170
|
return result;
|
|
1173
1171
|
}, {});
|
|
1174
1172
|
}
|
|
1175
|
-
const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
|
|
1173
|
+
const { pairs: keyValuePairsToSet, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);
|
|
1174
|
+
// Removals of keys that are neither cached nor persisted are no-ops and skipped. When the key
|
|
1175
|
+
// index has not been loaded yet (empty set), keep the removal to be safe.
|
|
1176
|
+
const persistedKeys = OnyxCache_1.default.getAllKeys();
|
|
1177
|
+
const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.size === 0 || persistedKeys.has(key));
|
|
1176
1178
|
// Group collection members by their parent collection key so each collection can be notified
|
|
1177
1179
|
// via a single batched keysChanged() call instead of one keyChanged() per member. For each
|
|
1178
1180
|
// collection, `partial` holds the new values being set and `previous` holds the cached values
|
|
@@ -1210,6 +1212,26 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1210
1212
|
}
|
|
1211
1213
|
}
|
|
1212
1214
|
}
|
|
1215
|
+
// Null keys join the same per-collection batches (as undefined) and are deleted from storage
|
|
1216
|
+
// in one batched call below, so cross-tab sync raises a single event instead of one per key.
|
|
1217
|
+
for (const key of keysToRemove) {
|
|
1218
|
+
const previousValue = OnyxCache_1.default.get(key);
|
|
1219
|
+
OnyxCache_1.default.drop(key);
|
|
1220
|
+
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
1221
|
+
if (collectionKey && OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key)) {
|
|
1222
|
+
let batch = collectionBatches.get(collectionKey);
|
|
1223
|
+
if (!batch) {
|
|
1224
|
+
batch = { partial: {}, previous: {} };
|
|
1225
|
+
collectionBatches.set(collectionKey, batch);
|
|
1226
|
+
}
|
|
1227
|
+
batch.partial[key] = undefined;
|
|
1228
|
+
batch.previous[key] = previousValue;
|
|
1229
|
+
}
|
|
1230
|
+
else if (!retryAttempt) {
|
|
1231
|
+
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1232
|
+
keyChanged(key, undefined);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1213
1235
|
// One keysChanged() per collection — fires each collection-level subscriber once and lets
|
|
1214
1236
|
// keysChanged() internally decide which individual member subscribers need notification.
|
|
1215
1237
|
// Skip on retry — already notified on attempt 0 (see same-reason comment above).
|
|
@@ -1223,8 +1245,14 @@ function multiSetWithRetry(data, retryAttempt) {
|
|
|
1223
1245
|
// Filter out the RAM-only key value pairs, as they should not be saved to storage
|
|
1224
1246
|
return !OnyxKeys_1.default.isRamOnlyKey(key);
|
|
1225
1247
|
});
|
|
1248
|
+
const keysToRemoveFromStorage = keysToRemove.filter((key) => !OnyxKeys_1.default.isRamOnlyKey(key));
|
|
1226
1249
|
const inFlightKeys = new Set(keyValuePairsToSet.map(([key]) => key));
|
|
1227
|
-
|
|
1250
|
+
// A failed removal is logged, not retried — keysToRemove cannot be re-derived after the cache update.
|
|
1251
|
+
const storagePromises = [storage_1.default.multiSet(keyValuePairsToStore)];
|
|
1252
|
+
if (keysToRemoveFromStorage.length > 0) {
|
|
1253
|
+
storagePromises.push(storage_1.default.removeItems(keysToRemoveFromStorage).catch((error) => Logger.logAlert(`multiSet failed to remove keys from storage. Error: ${error}`)));
|
|
1254
|
+
}
|
|
1255
|
+
return Promise.all(storagePromises)
|
|
1228
1256
|
.then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
|
|
1229
1257
|
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt, inFlightKeys))
|
|
1230
1258
|
.then(() => {
|
|
@@ -1278,14 +1306,21 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
|
|
|
1278
1306
|
}
|
|
1279
1307
|
mutableCollection[key] = null;
|
|
1280
1308
|
}
|
|
1281
|
-
const keyValuePairs = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true
|
|
1309
|
+
const { pairs: keyValuePairs, keysToRemove: removalCandidates } = OnyxUtils.prepareKeyValuePairsForStorage(mutableCollection, true);
|
|
1310
|
+
// Removals of keys that are neither cached nor persisted are no-ops and skipped.
|
|
1311
|
+
const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
|
|
1312
|
+
// Snapshot before cache mutations so keysChanged() can diff removed members.
|
|
1282
1313
|
const previousCollection = OnyxUtils.getCachedCollection(collectionKey);
|
|
1283
1314
|
for (const [key, value] of keyValuePairs)
|
|
1284
1315
|
OnyxCache_1.default.set(key, value);
|
|
1316
|
+
for (const key of keysToRemove)
|
|
1317
|
+
OnyxCache_1.default.drop(key);
|
|
1285
1318
|
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1286
1319
|
// Collection-root subscribers re-fire on every keysChanged by contract.
|
|
1287
1320
|
if (!retryAttempt) {
|
|
1288
|
-
|
|
1321
|
+
// Removed members are notified as undefined, matching mergeCollection/multiSet.
|
|
1322
|
+
const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
|
|
1323
|
+
keysChanged(collectionKey, partialForNotify, previousCollection);
|
|
1289
1324
|
}
|
|
1290
1325
|
// RAM-only keys are not supposed to be saved to storage
|
|
1291
1326
|
if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
|
|
@@ -1293,7 +1328,13 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
|
|
|
1293
1328
|
return;
|
|
1294
1329
|
}
|
|
1295
1330
|
const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
|
|
1296
|
-
|
|
1331
|
+
// One batched removal = one cross-tab sync event instead of one per key. A failed removal is
|
|
1332
|
+
// logged, not retried — keysToRemove cannot be re-derived after the cache update.
|
|
1333
|
+
const storagePromises = [storage_1.default.multiSet(keyValuePairs)];
|
|
1334
|
+
if (keysToRemove.length > 0) {
|
|
1335
|
+
storagePromises.push(storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`setCollection failed to remove keys from storage. Error: ${error}`)));
|
|
1336
|
+
}
|
|
1337
|
+
return Promise.all(storagePromises)
|
|
1297
1338
|
.then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
|
|
1298
1339
|
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, { collectionKey, collection }, retryAttempt, inFlightKeys))
|
|
1299
1340
|
.then(() => {
|
|
@@ -1311,10 +1352,9 @@ function setCollectionWithRetry({ collectionKey, collection }, retryAttempt) {
|
|
|
1311
1352
|
* @param params.collection Object collection keyed by individual collection member keys and values
|
|
1312
1353
|
* @param params.mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
|
|
1313
1354
|
* tuples that we'll use to replace the nested objects of that collection member record with something else.
|
|
1314
|
-
* @param params.isProcessingCollectionUpdate whether this is part of a collection update operation.
|
|
1315
1355
|
* @param retryAttempt retry attempt
|
|
1316
1356
|
*/
|
|
1317
|
-
function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNullPatches
|
|
1357
|
+
function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNullPatches }, retryAttempt) {
|
|
1318
1358
|
if (!isValidNonEmptyCollectionForMerge(collection)) {
|
|
1319
1359
|
Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
|
|
1320
1360
|
return Promise.resolve();
|
|
@@ -1344,14 +1384,30 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1344
1384
|
resultCollectionKeys = Object.keys(resultCollection);
|
|
1345
1385
|
return getAllKeys()
|
|
1346
1386
|
.then((persistedKeys) => {
|
|
1347
|
-
// Split to keys that exist in storage and keys that don't
|
|
1387
|
+
// Split to keys that exist in storage and keys that don't. Null members are collected
|
|
1388
|
+
// for one batched removal below; nulls that are neither cached nor persisted are no-ops and skipped.
|
|
1389
|
+
const keysToRemove = [];
|
|
1348
1390
|
const keys = resultCollectionKeys.filter((key) => {
|
|
1349
1391
|
if (resultCollection[key] === null) {
|
|
1350
|
-
|
|
1392
|
+
if (OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key)) {
|
|
1393
|
+
keysToRemove.push(key);
|
|
1394
|
+
}
|
|
1351
1395
|
return false;
|
|
1352
1396
|
}
|
|
1353
1397
|
return true;
|
|
1354
1398
|
});
|
|
1399
|
+
// Drop removed members before the pre-warm await below, so a concurrent write to one of
|
|
1400
|
+
// these keys during the pre-warm is not wiped out by a late drop.
|
|
1401
|
+
const removedPreviousValues = {};
|
|
1402
|
+
for (const key of keysToRemove) {
|
|
1403
|
+
removedPreviousValues[key] = OnyxCache_1.default.get(key);
|
|
1404
|
+
OnyxCache_1.default.drop(key);
|
|
1405
|
+
}
|
|
1406
|
+
// One batched removal = one cross-tab sync event instead of one per key. Issued at drop time
|
|
1407
|
+
// so a concurrent later write to a removed key persists after the removal.
|
|
1408
|
+
const removalPromise = !OnyxKeys_1.default.isRamOnlyKey(collectionKey) && keysToRemove.length > 0
|
|
1409
|
+
? storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`mergeCollection failed to remove keys from storage. Error: ${error}`))
|
|
1410
|
+
: undefined;
|
|
1355
1411
|
const existingKeys = keys.filter((key) => persistedKeys.has(key));
|
|
1356
1412
|
const cachedCollectionForExistingKeys = getCachedCollection(collectionKey, existingKeys);
|
|
1357
1413
|
const existingKeyCollection = existingKeys.reduce((obj, key) => {
|
|
@@ -1380,10 +1436,10 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1380
1436
|
// When (multi-)merging the values with the existing values in storage,
|
|
1381
1437
|
// we don't want to remove nested null values from the data that we pass to the storage layer,
|
|
1382
1438
|
// because the storage layer uses them to remove nested keys from storage natively.
|
|
1383
|
-
const keyValuePairsForExistingCollection = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
|
|
1439
|
+
const { pairs: keyValuePairsForExistingCollection } = prepareKeyValuePairsForStorage(existingKeyCollection, false, mergeReplaceNullPatches);
|
|
1384
1440
|
// We can safely remove nested null values when using (multi-)set,
|
|
1385
1441
|
// because we will simply overwrite the existing values in storage.
|
|
1386
|
-
const keyValuePairsForNewCollection = prepareKeyValuePairsForStorage(newCollection, true);
|
|
1442
|
+
const { pairs: keyValuePairsForNewCollection } = prepareKeyValuePairsForStorage(newCollection, true);
|
|
1387
1443
|
// finalMergedCollection contains all the keys that were merged, without the keys of incompatible updates
|
|
1388
1444
|
const finalMergedCollection = Object.assign(Object.assign({}, existingKeyCollection), newCollection);
|
|
1389
1445
|
// Pre-warm cache for cache-miss existingKeys so cache.merge() merges the new delta into
|
|
@@ -1408,9 +1464,16 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1408
1464
|
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1409
1465
|
// Collection-root subscribers re-fire on every keysChanged by contract.
|
|
1410
1466
|
if (!retryAttempt) {
|
|
1411
|
-
|
|
1467
|
+
const partialForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, finalMergedCollection), Object.fromEntries(keysToRemove.map((key) => [key, undefined]))) : finalMergedCollection;
|
|
1468
|
+
const previousForNotify = keysToRemove.length > 0 ? Object.assign(Object.assign({}, previousCollection), removedPreviousValues) : previousCollection;
|
|
1469
|
+
if (Object.keys(partialForNotify).length > 0) {
|
|
1470
|
+
keysChanged(collectionKey, partialForNotify, previousForNotify);
|
|
1471
|
+
}
|
|
1412
1472
|
}
|
|
1413
1473
|
const promises = [];
|
|
1474
|
+
if (removalPromise) {
|
|
1475
|
+
promises.push(removalPromise);
|
|
1476
|
+
}
|
|
1414
1477
|
// New keys go through multiSet and existing keys through multiMerge. multiMerge on a
|
|
1415
1478
|
// missing key stores the value just like multiSet across all backends; splitting them lets
|
|
1416
1479
|
// multiSet strip nested nulls (the merge layer keeps them to delete nested storage keys).
|
|
@@ -1429,7 +1492,6 @@ function mergeCollectionWithPatches({ collectionKey, collection, mergeReplaceNul
|
|
|
1429
1492
|
collectionKey,
|
|
1430
1493
|
collection: resultCollection,
|
|
1431
1494
|
mergeReplaceNullPatches,
|
|
1432
|
-
isProcessingCollectionUpdate,
|
|
1433
1495
|
}, retryAttempt, inFlightKeys))
|
|
1434
1496
|
.then(() => {
|
|
1435
1497
|
sendActionToDevTools(METHOD.MERGE_COLLECTION, undefined, resultCollection);
|
|
@@ -1476,21 +1538,34 @@ function partialSetCollection({ collectionKey, collection }, retryAttempt) {
|
|
|
1476
1538
|
return getAllKeys().then((persistedKeys) => {
|
|
1477
1539
|
const mutableCollection = Object.assign({}, resultCollection);
|
|
1478
1540
|
const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
|
|
1541
|
+
const { pairs: keyValuePairs, keysToRemove: removalCandidates } = prepareKeyValuePairsForStorage(mutableCollection, true);
|
|
1542
|
+
// Removals of keys that are neither cached nor persisted are no-ops and skipped.
|
|
1543
|
+
const keysToRemove = removalCandidates.filter((key) => OnyxCache_1.default.get(key) !== undefined || persistedKeys.has(key));
|
|
1544
|
+
// Snapshot before cache mutations so keysChanged() can diff removed members.
|
|
1479
1545
|
const previousCollection = getCachedCollection(collectionKey, existingKeys);
|
|
1480
|
-
const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
|
|
1481
1546
|
for (const [key, value] of keyValuePairs)
|
|
1482
1547
|
OnyxCache_1.default.set(key, value);
|
|
1548
|
+
for (const key of keysToRemove)
|
|
1549
|
+
OnyxCache_1.default.drop(key);
|
|
1483
1550
|
// Skip subscriber notification on retry — already notified on attempt 0.
|
|
1484
1551
|
// Collection-root subscribers re-fire on every keysChanged by contract.
|
|
1485
1552
|
if (!retryAttempt) {
|
|
1486
|
-
|
|
1553
|
+
// Removed members are notified as undefined, matching mergeCollection/multiSet.
|
|
1554
|
+
const partialForNotify = Object.fromEntries(Object.entries(mutableCollection).map(([key, value]) => [key, value !== null && value !== void 0 ? value : undefined]));
|
|
1555
|
+
keysChanged(collectionKey, partialForNotify, previousCollection);
|
|
1487
1556
|
}
|
|
1488
1557
|
if (OnyxKeys_1.default.isRamOnlyKey(collectionKey)) {
|
|
1489
1558
|
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
|
|
1490
1559
|
return;
|
|
1491
1560
|
}
|
|
1492
1561
|
const inFlightKeys = new Set(keyValuePairs.map(([key]) => key));
|
|
1493
|
-
|
|
1562
|
+
// One batched removal = one cross-tab sync event instead of one per key. A failed removal is
|
|
1563
|
+
// logged, not retried — keysToRemove cannot be re-derived after the cache update.
|
|
1564
|
+
const storagePromises = [storage_1.default.multiSet(keyValuePairs)];
|
|
1565
|
+
if (keysToRemove.length > 0) {
|
|
1566
|
+
storagePromises.push(storage_1.default.removeItems(keysToRemove).catch((error) => Logger.logAlert(`setCollection failed to remove keys from storage. Error: ${error}`)));
|
|
1567
|
+
}
|
|
1568
|
+
return Promise.all(storagePromises)
|
|
1494
1569
|
.then(() => StorageCircuitBreaker_1.default.recordWriteSuccess())
|
|
1495
1570
|
.catch((error) => retryOperation(error, partialSetCollection, { collectionKey, collection }, retryAttempt, inFlightKeys))
|
|
1496
1571
|
.then(() => {
|
|
@@ -126,6 +126,9 @@ const InstanceSync = {
|
|
|
126
126
|
*/
|
|
127
127
|
init: (onStorageKeysChanged, store) => {
|
|
128
128
|
storage = store;
|
|
129
|
+
// Coalesce storage events into one dispatch per tick: a per-key sender would otherwise re-run
|
|
130
|
+
// the whole notification pipeline once per key and can flood the receiving tab into unresponsiveness.
|
|
131
|
+
let pendingSyncKeys = null;
|
|
129
132
|
// This listener will only be triggered by events coming from other tabs
|
|
130
133
|
global.addEventListener('storage', (event) => {
|
|
131
134
|
// Ignore events that don't originate from the SYNC_ONYX logic
|
|
@@ -133,7 +136,18 @@ const InstanceSync = {
|
|
|
133
136
|
return;
|
|
134
137
|
}
|
|
135
138
|
const onyxKeys = parseSyncOnyxStorageEventValue(event.newValue);
|
|
136
|
-
|
|
139
|
+
if (pendingSyncKeys) {
|
|
140
|
+
for (const onyxKey of onyxKeys) {
|
|
141
|
+
pendingSyncKeys.add(onyxKey);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
pendingSyncKeys = new Set(onyxKeys);
|
|
146
|
+
setTimeout(() => {
|
|
147
|
+
const keys = Array.from(pendingSyncKeys !== null && pendingSyncKeys !== void 0 ? pendingSyncKeys : []);
|
|
148
|
+
pendingSyncKeys = null;
|
|
149
|
+
storage.multiGet(keys).then((pairs) => onStorageKeysChanged(pairs));
|
|
150
|
+
}, 0);
|
|
137
151
|
});
|
|
138
152
|
},
|
|
139
153
|
setItem: raiseStorageSyncEvent,
|
package/dist/types.d.ts
CHANGED
|
@@ -303,7 +303,6 @@ type MergeCollectionWithPatchesParams<TKey extends CollectionKeyBase> = {
|
|
|
303
303
|
collectionKey: TKey;
|
|
304
304
|
collection: OnyxMergeCollectionInput<TKey>;
|
|
305
305
|
mergeReplaceNullPatches?: MultiMergeReplaceNullPatches;
|
|
306
|
-
isProcessingCollectionUpdate?: boolean;
|
|
307
306
|
};
|
|
308
307
|
type RetriableOnyxOperation = typeof OnyxUtils.setWithRetry | typeof OnyxUtils.multiSetWithRetry | typeof OnyxUtils.setCollectionWithRetry | typeof OnyxUtils.mergeCollectionWithPatches | typeof OnyxUtils.partialSetCollection;
|
|
309
308
|
/**
|
package/dist/utils.d.ts
CHANGED
|
@@ -37,6 +37,11 @@ type FastMergeResult<TValue> = {
|
|
|
37
37
|
declare function fastMerge<TValue>(target: TValue, source: TValue, options?: FastMergeOptions, metadata?: FastMergeMetadata, basePath?: string[]): FastMergeResult<TValue>;
|
|
38
38
|
/** Checks whether the given object is an object and not null/undefined. */
|
|
39
39
|
declare function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue;
|
|
40
|
+
/**
|
|
41
|
+
* Reports whether a value needs cleaning (nested null/undefined, or the replace-object mark) before it's
|
|
42
|
+
* safe to store by reference. Read-only, non-allocating.
|
|
43
|
+
*/
|
|
44
|
+
declare function needsNormalization(value: unknown): boolean;
|
|
40
45
|
/** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
|
|
41
46
|
declare function removeNestedNullValues<TValue extends OnyxInput<OnyxKey> | null>(value: TValue): TValue;
|
|
42
47
|
/** Formats the action name by uppercasing and adding the key if provided. */
|
|
@@ -73,6 +78,7 @@ declare const _default: {
|
|
|
73
78
|
isEmptyObject: typeof isEmptyObject;
|
|
74
79
|
formatActionName: typeof formatActionName;
|
|
75
80
|
removeNestedNullValues: typeof removeNestedNullValues;
|
|
81
|
+
needsNormalization: typeof needsNormalization;
|
|
76
82
|
checkCompatibilityWithExistingValue: typeof checkCompatibilityWithExistingValue;
|
|
77
83
|
pick: typeof pick;
|
|
78
84
|
omit: typeof omit;
|
package/dist/utils.js
CHANGED
|
@@ -129,6 +129,30 @@ function isMergeableObject(value) {
|
|
|
129
129
|
const isNonNullObject = value != null ? typeof value === 'object' : false;
|
|
130
130
|
return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
|
|
131
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Reports whether a value needs cleaning (nested null/undefined, or the replace-object mark) before it's
|
|
134
|
+
* safe to store by reference. Read-only, non-allocating.
|
|
135
|
+
*/
|
|
136
|
+
function needsNormalization(value) {
|
|
137
|
+
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
// Use for-in loop to avoid an unnecessary array allocation from Object.keys()
|
|
141
|
+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
|
|
142
|
+
for (const key in value) {
|
|
143
|
+
if (key === ONYX_INTERNALS__REPLACE_OBJECT_MARK) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
const propertyValue = value[key];
|
|
147
|
+
if (propertyValue === null || propertyValue === undefined) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
if (typeof propertyValue === 'object' && !Array.isArray(propertyValue) && needsNormalization(propertyValue)) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
132
156
|
/** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
|
|
133
157
|
function removeNestedNullValues(value) {
|
|
134
158
|
if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -259,6 +283,7 @@ exports.default = {
|
|
|
259
283
|
isEmptyObject,
|
|
260
284
|
formatActionName,
|
|
261
285
|
removeNestedNullValues,
|
|
286
|
+
needsNormalization,
|
|
262
287
|
checkCompatibilityWithExistingValue,
|
|
263
288
|
pick,
|
|
264
289
|
omit,
|