react-native-onyx 3.0.115 → 3.0.117

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/API.md CHANGED
@@ -78,36 +78,40 @@ Connects to an Onyx key given the options passed and listens to its changes.
78
78
  This method will be deprecated soon. Please use `Onyx.connectWithoutView()` instead.
79
79
 
80
80
  **Kind**: global function
81
- **Returns**: The connection object to use when calling `Onyx.disconnect()`.
81
+ **Returns**: The `Connection` handle to use when calling `Onyx.disconnect()`.
82
82
 
83
83
  | Param | Description |
84
84
  | --- | --- |
85
85
  | connectOptions | The options object that will define the behavior of the connection. |
86
86
  | connectOptions.key | The Onyx key to subscribe to. |
87
87
  | connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
88
- | connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally cause the component to re-render (and that can be expensive from a performance standpoint). |
89
88
 
90
89
  **Example**
91
90
  ```ts
92
- const connection = Onyx.connectWithoutView({
91
+ const connection = Onyx.connect({
93
92
  key: ONYXKEYS.SESSION,
94
93
  callback: onSessionChange,
95
94
  });
96
95
  ```
96
+
97
+ For a collection root key, the callback fires with the entire frozen collection
98
+ object whenever any member changes; signature `(collection, collectionKey)`.
99
+ For any other key, the callback fires with the value at that key; signature
100
+ `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
101
+ cache after any same-tick writes have applied.
97
102
  <a name="connectWithoutView"></a>
98
103
 
99
104
  ## connectWithoutView(connectOptions) ⇒
100
105
  Connects to an Onyx key given the options passed and listens to its changes.
101
106
 
102
107
  **Kind**: global function
103
- **Returns**: The connection object to use when calling `Onyx.disconnect()`.
108
+ **Returns**: The `Connection` handle to use when calling `Onyx.disconnect()`.
104
109
 
105
110
  | Param | Description |
106
111
  | --- | --- |
107
112
  | connectOptions | The options object that will define the behavior of the connection. |
108
113
  | connectOptions.key | The Onyx key to subscribe to. |
109
114
  | connectOptions.callback | A function that will be called when the Onyx data we are subscribed changes. |
110
- | connectOptions.selector | This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.** Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render when the subset of data changes. Otherwise, any change of data on any property would normally cause the component to re-render (and that can be expensive from a performance standpoint). |
111
115
 
112
116
  **Example**
113
117
  ```ts
@@ -116,6 +120,12 @@ const connection = Onyx.connectWithoutView({
116
120
  callback: onSessionChange,
117
121
  });
118
122
  ```
123
+
124
+ For a collection root key, the callback fires with the entire frozen collection
125
+ object whenever any member changes; signature `(collection, collectionKey)`.
126
+ For any other key, the callback fires with the value at that key; signature
127
+ `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
128
+ cache after any same-tick writes have applied.
119
129
  <a name="disconnect"></a>
120
130
 
121
131
  ## disconnect(connection)
package/README.md CHANGED
@@ -9,7 +9,7 @@ Awesome persistent storage solution wrapped in a Pub/Sub library.
9
9
  - Onyx allows other code to subscribe to changes in data, and then publishes change events whenever data is changed
10
10
  - Anything needing to read Onyx data needs to:
11
11
  1. Know what key the data is stored in (for web, you can find this by looking in the JS console > Application > local storage)
12
- 2. Subscribe to changes of the data for a particular key or set of keys. React function components use the `useOnyx()` hook and non-React libs use `Onyx.connect()`.
12
+ 2. Subscribe to changes of the data for a particular key or set of keys. React function components use the `useOnyx()` hook and non-React libs use `Onyx.connectWithoutView()`.
13
13
  3. Get initialized with the current value of that key from persistent storage (Onyx does this by calling `setState()` or triggering the `callback` with the values currently on disk as part of the connection process)
14
14
  - Subscribing to Onyx keys is done using a constant defined in `ONYXKEYS`. Each Onyx key represents either a collection of items or a specific entry in storage. For example, since all reports are stored as individual keys like `report_1234`, if code needs to know about all the reports (e.g. display a list of them in the nav menu), then it would subscribe to the key `ONYXKEYS.COLLECTION.REPORT`.
