react-native-onyx 3.0.34 → 3.0.36

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 CHANGED
@@ -151,7 +151,7 @@ function disconnect(connection) {
151
151
  * @param options optional configuration object
152
152
  */
153
153
  function set(key, value, options) {
154
- return OnyxUtils_1.default.setWithRetry({ key, value, options });
154
+ return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setWithRetry({ key, value, options }));
155
155
  }
156
156
  /**
157
157
  * Sets multiple keys and values
@@ -161,7 +161,7 @@ function set(key, value, options) {
161
161
  * @param data object keyed by ONYXKEYS and the values to set
162
162
  */
163
163
  function multiSet(data) {
164
- return OnyxUtils_1.default.multiSetWithRetry(data);
164
+ return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.multiSetWithRetry(data));
165
165
  }
166
166
  /**
167
167
  * Merge a new value into an existing value at a key.
@@ -180,71 +180,73 @@ function multiSet(data) {
180
180
  * Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
181
181
  */
182
182
  function merge(key, changes) {
183
- const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
184
- if (skippableCollectionMemberIDs.size) {
185
- try {
186
- const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key);
187
- if (skippableCollectionMemberIDs.has(collectionMemberID)) {
188
- // The key is a skippable one, so we set the new changes to undefined.
189
- // eslint-disable-next-line no-param-reassign
190
- changes = undefined;
183
+ return OnyxUtils_1.default.afterInit(() => {
184
+ const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
185
+ if (skippableCollectionMemberIDs.size) {
186
+ try {
187
+ const [, collectionMemberID] = OnyxUtils_1.default.splitCollectionMemberKey(key);
188
+ if (skippableCollectionMemberIDs.has(collectionMemberID)) {
189
+ // The key is a skippable one, so we set the new changes to undefined.
190
+ // eslint-disable-next-line no-param-reassign
191
+ changes = undefined;
192
+ }
193
+ }
194
+ catch (e) {
195
+ // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
191
196
  }
192
197
  }
193
- catch (e) {
194
- // The key is not a collection one or something went wrong during split, so we proceed with the function's logic.
198
+ const mergeQueue = OnyxUtils_1.default.getMergeQueue();
199
+ const mergeQueuePromise = OnyxUtils_1.default.getMergeQueuePromise();
200
+ // Top-level undefined values are ignored
201
+ // Therefore, we need to prevent adding them to the merge queue
202
+ if (changes === undefined) {
203
+ return mergeQueue[key] ? mergeQueuePromise[key] : Promise.resolve();
195
204
  }
196
- }
197
- const mergeQueue = OnyxUtils_1.default.getMergeQueue();
198
- const mergeQueuePromise = OnyxUtils_1.default.getMergeQueuePromise();
199
- // Top-level undefined values are ignored
200
- // Therefore, we need to prevent adding them to the merge queue
201
- if (changes === undefined) {
202
- return mergeQueue[key] ? mergeQueuePromise[key] : Promise.resolve();
203
- }
204
- // Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
205
- // Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
206
- if (mergeQueue[key]) {
207
- mergeQueue[key].push(changes);
208
- return mergeQueuePromise[key];
209
- }
210
- mergeQueue[key] = [changes];
211
- mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => {
212
- // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
213
- if (mergeQueue[key] == null) {
214
- return Promise.resolve();
205
+ // Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
206
+ // Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
207
+ if (mergeQueue[key]) {
208
+ mergeQueue[key].push(changes);
209
+ return mergeQueuePromise[key];
215
210
  }
216
- try {
217
- const validChanges = mergeQueue[key].filter((change) => {
218
- const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
219
- if (!isCompatible) {
220
- Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
221
- }
222
- return isCompatible;
223
- });
224
- // Clean up the write queue, so we don't apply these changes again.
225
- delete mergeQueue[key];
226
- delete mergeQueuePromise[key];
227
- if (!validChanges.length) {
211
+ mergeQueue[key] = [changes];
212
+ mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => {
213
+ // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
214
+ if (mergeQueue[key] == null) {
228
215
  return Promise.resolve();
229
216
  }
230
- // If the last change is null, we can just delete the key.
231
- // Therefore, we don't need to further broadcast and update the value so we can return early.
232
- if (validChanges.at(-1) === null) {
233
- OnyxUtils_1.default.remove(key);
234
- OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.MERGE, key);
217
+ try {
218
+ const validChanges = mergeQueue[key].filter((change) => {
219
+ const { isCompatible, existingValueType, newValueType } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
220
+ if (!isCompatible) {
221
+ Logger.logAlert(logMessages_1.default.incompatibleUpdateAlert(key, 'merge', existingValueType, newValueType));
222
+ }
223
+ return isCompatible;
224
+ });
225
+ // Clean up the write queue, so we don't apply these changes again.
226
+ delete mergeQueue[key];
227
+ delete mergeQueuePromise[key];
228
+ if (!validChanges.length) {
229
+ return Promise.resolve();
230
+ }
231
+ // If the last change is null, we can just delete the key.
232
+ // Therefore, we don't need to further broadcast and update the value so we can return early.
233
+ if (validChanges.at(-1) === null) {
234
+ OnyxUtils_1.default.remove(key);
235
+ OnyxUtils_1.default.logKeyRemoved(OnyxUtils_1.default.METHOD.MERGE, key);
236
+ return Promise.resolve();
237
+ }
238
+ return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue, updatePromise }) => {
239
+ OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, mergedValue);
240
+ return updatePromise;
241
+ });
242
+ }
243
+ catch (error) {
244
+ Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
235
245
  return Promise.resolve();
236
246
  }
237
- return OnyxMerge_1.default.applyMerge(key, existingValue, validChanges).then(({ mergedValue, updatePromise }) => {
238
- OnyxUtils_1.default.sendActionToDevTools(OnyxUtils_1.default.METHOD.MERGE, key, changes, mergedValue);
239
- return updatePromise;
240
- });
241
- }
242
- catch (error) {
243
- Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
244
- return Promise.resolve();
245
- }
247
+ });
248
+ return mergeQueuePromise[key];
246
249
  });
247
- return mergeQueuePromise[key];
248
250
  }
