From 2f404e649c96264b849e8ffeb5cd20bf496083ed Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 16 Oct 2025 16:03:47 +0200 Subject: [PATCH 1/9] Remove the current batching mechanism --- jestSetup.js | 3 - lib/OnyxUtils.ts | 81 +++++--------------------- lib/batch.native.ts | 3 - lib/batch.ts | 3 - package-lock.json | 27 --------- package.json | 3 - tests/perf-test/OnyxUtils.perf-test.ts | 7 --- tests/unit/onyxUtilsTest.ts | 1 - 8 files changed, 13 insertions(+), 115 deletions(-) delete mode 100644 lib/batch.native.ts delete mode 100644 lib/batch.ts diff --git a/jestSetup.js b/jestSetup.js index 82f8f4d5d..156828a67 100644 --- a/jestSetup.js +++ b/jestSetup.js @@ -10,6 +10,3 @@ jest.mock('react-native-nitro-sqlite', () => ({ })); jest.useRealTimers(); - -const unstable_batchedUpdates_jest = require('react-test-renderer').unstable_batchedUpdates; -require('./lib/batch.native').default = unstable_batchedUpdates_jest; diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 39e8afd06..f87638e6e 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -8,7 +8,6 @@ import * as Logger from './Logger'; import type Onyx from './Onyx'; import cache, {TASK} from './OnyxCache'; import * as Str from './Str'; -import unstable_batchedUpdates from './batch'; import Storage from './storage'; import type { CollectionKey, @@ -67,9 +66,6 @@ let onyxKeyToSubscriptionIDs = new Map(); // Optional user-provided key value states set when Onyx initializes or clears let defaultKeyStates: Record> = {}; -let batchUpdatesPromise: Promise | null = null; -let batchUpdatesQueue: Array<() => void> = []; - // Used for comparison with a new update to avoid invoking the Onyx.connect callback with the same data. let lastConnectionCallbackData = new Map>(); @@ -191,43 +187,6 @@ function sendActionToDevTools( DevTools.registerAction(utils.formatActionName(method, key), value, key ? {[key]: mergedValue || value} : (value as OnyxCollection)); } -/** - * We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other. - * This happens for example in the Onyx.update function, where we process API responses that might contain a lot of - * update operations. Instead of calling the subscribers for each update operation, we batch them together which will - * cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization. - */ -function maybeFlushBatchUpdates(): Promise { - if (batchUpdatesPromise) { - return batchUpdatesPromise; - } - - batchUpdatesPromise = new Promise((resolve) => { - /* We use (setTimeout, 0) here which should be called once native module calls are flushed (usually at the end of the frame) - * We may investigate if (setTimeout, 1) (which in React Native is equal to requestAnimationFrame) works even better - * then the batch will be flushed on next frame. - */ - setTimeout(() => { - const updatesCopy = batchUpdatesQueue; - batchUpdatesQueue = []; - batchUpdatesPromise = null; - unstable_batchedUpdates(() => { - updatesCopy.forEach((applyUpdates) => { - applyUpdates(); - }); - }); - - resolve(); - }, 0); - }); - return batchUpdatesPromise; -} - -function batchUpdates(updates: () => void): Promise { - batchUpdatesQueue.push(updates); - return maybeFlushBatchUpdates(); -} - /** * Takes a collection of items (eg. {testKey_1:{a:'a'}, testKey_2:{b:'b'}}) * and runs it through a reducer function to return a subset of the data according to a selector. @@ -597,7 +556,6 @@ function keysChanged( collectionKey: TKey, partialCollection: OnyxCollection, partialPreviousCollection: OnyxCollection | undefined, - notifyConnectSubscribers = true, ): void { // We prepare the "cached collection" which is the entire collection + the new partial data that // was merged in via mergeCollection(). @@ -633,10 +591,6 @@ function keysChanged( // Regular Onyx.connect() subscriber found. if (typeof subscriber.callback === 'function') { - if (!notifyConnectSubscribers) { - continue; - } - // If they are subscribed to the collection key and using waitForCollectionCallback then we'll // send the whole cached collection. if (isSubscribedToCollectionKey) { @@ -682,12 +636,7 @@ function keysChanged( * @example * keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false) */ -function keyChanged( - key: TKey, - value: OnyxValue, - canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, - notifyConnectSubscribers = true, -): void { +function keyChanged(key: TKey, value: OnyxValue, canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true): void { // Add or remove this key from the recentlyAccessedKeys lists if (value !== null) { cache.addLastAccessedKey(key, isCollectionKey(key)); @@ -727,9 +676,6 @@ function keyChanged( // Subscriber is a regular call to connect() and provided a callback if (typeof subscriber.callback === 'function') { - if (!notifyConnectSubscribers) { - continue; - } if (lastConnectionCallbackData.has(subscriber.subscriptionID) && lastConnectionCallbackData.get(subscriber.subscriptionID) === value) { continue; } @@ -818,9 +764,11 @@ function scheduleSubscriberUpdate( value: OnyxValue, canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, ): Promise { - const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber, true)); - batchUpdates(() => keyChanged(key, value, canUpdateSubscriber, false)); - return Promise.all([maybeFlushBatchUpdates(), promise]).then(() => undefined); + const promise0 = new Promise((resolve) => { + setTimeout(resolve, 0); + }); + const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber)); + return Promise.all([promise0, promise]).then(() => undefined); } /** @@ -833,9 +781,13 @@ function scheduleNotifyCollectionSubscribers( value: OnyxCollection, previousValue?: OnyxCollection, ): Promise { - const promise = Promise.resolve().then(() => keysChanged(key, value, previousValue, true)); - batchUpdates(() => keysChanged(key, value, previousValue, false)); - return Promise.all([maybeFlushBatchUpdates(), promise]).then(() => undefined); + const promise0 = new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 0); + }); + const promise = Promise.resolve().then(() => keysChanged(key, value, previousValue)); + return Promise.all([promise0, promise]).then(() => undefined); } /** @@ -1420,7 +1372,6 @@ function clearOnyxUtilsInternals() { mergeQueuePromise = {}; callbackToStateMapping = {}; onyxKeyToSubscriptionIDs = new Map(); - batchUpdatesQueue = []; lastConnectionCallbackData = new Map(); } @@ -1432,8 +1383,6 @@ const OnyxUtils = { getDeferredInitTask, initStoreValues, sendActionToDevTools, - maybeFlushBatchUpdates, - batchUpdates, get, getAllKeys, getCollectionKeys, @@ -1487,10 +1436,6 @@ GlobalSettings.addGlobalSettingsChangeListener(({enablePerformanceMetrics}) => { // @ts-expect-error Reassign initStoreValues = decorateWithMetrics(initStoreValues, 'OnyxUtils.initStoreValues'); - // @ts-expect-error Reassign - maybeFlushBatchUpdates = decorateWithMetrics(maybeFlushBatchUpdates, 'OnyxUtils.maybeFlushBatchUpdates'); - // @ts-expect-error Reassign - batchUpdates = decorateWithMetrics(batchUpdates, 'OnyxUtils.batchUpdates'); // @ts-expect-error Complex type signature get = decorateWithMetrics(get, 'OnyxUtils.get'); // @ts-expect-error Reassign diff --git a/lib/batch.native.ts b/lib/batch.native.ts deleted file mode 100644 index fb7ef4ee5..000000000 --- a/lib/batch.native.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {unstable_batchedUpdates} from 'react-native'; - -export default unstable_batchedUpdates; diff --git a/lib/batch.ts b/lib/batch.ts deleted file mode 100644 index 3ff0368fe..000000000 --- a/lib/batch.ts +++ /dev/null @@ -1,3 +0,0 @@ -import {unstable_batchedUpdates} from 'react-dom'; - -export default unstable_batchedUpdates; diff --git a/package-lock.json b/package-lock.json index 16b6c2bc5..3e2240108 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,6 @@ "@types/lodash": "^4.14.202", "@types/node": "^20.11.5", "@types/react": "^18.2.14", - "@types/react-dom": "^18.2.18", "@types/react-native": "^0.70.0", "@types/underscore": "^1.11.15", "@typescript-eslint/eslint-plugin": "^6.19.0", @@ -53,7 +52,6 @@ "prettier": "^2.8.8", "prop-types": "^15.7.2", "react": "18.2.0", - "react-dom": "18.2.0", "react-native": "0.76.3", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": "^0.26.2", @@ -72,7 +70,6 @@ "peerDependencies": { "idb-keyval": "^6.2.1", "react": ">=18.1.0", - "react-dom": ">=18.1.0", "react-native": ">=0.75.0", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": ">=0.26.2", @@ -4111,16 +4108,6 @@ "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "@types/react": "^18.0.0" - } - }, "node_modules/@types/react-native": { "version": "0.70.19", "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.19.tgz", @@ -12745,20 +12732,6 @@ } } }, - "node_modules/react-dom": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", - "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.0" - }, - "peerDependencies": { - "react": "^18.2.0" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", diff --git a/package.json b/package.json index 6200c179a..94e25eaea 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,6 @@ "@types/lodash": "^4.14.202", "@types/node": "^20.11.5", "@types/react": "^18.2.14", - "@types/react-dom": "^18.2.18", "@types/react-native": "^0.70.0", "@types/underscore": "^1.11.15", "@typescript-eslint/eslint-plugin": "^6.19.0", @@ -86,7 +85,6 @@ "prettier": "^2.8.8", "prop-types": "^15.7.2", "react": "18.2.0", - "react-dom": "18.2.0", "react-native": "0.76.3", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": "^0.26.2", @@ -101,7 +99,6 @@ "peerDependencies": { "idb-keyval": "^6.2.1", "react": ">=18.1.0", - "react-dom": ">=18.1.0", "react-native": ">=0.75.0", "react-native-device-info": "^10.3.0", "react-native-nitro-modules": ">=0.26.2", diff --git a/tests/perf-test/OnyxUtils.perf-test.ts b/tests/perf-test/OnyxUtils.perf-test.ts index 5a817b859..f04deb931 100644 --- a/tests/perf-test/OnyxUtils.perf-test.ts +++ b/tests/perf-test/OnyxUtils.perf-test.ts @@ -112,13 +112,6 @@ describe('OnyxUtils', () => { }); }); - describe('batchUpdates / maybeFlushBatchUpdates', () => { - test('one call with 1k updates', async () => { - const updates: Array<() => void> = Array.from({length: 1000}, () => jest.fn); - await measureAsyncFunction(() => Promise.all(updates.map((update) => OnyxUtils.batchUpdates(update)))); - }); - }); - describe('get', () => { test('10k calls with heavy objects', async () => { await measureAsyncFunction(() => Promise.all(mockedReportActionsKeys.map((key) => OnyxUtils.get(key))), { diff --git a/tests/unit/onyxUtilsTest.ts b/tests/unit/onyxUtilsTest.ts index 9b120a580..88ce3e8dd 100644 --- a/tests/unit/onyxUtilsTest.ts +++ b/tests/unit/onyxUtilsTest.ts @@ -283,7 +283,6 @@ describe('OnyxUtils', () => { ONYXKEYS.COLLECTION.TEST_KEY, {[entryKey]: updatedEntryData}, // new collection initialCollection, // previous collection - true, // notify connect subscribers ); // Should be called again because data changed From fec6d6bcebc086f1ad14170b4d91706df03fe1d1 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 16 Oct 2025 16:18:09 +0200 Subject: [PATCH 2/9] Make code more readable --- lib/OnyxUtils.ts | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index f87638e6e..c7421dcfe 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -753,6 +753,13 @@ function getCollectionDataAndSendAsObject(matchingKeys: Co }); } +// !!!DO NOT MERGE THIS CODE, METHODS FOR READABILITY ONLY +const nextMicrotask = () => Promise.resolve(); +const nextMacrotask = () => + new Promise((resolve) => { + setTimeout(resolve, 0); + }); + /** * Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately). * @@ -764,11 +771,7 @@ function scheduleSubscriberUpdate( value: OnyxValue, canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, ): Promise { - const promise0 = new Promise((resolve) => { - setTimeout(resolve, 0); - }); - const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber)); - return Promise.all([promise0, promise]).then(() => undefined); + return Promise.all([nextMacrotask(), nextMicrotask().then(() => keyChanged(key, value, canUpdateSubscriber))]).then(() => undefined); } /** @@ -781,13 +784,7 @@ function scheduleNotifyCollectionSubscribers( value: OnyxCollection, previousValue?: OnyxCollection, ): Promise { - const promise0 = new Promise((resolve) => { - setTimeout(() => { - resolve(); - }, 0); - }); - const promise = Promise.resolve().then(() => keysChanged(key, value, previousValue)); - return Promise.all([promise0, promise]).then(() => undefined); + return Promise.all([nextMacrotask(), nextMicrotask().then(() => keysChanged(key, value, previousValue))]).then(() => undefined); } /** From 6308802a1f0a522d0a181be42afb57bd68e5d70e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 21 Oct 2025 16:19:27 +0200 Subject: [PATCH 3/9] Have one next macrotask instead of multiple --- lib/OnyxUtils.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index c7421dcfe..a0e9c2842 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -755,9 +755,13 @@ function getCollectionDataAndSendAsObject(matchingKeys: Co // !!!DO NOT MERGE THIS CODE, METHODS FOR READABILITY ONLY const nextMicrotask = () => Promise.resolve(); +let nextMacrotaskPromise: Promise | null = null; const nextMacrotask = () => new Promise((resolve) => { - setTimeout(resolve, 0); + setTimeout(() => { + nextMacrotaskPromise = null; + resolve(); + }, 0); }); /** @@ -771,7 +775,10 @@ function scheduleSubscriberUpdate( value: OnyxValue, canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, ): Promise { - return Promise.all([nextMacrotask(), nextMicrotask().then(() => keyChanged(key, value, canUpdateSubscriber))]).then(() => undefined); + if (!nextMacrotaskPromise) { + nextMacrotaskPromise = nextMacrotask(); + } + return Promise.all([nextMacrotaskPromise, nextMicrotask().then(() => keyChanged(key, value, canUpdateSubscriber))]).then(() => undefined); } /** @@ -784,7 +791,10 @@ function scheduleNotifyCollectionSubscribers( value: OnyxCollection, previousValue?: OnyxCollection, ): Promise { - return Promise.all([nextMacrotask(), nextMicrotask().then(() => keysChanged(key, value, previousValue))]).then(() => undefined); + if (!nextMacrotaskPromise) { + nextMacrotaskPromise = nextMacrotask(); + } + return Promise.all([nextMacrotaskPromise, nextMicrotask().then(() => keysChanged(key, value, previousValue))]).then(() => undefined); } /** From e93419ee903b7bfa881909bc6a2a003e2922b5ea Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Jan 2026 10:15:13 +0100 Subject: [PATCH 4/9] Fix TS error --- lib/Onyx.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Onyx.ts b/lib/Onyx.ts index 27751ae60..bebe3cb9a 100644 --- a/lib/Onyx.ts +++ b/lib/Onyx.ts @@ -62,7 +62,7 @@ function init({ // Setting isProcessingCollectionUpdate=true prevents triggering collection callbacks for each individual update const isKeyCollectionMember = OnyxUtils.isCollectionMember(key); - OnyxUtils.keyChanged(key, value as OnyxValue, undefined, true, isKeyCollectionMember); + OnyxUtils.keyChanged(key, value as OnyxValue, undefined, isKeyCollectionMember); }); } From b0bad9815a73e84c7b5c6a7da333b083458ecc80 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Jan 2026 16:24:58 +0100 Subject: [PATCH 5/9] Improve the logic to schedule the macrotask only when needed --- lib/OnyxUtils.ts | 68 +++++++++++++++++++++++++++++------------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index b73c7185b..d9ed15d61 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -85,6 +85,9 @@ let onyxCollectionKeySet = new Set(); // Holds a mapping of the connected key to the subscriptionID for faster lookups let onyxKeyToSubscriptionIDs = new Map(); +// Keys with subscriptions currently being established +const pendingSubscriptionKeys = new Set(); + // Optional user-provided key value states set when Onyx initializes or clears let defaultKeyStates: Record> = {}; @@ -801,16 +804,31 @@ function getCollectionDataAndSendAsObject(matchingKeys: Co }); } -// !!!DO NOT MERGE THIS CODE, METHODS FOR READABILITY ONLY -const nextMicrotask = () => Promise.resolve(); -let nextMacrotaskPromise: Promise | null = null; -const nextMacrotask = () => - new Promise((resolve) => { - setTimeout(() => { - nextMacrotaskPromise = null; - resolve(); - }, 0); - }); +/** Helps to schedule subscriber update. Schedule the macrotask if the key subscription is in progress to avoid race condition. + * + * @param key Onyx key + * @param callback The keyChanged/keysChanged callback + * */ +function prepareSubscriberUpdate(key: TKey, callback: () => void): Promise { + let collectionKey: string | undefined; + try { + collectionKey = getCollectionKey(key); + } catch (e) { + // If getCollectionKey() throws an error it means the key is not a collection key. + collectionKey = undefined; + } + + callback(); + + // If subscription is in progress, schedule a macrotask to prevent race condition with data from subscribeToKey deferred logic. + if (pendingSubscriptionKeys.has(key) || (collectionKey && pendingSubscriptionKeys.has(collectionKey))) { + return new Promise((resolve) => { + setTimeout(() => resolve()); + }); + } + + return Promise.resolve(); +} /** * Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately). @@ -824,14 +842,11 @@ function scheduleSubscriberUpdate( canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, isProcessingCollectionUpdate = false, ): Promise { - if (!nextMacrotaskPromise) { - nextMacrotaskPromise = nextMacrotask(); - } - return Promise.all([nextMacrotaskPromise, nextMicrotask().then(() => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate))]).then(() => undefined); + return prepareSubscriberUpdate(key, () => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate)); } /** - * This method is similar to notifySubscribersOnNextTick but it is built for working specifically with collections + * 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. */ @@ -840,10 +855,7 @@ function scheduleNotifyCollectionSubscribers( value: OnyxCollection, previousValue?: OnyxCollection, ): Promise { - if (!nextMacrotaskPromise) { - nextMacrotaskPromise = nextMacrotask(); - } - return Promise.all([nextMacrotaskPromise, nextMicrotask().then(() => keysChanged(key, value, previousValue))]).then(() => undefined); + return prepareSubscriberUpdate(key, () => keysChanged(key, value, previousValue)); } /** @@ -1092,7 +1104,10 @@ function subscribeToKey(connectOptions: ConnectOptions(connectOptions: ConnectOptions(connectOptions: ConnectOptions { + return multiGet(matchingKeys).then((values) => { values.forEach((val, key) => { sendDataToConnection(mapping, val as OnyxValue, key as TKey); }); }); - return; } // If we are not subscribed to a collection key then there's only a single key to send an update for. - get(mapping.key).then((val) => sendDataToConnection(mapping, val as OnyxValue, mapping.key)); - return; + return get(mapping.key).then((val) => sendDataToConnection(mapping, val as OnyxValue, mapping.key)); } console.error('Warning: Onyx.connect() was found without a callback'); + }) + .then(() => { + pendingSubscriptionKeys.delete(mapping.key); }); // The subscriptionID is returned back to the caller so that it can be used to clean up the connection when it's no longer needed From 605bc69a1324f9cad057530bd805b2e9504cff35 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Jan 2026 16:53:04 +0100 Subject: [PATCH 6/9] Re-run checks From 4e3e9568e1ea06f2739edee768838f2d413ccc71 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 7 Jan 2026 17:12:55 +0100 Subject: [PATCH 7/9] Re-run reassure check From e423194520b5e9cf66d9a00d6255d14c0ed5e14e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 8 Jan 2026 12:39:42 +0100 Subject: [PATCH 8/9] Fix E/App tests --- lib/OnyxUtils.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index d9ed15d61..29e407a5f 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -818,16 +818,15 @@ function prepareSubscriberUpdate(key: TKey, callback: () = collectionKey = undefined; } - callback(); - // If subscription is in progress, schedule a macrotask to prevent race condition with data from subscribeToKey deferred logic. if (pendingSubscriptionKeys.has(key) || (collectionKey && pendingSubscriptionKeys.has(collectionKey))) { - return new Promise((resolve) => { - setTimeout(() => resolve()); + const macrotaskPromise = new Promise((resolve) => { + setTimeout(() => resolve(), 0); }); + return Promise.all([macrotaskPromise, Promise.resolve().then(callback)]).then(); } - return Promise.resolve(); + return Promise.resolve().then(callback); } /** From fd10feadb68bd4f0547850053344eb12b539a333 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 8 Jan 2026 16:12:57 +0100 Subject: [PATCH 9/9] Fix the test of E/App tests --- lib/OnyxUtils.ts | 56 ++++++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 33 deletions(-) diff --git a/lib/OnyxUtils.ts b/lib/OnyxUtils.ts index 29e407a5f..05deec4b2 100644 --- a/lib/OnyxUtils.ts +++ b/lib/OnyxUtils.ts @@ -76,6 +76,9 @@ type OnyxMethod = ValueOf; let mergeQueue: Record>> = {}; let mergeQueuePromise: Record> = {}; +// Used to schedule subscriber update to the macro task queue +let nextMacrotaskPromise: Promise | null = null; + // Holds a mapping of all the React components that want their state subscribed to a store key let callbackToStateMapping: Record> = {}; @@ -85,9 +88,6 @@ let onyxCollectionKeySet = new Set(); // Holds a mapping of the connected key to the subscriptionID for faster lookups let onyxKeyToSubscriptionIDs = new Map(); -// Keys with subscriptions currently being established -const pendingSubscriptionKeys = new Set(); - // Optional user-provided key value states set when Onyx initializes or clears let defaultKeyStates: Record> = {}; @@ -804,29 +804,21 @@ function getCollectionDataAndSendAsObject(matchingKeys: Co }); } -/** Helps to schedule subscriber update. Schedule the macrotask if the key subscription is in progress to avoid race condition. +/** + * Delays promise resolution until the next macrotask to prevent race condition if the key subscription is in progress. * - * @param key Onyx key * @param callback The keyChanged/keysChanged callback * */ -function prepareSubscriberUpdate(key: TKey, callback: () => void): Promise { - let collectionKey: string | undefined; - try { - collectionKey = getCollectionKey(key); - } catch (e) { - // If getCollectionKey() throws an error it means the key is not a collection key. - collectionKey = undefined; - } - - // If subscription is in progress, schedule a macrotask to prevent race condition with data from subscribeToKey deferred logic. - if (pendingSubscriptionKeys.has(key) || (collectionKey && pendingSubscriptionKeys.has(collectionKey))) { - const macrotaskPromise = new Promise((resolve) => { - setTimeout(() => resolve(), 0); +function prepareSubscriberUpdate(callback: () => void): Promise { + if (!nextMacrotaskPromise) { + nextMacrotaskPromise = new Promise((resolve) => { + setTimeout(() => { + nextMacrotaskPromise = null; + resolve(); + }, 0); }); - return Promise.all([macrotaskPromise, Promise.resolve().then(callback)]).then(); } - - return Promise.resolve().then(callback); + return Promise.all([nextMacrotaskPromise, Promise.resolve().then(callback)]).then(); } /** @@ -841,7 +833,7 @@ function scheduleSubscriberUpdate( canUpdateSubscriber: (subscriber?: CallbackToStateMapping) => boolean = () => true, isProcessingCollectionUpdate = false, ): Promise { - return prepareSubscriberUpdate(key, () => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate)); + return prepareSubscriberUpdate(() => keyChanged(key, value, canUpdateSubscriber, isProcessingCollectionUpdate)); } /** @@ -854,7 +846,7 @@ function scheduleNotifyCollectionSubscribers( value: OnyxCollection, previousValue?: OnyxCollection, ): Promise { - return prepareSubscriberUpdate(key, () => keysChanged(key, value, previousValue)); + return prepareSubscriberUpdate(() => keysChanged(key, value, previousValue)); } /** @@ -1103,10 +1095,7 @@ function subscribeToKey(connectOptions: ConnectOptions(connectOptions: ConnectOptions(connectOptions: ConnectOptions { + multiGet(matchingKeys).then((values) => { values.forEach((val, key) => { sendDataToConnection(mapping, val as OnyxValue, key as TKey); }); }); + return; } // If we are not subscribed to a collection key then there's only a single key to send an update for. - return get(mapping.key).then((val) => sendDataToConnection(mapping, val as OnyxValue, mapping.key)); + get(mapping.key).then((val) => sendDataToConnection(mapping, val as OnyxValue, mapping.key)); + return; } console.error('Warning: Onyx.connect() was found without a callback'); - }) - .then(() => { - pendingSubscriptionKeys.delete(mapping.key); }); // The subscriptionID is returned back to the caller so that it can be used to clean up the connection when it's no longer needed