Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b0eb40b
Remove macro/micro tasks during subscriber update
VickyStash Jan 23, 2026
62ceec2
Don't send data during connection, if it's already there
VickyStash Jan 26, 2026
8cffc45
Merge branch 'main' into VickyStash/poc/get-rid-of-macro-micro-tasks
VickyStash Jan 26, 2026
5fdc333
Merge branch 'main' into VickyStash/poc/get-rid-of-macro-micro-tasks
VickyStash Feb 11, 2026
e0b0fd7
Adjustment after merging main
VickyStash Feb 11, 2026
35496b7
Clean up the code
VickyStash Feb 11, 2026
864857a
Remove scheduleSubscriberUpdate and scheduleNotifyCollectionSubscribe…
VickyStash Feb 13, 2026
7813305
Keep the logic consistent with how it was
VickyStash Feb 13, 2026
5c763e1
Merge branch 'main' into VickyStash/poc/get-rid-of-macro-micro-tasks
VickyStash Feb 13, 2026
c5263aa
Run prettier
VickyStash Feb 13, 2026
790ce25
Remove outdated perf tests
VickyStash Feb 13, 2026
90c26de
Return updates back
VickyStash Feb 13, 2026
d21a2fc
Fix race condition in keysChanged for collection subscribers
VickyStash Feb 17, 2026
66140d3
Remove unnecessary OnyxUtils prefix
VickyStash Feb 17, 2026
6318f5e
Re-run checks
VickyStash Feb 19, 2026
1b6ea57
Remove unused promise variables for void-returning keyChanged/keysCha…
VickyStash Feb 24, 2026
bf33b18
Merge branch 'main' into VickyStash/poc/get-rid-of-macro-micro-tasks
VickyStash Feb 24, 2026
7d4e71b
Restore promise chaining for previousCollectionPromise
VickyStash Feb 24, 2026
f34b695
Align the code with how it was
VickyStash Feb 24, 2026
74d444b
Move up lastConnectionCallbackData.set
VickyStash Feb 25, 2026
3d96fe9
Re-run perf test
VickyStash Feb 25, 2026
5d161c8
Revert "Move up lastConnectionCallbackData.set"
VickyStash Feb 25, 2026
ea9f16e
Reapply "Move up lastConnectionCallbackData.set"
VickyStash Feb 25, 2026
95947ec
Update sendDataToConnection function
VickyStash Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 9 additions & 13 deletions lib/Onyx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,9 +254,8 @@ function merge<TKey extends OnyxKey>(key: TKey, changes: OnyxMergeInput<TKey>):
return Promise.resolve();
}

return OnyxMerge.applyMerge(key, existingValue, validChanges).then(({mergedValue, updatePromise}) => {
return OnyxMerge.applyMerge(key, existingValue, validChanges).then(({mergedValue}) => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MERGE, key, changes, mergedValue);
return updatePromise;
});
} catch (error) {
Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
Expand Down Expand Up @@ -368,16 +367,6 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
keysToBeClearedFromStorage.push(key);
}

const updatePromises: Array<Promise<void>> = [];

// Notify the subscribers for each key/value group so they can receive the new values
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
updatePromises.push(OnyxUtils.scheduleSubscriberUpdate(key, value));
}
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
updatePromises.push(OnyxUtils.scheduleNotifyCollectionSubscribers(key, value.newValues, value.oldValues));
}

// Exclude RAM-only keys to prevent them from being saved to storage
const defaultKeyValuePairs = Object.entries(
Object.keys(defaultKeyStates)
Expand All @@ -396,7 +385,14 @@ function clear(keysToPreserve: OnyxKey[] = []): Promise<void> {
.then(() => Storage.multiSet(defaultKeyValuePairs))
.then(() => {
DevTools.clearState(keysToPreserve);
return Promise.all(updatePromises);

// Notify the subscribers for each key/value group so they can receive the new values
for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
OnyxUtils.keyChanged(key, value);
}
for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
OnyxUtils.keysChanged(key, value.newValues, value.oldValues);
}
});
})
.then(() => undefined);
Expand Down
5 changes: 2 additions & 3 deletions lib/OnyxMerge/index.native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,20 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.MERGE, key, mergedValue, hasChanged);

// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);

const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key);

// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
if (shouldSkipStorageOperations) {
return Promise.resolve({mergedValue, updatePromise});
return Promise.resolve({mergedValue});
}

// For native platforms we use `mergeItem` that will take advantage of JSON_PATCH and JSON_REPLACE SQL operations to
// merge the object in a performant way.
return Storage.mergeItem(key, batchedChanges as OnyxValue<TKey>, replaceNullPatches).then(() => ({
mergedValue,
updatePromise,
}));
};

Expand Down
5 changes: 2 additions & 3 deletions lib/OnyxMerge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,19 @@ const applyMerge: ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<T
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.MERGE, key, mergedValue, hasChanged);

// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
const updatePromise = OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);
OnyxUtils.broadcastUpdate(key, mergedValue as OnyxValue<TKey>, hasChanged);

const shouldSkipStorageOperations = !hasChanged || OnyxUtils.isRamOnlyKey(key);

// If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
// If the key is marked as RAM-only, it should not be saved nor updated in the storage.
if (shouldSkipStorageOperations) {
return Promise.resolve({mergedValue, updatePromise});
return Promise.resolve({mergedValue});
}

// For web platforms we use `setItem` since the object was already merged with its changes before.
return Storage.setItem(key, mergedValue as OnyxValue<TKey>).then(() => ({
mergedValue,
updatePromise,
}));
};

Expand Down
1 change: 0 additions & 1 deletion lib/OnyxMerge/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type {OnyxInput, OnyxKey} from '../types';

type ApplyMergeResult<TValue> = {
mergedValue: TValue;
updatePromise: Promise<void>;
};