15
15
 
@@ -120,20 +120,20 @@ You should avoid arrays as much as possible. They do not work well with `merge()
120
120
 
121
121
  ## Subscribing to data changes
122
122
 
123
- To set up a basic subscription for a given key use the `Onyx.connect()` method.
123
+ To set up a basic subscription for a given key outside of a React component use the `Onyx.connectWithoutView()` method. It returns a connection handle.
124
124
 
125
125
  ```javascript
126
126
  let session;
127
- const connectionID = Onyx.connect({
127
+ const connection = Onyx.connectWithoutView({
128
128
  key: ONYXKEYS.SESSION,
129
129
  callback: (val) => session = val || {},
130
130
  });
131
131
  ```
132
132
 
133
- To teardown the subscription call `Onyx.disconnect()` with the `connectionID` returned from `Onyx.connect()`. It's recommended to clean up subscriptions anytime you are connecting from within a function to prevent memory leaks.
133
+ To teardown the subscription call `Onyx.disconnect()` with the connection returned from `Onyx.connectWithoutView()`. It's recommended to clean up subscriptions anytime you are connecting from within a function to prevent memory leaks.
134
134
 
135
135
  ```javascript
136
- Onyx.disconnect(connectionID);
136
+ Onyx.disconnect(connection);
137
137
  ```
138
138
 
139
139
  We can also access values inside React function components via the `useOnyx()` [hook](https://react.dev/reference/react/hooks). When the data changes the component will re-render.
@@ -204,7 +204,7 @@ export default App;
204
204
 
205
205
  * It is VERY important to NOT use empty string default values like `report.policyID || ''`. This results in the key returned to `useOnyx` as `policies_`, which subscribes to the ENTIRE POLICY COLLECTION and is most assuredly not what you were intending. You can use a default of `0` (as long as you are reasonably sure that there is never a policyID=0). This allows Onyx to return `undefined` as the value of the policy key, which is handled by `useOnyx` appropriately.
206
206
 
207
- It's also beneficial to use a [selector](https://github.com/Expensify/react-native-onyx/blob/main/API.md#connectmapping--number) with the mapping in case you need to grab a single item in a collection (like a single report action).
207
+ It's also beneficial to use `useOnyx()`'s `selector` option in case you need to grab a single item in a collection (like a single report action).
208
208
 
209
209
  ## Collections
210
210
 
@@ -257,7 +257,7 @@ export default MyComponent;
257
257
  This will add a prop to the component called `allReports` which is an object of collection member key/values. Changes to the individual member keys will modify the entire object and new props will be passed with each individual key update. The prop doesn't update on the initial rendering of the component until the entire collection has been read out of Onyx.
258
258
 
259
259
  ```js
260
- Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT}, callback: (allReports, collectionKey) => {...});
260
+ Onyx.connectWithoutView({key: ONYXKEYS.COLLECTION.REPORT, callback: (allReports, collectionKey) => {...}});
261
261
  ```
262
262
 
263
263
  This will fire the callback once with the entire collection initially and later with an updated version of the collection when individual keys update.
@@ -293,7 +293,7 @@ function signOut() {
293
293
  `Onyx.exportState()` returns a plain object containing persisted keys and values from the active storage provider. It works on native and web without opening another database connection. The export excludes stale persisted rows for keys that are now RAM-only. Current RAM-only values and writes still in progress are never included. Treat the returned object and its nested values as read-only because some providers return references to their stored values. The result can contain sensitive data; callers should redact it before sharing.
294
294
 
295
295
  ## Storage Providers
296
- `Onyx.get`, `Onyx.set`, and the rest of the API accesses the underlying storage
296
+ `Onyx.set`, `Onyx.merge`, and the rest of the API accesses the underlying storage
297
297
  differently depending on the platform
298
298
 
299
299
  Under the hood storage access calls are delegated to a [`StorageProvider`](lib/storage/index.js)
package/dist/Onyx.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import * as Logger from './Logger';
2
- import type { CollectionKeyBase, ConnectOptions, InitOptions, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetCollectionInput, OnyxSetInput, OnyxUpdate, OnyxValue, SetOptions } from './types';
3
- import type { Connection } from './OnyxConnectionManager';
2
+ import type { CollectionKeyBase, Connection, ConnectOptions, InitOptions, OnyxKey, OnyxMergeCollectionInput, OnyxMergeInput, OnyxMultiSetInput, OnyxSetCollectionInput, OnyxSetInput, OnyxUpdate, OnyxValue, SetOptions } from './types';
4
3
  /** Initialize the store with actions and listening for storage events */
5
4
  declare function init({ keys, initialKeyStates, evictableKeys, shouldSyncMultipleInstances, enableDevTools, skippableCollectionMemberIDs, ramOnlyKeys, snapshotMergeKeys, }: InitOptions): void;
6
5
  /**
@@ -9,20 +8,22 @@ declare function init({ keys, initialKeyStates, evictableKeys, shouldSyncMultipl
9
8
  *
10
9
  * @example
11
10
  * ```ts
