react-native-onyx 3.0.6 → 3.0.8

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/README.md CHANGED
@@ -441,7 +441,24 @@ To use the extension, simply install it from your favorite web browser store:
441
441
  - [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/redux-devtools/nnkgneoiohoecpdiaponcejilbhhikei)
442
442
  - [Mozilla Firefox](https://addons.mozilla.org/en-US/firefox/addon/reduxdevtools/)
443
443
 
444
- After installing the extension, Onyx will automatically connect to it and start logging any updates made to the local storage.
444
+ ### Enabling or Disabling Redux DevTools
445
+
446
+ You can control whether the Redux DevTools integration is enabled by setting the `enableDevTools` option in the `Onyx.init()` configuration.
447
+
448
+ - To **enable** Redux DevTools and start logging updates to local storage, set `enableDevTools: true`.
449
+ - To **disable** Redux DevTools and prevent any logging to the extension, set `enableDevTools: false`.
450
+
451
+ This option defaults to `true` (enabled) on Web, so you only need to set it to `false` if you want to disable the integration.
452
+
453
+ ```javascript
454
+ import Onyx from 'react-native-onyx';
455
+ import Config from './config';
456
+
457
+ Onyx.init({
458
+ keys: ONYXKEYS,
459
+ enableDevTools: Config.ENABLE_ONYX_DEVTOOLS,
460
+ });
461
+ ```
445
462
 
446
463
  ### Usage
447
464
 
@@ -0,0 +1,11 @@
1
+ import type { IDevTools } from './types';
2
+ /**
3
+ * No-op implementation of DevTools that does nothing
4
+ * Used when DevTools is disabled
5
+ */
6
+ declare class NoOpDevTools implements IDevTools {
7
+ registerAction(): void;
8
+ initState(): void;
9
+ clearState(): void;
10
+ }
11
+ export default NoOpDevTools;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * No-op implementation of DevTools that does nothing
5
+ * Used when DevTools is disabled
6
+ */
7
+ class NoOpDevTools {
8
+ registerAction() {
9
+ // do nothing
10
+ }
11
+ initState() {
12
+ // do nothing
13
+ }
14
+ clearState() {
15
+ // do nothing
16
+ }
17
+ }
18
+ exports.default = NoOpDevTools;
@@ -0,0 +1,25 @@
1
+ import type { IDevTools, DevtoolsOptions, DevtoolsConnection } from './types';
2
+ /**
3
+ * Real implementation of DevTools that connects to Redux DevTools Extension
4
+ */
5
+ declare class RealDevTools implements IDevTools {
6
+ private remoteDev?;
7
+ private state;
8
+ private defaultState;
9
+ constructor();
10
+ connectViaExtension(options?: DevtoolsOptions): DevtoolsConnection | undefined;
11
+ /**
12
+ * Registers an action that updated the current state of the storage
13
+ *
14
+ * @param type - name of the action
15
+ * @param payload - data written to the storage
16
+ * @param stateChanges - partial state that got updated after the changes
17
+ */
18
+ registerAction(type: string, payload: unknown, stateChanges?: Record<string, unknown> | null): void;
19
+ initState(initialState?: Record<string, unknown>): void;
20
+ /**
21
+ * This clears the internal state of the DevTools, preserving the keys included in `keysToPreserve`
22
+ */
23
+ clearState(keysToPreserve?: string[]): void;
24
+ }
25
+ export default RealDevTools;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ERROR_LABEL = 'Onyx DevTools - Error: ';
4
+ /**
5
+ * Real implementation of DevTools that connects to Redux DevTools Extension
6
+ */
7
+ class RealDevTools {
8
+ constructor() {
9
+ this.remoteDev = this.connectViaExtension();
10
+ this.state = {};
11
+ this.defaultState = {};
12
+ }
13
+ connectViaExtension(options) {
14
+ try {
15
+ // We don't want to augment the window type in a library code, so we use type assertion instead
16
+ // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-explicit-any
17
+ const reduxDevtools = typeof window === 'undefined' ? undefined : window.__REDUX_DEVTOOLS_EXTENSION__;
18
+ if ((options === null || options === void 0 ? void 0 : options.remote) || !reduxDevtools) {
19
+ return;
20
+ }
21
+ return reduxDevtools.connect(options);
22
+ }
23
+ catch (e) {
24
+ console.error(ERROR_LABEL, e);
25
+ }
26
+ }
27
+ /**
28
+ * Registers an action that updated the current state of the storage
29
+ *
30
+ * @param type - name of the action
31
+ * @param payload - data written to the storage
32
+ * @param stateChanges - partial state that got updated after the changes
33
+ */
34
+ registerAction(type, payload, stateChanges = {}) {
35
+ try {
36
+ if (!this.remoteDev) {
37
+ return;
38
+ }
39
+ const newState = Object.assign(Object.assign({}, this.state), stateChanges);
40
+ this.remoteDev.send({ type, payload }, newState);
41
+ this.state = newState;
42
+ }
43
+ catch (e) {
44
+ console.error(ERROR_LABEL, e);
45
+ }
46
+ }
47
+ initState(initialState = {}) {
48
+ try {
49
+ if (!this.remoteDev) {
50
+ return;
51
+ }
52
+ this.remoteDev.init(initialState);
53
+ this.state = initialState;
54
+ this.defaultState = initialState;
55
+ }
56
+ catch (e) {
57
+ console.error(ERROR_LABEL, e);
58
+ }
59
+ }
60
+ /**
61
+ * This clears the internal state of the DevTools, preserving the keys included in `keysToPreserve`
62
+ */
63
+ clearState(keysToPreserve = []) {
64
+ const newState = Object.entries(this.state).reduce((obj, [key, value]) => {
65
+ // eslint-disable-next-line no-param-reassign
66
+ obj[key] = keysToPreserve.includes(key) ? value : this.defaultState[key];
67
+ return obj;
68
+ }, {});
69
+ this.registerAction('CLEAR', undefined, newState);
70
+ }
71
+ }
72
+ exports.default = RealDevTools;
@@ -0,0 +1,31 @@
1
+ type DevtoolsOptions = {
2
+ maxAge?: number;
3
+ name?: string;
4
+ postTimelineUpdate?: () => void;
5
+ preAction?: () => void;
6
+ logTrace?: boolean;
7
+ remote?: boolean;
8
+ };
9
+ type DevtoolsSubscriber = (message: {
10
+ type: string;
11
+ payload: unknown;
12
+ state: string;
13
+ }) => void;
14
+ type DevtoolsConnection = {
15
+ send(data: Record<string, unknown>, state: Record<string, unknown>): void;
16
+ init(state: Record<string, unknown>): void;
17
+ unsubscribe(): void;
18
+ subscribe(cb: DevtoolsSubscriber): () => void;
19
+ };
20
+ type ReduxDevtools = {
21
+ connect(options?: DevtoolsOptions): DevtoolsConnection;
22
+ };
23
+ /**
24
+ * Type definition for DevTools instance
25
+ */
26
+ type IDevTools = {
27
+ registerAction(type: string, payload: unknown, stateChanges?: Record<string, unknown> | null): void;
28
+ initState(initialState?: Record<string, unknown>): void;
29
+ clearState(keysToPreserve?: string[]): void;
30
+ };
31
+ export type { DevtoolsOptions, DevtoolsSubscriber, DevtoolsConnection, ReduxDevtools, IDevTools };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -1,42 +1,19 @@
1
- type DevtoolsOptions = {
2
- maxAge?: number;
3
- name?: string;
4
- postTimelineUpdate?: () => void;
5
- preAction?: () => void;
6
- logTrace?: boolean;
7
- remote?: boolean;
8
- };
9
- type DevtoolsSubscriber = (message: {
10
- type: string;
11
- payload: unknown;
12
- state: string;
13
- }) => void;
14
- type DevtoolsConnection = {
15
- send(data: Record<string, unknown>, state: Record<string, unknown>): void;
16
- init(state: Record<string, unknown>): void;
17
- unsubscribe(): void;
18
- subscribe(cb: DevtoolsSubscriber): () => void;
19
- };
20
- declare class DevTools {
21
- private remoteDev?;
22
- private state;
23
- private defaultState;
24
- constructor();
25
- connectViaExtension(options?: DevtoolsOptions): DevtoolsConnection | undefined;
26
- /**
27
- * Registers an action that updated the current state of the storage
28
- *
29
- * @param type - name of the action
30
- * @param payload - data written to the storage
31
- * @param stateChanges - partial state that got updated after the changes
32
- */
33
- registerAction(type: string, payload: unknown, stateChanges?: Record<string, unknown> | null): void;
34
- initState(initialState?: Record<string, unknown>): void;
35
- /**
36
- * This clears the internal state of the DevTools, preserving the keys included in `keysToPreserve`
37
- */
38
- clearState(keysToPreserve?: string[]): void;
39
- }
40
- declare const _default: DevTools;
41
- export default _default;
42
- export type { DevtoolsConnection };
1
+ import type { IDevTools, DevtoolsConnection } from './DevTools/types';
2
+ /**
3
+ * Initializes DevTools with the given enabled flag
4
+ */
5
+ declare function initDevTools(enabled: boolean): void;
6
+ /**
7
+ * Gets the current DevTools instance (for testing purposes only)
8
+ * @private
9
+ */
10
+ declare function getDevToolsInstance(): IDevTools;
11
+ /**
12
+ * Export a default object that delegates to the current devToolsInstance
13
+ * This allows the instance to be swapped out while keeping the same import signature
14
+ */
15
+ declare const DevTools: IDevTools;
16
+ export default DevTools;
17
+ export { initDevTools, getDevToolsInstance };
18
+ export type { DevtoolsConnection, IDevTools };
19
+ export type { default as RealDevTools } from './DevTools/RealDevTools';
package/dist/DevTools.js CHANGED
@@ -1,69 +1,40 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
- const ERROR_LABEL = 'Onyx DevTools - Error: ';
4
- class DevTools {
5
- constructor() {
6
- this.remoteDev = this.connectViaExtension();
7
- this.state = {};
8
- this.defaultState = {};
9
- }
10
- connectViaExtension(options) {
11
- try {
12
- // We don't want to augment the window type in a library code, so we use type assertion instead
13
- // eslint-disable-next-line no-underscore-dangle, @typescript-eslint/no-explicit-any
14
- const reduxDevtools = typeof window === 'undefined' ? undefined : window.__REDUX_DEVTOOLS_EXTENSION__;
15
- if ((options === null || options === void 0 ? void 0 : options.remote) || !reduxDevtools) {
16
- return;
17
- }
18
- return reduxDevtools.connect(options);
19
- }
20
- catch (e) {
21
- console.error(ERROR_LABEL, e);
22
- }
23
- }
24
- /**
25
- * Registers an action that updated the current state of the storage
26
- *
27
- * @param type - name of the action
28
- * @param payload - data written to the storage
29
- * @param stateChanges - partial state that got updated after the changes
30
- */
6
+ exports.initDevTools = initDevTools;
7
+ exports.getDevToolsInstance = getDevToolsInstance;
8
+ const RealDevTools_1 = __importDefault(require("./DevTools/RealDevTools"));
9
+ const NoOpDevTools_1 = __importDefault(require("./DevTools/NoOpDevTools"));
10
+ // Start with a no-op instance
11
+ let devToolsInstance = new NoOpDevTools_1.default();
12
+ /**
13
+ * Initializes DevTools with the given enabled flag
14
+ */
15
+ function initDevTools(enabled) {
16
+ devToolsInstance = enabled ? new RealDevTools_1.default() : new NoOpDevTools_1.default();
17
+ }
18
+ /**
19
+ * Gets the current DevTools instance (for testing purposes only)
20
+ * @private
21
+ */
22
+ function getDevToolsInstance() {
23
+ return devToolsInstance;
24
+ }
25
+ /**
26
+ * Export a default object that delegates to the current devToolsInstance
27
+ * This allows the instance to be swapped out while keeping the same import signature
28
+ */
29
+ const DevTools = {
31
30
  registerAction(type, payload, stateChanges = {}) {
32
- try {
33
- if (!this.remoteDev) {
34
- return;
35
- }
36
- const newState = Object.assign(Object.assign({}, this.state), stateChanges);
37
- this.remoteDev.send({ type, payload }, newState);
38
- this.state = newState;
39
- }
40
- catch (e) {
41
- console.error(ERROR_LABEL, e);
42
- }
43
- }
31
+ devToolsInstance.registerAction(type, payload, stateChanges);
32
+ },
44
33
  initState(initialState = {}) {
45
- try {
46
- if (!this.remoteDev) {
47
- return;
48
- }
49
- this.remoteDev.init(initialState);
50
- this.state = initialState;
51
- this.defaultState = initialState;
52
- }
53
- catch (e) {
54
- console.error(ERROR_LABEL, e);
55
- }
56
- }
57
- /**
58
- * This clears the internal state of the DevTools, preserving the keys included in `keysToPreserve`
59
- */
34
+ devToolsInstance.initState(initialState);
35
+ },
60
36
  clearState(keysToPreserve = []) {
61
- const newState = Object.entries(this.state).reduce((obj, [key, value]) => {
62
- // eslint-disable-next-line no-param-reassign
63
- obj[key] = keysToPreserve.includes(key) ? value : this.defaultState[key];
64
- return obj;
65
- }, {});
66
- this.registerAction('CLEAR', undefined, newState);
67
- }
68
- }
69
- exports.default = new DevTools();
37
+ devToolsInstance.clearState(keysToPreserve);
38
+ },
39
+ };
40
+ exports.default = DevTools;
package/dist/Onyx.d.ts CHANGED
@@ -2,7 +2,7 @@ import * as Logger from './Logger';
2
2
  import type { CollectionKeyBase, ConnectOptions, InitOptions, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetInput, OnyxUpdate, SetOptions } from './types';