type ApplyMerge = <TKey extends OnyxKey, TValue extends OnyxInput<OnyxKey> | undefined, TChange extends OnyxInput<OnyxKey> | null>(
Expand Down
114 changes: 35 additions & 79 deletions lib/OnyxUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {deepEqual} from 'fast-equals';
import {deepEqual, shallowEqual} from 'fast-equals';
import type {ValueOf} from 'type-fest';
import _ from 'underscore';
import DevTools from './DevTools';
Expand Down Expand Up @@ -74,9 +74,6 @@ type OnyxMethod = ValueOf<typeof METHOD>;
let mergeQueue: Record<OnyxKey, Array<OnyxValue<OnyxKey>>> = {};
let mergeQueuePromise: Record<OnyxKey, Promise<void>> = {};

// Used to schedule subscriber update to the macro task queue
let nextMacrotaskPromise: Promise<void> | null = null;

// Holds a mapping of all the React components that want their state subscribed to a store key
let callbackToStateMapping: Record<string, CallbackToStateMapping<OnyxKey>> = {};

Expand Down Expand Up @@ -689,6 +686,8 @@ function keysChanged<TKey extends CollectionKeyBase>(
// If they are subscribed to the collection key and using waitForCollectionCallback then we'll
// send the whole cached collection.
if (isSubscribedToCollectionKey) {
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);

if (subscriber.waitForCollectionCallback) {
subscriber.callback(cachedCollection, subscriber.key, partialCollection);
continue;
Expand Down Expand Up @@ -788,6 +787,7 @@ function keyChanged<TKey extends OnyxKey>(
}

cachedCollection[key] = value;
lastConnectionCallbackData.set(subscriber.subscriptionID, cachedCollection);
subscriber.callback(cachedCollection, subscriber.key, {[key]: value});
continue;
}
Expand Down Expand Up @@ -815,11 +815,23 @@ function sendDataToConnection<TKey extends OnyxKey>(mapping: CallbackToStateMapp

// For regular callbacks, we never want to pass null values, but always just undefined if a value is not set in cache or storage.
const valueToPass = value === null ? undefined : value;
const lastValue = lastConnectionCallbackData.get(mapping.subscriptionID);
lastConnectionCallbackData.get(mapping.subscriptionID);

// If the value has not changed we do not need to trigger the callback
if (lastConnectionCallbackData.has(mapping.subscriptionID) && valueToPass === lastValue) {
// If the subscriber was already notified (e.g. by a synchronous keyChanged call),
// skip the initial data delivery to prevent duplicate callbacks.
if (lastConnectionCallbackData.has(mapping.subscriptionID)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve initial hydration after synchronous updates

Keep sendDataToConnection() from returning solely on lastConnectionCallbackData.has(...); this now drops the initial hydration callback whenever any synchronous keyChanged/keysChanged ran first. In the common race where Onyx.connect() is followed by an immediate Onyx.set() in the same tick, the subscription gets marked as "already notified" and the later storage-backed init payload is skipped even if it contains additional data (especially for collection subscribers), leaving subscribers with a partial state until a future update arrives.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, by the time the hydration promise resolves, it would only have equal or older data, making the skip correct.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VickyStash I asked Claude about this comment and according to it it's valid, here's a unit test it designed for me

diff --git a/tests/unit/onyxTest.ts b/tests/unit/onyxTest.ts
index 64caec5..94aff05 100644
--- a/tests/unit/onyxTest.ts
+++ b/tests/unit/onyxTest.ts
@@ -140,6 +140,51 @@ describe('Onyx', () => {
         });
     });
 
+    it('should deliver full collection data when connect() is followed by immediate set() of a single member in the same tick', () => {
+        const mockCallback = jest.fn();
+        const collectionKey = ONYX_KEYS.COLLECTION.TEST_CONNECT_COLLECTION;
+
+        // Write collection members directly to storage, bypassing Onyx cache values.
+        // This simulates data that exists in persistent storage but hasn't been loaded into cache yet
+        // (e.g. from a previous session).
+        return StorageMock.setItem(`${collectionKey}1`, {ID: 1, value: 'one'})
+            .then(() => StorageMock.setItem(`${collectionKey}2`, {ID: 2, value: 'two'}))
+            .then(() => StorageMock.setItem(`${collectionKey}3`, {ID: 3, value: 'three'}))
+            .then(() => {
+                // Register the keys in Onyx's key cache so getAllKeys() can discover them.
+                // We intentionally do NOT add values to the cache — only keys — to simulate
+                // data that is known to exist but whose values haven't been hydrated yet.
+                cache.addKey(`${collectionKey}1`);
+                cache.addKey(`${collectionKey}2`);
+                cache.addKey(`${collectionKey}3`);
+
+                // Connect to the collection — this starts an async storage read (microtask)
+                connection = Onyx.connect({
+                    key: collectionKey,
+                    waitForCollectionCallback: true,
+                    callback: mockCallback,
+                });
+
+                // Immediately set a single member in the same synchronous tick.
+                // This triggers synchronous keyChanged() which calls the subscriber with a partial
+                // collection (just _1 from the cache). This sets lastConnectionCallbackData for this
+                // subscriber. The async hydration from subscribeToKey should still deliver the full
+                // collection afterwards, since the data is different.
+                Onyx.set(`${collectionKey}1`, {ID: 1, value: 'updated'});
+
+                // Wait for all async operations (storage reads from subscribeToKey) to complete
+                return waitForPromisesToResolve();
+            })
+            .then(() => {
+                // The subscriber's final state should contain ALL collection members,
+                // including _2 and _3 that were only in storage (not cache) at the time of the synchronous keyChanged call.
+                const lastCall = mockCallback.mock.calls[mockCallback.mock.calls.length - 1][0];
+                expect(lastCall).toHaveProperty(`${collectionKey}1`);
+                expect(lastCall).toHaveProperty(`${collectionKey}2`);
+                expect(lastCall).toHaveProperty(`${collectionKey}3`);
+            });
+    });
+
     it('should merge an object with another object', () => {
         let testKeyValue: unknown;
 

Could you validate if it makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@VickyStash I asked Claude about this comment and according to it it's valid, here's a unit test it designed for me

Wow, nice catch! Claude have gave me totally different results when I asked it back to then.
I agree, that's truly can be a problem, I'm looking into how to handle it correctly.

Adjusted jest test, if anyone is interested
test('connect() followed by immediate set() should still deliver full collection from storage', async () => {
    const mockCallback = jest.fn();

    // ===== Session 1 =====
    // Data is written to persistent storage (simulates a previous app session).
    await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, {id: 1, title: 'Test One'});
    await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}2`, {id: 2, title: 'Test Two'});
    await StorageMock.setItem(`${ONYX_KEYS.COLLECTION.TEST_KEY}3`, {id: 3, title: 'Test Three'});

    // ===== Session 2 =====
    // App restarts. Onyx.init() calls getAllKeys() which populates storageKeys
    // with all 3 keys, but their values are NOT read into cache yet.
    Onyx.init({keys: ONYX_KEYS});

    // A component connects to the collection (starts async hydration via multiGet).
    Onyx.connect({
        key: ONYX_KEYS.COLLECTION.TEST_KEY,
        waitForCollectionCallback: true,
        callback: mockCallback,
    });

    Onyx.set(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`, {id: 1, title: 'Updated Test One'});

    await waitForPromisesToResolve();

    // The subscriber should eventually receive ALL collection members.
    // The async hydration reads test_2 and test_3 from storage.
    const lastCall = mockCallback.mock.calls[mockCallback.mock.calls.length - 1][0];
    expect(lastCall).toHaveProperty(`${ONYX_KEYS.COLLECTION.TEST_KEY}1`);
    expect(lastCall).toHaveProperty(`${ONYX_KEYS.COLLECTION.TEST_KEY}2`);
    expect(lastCall).toHaveProperty(`${ONYX_KEYS.COLLECTION.TEST_KEY}3`);

    // Verify the updated value is present (not stale)
    expect(lastCall[`${ONYX_KEYS.COLLECTION.TEST_KEY}1`]).toEqual({id: 1, title: 'Updated Test One'});
});

// For collection subscribers with waitForCollectionCallback, the synchronous keyChanged()
// may have delivered a partial collection (only members cached at that time). Now that
// multiGet has populated the cache with all members from storage, re-read the full
// collection from cache and deliver it if it differs from what was previously sent.
if (mapping.waitForCollectionCallback && isCollectionKey(mapping.key)) {
const lastValue = lastConnectionCallbackData.get(mapping.subscriptionID);
const freshCollection = getCachedCollection(mapping.key);

if (!shallowEqual(lastValue, freshCollection)) {
lastConnectionCallbackData.set(mapping.subscriptionID, freshCollection);
mapping.callback?.(freshCollection, matchedKey as TKey);
}
}
return;
}

Expand Down Expand Up @@ -852,57 +864,12 @@ function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matchingKeys: Co
});
}