12
- * const connection = Onyx.connectWithoutView({
11
+ * const connection = Onyx.connect({
13
12
  * key: ONYXKEYS.SESSION,
14
13
  * callback: onSessionChange,
15
14
  * });
16
15
  * ```
17
16
  *
17
+ * For a collection root key, the callback fires with the entire frozen collection
18
+ * object whenever any member changes; signature `(collection, collectionKey)`.
19
+ * For any other key, the callback fires with the value at that key; signature
20
+ * `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
21
+ * cache after any same-tick writes have applied.
22
+ *
18
23
  * @param connectOptions The options object that will define the behavior of the connection.
19
24
  * @param connectOptions.key The Onyx key to subscribe to.
20
25
  * @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
21
- * @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
22
- * Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
23
- * when the subset of data changes. Otherwise, any change of data on any property would normally
24
- * cause the component to re-render (and that can be expensive from a performance standpoint).
25
- * @returns The connection object to use when calling `Onyx.disconnect()`.
26
+ * @returns The `Connection` handle to use when calling `Onyx.disconnect()`.
26
27
  */
27
28
  declare function connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): Connection;
28
29
  /**
@@ -36,14 +37,16 @@ declare function connect<TKey extends OnyxKey>(connectOptions: ConnectOptions<TK
36
37
  * });
37
38
  * ```
38
39
  *
40
+ * For a collection root key, the callback fires with the entire frozen collection
41
+ * object whenever any member changes; signature `(collection, collectionKey)`.
42
+ * For any other key, the callback fires with the value at that key; signature
43
+ * `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
44
+ * cache after any same-tick writes have applied.
45
+ *
39
46
  * @param connectOptions The options object that will define the behavior of the connection.
40
47
  * @param connectOptions.key The Onyx key to subscribe to.
41
48
  * @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
42
- * @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
43
- * Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
44
- * when the subset of data changes. Otherwise, any change of data on any property would normally
45
- * cause the component to re-render (and that can be expensive from a performance standpoint).
46
- * @returns The connection object to use when calling `Onyx.disconnect()`.
49
+ * @returns The `Connection` handle to use when calling `Onyx.disconnect()`.
47
50
  */
48
51
  declare function connectWithoutView<TKey extends OnyxKey>(connectOptions: ConnectOptions<TKey>): Connection;
49
52
  /**
@@ -182,4 +185,4 @@ declare const Onyx: {
182
185
  registerLogger: typeof Logger.registerLogger;
183
186
  };
184
187
  export default Onyx;
185
- export type { OnyxUpdate, ConnectOptions, SetOptions };
188
+ export type { OnyxUpdate, ConnectOptions, SetOptions, Connection };
package/dist/Onyx.js CHANGED
@@ -43,8 +43,9 @@ const utils_1 = __importDefault(require("./utils"));
43
43
  const DevTools_1 = __importStar(require("./DevTools"));
44
44
  const OnyxUtils_1 = __importDefault(require("./OnyxUtils"));
45
45
  const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
46
+ const PendingWrites_1 = __importDefault(require("./PendingWrites"));
46
47
  const logMessages_1 = __importDefault(require("./logMessages"));
47
- const OnyxConnectionManager_1 = __importDefault(require("./OnyxConnectionManager"));
48
+ const OnyxSubscriptionManager_1 = __importDefault(require("./OnyxSubscriptionManager"));
48
49
  const OnyxMerge_1 = __importDefault(require("./OnyxMerge"));
49
50
  /** Initialize the store with actions and listening for storage events */
50
51
  function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSyncMultipleInstances = !!global.localStorage, enableDevTools = true, skippableCollectionMemberIDs = [], ramOnlyKeys = [], snapshotMergeKeys = [], }) {
@@ -70,7 +71,7 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
70
71
  }