3
3
  import type { Connection } from './OnyxConnectionManager';
4
4
  /** Initialize the store with actions and listening for storage events */
5
- declare function init({ keys, initialKeyStates, evictableKeys, maxCachedKeysCount, shouldSyncMultipleInstances, enablePerformanceMetrics, skippableCollectionMemberIDs, }: InitOptions): void;
5
+ declare function init({ keys, initialKeyStates, evictableKeys, maxCachedKeysCount, shouldSyncMultipleInstances, enablePerformanceMetrics, enableDevTools, skippableCollectionMemberIDs, }: InitOptions): void;
6
6
  /**
7
7
  * Connects to an Onyx key given the options passed and listens to its changes.
8
8
  * This method will be deprecated soon. Please use `Onyx.connectWithoutView()` instead.
package/dist/Onyx.js CHANGED
@@ -40,7 +40,7 @@ const Logger = __importStar(require("./Logger"));
40
40
  const OnyxCache_1 = __importStar(require("./OnyxCache"));
41
41
  const storage_1 = __importDefault(require("./storage"));
42
42
  const utils_1 = __importDefault(require("./utils"));
43
- const DevTools_1 = __importDefault(require("./DevTools"));
43
+ const DevTools_1 = __importStar(require("./DevTools"));
44
44
  const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
45
45
  const logMessages_1 = __importDefault(require("./logMessages"));
46
46
  const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
@@ -48,12 +48,13 @@ const GlobalSettings = __importStar(require("./GlobalSettings"));
48
48
  const metrics_1 = __importDefault(require("./metrics"));
49
49
  const OnyxMerge_1 = __importDefault(require("./OnyxMerge"));
50
50
  /** Initialize the store with actions and listening for storage events */