/**
* Delays promise resolution until the next macrotask to prevent race condition if the key subscription is in progress.
*
* @param callback The keyChanged/keysChanged callback
* */
function prepareSubscriberUpdate(callback: () => void): Promise<void> {
if (!nextMacrotaskPromise) {
nextMacrotaskPromise = new Promise<void>((resolve) => {
setTimeout(() => {
nextMacrotaskPromise = null;
resolve();
}, 0);
});
}
return Promise.all([nextMacrotaskPromise, Promise.resolve().then(callback)]).then();
}

/**
* Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
*
* @example
* scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
*/
function scheduleSubscriberUpdate<TKey extends OnyxKey>(
key: TKey,
value: OnyxValue<TKey>,
canUpdateSubscriber: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean = () => true,
isProcessingCollectionUpdate = false,
): Promise<void> {
return prepareSubscriberUpdate(() => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate));
}

/**
* This method is similar to scheduleSubscriberUpdate but it is built for working specifically with collections
* so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
* subscriber callbacks receive the data in a different format than they normally expect and it breaks code.
*/
function scheduleNotifyCollectionSubscribers<TKey extends OnyxKey>(
key: TKey,
value: OnyxCollection<KeyValueMapping[TKey]>,
previousValue?: OnyxCollection<KeyValueMapping[TKey]>,
): Promise<void> {
return prepareSubscriberUpdate(() => keysChanged(key, value, previousValue));
}