71
72
  const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
72
73
  const isCollectionMember = !!collectionKey && OnyxKeys_1.default.isCollectionMemberKey(collectionKey, key);
73
- // Capture the previous cached value BEFORE cache.set() so keysChanged() can diff old vs new per member.
74
+ // Capture the previous cached value before cache.set() so notifyCollection() can diff old vs new per member.
74
75
  const previousValue = isCollectionMember ? OnyxCache_1.default.get(key) : undefined;
75
76
  OnyxCache_1.default.set(key, value);
76
77
  if (isCollectionMember && collectionKey) {
@@ -89,14 +90,14 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
89
90
  individual.push([key, value]);
90
91
  }
91
92
  }
92
- // Non-collection keys: notify individually, matching keyChanged() semantics for exact keys.
93
+ // Non-collection keys: notify individually, matching notifyKey() semantics for exact keys.
93
94
  for (const [key, value] of individual) {
94
- OnyxUtils_1.default.keyChanged(key, value);
95
+ OnyxUtils_1.default.notifyKey(key, value);
95
96
  }
96
- // One keysChanged() per collection notifies the collection-root subscriber once and lets
97
- // keysChanged() decide which individual member subscribers actually changed.
97
+ // One notifyCollection() per collection notifies the collection-root subscriber once and lets
98
+ // notifyCollection() decide which individual member subscribers actually changed.
98
99
  for (const [collectionKey, { partial, previous }] of collectionBatches) {
99
- OnyxUtils_1.default.keysChanged(collectionKey, partial, previous);
100
+ OnyxUtils_1.default.notifyCollection(collectionKey, partial, previous);
100
101
  }
101
102
  });
102
103
  }
@@ -115,23 +116,89 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
115
116
  *
116
117
  * @example
117
118
  * ```ts
118
- * const connection = Onyx.connectWithoutView({
119
+ * const connection = Onyx.connect({
119
120
  * key: ONYXKEYS.SESSION,
120
121
  * callback: onSessionChange,
121
122
  * });
122
123
  * ```
123
124
  *