51
- function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = !!global.localStorage, enablePerformanceMetrics = false, skippableCollectionMemberIDs = [], }) {
51
+ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], maxCachedKeysCount = 1000, shouldSyncMultipleInstances = !!global.localStorage, enablePerformanceMetrics = false, enableDevTools = true, skippableCollectionMemberIDs = [], }) {
52
52
  var _a;
53
53
  if (enablePerformanceMetrics) {
54
54
  GlobalSettings.setPerformanceMetricsEnabled(true);
55
55
  applyDecorators();
56
56
  }
57
+ (0, DevTools_1.initDevTools)(enableDevTools);
57
58
  storage_1.default.init();
58
59
  OnyxUtils_1.default.setSkippableCollectionMemberIDs(new Set(skippableCollectionMemberIDs));
59
60
  if (shouldSyncMultipleInstances) {
@@ -343,7 +344,7 @@ function merge(key, changes) {
343
344
  * @param collection Object collection keyed by individual collection member keys and values
344
345
  */
345
346
  function mergeCollection(collectionKey, collection) {
346
- return OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, collection);
347
+ return OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, collection, undefined, true);
347
348
  }
348
349
  /**
349
350
  * Clear out all the data in the store
@@ -552,7 +553,7 @@ function update(data) {
552
553
  set: {},
553
554
  });
554
555
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.merge)) {
555
- promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, batchedCollectionUpdates.merge, batchedCollectionUpdates.mergeReplaceNullPatches));
556
+ promises.push(() => OnyxUtils_1.default.mergeCollectionWithPatches(collectionKey, batchedCollectionUpdates.merge, batchedCollectionUpdates.mergeReplaceNullPatches, true));
556
557
  }
557
558
  if (!utils_1.default.isEmptyObject(batchedCollectionUpdates.set)) {
558
559
  promises.push(() => OnyxUtils_1.default.partialSetCollection(collectionKey, batchedCollectionUpdates.set));
@@ -623,7 +624,7 @@ function setCollection(collectionKey, collection) {
623
624
  }
624
625
  mutableCollection[key] = null;
625
626
  });
626
- const keyValuePairs = OnyxUtils_1.default.prepareKeyValuePairsForStorage(mutableCollection, true);
627
+ const keyValuePairs = OnyxUtils_1.default.prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
627
628
  const previousCollection = OnyxUtils_1.default.getCachedCollection(collectionKey);
628
629
  keyValuePairs.forEach(([key, value]) => OnyxCache_1.default.set(key, value));
629
630
  const updatePromise = OnyxUtils_1.default.scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
@@ -149,7 +149,7 @@ declare function keysChanged<TKey extends CollectionKeyBase>(collectionKey: TKey
149
149
  * @example
150
150
  * keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
151
151
  */
152
- declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean, notifyConnectSubscribers?: boolean): void;
152
+ declare function keyChanged<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean, notifyConnectSubscribers?: boolean, isProcessingCollectionUpdate?: boolean): void;
153
153
  /**
154
154
  * Sends the data obtained from the keys to the connection.
155
155
  */