249
251
  /**
250
252
  * Merges a collection based on their keys.
@@ -260,7 +262,7 @@ function merge(key, changes) {
260
262
  * @param collection Object collection keyed by individual collection member keys and values
261
263
  */
262
264
  function mergeCollection(collectionKey, collection) {
263
- return OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection, isProcessingCollectionUpdate: true });
265
+ return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection, isProcessingCollectionUpdate: true }));
264
266
  }
265
267
  /**
266
268
  * Clear out all the data in the store
@@ -284,91 +286,93 @@ function mergeCollection(collectionKey, collection) {
284
286
  * @param keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
285
287
  */
286
288
  function clear(keysToPreserve = []) {
287
- const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
288
- const initialKeys = Object.keys(defaultKeyStates);
289
- const promise = OnyxUtils_1.default.getAllKeys()
290
- .then((cachedKeys) => {
291
- var _a;
292
- OnyxCache_1.default.clearNullishStorageKeys();
293
- const keysToBeClearedFromStorage = [];
294
- const keyValuesToResetIndividually = {};
295
- // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx
296
- // because the notification process needs the old values in cache but at that point they will be already removed from it.
297
- const keyValuesToResetAsCollection = {};
298
- const allKeys = new Set([...cachedKeys, ...initialKeys]);
299
- // The only keys that should not be cleared are:
300
- // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
301
- // status, or activeClients need to remain in Onyx even when signed out)
302
- // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
303
- // to null would cause unknown behavior)
304
- // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
305
- for (const key of allKeys) {
306
- const isKeyToPreserve = keysToPreserve.includes(key);
307
- const isDefaultKey = key in defaultKeyStates;
308
- // If the key is being removed or reset to default:
309
- // 1. Update it in the cache
310
- // 2. Figure out whether it is a collection key or not,
311
- // since collection key subscribers need to be updated differently
312
- if (!isKeyToPreserve) {
313
- const oldValue = OnyxCache_1.default.get(key);
314
- const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null;
315
- if (newValue !== oldValue) {
316
- OnyxCache_1.default.set(key, newValue);
317
- let collectionKey;
318
- try {
319
- collectionKey = OnyxUtils_1.default.getCollectionKey(key);
320
- }
321
- catch (e) {
322
- // If getCollectionKey() throws an error it means the key is not a collection key.
323
- collectionKey = undefined;
324
- }
325
- if (collectionKey) {
326
- if (!keyValuesToResetAsCollection[collectionKey]) {
327
- keyValuesToResetAsCollection[collectionKey] = { oldValues: {}, newValues: {} };
289
+ return OnyxUtils_1.default.afterInit(() => {
290
+ const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
291
+ const initialKeys = Object.keys(defaultKeyStates);
292
+ const promise = OnyxUtils_1.default.getAllKeys()
293
+ .then((cachedKeys) => {
294
+ var _a;
295
+ OnyxCache_1.default.clearNullishStorageKeys();
296
+ const keysToBeClearedFromStorage = [];
297
+ const keyValuesToResetIndividually = {};
298
+ // We need to store old and new values for collection keys to properly notify subscribers when clearing Onyx
299
+ // because the notification process needs the old values in cache but at that point they will be already removed from it.
300
+ const keyValuesToResetAsCollection = {};
301
+ const allKeys = new Set([...cachedKeys, ...initialKeys]);
302
+ // The only keys that should not be cleared are:
303
+ // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
304
+ // status, or activeClients need to remain in Onyx even when signed out)
305
+ // 2. Any keys with a default state (because they need to remain in Onyx as their default, and setting them
306
+ // to null would cause unknown behavior)
307
+ // 2.1 However, if a default key was explicitly set to null, we need to reset it to the default value
308
+ for (const key of allKeys) {
309
+ const isKeyToPreserve = keysToPreserve.includes(key);
310
+ const isDefaultKey = key in defaultKeyStates;
311
+ // If the key is being removed or reset to default:
312
+ // 1. Update it in the cache
313
+ // 2. Figure out whether it is a collection key or not,
314
+ // since collection key subscribers need to be updated differently
315
+ if (!isKeyToPreserve) {
316
+ const oldValue = OnyxCache_1.default.get(key);
317
+ const newValue = (_a = defaultKeyStates[key]) !== null && _a !== void 0 ? _a : null;
318
+ if (newValue !== oldValue) {
319
+ OnyxCache_1.default.set(key, newValue);
320
+ let collectionKey;
321
+ try {
322
+ collectionKey = OnyxUtils_1.default.getCollectionKey(key);
323
+ }
324
+ catch (e) {
325
+ // If getCollectionKey() throws an error it means the key is not a collection key.
326
+ collectionKey = undefined;
327
+ }
328
+ if (collectionKey) {
329
+ if (!keyValuesToResetAsCollection[collectionKey]) {
330
+ keyValuesToResetAsCollection[collectionKey] = { oldValues: {}, newValues: {} };
331
+ }
332
+ keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue;
333
+ keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
334
+ }
335
+ else {
336
+ keyValuesToResetIndividually[key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
328
337
  }
329
- keyValuesToResetAsCollection[collectionKey].oldValues[key] = oldValue;
330
- keyValuesToResetAsCollection[collectionKey].newValues[key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
331
- }
332
- else {
333
- keyValuesToResetIndividually[key] = newValue !== null && newValue !== void 0 ? newValue : undefined;
334
338
  }
335
339
  }
340
+ if (isKeyToPreserve || isDefaultKey) {
341
+ continue;
342
+ }
343
+ // If it isn't preserved and doesn't have a default, we'll remove it
344
+ keysToBeClearedFromStorage.push(key);
336
345
  }
337
- if (isKeyToPreserve || isDefaultKey) {
338
- continue;
346
+ const updatePromises = [];
347
+ // Notify the subscribers for each key/value group so they can receive the new values
348
+ for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
349
+ updatePromises.push(OnyxUtils_1.default.scheduleSubscriberUpdate(key, value));
339
350
  }
340
- // If it isn't preserved and doesn't have a default, we'll remove it
341
- keysToBeClearedFromStorage.push(key);
342
- }
343
- const updatePromises = [];
344
- // Notify the subscribers for each key/value group so they can receive the new values
345
- for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
346
- updatePromises.push(OnyxUtils_1.default.scheduleSubscriberUpdate(key, value));
347
- }
348
- for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
349
- updatePromises.push(OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues));
350
- }
351
- // Exclude RAM-only keys to prevent them from being saved to storage
352
- const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates)
353
- .filter((key) => !keysToPreserve.includes(key) && !OnyxUtils_1.default.isRamOnlyKey(key))
354
- .reduce((obj, key) => {
355
- // eslint-disable-next-line no-param-reassign
356
- obj[key] = defaultKeyStates[key];
357
- return obj;
358
- }, {}));
359
- // Remove only the items that we want cleared from storage, and reset others to default
360
- for (const key of keysToBeClearedFromStorage)
361
- OnyxCache_1.default.drop(key);
362
- return storage_1.default.removeItems(keysToBeClearedFromStorage)
363
- .then(() => OnyxConnectionManager_1.default.refreshSessionID())
364
- .then(() => storage_1.default.multiSet(defaultKeyValuePairs))
365
- .then(() => {
366
- DevTools_1.default.clearState(keysToPreserve);
367
- return Promise.all(updatePromises);
368
- });
369
- })
370
- .then(() => undefined);
371
- return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.CLEAR, promise);
351
+ for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
352
+ updatePromises.push(OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues));
353
+ }
354
+ // Exclude RAM-only keys to prevent them from being saved to storage
355
+ const defaultKeyValuePairs = Object.entries(Object.keys(defaultKeyStates)
356
+ .filter((key) => !keysToPreserve.includes(key) && !OnyxUtils_1.default.isRamOnlyKey(key))
357
+ .reduce((obj, key) => {
358
+ // eslint-disable-next-line no-param-reassign
359
+ obj[key] = defaultKeyStates[key];
360
+ return obj;
361
+ }, {}));
362
+ // Remove only the items that we want cleared from storage, and reset others to default
363
+ for (const key of keysToBeClearedFromStorage)
364
+ OnyxCache_1.default.drop(key);
365
+ return storage_1.default.removeItems(keysToBeClearedFromStorage)
366
+ .then(() => OnyxConnectionManager_1.default.refreshSessionID())
367
+ .then(() => storage_1.default.multiSet(defaultKeyValuePairs))
368
+ .then(() => {
369
+ DevTools_1.default.clearState(keysToPreserve);
370
+ return Promise.all(updatePromises);
371
+ });
372
+ })
373
+ .then(() => undefined);
374
+ return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.CLEAR, promise);
375
+ });
372
376
  }