125
+ * For a collection root key, the callback fires with the entire frozen collection
126
+ * object whenever any member changes; signature `(collection, collectionKey)`.
127
+ * For any other key, the callback fires with the value at that key; signature
128
+ * `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
129
+ * cache after any same-tick writes have applied.
130
+ *
124
131
  * @param connectOptions The options object that will define the behavior of the connection.
125
132
  * @param connectOptions.key The Onyx key to subscribe to.
126
133
  * @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
127
- * @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
128
- * Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
129
- * when the subset of data changes. Otherwise, any change of data on any property would normally
130
- * cause the component to re-render (and that can be expensive from a performance standpoint).
131
- * @returns The connection object to use when calling `Onyx.disconnect()`.
134
+ * @returns The `Connection` handle to use when calling `Onyx.disconnect()`.
132
135
  */
133
136
  function connect(connectOptions) {
134
- return OnyxConnectionManager_1.default.connect(connectOptions);
137
+ const { key, callback } = connectOptions;
138
+ let active = true;
139
+ let unsubscribeFn = null;
140
+ const startSubscription = () => {
141
+ if (!active) {
142
+ return;
143
+ }
144
+ if (OnyxKeys_1.default.isCollectionKey(key)) {
145
+ // Collection-root mode: dedup skips identical collection refs. Initial fire delivers
146
+ // the current collection object: frozen `{}` for an empty-but-known collection,
147
+ // `undefined` only if the collection key has not been seen yet.
148
+ let lastDeliveredCollection = OnyxUtils_1.default.NOT_DELIVERED;
149
+ const deliverCollection = (rawCollection, k) => {
150
+ if (Object.is(lastDeliveredCollection, rawCollection)) {
151
+ return;
152
+ }
153
+ lastDeliveredCollection = rawCollection;
154
+ callback === null || callback === void 0 ? void 0 : callback(rawCollection, k);
155
+ };
156
+ unsubscribeFn = OnyxSubscriptionManager_1.default.subscribe(key, (value, k) => {
157
+ deliverCollection(value, k);
158
+ });
159
+ PendingWrites_1.default.scheduleInitialSubscriberNotification(key, () => {
160
+ if (!active) {
161
+ return;
162
+ }
163
+ deliverCollection(OnyxSubscriptionManager_1.default.getState(key), key);
164
+ });
165
+ return;
166
+ }
167
+ // Non-collection key (or a specific collection member): single-value subscription.
168
+ let lastDelivered = OnyxUtils_1.default.NOT_DELIVERED;
169
+ const deliverValue = (value, k) => {
170
+ if (Object.is(lastDelivered, value)) {
171
+ return;
172
+ }
173
+ lastDelivered = value;
174
+ callback === null || callback === void 0 ? void 0 : callback(value, k);
175
+ };
176
+ unsubscribeFn = OnyxSubscriptionManager_1.default.subscribe(key, (value, k) => {
177
+ deliverValue(value, k);
178
+ });
179
+ PendingWrites_1.default.scheduleInitialSubscriberNotification(key, () => {
180
+ if (!active) {
181
+ return;
182
+ }
183
+ deliverValue(OnyxSubscriptionManager_1.default.getState(key), key);
184
+ });
185
+ };
186
+ OnyxUtils_1.default.afterInit(() => {
187
+ startSubscription();
188
+ return Promise.resolve();
189
+ });
190
+ return {
191
+ unsubscribe: () => {
192
+ if (!active) {
193
+ return;
194
+ }
195
+ active = false;
196
+ if (unsubscribeFn) {
197
+ unsubscribeFn();
198
+ unsubscribeFn = null;
199
+ }
200
+ },
201
+ };
135
202
  }
136
203
  /**
137
204
  * Connects to an Onyx key given the options passed and listens to its changes.
@@ -144,17 +211,19 @@ function connect(connectOptions) {
144
211
  * });
145
212
  * ```
146
213
  *
214
+ * For a collection root key, the callback fires with the entire frozen collection
215
+ * object whenever any member changes; signature `(collection, collectionKey)`.
216
+ * For any other key, the callback fires with the value at that key; signature
217
+ * `(value, key)`. Initial fire is deferred via `scheduleInitialSubscriberNotification` so it reads
218
+ * cache after any same-tick writes have applied.
219
+ *
147
220
  * @param connectOptions The options object that will define the behavior of the connection.
148
221
  * @param connectOptions.key The Onyx key to subscribe to.
149
222
  * @param connectOptions.callback A function that will be called when the Onyx data we are subscribed changes.
150
- * @param connectOptions.selector This will be used to subscribe to a subset of an Onyx key's data. **Only used inside `useOnyx()` hook.**
151
- * Using this setting on `useOnyx()` can have very positive performance benefits because the component will only re-render
152
- * when the subset of data changes. Otherwise, any change of data on any property would normally
153
- * cause the component to re-render (and that can be expensive from a performance standpoint).
154
- * @returns The connection object to use when calling `Onyx.disconnect()`.
223
+ * @returns The `Connection` handle to use when calling `Onyx.disconnect()`.
155
224
  */
156
225
  function connectWithoutView(connectOptions) {
157
- return OnyxConnectionManager_1.default.connect(connectOptions);
226
+ return connect(connectOptions);
158
227
  }
159
228
  /**
160
229
  * Disconnects and removes the listener from the Onyx key.
@@ -172,7 +241,10 @@ function connectWithoutView(connectOptions) {
172
241
  * @param connection Connection object returned by calling `Onyx.connect()` or `Onyx.connectWithoutView()`.
173
242
  */
174
243
  function disconnect(connection) {
175
- OnyxConnectionManager_1.default.disconnect(connection);
244
+ if (!connection) {
245
+ return;
246
+ }
247
+ connection.unsubscribe();
176
248
  }