@@ -169,7 +169,7 @@ declare function getCollectionDataAndSendAsObject<TKey extends OnyxKey>(matching
169
169
  * @example
170
170
  * scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
171
171
  */
172
- declare function scheduleSubscriberUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean): Promise<void>;
172
+ declare function scheduleSubscriberUpdate<TKey extends OnyxKey>(key: TKey, value: OnyxValue<TKey>, canUpdateSubscriber?: (subscriber?: CallbackToStateMapping<OnyxKey>) => boolean, isProcessingCollectionUpdate?: boolean): Promise<void>;
173
173
  /**
174
174
  * This method is similar to notifySubscribersOnNextTick but it is built for working specifically with collections
175
175
  * so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
@@ -179,7 +179,7 @@ declare function scheduleNotifyCollectionSubscribers<TKey extends OnyxKey>(key:
179
179
  /**
180
180
  * Remove a key from Onyx and update the subscribers
181
181
  */
182
- declare function remove<TKey extends OnyxKey>(key: TKey): Promise<void>;
182
+ declare function remove<TKey extends OnyxKey>(key: TKey, isProcessingCollectionUpdate?: boolean): Promise<void>;
183
183
  declare function reportStorageQuota(): Promise<void>;
184
184
  /**
185
185
  * If we fail to set or merge we must handle this by
@@ -199,7 +199,7 @@ declare function hasPendingMergeForKey(key: OnyxKey): boolean;
199
199
  *
200
200
  * @return an array of key - value pairs <[key, value]>
201
201
  */