/**
* Remove a key from Onyx and update the subscribers
*/
function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void> {
cache.drop(key);
scheduleSubscriberUpdate(key, undefined as OnyxValue<TKey>, undefined, isProcessingCollectionUpdate);
keyChanged(key, undefined as OnyxValue<TKey>, undefined, isProcessingCollectionUpdate);

if (isRamOnlyKey(key)) {
return Promise.resolve();
Expand Down Expand Up @@ -973,7 +940,7 @@ function retryOperation<TMethod extends RetriableOnyxOperation>(error: Error, on
/**
* Notifies subscribers and writes current value to cache
*/
function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, hasChanged?: boolean): Promise<void> {
function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, hasChanged?: boolean): void {
// Update subscribers if the cached value has changed, or when the subscriber specifically requires
// all updates regardless of value changes (indicated by initWithStoredValues set to false).
if (hasChanged) {
Expand All @@ -982,7 +949,7 @@ function broadcastUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>
cache.addToAccessedKeys(key);
}

return scheduleSubscriberUpdate(key, value, (subscriber) => hasChanged || subscriber?.initWithStoredValues === false).then(() => undefined);
keyChanged(key, value, (subscriber) => hasChanged || subscriber?.initWithStoredValues === false);
}

function hasPendingMergeForKey(key: OnyxKey): boolean {
Expand Down Expand Up @@ -1376,24 +1343,23 @@ function setWithRetry<TKey extends OnyxKey>({key, value, options}: SetParams<TKe
OnyxUtils.logKeyChanged(OnyxUtils.METHOD.SET, key, value, hasChanged);

// This approach prioritizes fast UI changes without waiting for data to be stored in device storage.
const updatePromise = OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);
OnyxUtils.broadcastUpdate(key, valueWithoutNestedNullValues, hasChanged);

// If the value has not changed and this isn't a retry attempt, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
if (!hasChanged && !retryAttempt) {
return updatePromise;
return Promise.resolve();
}

// If a key is a RAM-only key or a member of RAM-only collection, we skip the step that modifies the storage
if (isRamOnlyKey(key)) {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
return updatePromise;
return Promise.resolve();
}

return Storage.setItem(key, valueWithoutNestedNullValues)
.catch((error) => OnyxUtils.retryOperation(error, setWithRetry, {key, value: valueWithoutNestedNullValues, options}, retryAttempt))
.then(() => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET, key, valueWithoutNestedNullValues);
return updatePromise;
});
}

Expand Down Expand Up @@ -1427,7 +1393,7 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom

const keyValuePairsToSet = OnyxUtils.prepareKeyValuePairsForStorage(newData, true);