177
249
  /**
178
250
  * Write a value to our store with the given key
@@ -182,7 +254,7 @@ function disconnect(connection) {
182
254
  * @param options optional configuration object
183
255
  */
184
256
  function set(key, value, options) {
185
- return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setWithRetry({ key, value, options }));
257
+ return PendingWrites_1.default.trackPendingWrite(key, OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setWithRetry({ key, value, options })));
186
258
  }
187
259
  /**
188
260
  * Sets multiple keys and values
@@ -192,7 +264,7 @@ function set(key, value, options) {
192
264
  * @param data object keyed by ONYXKEYS and the values to set
193
265
  */
194
266
  function multiSet(data) {
195
- return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.multiSetWithRetry(data));
267
+ return PendingWrites_1.default.trackPendingWrite(Object.keys(data), OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.multiSetWithRetry(data)));
196
268
  }
197
269
  /**
198
270
  * Merge a new value into an existing value at a key.
@@ -211,7 +283,7 @@ function multiSet(data) {
211
283
  * Onyx.merge(ONYXKEYS.POLICY, {name: 'My Workspace'}); // -> {id: 1, name: 'My Workspace'}
212
284
  */
213
285
  function merge(key, changes) {
214
- return OnyxUtils_1.default.afterInit(() => {
286
+ return PendingWrites_1.default.trackPendingWrite(key, OnyxUtils_1.default.afterInit(() => {
215
287
  const skippableCollectionMemberIDs = OnyxUtils_1.default.getSkippableCollectionMemberIDs();
216
288
  if (skippableCollectionMemberIDs.size) {
217
289
  try {
@@ -233,22 +305,30 @@ function merge(key, changes) {
233
305
  if (changes === undefined) {
234
306
  return mergeQueue[key] ? mergeQueuePromise[key] : Promise.resolve();
235
307
  }
236
- // Merge attempts are batched together. The delta should be applied after a single call to get() to prevent a race condition.
237
- // Using the initial value from storage in subsequent merge attempts will lead to an incorrect final merged value.
308
+ // Merge attempts are batched together. The delta should be applied after a single read of the
309
+ // existing value to prevent a race condition. Using the initial value from storage in subsequent
310
+ // merge attempts will lead to an incorrect final merged value.
238
311
  if (mergeQueue[key]) {
239
312
  mergeQueue[key].push(changes);
240
313
  return mergeQueuePromise[key];
241
314
  }
242
- mergeQueue[key] = [changes];
243
- mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((valueFromGet) => {
315
+ const queuedChanges = [changes];
316
+ mergeQueue[key] = queuedChanges;
317
+ // Eager hydration means an uncached key has nothing stored, and a storage round trip would land
318
+ // this write after the same update's cached keys, where subscribers can see a half-applied batch.
319
+ // A running `Onyx.clear` resets keys, so read what it leaves.
320
+ const readMergeBase = () => (OnyxCache_1.default.hasCacheForKey(key) ? OnyxCache_1.default.get(key) : undefined);
321
+ const pendingClear = OnyxCache_1.default.getTaskPromise(OnyxCache_1.TASK.CLEAR);
322
+ mergeQueuePromise[key] = (pendingClear ? pendingClear.then(readMergeBase) : Promise.resolve(readMergeBase())).then((valueFromGet) => {
244
323
  // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
245
- if (mergeQueue[key] == null) {
324
+ if (mergeQueue[key] !== queuedChanges) {
246
325
  return Promise.resolve();
247
326
  }
248
327
  // Other writers (notably Onyx.update's mergeCollection path, which doesn't participate in mergeQueue)
249
- // can land between get() resolving and this callback running. Applying the delta on top of the value
328
+ // can land between the read above and this callback running. Applying the delta on top of the value
250
329
  // captured back then and broadcasting it would overwrite those writes wholesale, so re-read the cache.
251
- const existingValue = OnyxCache_1.default.hasCacheForKey(key) ? OnyxCache_1.default.get(key) : valueFromGet;
330
+ const baseValue = OnyxUtils_1.default.hasStaleMergeRead(queuedChanges) ? undefined : valueFromGet;
331
+ const existingValue = OnyxCache_1.default.hasCacheForKey(key) ? OnyxCache_1.default.get(key) : baseValue;
252
332
  try {
253
333
  const validChanges = mergeQueue[key].filter((change) => {
254
334
  const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
@@ -286,7 +366,7 @@ function merge(key, changes) {
286
366
  }
287
367
  });
288
368
  return mergeQueuePromise[key];
289
- });
369
+ }));
290
370
  }
291
371
  /**
292
372
  * Merges a collection based on their keys.
@@ -302,7 +382,7 @@ function merge(key, changes) {
302
382
  * @param collection Object collection keyed by individual collection member keys and values
303
383
  */
304
384
  function mergeCollection(collectionKey, collection) {
305
- return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection }));
385
+ return PendingWrites_1.default.trackPendingWrite(Object.keys(collection), OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.mergeCollectionWithPatches({ collectionKey, collection })));
306
386
  }
307
387
  /**
308
388
  * Clear out all the data in the store
@@ -326,7 +406,8 @@ function mergeCollection(collectionKey, collection) {
326
406
  * @param keysToPreserve is a list of ONYXKEYS that should not be cleared with the rest of the data
327
407
  */
328
408
  function clear(keysToPreserve = []) {
329
- return OnyxUtils_1.default.afterInit(() => {
409
+ return PendingWrites_1.default.trackPendingGlobalWrite(OnyxUtils_1.default.afterInit(() => {
410
+ const pendingMergeEntries = OnyxUtils_1.default.getPendingMergeEntries(keysToPreserve);
330
411
  const defaultKeyStates = OnyxUtils_1.default.getDefaultKeyStates();
331
412
  const initialKeys = Object.keys(defaultKeyStates);
332
413
  const promise = OnyxUtils_1.default.getAllKeys()
@@ -339,6 +420,7 @@ function clear(keysToPreserve = []) {
339
420
  // because the notification process needs the old values in cache but at that point they will be already removed from it.
340
421
  const keyValuesToResetAsCollection = {};
341
422
  const allKeys = new Set([...cachedKeys, ...initialKeys]);
423
+ OnyxUtils_1.default.cancelPendingMerges(pendingMergeEntries);
342
424
  // The only keys that should not be cleared are:
343
425
  // 1. Anything specifically passed in keysToPreserve (because some keys like language preferences, offline
344
426
  // status, or activeClients need to remain in Onyx even when signed out)
@@ -388,22 +470,21 @@ function clear(keysToPreserve = []) {
388
470
  for (const key of keysToBeClearedFromStorage)
389
471
  OnyxCache_1.default.drop(key);
390
472
  return storage_1.default.removeItems(keysToBeClearedFromStorage)
391
- .then(() => OnyxConnectionManager_1.default.refreshSessionID())
392
473
  .then(() => storage_1.default.multiSet(defaultKeyValuePairs))
393
474
  .then(() => {
394
475
  DevTools_1.default.clearState(keysToPreserve);
395
476
  // Notify the subscribers for each key/value group so they can receive the new values
396
477
  for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
397
- OnyxUtils_1.default.keyChanged(key, value);
478
+ OnyxUtils_1.default.notifyKey(key, value);
398
479
  }
399
480
  for (const [key, value] of Object.entries(keyValuesToResetAsCollection)) {
400
- OnyxUtils_1.default.keysChanged(key, value.newValues, value.oldValues);
481
+ OnyxUtils_1.default.notifyCollection(key, value.newValues, value.oldValues);
401
482
  }
402
483
  });
403
484
  })
404
485
  .then(() => undefined);
405
486
  return OnyxCache_1.default.captureTask(OnyxCache_1.TASK.CLEAR, promise);
406
- });
487
+ }));
407
488
  }
408
489
  /**
409
490
  * Insert API responses and lifecycle data into Onyx
@@ -412,7 +493,17 @@ function clear(keysToPreserve = []) {
412
493
  * @returns resolves when all operations are complete
413
494
  */
414
495
  function update(data) {
415
- return OnyxUtils_1.default.afterInit(() => {
496
+ return PendingWrites_1.default.trackPendingWrite(
497
+ // multiSet/collection items keep their keys in `.value`, not `.key`.
498
+ data.flatMap((updateItem) => {
499
+ var _a;
500
+ if (updateItem.onyxMethod === OnyxUtils_1.default.METHOD.MULTI_SET ||
501
+ updateItem.onyxMethod === OnyxUtils_1.default.METHOD.MERGE_COLLECTION ||
502
+ updateItem.onyxMethod === OnyxUtils_1.default.METHOD.SET_COLLECTION) {
503
+ return Object.keys((_a = updateItem.value) !== null && _a !== void 0 ? _a : {});
504
+ }
505
+ return updateItem.key;
506
+ }), OnyxUtils_1.default.afterInit(() => {
416
507
  // The queue of operations within a single `update` call in the format of <item key - list of operations updating the item>.
417
508
  // This allows us to batch the operations per item and merge them into one operation in the order they were requested.
418
509
  const updateQueue = {};
@@ -536,7 +627,7 @@ function update(data) {
536
627
  // 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
537
628
  const finalPromises = snapshotPromises.concat(promises);
538
629
  return clearPromise.then(() => Promise.all(finalPromises.map((p) => p()))).then(() => undefined);
539
- });
630
+ }));
540
631
  }
541
632
  /**
542
633
  * Sets a collection by replacing all existing collection members with new values.
@@ -552,7 +643,7 @@ function update(data) {
552
643
  * @param collection Object collection keyed by individual collection member keys and values
553
644
  */
554
645
  function setCollection(collectionKey, collection) {
555
- return OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection }));
646
+ return PendingWrites_1.default.trackPendingWrite(Object.keys(collection), OnyxUtils_1.default.afterInit(() => OnyxUtils_1.default.setCollectionWithRetry({ collectionKey, collection })));
556
647
  }
557
648
  /**
558
649
  * Returns persisted Onyx key-value pairs as a plain object.
@@ -1,5 +1,5 @@
1
1
  import type { ValueOf } from 'type-fest';
2
- import type { OnyxKey, OnyxValue } from './types';
2
+ import type { KeyValueMapping, OnyxCollection, OnyxKey, OnyxValue } from './types';
3
3
  declare const TASK: {
4
4
  readonly GET: "get";
5
5
  readonly GET_ALL_KEYS: "getAllKeys";
@@ -151,7 +151,7 @@ declare class OnyxCache {
151
151
  * Returns a frozen snapshot with structural sharing — safe to return by reference.
152
152
  * Lazily rebuilds the snapshot if the collection was modified since the last read.
153
153
  */
154
- getCollectionData(collectionKey: OnyxKey): Record<OnyxKey, OnyxValue<OnyxKey>> | undefined;
154
+ getCollectionData(collectionKey: OnyxKey): OnyxCollection<KeyValueMapping[OnyxKey]> | undefined;
155
155
  }
156
156
  declare const instance: OnyxCache;
157
157
  export default instance;
package/dist/OnyxCache.js CHANGED
@@ -437,18 +437,13 @@ class OnyxCache {
437
437
  this.dirtyCollections.delete(collectionKey);
438
438
  }
439
439
  const snapshot = this.collectionSnapshots.get(collectionKey);
440
- // We never stored anything for this collection key.
440
+ // Returning `undefined` for this collection key means init hasn't seeded it yet (pre-load), so there's
441
+ // nothing to return. `setCollectionKeys()` (called inside `Onyx.init`) seeds every
442
+ // known collection with a frozen empty entry, so the presence of an entry is the reliable
443
+ // post-init "loaded" signal.
441
444
  if (snapshot === undefined) {
442
445
  return undefined;
443
446
  }
444
- // The collection is empty (it holds our shared empty object). But "empty" is ambiguous
445
- // during startup: we can't tell an actually-empty collection apart from one whose data
446
- // hasn't loaded yet. Once any key exists, we know setAllKeys has run and loaded everything,
447
- // so an empty collection really is empty. Before that, return undefined so subscribers
448
- // don't briefly see a collection as empty when it just hasn't loaded.
449
- if (snapshot === FROZEN_EMPTY_COLLECTION) {
450
- return this.storageKeys.size > 0 ? FROZEN_EMPTY_COLLECTION : undefined;
451
- }
452
447
  return snapshot;
453
448
  }
454
449
  }