202
- declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches): StorageKeyValuePair[];
202
+ declare function prepareKeyValuePairsForStorage(data: Record<OnyxKey, OnyxInput<OnyxKey>>, shouldRemoveNestedNulls?: boolean, replaceNullPatches?: MultiMergeReplaceNullPatches, isProcessingCollectionUpdate?: boolean): StorageKeyValuePair[];
203
203
  /**
204
204
  * Merges an array of changes with an existing value or creates a single change.
205
205
  *
@@ -251,7 +251,7 @@ declare function updateSnapshots(data: OnyxUpdate[], mergeFn: typeof Onyx.merge)
251
251
  * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
252
252
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
253
253
  */
254
- declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>, mergeReplaceNullPatches?: MultiMergeReplaceNullPatches): Promise<void>;
254
+ declare function mergeCollectionWithPatches<TKey extends CollectionKeyBase, TMap>(collectionKey: TKey, collection: OnyxMergeCollectionInput<TKey, TMap>, mergeReplaceNullPatches?: MultiMergeReplaceNullPatches, isProcessingCollectionUpdate?: boolean): Promise<void>;
255
255
  /**
256
256
  * Sets keys in a collection by replacing all targeted collection members with new values.
257
257
  * Any existing collection members not included in the new data will not be removed.
package/dist/OnyxUtils.js CHANGED
@@ -564,7 +564,7 @@ function keysChanged(collectionKey, partialCollection, partialPreviousCollection
564
564
  * @example
565
565
  * keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
566
566
  */