373
377
  /**
374
378
  * Insert API responses and lifecycle data into Onyx
@@ -377,132 +381,132 @@ function clear(keysToPreserve = []) {
377
381
  * @returns resolves when all operations are complete
378
382
  */
379
383
  function update(data) {
380
- // First, validate the Onyx object is in the format we expect
381
- for (const { onyxMethod, key, value } of data) {
382
- if (!Object.values(OnyxUtils_1.default.METHOD).includes(onyxMethod)) {
383
- throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
384
- }
385
- if (onyxMethod === OnyxUtils_1.default.METHOD.MULTI_SET) {
386
- // For multiset, we just expect the value to be an object
387
- if (typeof value !== 'object' || Array.isArray(value) || typeof value === 'function') {
388
- throw new Error('Invalid value provided in Onyx multiSet. Onyx multiSet value must be of type object.');
384
+ return OnyxUtils_1.default.afterInit(() => {
385
+ // The queue of operations within a single `update` call in the format of <item key - list of operations updating the item>.
386
+ // This allows us to batch the operations per item and merge them into one operation in the order they were requested.
387
+ const updateQueue = {};
388
+ const enqueueSetOperation = (key, value) => {
389
+ // If a `set` operation is enqueued, we should clear the whole queue.
390
+ // Since the `set` operation replaces the value entirely, there's no need to perform any previous operations.
391
+ // To do this, we first put `null` in the queue, which removes the existing value, and then merge the new value.
392
+ updateQueue[key] = [null, value];
393
+ };
394
+ const enqueueMergeOperation = (key, value) => {
395
+ if (value === null) {
396
+ // If we merge `null`, the value is removed and all the previous operations are discarded.
397
+ updateQueue[key] = [null];
398
+ }
399
+ else if (!updateQueue[key]) {
400
+ updateQueue[key] = [value];
401
+ }
402
+ else {
403
+ updateQueue[key].push(value);
389
404
  }
390
- }
391
- else if (onyxMethod !== OnyxUtils_1.default.METHOD.CLEAR && typeof key !== 'string') {
392
- throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
393
- }
394
- }
395
- // The queue of operations within a single `update` call in the format of <item key - list of operations updating the item>.
396
- // This allows us to batch the operations per item and merge them into one operation in the order they were requested.
397
- const updateQueue = {};
398
- const enqueueSetOperation = (key, value) => {
399
- // If a `set` operation is enqueued, we should clear the whole queue.
400
- // Since the `set` operation replaces the value entirely, there's no need to perform any previous operations.
401
- // To do this, we first put `null` in the queue, which removes the existing value, and then merge the new value.
402
- updateQueue[key] = [null, value];
403
- };
404
- const enqueueMergeOperation = (key, value) => {
405
- if (value === null) {
406
- // If we merge `null`, the value is removed and all the previous operations are discarded.
407
- updateQueue[key] = [null];
408
- }
409
- else if (!updateQueue[key]) {
410
- updateQueue[key] = [value];
411
- }
412
- else {
413
- updateQueue[key].push(value);
414
- }
415
- };
416
- const promises = [];
417
- let clearPromise = Promise.resolve();
418
- for (const { onyxMethod, key, value } of data) {
419
- const handlers = {
420
- [OnyxUtils_1.default.METHOD.SET]: enqueueSetOperation,
421
- [OnyxUtils_1.default.METHOD.MERGE]: enqueueMergeOperation,
422
- [OnyxUtils_1.default.METHOD.MERGE_COLLECTION]: () => {
423
- const collection = value;
424
- if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
425
- Logger.logInfo('mergeCollection enqueued within update() with invalid or empty value. Skipping this operation.');
426
- return;
427
- }
428
- // Confirm all the collection keys belong to the same parent
429
- const collectionKeys = Object.keys(collection);
430
- if (OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(key, collectionKeys)) {
431
- const mergedCollection = collection;
432
- for (const collectionKey of collectionKeys)
433
- enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]);
434
- }
435
- },
436
- [OnyxUtils_1.default.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v)),
437
- [OnyxUtils_1.default.METHOD.MULTI_SET]: (k, v) => {
438
- for (const [entryKey, entryValue] of Object.entries(v))
439
- enqueueSetOperation(entryKey, entryValue);
440
- },
441
- [OnyxUtils_1.default.METHOD.CLEAR]: () => {
442
- clearPromise = clear();
443
- },
444
405
  };
445
- handlers[onyxMethod](key, value);
446
- }
447
- // Group all the collection-related keys and update each collection in a single `mergeCollection` call.
448
- // This is needed to prevent multiple `mergeCollection` calls for the same collection and `merge` calls for the individual items of the said collection.
449
- // This way, we ensure there is no race condition in the queued updates of the same key.
450
- for (const collectionKey of OnyxUtils_1.default.getCollectionKeys()) {
451
- const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxUtils_1.default.isKeyMatch(collectionKey, key));
452
- if (collectionItemKeys.length <= 1) {
453
- // If there are no items of this collection in the updateQueue, we should skip it.
454
- // If there is only one item, we should update it individually, therefore retain it in the updateQueue.
455
- continue;
406
+ const promises = [];
407
+ let clearPromise = Promise.resolve();
408
+ const onyxMethods = Object.values(OnyxUtils_1.default.METHOD);
409
+ for (const { onyxMethod, key, value } of data) {
410
+ if (!onyxMethods.includes(onyxMethod)) {
411
+ Logger.logInfo(`Invalid onyxMethod ${onyxMethod} in Onyx update. Skipping this operation.`);
412
+ continue;
413
+ }
414
+ if (onyxMethod !== OnyxUtils_1.default.METHOD.CLEAR && onyxMethod !== OnyxUtils_1.default.METHOD.MULTI_SET && typeof key !== 'string') {
415
+ Logger.logInfo(`Invalid ${typeof key} key provided in Onyx update. Key must be of type string. Skipping this operation.`);
416
+ continue;
417
+ }
418
+ const handlers = {
419
+ [OnyxUtils_1.default.METHOD.SET]: enqueueSetOperation,
420
+ [OnyxUtils_1.default.METHOD.MERGE]: enqueueMergeOperation,
421
+ [OnyxUtils_1.default.METHOD.MERGE_COLLECTION]: () => {
422
+ const collection = value;
423
+ if (!OnyxUtils_1.default.isValidNonEmptyCollectionForMerge(collection)) {
424
+ Logger.logInfo('Invalid or empty value provided in Onyx mergeCollection. Skipping this operation.');
425
+ return;
426
+ }
427
+ // Confirm all the collection keys belong to the same parent
428
+ const collectionKeys = Object.keys(collection);
429
+ if (OnyxUtils_1.default.doAllCollectionItemsBelongToSameParent(key, collectionKeys)) {
430
+ const mergedCollection = collection;
431
+ for (const collectionKey of collectionKeys)
432
+ enqueueMergeOperation(collectionKey, mergedCollection[collectionKey]);
433
+ }
434
+ },
435
+ [OnyxUtils_1.default.METHOD.SET_COLLECTION]: (k, v) => promises.push(() => setCollection(k, v)),
436
+ [OnyxUtils_1.default.METHOD.MULTI_SET]: (k, v) => {
437
+ if (typeof value !== 'object' || Array.isArray(value) || typeof value === 'function') {
438
+ Logger.logInfo(`Invalid value provided in Onyx multiSet. Value must be of type object. Skipping this operation.`);
439
+ return;
440
+ }
441
+ for (const [entryKey, entryValue] of Object.entries(v))
442
+ enqueueSetOperation(entryKey, entryValue);
443
+ },
444
+ [OnyxUtils_1.default.METHOD.CLEAR]: () => {
445
+ clearPromise = clear();
446
+ },
447
+ };
448
+ handlers[onyxMethod](key, value);
456
449
  }
457
- const batchedCollectionUpdates = collectionItemKeys.reduce((queue, key) => {
458
- const operations = updateQueue[key];
459
- // Remove the collection-related key from the updateQueue so that it won't be processed individually.
460
- delete updateQueue[key];
461
- const batchedChanges = OnyxUtils_1.default.mergeAndMarkChanges(operations);
462
- if (operations[0] === null) {
463
- // eslint-disable-next-line no-param-reassign
464
- queue.set[key] = batchedChanges.result;
450
+ // Group all the collection-related keys and update each collection in a single `mergeCollection` call.
451
+ // This is needed to prevent multiple `mergeCollection` calls for the same collection and `merge` calls for the individual items of the said collection.
452
+ // This way, we ensure there is no race condition in the queued updates of the same key.
453
+ for (const collectionKey of OnyxUtils_1.default.getCollectionKeys()) {
454
+ const collectionItemKeys = Object.keys(updateQueue).filter((key) => OnyxUtils_1.default.isKeyMatch(collectionKey, key));
455
+ if (collectionItemKeys.length <= 1) {
456
+ // If there are no items of this collection in the updateQueue, we should skip it.
457
+ // If there is only one item, we should update it individually, therefore retain it in the updateQueue.
458
+ continue;
465
459
  }
466
- else {
467
- // eslint-disable-next-line no-param-reassign
468
- queue.merge[key] = batchedChanges.result;
469
- if (batchedChanges.replaceNullPatches.length > 0) {
460
+ const batchedCollectionUpdates = collectionItemKeys.reduce((queue, key) => {
461
+ const operations = updateQueue[key];
462
+ // Remove the collection-related key from the updateQueue so that it won't be processed individually.
463
+ delete updateQueue[key];
464
+ const batchedChanges = OnyxUtils_1.default.mergeAndMarkChanges(operations);
465
+ if (operations[0] === null) {
466
+ // eslint-disable-next-line no-param-reassign
467
+ queue.set[key] = batchedChanges.result;
468
+ }
469
+ else {
470
470
  // eslint-disable-next-line no-param-reassign
471
- queue.mergeReplaceNullPatches[key] = batchedChanges.replaceNullPatches;
471
+ queue.merge[key] = batchedChanges.result;
472
+ if (batchedChanges.replaceNullPatches.length > 0) {
473
+ // eslint-disable-next-line no-param-reassign
474
+ queue.mergeReplaceNullPatches[key] = batchedChanges.replaceNullPatches;
475
+ }
472
476
  }
477
+ return queue;
478
+ }, {
479
+ merge: {},
480
+ mergeReplaceNullPatches: {},
481
+ set: {},
482
+ });
483
+ if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
484
+ promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches({
485
+ collectionKey,
486
+ collection: batchedCollectionUpdates.merge,
487
+ mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
488
+ isProcessingCollectionUpdate: true,
489
+ }));
490
+ }
491
+ if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
492
+ promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set }));
473
493
  }
474
- return queue;
475
- }, {
476
- merge: {},
477
- mergeReplaceNullPatches: {},
478
- set: {},
479
- });
480
- if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
481
- promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches({
482
- collectionKey,
483
- collection: batchedCollectionUpdates.merge,
484
- mergeReplaceNullPatches: batchedCollectionUpdates.mergeReplaceNullPatches,
485
- isProcessingCollectionUpdate: true,
486
- }));
487
- }
488
- if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
489
- promises.push(() => OnyxUtils_1.default.partialSetCollection({ collectionKey, collection: batchedCollectionUpdates.set }));
490
- }
491
- }
492
- for (const [key, operations] of Object.entries(updateQueue)) {
493
- if (operations[0] === null) {
494
- const batchedChanges = OnyxUtils_1.default.mergeChanges(operations).result;
495
- promises.push(() => set(key, batchedChanges));
496
- continue;
497
494
  }
498
- for (const operation of operations) {
499
- promises.push(() => merge(key, operation));
495
+ for (const [key, operations] of Object.entries(updateQueue)) {
496
+ if (operations[0] === null) {
497
+ const batchedChanges = OnyxUtils_1.default.mergeChanges(operations).result;
498
+ promises.push(() => set(key, batchedChanges));
499
+ continue;
500
+ }
501
+ for (const operation of operations) {
502
+ promises.push(() => merge(key, operation));
503
+ }
500
504
  }
501
- }
502
- const snapshotPromises = OnyxUtils_1.default.updateSnapshots(data, merge);
503
- // We need to run the snapshot updates before the other updates so the snapshot data can be updated before the loading state in the snapshot
504
- const finalPromises = snapshotPromises.concat(promises);
505
- return clearPromise.then(() => Promise.all(finalPromises.map((p) => p()))).then(() => undefined);
505
+ const snapshotPromises = OnyxUtils_1.default.updateSnapshots(data, merge);
506
+ // We need to run the snapshot updates before the other updates so the snapshot data can be updated before the loading state in the snapshot
507
+ const finalPromises = snapshotPromises.concat(promises);
508
+ return clearPromise.then(() => Promise.all(finalPromises.map((p) => p()))).then(() => undefined);
509
+ });
506
510
  }