const updatePromises = keyValuePairsToSet.map(([key, value]) => {
for (const [key, value] of keyValuePairsToSet) {
// When we use multiSet to set a key we want to clear the current delta changes from Onyx.merge that were queued
// before the value was set. If Onyx.merge is currently reading the old value from storage, it will then not apply the changes.
if (OnyxUtils.hasPendingMergeForKey(key)) {
Expand All @@ -1436,8 +1402,8 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom

// Update cache and optimistically inform subscribers on the next tick
cache.set(key, value);
return OnyxUtils.scheduleSubscriberUpdate(key, value);
});
keyChanged(key, value);
}

const keyValuePairsToStore = keyValuePairsToSet.filter((keyValuePair) => {
const [key] = keyValuePair;
Expand All @@ -1449,9 +1415,7 @@ function multiSetWithRetry(data: OnyxMultiSetInput, retryAttempt?: number): Prom
.catch((error) => OnyxUtils.retryOperation(error, multiSetWithRetry, newData, retryAttempt))
.then(() => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.MULTI_SET, undefined, newData);
return Promise.all(updatePromises);
})
.then(() => undefined);
});
}

/**
Expand Down Expand Up @@ -1512,19 +1476,18 @@ function setCollectionWithRetry<TKey extends CollectionKeyBase>({collectionKey,

for (const [key, value] of keyValuePairs) cache.set(key, value);

const updatePromise = OnyxUtils.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
keysChanged(collectionKey, mutableCollection, previousCollection);

// RAM-only keys are not supposed to be saved to storage
if (isRamOnlyKey(collectionKey)) {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
return updatePromise;
return;
}

return Storage.multiSet(keyValuePairs)
.catch((error) => OnyxUtils.retryOperation(error, setCollectionWithRetry, {collectionKey, collection}, retryAttempt))
.then(() => {
OnyxUtils.sendActionToDevTools(OnyxUtils.METHOD.SET_COLLECTION, undefined, mutableCollection);
return updatePromise;
});
});
}
Expand Down Expand Up @@ -1647,7 +1610,7 @@ function mergeCollectionWithPatches<TKey extends CollectionKeyBase>(
// and update all subscribers
const promiseUpdate = previousCollectionPromise.then((previousCollection) => {
cache.merge(finalMergedCollection);
return scheduleNotifyCollectionSubscribers(collectionKey, finalMergedCollection, previousCollection);
keysChanged(collectionKey, finalMergedCollection, previousCollection);
});

return Promise.all(promises)
Expand Down Expand Up @@ -1713,18 +1676,17 @@ function partialSetCollection<TKey extends CollectionKeyBase>({collectionKey, co

for (const [key, value] of keyValuePairs) cache.set(key, value);

const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
keysChanged(collectionKey, mutableCollection, previousCollection);

if (isRamOnlyKey(collectionKey)) {
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
return updatePromise;
return;
}

return Storage.multiSet(keyValuePairs)
.catch((error) => retryOperation(error, partialSetCollection, {collectionKey, collection}, retryAttempt))
.then(() => {
sendActionToDevTools(METHOD.SET_COLLECTION, undefined, mutableCollection);
return updatePromise;
});
});
}
Expand Down Expand Up @@ -1772,8 +1734,6 @@ const OnyxUtils = {
sendDataToConnection,
getCollectionKey,
getCollectionDataAndSendAsObject,
scheduleSubscriberUpdate,
scheduleNotifyCollectionSubscribers,
remove,
reportStorageQuota,
retryOperation,
Expand Down Expand Up @@ -1830,10 +1790,6 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => {
// @ts-expect-error Reassign
sendDataToConnection = decorateWithMetrics(sendDataToConnection, 'OnyxUtils.sendDataToConnection');
// @ts-expect-error Reassign
scheduleSubscriberUpdate = decorateWithMetrics(scheduleSubscriberUpdate, 'OnyxUtils.scheduleSubscriberUpdate');
// @ts-expect-error Reassign
scheduleNotifyCollectionSubscribers = decorateWithMetrics(scheduleNotifyCollectionSubscribers, 'OnyxUtils.scheduleNotifyCollectionSubscribers');
// @ts-expect-error Reassign
remove = decorateWithMetrics(remove, 'OnyxUtils.remove');
// @ts-expect-error Reassign
reportStorageQuota = decorateWithMetrics(reportStorageQuota, 'OnyxUtils.reportStorageQuota');
Expand Down
Loading