567
- function keyChanged(key, value, canUpdateSubscriber = () => true, notifyConnectSubscribers = true) {
567
+ function keyChanged(key, value, canUpdateSubscriber = () => true, notifyConnectSubscribers = true, isProcessingCollectionUpdate = false) {
568
568
  var _a, _b;
569
569
  // Add or remove this key from the recentlyAccessedKeys lists
570
570
  if (value !== null) {
@@ -609,6 +609,11 @@ function keyChanged(key, value, canUpdateSubscriber = () => true, notifyConnectS
609
609
  continue;
610
610
  }
611
611
  if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
612
+ // Skip individual key changes for collection callbacks during collection updates
613
+ // to prevent duplicate callbacks - the collection update will handle this properly
614
+ if (isProcessingCollectionUpdate) {
615
+ continue;
616
+ }
612
617
  let cachedCollection = cachedCollections[subscriber.key];
613
618
  if (!cachedCollection) {
614
619
  cachedCollection = getCachedCollection(subscriber.key);
@@ -674,9 +679,9 @@ function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
674
679
  * @example
675
680
  * scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
676
681
  */
677
- function scheduleSubscriberUpdate(key, value, canUpdateSubscriber = () => true) {
678
- const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber, true));
679
- batchUpdates(() => keyChanged(key, value, canUpdateSubscriber, false));
682
+ function scheduleSubscriberUpdate(key, value, canUpdateSubscriber = () => true, isProcessingCollectionUpdate = false) {
683
+ const promise = Promise.resolve().then(() => keyChanged(key, value, canUpdateSubscriber, true, isProcessingCollectionUpdate));
684
+ batchUpdates(() => keyChanged(key, value, canUpdateSubscriber, false, isProcessingCollectionUpdate));
680
685
  return Promise.all([maybeFlushBatchUpdates(), promise]).then(() => undefined);
681
686
  }
682
687
  /**
@@ -692,9 +697,9 @@ function scheduleNotifyCollectionSubscribers(key, value, previousValue) {
692
697
  /**
693
698
  * Remove a key from Onyx and update the subscribers
694
699
  */
695
- function remove(key) {
700
+ function remove(key, isProcessingCollectionUpdate) {
696
701
  OnyxCache_1.default.drop(key);
697
- scheduleSubscriberUpdate(key, undefined);
702
+ scheduleSubscriberUpdate(key, undefined, undefined, isProcessingCollectionUpdate);
698
703
  return storage_1.default.removeItem(key).then(() => undefined);
699
704
  }
700
705
  function reportStorageQuota() {
@@ -756,11 +761,11 @@ function hasPendingMergeForKey(key) {
756
761
  *
757
762
  * @return an array of key - value pairs <[key, value]>
758
763
  */
759
- function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches) {
764
+ function prepareKeyValuePairsForStorage(data, shouldRemoveNestedNulls, replaceNullPatches, isProcessingCollectionUpdate) {
760
765
  const pairs = [];
761
766
  Object.entries(data).forEach(([key, value]) => {
762
767
  if (value === null) {
763
- remove(key);
768
+ remove(key, isProcessingCollectionUpdate);
764
769
  return;
765
770
  }
766
771
  const valueWithoutNestedNullValues = (shouldRemoveNestedNulls !== null && shouldRemoveNestedNulls !== void 0 ? shouldRemoveNestedNulls : true) ? utils_1.default.removeNestedNullValues(value) : value;
@@ -1010,7 +1015,7 @@ function updateSnapshots(data, mergeFn) {
1010
1015
  * @param mergeReplaceNullPatches Record where the key is a collection member key and the value is a list of
1011
1016
  * tuples that we'll use to replace the nested objects of that collection member record with something else.
1012
1017
  */
1013
- function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches) {
1018
+ function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullPatches, isProcessingCollectionUpdate = false) {
1014
1019
  if (!isValidNonEmptyCollectionForMerge(collection)) {
1015
1020
  Logger.logInfo('mergeCollection() called with invalid or empty value. Skipping this update.');
1016
1021
  return Promise.resolve();
@@ -1043,7 +1048,7 @@ function mergeCollectionWithPatches(collectionKey, collection, mergeReplaceNullP
1043
1048
  // Split to keys that exist in storage and keys that don't
1044
1049
  const keys = resultCollectionKeys.filter((key) => {
1045
1050
  if (resultCollection[key] === null) {
1046
- remove(key);
1051
+ remove(key, isProcessingCollectionUpdate);
1047
1052
  return false;
1048
1053
  }
1049
1054
  return true;
@@ -1139,7 +1144,7 @@ function partialSetCollection(collectionKey, collection) {
1139
1144
  const mutableCollection = Object.assign({}, resultCollection);
1140
1145
  const existingKeys = resultCollectionKeys.filter((key) => persistedKeys.has(key));
1141
1146
  const previousCollection = getCachedCollection(collectionKey, existingKeys);
1142
- const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true);
1147
+ const keyValuePairs = prepareKeyValuePairsForStorage(mutableCollection, true, undefined, true);
1143
1148
  keyValuePairs.forEach(([key, value]) => OnyxCache_1.default.set(key, value));
1144
1149
  const updatePromise = scheduleNotifyCollectionSubscribers(collectionKey, mutableCollection, previousCollection);
1145
1150
  return storage_1.default.multiSet(keyValuePairs)
package/dist/types.d.ts CHANGED
@@ -337,6 +337,12 @@ type InitOptions = {
337
337
  * @default false
338
338
  */
339
339
  enablePerformanceMetrics?: boolean;
340
+ /**
341
+ * If enabled, it will connect to Redux DevTools Extension for debugging.
342
+ * This allows you to see all Onyx state changes in the Redux DevTools.
343
+ * @default true
344
+ */
345
+ enableDevTools?: boolean;
340
346
  /**
341
347
  * Array of collection member IDs which updates will be ignored when using Onyx methods.
342
348
  * Additionally, any subscribers from these keys to won't receive any data from Onyx.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.6",
3
+ "version": "3.0.8",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",