react-native-onyx 3.0.100 → 3.0.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/Onyx.js CHANGED
@@ -103,7 +103,7 @@ function init({ keys = {}, initialKeyStates = {}, evictableKeys = [], shouldSync
103
103
  OnyxUtils_1.default.initStoreValues(keys, initialKeyStates, evictableKeys);
104
104
  // Initialize all of our keys with data provided then give green light to any pending connections.
105
105
  // addEvictableKeysToRecentlyAccessedList must run after initializeWithDefaultKeyStates because
106
- // eager cache loading populates the key index (cache.setAllKeys) inside initializeWithDefaultKeyStates,
106
+ // eager cache loading populates the key index (cache.hydrate) inside initializeWithDefaultKeyStates,
107
107
  // and the evictable keys list depends on that index being populated.
108
108
  OnyxUtils_1.default.initializeWithDefaultKeyStates()
109
109
  .then(() => OnyxCache_1.default.addEvictableKeysToRecentlyAccessedList(OnyxKeys_1.default.isCollectionKey, OnyxUtils_1.default.getAllKeys))
@@ -66,6 +66,13 @@ declare class OnyxCache {
66
66
  set(key: OnyxKey, value: OnyxValue<OnyxKey>): OnyxValue<OnyxKey>;
67
67
  /** Forget the cached value for the given key */
68
68
  drop(key: OnyxKey): void;
69
+ /**
70
+ * Bulk-loads values into a cache that's expected to be empty, skipping merge()'s per-key clone when
71
+ * safe. Falls back to a real merge for any key that already has a value, in case the cache wasn't
72
+ * empty after all. Used only by `Onyx.init()`.
73
+ * @param data - a map of (cache) key - values
74
+ */
75
+ hydrate(data: Record<OnyxKey, OnyxValue<OnyxKey>>): void;
69
76
  /**
70
77
  * Deep merge data to cache, any non existing keys will be created
71
78
  * @param data - a map of (cache) key - values
package/dist/OnyxCache.js CHANGED
@@ -14,6 +14,15 @@ const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
14
14
  * which relies on === equality to detect changes.
15
15
  */
16
16
  const FROZEN_EMPTY_COLLECTION = Object.freeze({});
17
+ /**
18
+ * Merge options shared by every cache write path (`merge()` and `hydrate()`'s fallback), so the
19
+ * three call sites can't drift apart. Cached values must never hold nested nulls, and a source
20
+ * object carrying the replace mark must replace the target object rather than merge into it.
21
+ */
22
+ const CACHE_MERGE_OPTIONS = {
23
+ shouldRemoveNestedNulls: true,
24
+ objectRemovalMode: 'replace',
25
+ };
17
26
  // Task constants
18
27
  const TASK = {
19
28
  GET: 'get',
@@ -38,7 +47,7 @@ class OnyxCache {
38
47
  this.collectionSnapshots = new Map();
39
48
  this.dirtyCollections = new Set();
40
49
  // bind all public methods to prevent problems with `this`
41
- (0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'hasValueChanged', 'getCollectionData');
50
+ (0, bindAll_1.default)(this, 'getAllKeys', 'get', 'hasCacheForKey', 'addKey', 'addNullishStorageKey', 'hasNullishStorageKey', 'clearNullishStorageKeys', 'set', 'drop', 'merge', 'hydrate', 'hasPendingTask', 'getTaskPromise', 'captureTask', 'setAllKeys', 'setEvictionAllowList', 'isEvictableKey', 'removeLastAccessedKey', 'addLastAccessedKey', 'addEvictableKeysToRecentlyAccessedList', 'getKeyForEviction', 'setCollectionKeys', 'hasValueChanged', 'getCollectionData');
42
51
  }
43
52
  /** Get all the storage keys */
44
53
  getAllKeys() {
@@ -126,6 +135,69 @@ class OnyxCache {
126
135
  this.storageKeys.delete(key);
127
136
  OnyxKeys_1.default.deregisterMemberKey(key);
128
137
  }
138
+ /**
139
+ * Bulk-loads values into a cache that's expected to be empty, skipping merge()'s per-key clone when
140
+ * safe. Falls back to a real merge for any key that already has a value, in case the cache wasn't
141
+ * empty after all. Used only by `Onyx.init()`.
142
+ * @param data - a map of (cache) key - values
143
+ */
144
+ hydrate(data) {
145
+ if (data === null || typeof data !== 'object' || Array.isArray(data)) {
146
+ throw new Error('data passed to cache.hydrate() must be an Object of onyx key/value pairs');
147
+ }
148
+ const affectedCollections = new Set();
149
+ // Use for-in loop to avoid an unnecessary array allocation from Object.keys()
150
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
151
+ for (const key in data) {
152
+ const value = data[key];
153
+ this.addKey(key);
154
+ if (value === undefined) {
155
+ this.addNullishStorageKey(key);
156
+ // undefined means "no change" — skip storageMap modification
157
+ continue;
158
+ }
159
+ const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
160
+ if (value === null) {
161
+ this.addNullishStorageKey(key);
162
+ delete this.storageMap[key];
163
+ if (collectionKey) {
164
+ affectedCollections.add(collectionKey);
165
+ }
166
+ }
167
+ else {
168
+ this.nullishStorageKeys.delete(key);
169
+ const existing = this.storageMap[key];
170
+ if (existing !== undefined) {
171
+ // Key already has a value, so the empty-cache assumption doesn't hold here (e.g. a write
172
+ // landed while storage was still being read). Fall back to a real merge, which has exactly
173
+ // the same semantics as the old `cache.merge(allDataFromStorage)` init path: the value
174
+ // loaded from disk is the merge source, so it wins on any overlapping leaf key.
175
+ // Note: this only covers non-null writes. A `cache.set(key, null)` that lands before hydrate
176
+ // clears the nullish marker too, so a deletion racing init is still undone - same as before.
177
+ const merged = utils_1.default.fastMerge(existing, value, CACHE_MERGE_OPTIONS).result;
178
+ // fastMerge is reference-stable: returns the original target when
179
+ // nothing changed, so a simple === check detects no-ops.
180
+ if (merged === existing) {
181
+ continue;
182
+ }
183
+ this.storageMap[key] = merged;
184
+ }
185
+ else if (utils_1.default.needsNormalization(value)) {
186
+ this.storageMap[key] = utils_1.default.fastMerge(undefined, value, CACHE_MERGE_OPTIONS).result;
187
+ }
188
+ else {
189
+ this.storageMap[key] = value;
190
+ }
191
+ if (collectionKey) {
192
+ affectedCollections.add(collectionKey);
193
+ }
194
+ }
195
+ }
196
+ // Mark affected collections as dirty — snapshots will be lazily rebuilt on next read
197
+ for (const collectionKey of affectedCollections) {
198
+ this.dirtyCollections.add(collectionKey);
199
+ }
200
+ }
129
201
  /**
130
202
  * Deep merge data to cache, any non existing keys will be created
131
203
  * @param data - a map of (cache) key - values
@@ -135,7 +207,10 @@ class OnyxCache {
135
207
  throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
136
208
  }
137
209
  const affectedCollections = new Set();
138
- for (const [key, value] of Object.entries(data)) {
210
+ // Use for-in loop to avoid an unnecessary array allocation from Object.entries()
211
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
212
+ for (const key in data) {
213
+ const value = data[key];
139
214
  this.addKey(key);
140
215
  const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
141
216
  if (value === undefined) {
@@ -154,10 +229,7 @@ class OnyxCache {
154
229
  this.nullishStorageKeys.delete(key);
155
230
  // Per-key merge instead of spreading the entire storageMap
156
231
  const existing = this.storageMap[key];
157
- const merged = utils_1.default.fastMerge(existing, value, {
158
- shouldRemoveNestedNulls: true,
159
- objectRemovalMode: 'replace',
160
- }).result;
232
+ const merged = utils_1.default.fastMerge(existing, value, CACHE_MERGE_OPTIONS).result;
161
233
  // fastMerge is reference-stable: returns the original target when
162
234
  // nothing changed, so a simple === check detects no-ops.
163
235
  if (merged === existing) {
package/dist/OnyxUtils.js CHANGED
@@ -858,9 +858,12 @@ function initializeWithDefaultKeyStates() {
858
858
  }
859
859
  allDataFromStorage[key] = value;
860
860
  }
861
- // Load all storage data into cache silently (no subscriber notifications)
862
- OnyxCache_1.default.setAllKeys(Object.keys(allDataFromStorage));
863
- OnyxCache_1.default.merge(allDataFromStorage);
861
+ // Load all storage data into cache silently (no subscriber notifications).
862
+ // hydrate() rather than merge(): the cache is empty at this point, so a per-key fastMerge
863
+ // would only deep-clone every row it was handed.
864
+ // No setAllKeys() call is needed: hydrate() calls addKey() for every key, which populates the
865
+ // key index and registers collection member keys itself.
866
+ OnyxCache_1.default.hydrate(allDataFromStorage);
864
867
  // For keys that have a developer-defined default (via `initialKeyStates`), merge the
865
868
  // persisted value with the default so new properties added in code updates are applied
866
869
  // without wiping user data that already exists in storage.
package/dist/utils.d.ts CHANGED
@@ -37,6 +37,11 @@ type FastMergeResult<TValue> = {
37
37
  declare function fastMerge<TValue>(target: TValue, source: TValue, options?: FastMergeOptions, metadata?: FastMergeMetadata, basePath?: string[]): FastMergeResult<TValue>;
38
38
  /** Checks whether the given object is an object and not null/undefined. */
39
39
  declare function isEmptyObject<T>(obj: T | EmptyValue): obj is EmptyValue;
40
+ /**
41
+ * Reports whether a value needs cleaning (nested null/undefined, or the replace-object mark) before it's
42
+ * safe to store by reference. Read-only, non-allocating.
43
+ */
44
+ declare function needsNormalization(value: unknown): boolean;
40
45
  /** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
41
46
  declare function removeNestedNullValues<TValue extends OnyxInput<OnyxKey> | null>(value: TValue): TValue;
42
47
  /** Formats the action name by uppercasing and adding the key if provided. */
@@ -73,6 +78,7 @@ declare const _default: {
73
78
  isEmptyObject: typeof isEmptyObject;
74
79
  formatActionName: typeof formatActionName;
75
80
  removeNestedNullValues: typeof removeNestedNullValues;
81
+ needsNormalization: typeof needsNormalization;
76
82
  checkCompatibilityWithExistingValue: typeof checkCompatibilityWithExistingValue;
77
83
  pick: typeof pick;
78
84
  omit: typeof omit;
package/dist/utils.js CHANGED
@@ -129,6 +129,30 @@ function isMergeableObject(value) {
129
129
  const isNonNullObject = value != null ? typeof value === 'object' : false;
130
130
  return isNonNullObject && !(value instanceof RegExp) && !(value instanceof Date) && !Array.isArray(value);
131
131
  }
132
+ /**
133
+ * Reports whether a value needs cleaning (nested null/undefined, or the replace-object mark) before it's
134
+ * safe to store by reference. Read-only, non-allocating.
135
+ */
136
+ function needsNormalization(value) {
137
+ if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
138
+ return false;
139
+ }
140
+ // Use for-in loop to avoid an unnecessary array allocation from Object.keys()
141
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
142
+ for (const key in value) {
143
+ if (key === ONYX_INTERNALS__REPLACE_OBJECT_MARK) {
144
+ return true;
145
+ }
146
+ const propertyValue = value[key];
147
+ if (propertyValue === null || propertyValue === undefined) {
148
+ return true;
149
+ }
150
+ if (typeof propertyValue === 'object' && !Array.isArray(propertyValue) && needsNormalization(propertyValue)) {
151
+ return true;
152
+ }
153
+ }
154
+ return false;
155
+ }
132
156
  /** Deep removes the nested null values from the given value. Returns the original reference if no nulls were found. */
133
157
  function removeNestedNullValues(value) {
134
158
  if (value === null || value === undefined || typeof value !== 'object' || Array.isArray(value)) {
@@ -259,6 +283,7 @@ exports.default = {
259
283
  isEmptyObject,
260
284
  formatActionName,
261
285
  removeNestedNullValues,
286
+ needsNormalization,
262
287
  checkCompatibilityWithExistingValue,
263
288
  pick,
264
289
  omit,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.100",
3
+ "version": "3.0.101",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",