507
511
  /**
508
512
  * Sets a collection by replacing all existing collection members with new values.
@@ -518,7 +522,7 @@ function update(data) {
518
522
  * @param collection Object collection keyed by individual collection member keys and values
519
523
  */
520
524
  function setCollection(collectionKey, collection) {
521
- return OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection });
525
+ return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection }));
522
526
  }
523
527
  const Onyx = {
524
528
  METHOD: OnyxUtils_1.default.METHOD,
@@ -30,6 +30,15 @@ declare function getDefaultKeyStates(): Record<OnyxKey, OnyxValue<OnyxKey>>;
30
30
  * Getter - returns the deffered init task.
31
31
  */
32
32
  declare function getDeferredInitTask(): DeferredTask;
33
+ /**
34
+ * Executes an action after Onyx has been initialized.
35
+ * If Onyx is already initialized, the action is executed immediately.
36
+ * Otherwise, it waits for initialization to complete before executing.
37
+ *
38
+ * @param action The action to execute after initialization
39
+ * @returns The result of the action
40
+ */
41
+ declare function afterInit<T>(action: () => Promise<T>): Promise<T>;
33
42
  /**
34
43
  * Getter - returns the skippable collection member IDs.
35
44
  */
@@ -343,6 +352,7 @@ declare const OnyxUtils: {
343
352
  getMergeQueuePromise: typeof getMergeQueuePromise;
344
353
  getDefaultKeyStates: typeof getDefaultKeyStates;
345
354
  getDeferredInitTask: typeof getDeferredInitTask;
355
+ afterInit: typeof afterInit;
346
356
  initStoreValues: typeof initStoreValues;
347
357
  sendActionToDevTools: typeof sendActionToDevTools;
348
358
  get: typeof get;
package/dist/OnyxUtils.js CHANGED
@@ -122,6 +122,20 @@ function getDefaultKeyStates() {
122
122
  function getDeferredInitTask() {
123
123
  return deferredInitTask;
124
124
  }
125
+ /**
126
+ * Executes an action after Onyx has been initialized.
127
+ * If Onyx is already initialized, the action is executed immediately.
128
+ * Otherwise, it waits for initialization to complete before executing.
129
+ *
130
+ * @param action The action to execute after initialization
131
+ * @returns The result of the action
132
+ */
133
+ function afterInit(action) {
134
+ if (deferredInitTask.isResolved) {
135
+ return action();
136
+ }
137
+ return deferredInitTask.promise.then(action);
138
+ }
125
139
  /**
126
140
  * Getter - returns the skippable collection member IDs.
127
141
  */
@@ -1446,6 +1460,7 @@ const OnyxUtils = {
1446
1460
  getMergeQueuePromise,
1447
1461
  getDefaultKeyStates,
1448
1462
  getDeferredInitTask,
1463
+ afterInit,
1449
1464
  initStoreValues,
1450
1465
  sendActionToDevTools,
1451
1466
  get,
@@ -1,6 +1,7 @@
1
1
  type DeferredTask = {
2
2
  promise: Promise<void>;
3
- resolve?: () => void;
3
+ resolve: () => void;
4
+ isResolved: boolean;
4
5
  };
5
6
  /**
6
7
  * Create a deferred task that can be resolved when we call `resolve()`
@@ -7,9 +7,14 @@ exports.default = createDeferredTask;
7
7
  * Useful when we want to wait for a tasks that is resolved from an external action
8
8
  */
9
9
  function createDeferredTask() {
10
- const deferred = {};
11
- deferred.promise = new Promise((res) => {
12
- deferred.resolve = res;
13
- });
10
+ const { promise, resolve: originalResolve } = Promise.withResolvers();
11
+ const deferred = {
12
+ promise,
13
+ resolve: () => {
14
+ deferred.isResolved = true;
15
+ originalResolve();
16
+ },
17
+ isResolved: false,
18
+ };
14
19
  return deferred;
15
20
  }
package/dist/types.d.ts CHANGED
@@ -274,7 +274,7 @@ type ExpandOnyxKeys<TKey extends OnyxKey> = TKey extends CollectionKeyBase ? NoI
274
274
  * If a new method is added to OnyxUtils.METHOD constant, it must be added to OnyxMethodValueMap type.
275
275
  * Otherwise it will show static type errors.
276
276
  */
277
- type OnyxUpdate<TKey extends OnyxKey = OnyxKey> = {
277
+ type OnyxUpdate<TKey extends OnyxKey> = {
278
278
  [K in TKey]: {
279
279
  onyxMethod: typeof OnyxUtils.METHOD.SET;
280
280
  key: ExpandOnyxKeys<K>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.34",
3
+ "version": "3.0.36",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",