react-native-onyx 3.0.65 → 3.0.66
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/OnyxCache.d.ts +17 -4
- package/dist/OnyxCache.js +125 -39
- package/dist/utils.js +11 -1
- package/package.json +1 -1
package/dist/OnyxCache.d.ts
CHANGED
|
@@ -17,8 +17,6 @@ declare class OnyxCache {
|
|
|
17
17
|
private nullishStorageKeys;
|
|
18
18
|
/** A map of cached values */
|
|
19
19
|
private storageMap;
|
|
20
|
-
/** Cache of complete collection data objects for O(1) retrieval */
|
|
21
|
-
private collectionData;
|
|
22
20
|
/**
|
|
23
21
|
* Captured pending tasks for already running storage methods
|
|
24
22
|
* Using a map yields better performance on operations such a delete
|
|
@@ -28,6 +26,10 @@ declare class OnyxCache {
|
|
|
28
26
|
private evictionAllowList;
|
|
29
27
|
/** List of keys that have been directly subscribed to or recently modified from least to most recent */
|
|
30
28
|
private recentlyAccessedKeys;
|
|
29
|
+
/** Frozen collection snapshots for structural sharing */
|
|
30
|
+
private collectionSnapshots;
|
|
31
|
+
/** Collections whose snapshots need rebuilding (lazy — rebuilt on next read) */
|
|
32
|
+
private dirtyCollections;
|
|
31
33
|
constructor();
|
|
32
34
|
/** Get all the storage keys */
|
|
33
35
|
getAllKeys(): Set<OnyxKey>;
|
|
@@ -87,7 +89,7 @@ declare class OnyxCache {
|
|
|
87
89
|
* @param taskName - unique name for the task
|
|
88
90
|
*/
|
|
89
91
|
captureTask(taskName: CacheTask, promise: Promise<OnyxValue<OnyxKey>>): Promise<OnyxValue<OnyxKey>>;
|
|
90
|
-
/** Check if the value has changed */
|
|
92
|
+
/** Check if the value has changed. Uses reference equality as a fast path, falls back to deep equality. */
|
|
91
93
|
hasValueChanged(key: OnyxKey, value: OnyxValue<OnyxKey>): boolean;
|
|
92
94
|
/**
|
|
93
95
|
* Sets the list of keys that are considered safe for eviction
|
|
@@ -127,7 +129,18 @@ declare class OnyxCache {
|
|
|
127
129
|
*/
|
|
128
130
|
setCollectionKeys(collectionKeys: Set<OnyxKey>): void;
|
|
129
131
|
/**
|
|
130
|
-
*
|
|
132
|
+
* Rebuilds the frozen collection snapshot from current storageMap references.
|
|
133
|
+
* Uses the indexed collection->members map for O(collectionMembers) instead of O(totalKeys).
|
|
134
|
+
* Returns the previous snapshot reference when all member references are identical,
|
|
135
|
+
* preventing unnecessary re-renders in useSyncExternalStore.
|
|
136
|
+
*
|
|
137
|
+
* @param collectionKey - The collection key to rebuild
|
|
138
|
+
*/
|
|
139
|
+
private rebuildCollectionSnapshot;
|
|
140
|
+
/**
|
|
141
|
+
* Get all data for a collection key.
|
|
142
|
+
* Returns a frozen snapshot with structural sharing — safe to return by reference.
|
|
143
|
+
* Lazily rebuilds the snapshot if the collection was modified since the last read.
|
|
131
144
|
*/
|
|
132
145
|
getCollectionData(collectionKey: OnyxKey): Record<OnyxKey, OnyxValue<OnyxKey>> | undefined;
|
|
133
146
|
}
|
package/dist/OnyxCache.js
CHANGED
|
@@ -8,6 +8,12 @@ const fast_equals_1 = require("fast-equals");
|
|
|
8
8
|
const bindAll_1 = __importDefault(require("lodash/bindAll"));
|
|
9
9
|
const utils_1 = __importDefault(require("./utils"));
|
|
10
10
|
const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
|
|
11
|
+
/**
|
|
12
|
+
* Stable frozen empty object used as the canonical value for empty collections.
|
|
13
|
+
* Returning the same reference avoids unnecessary re-renders in useSyncExternalStore,
|
|
14
|
+
* which relies on === equality to detect changes.
|
|
15
|
+
*/
|
|
16
|
+
const FROZEN_EMPTY_COLLECTION = Object.freeze({});
|
|
11
17
|
// Task constants
|
|
12
18
|
const TASK = {
|
|
13
19
|
GET: 'get',
|
|
@@ -28,10 +34,11 @@ class OnyxCache {
|
|
|
28
34
|
this.storageKeys = new Set();
|
|
29
35
|
this.nullishStorageKeys = new Set();
|
|
30
36
|
this.storageMap = {};
|
|
31
|
-
this.collectionData = {};
|
|
32
37
|
this.pendingPromises = new Map();
|
|
38
|
+
this.collectionSnapshots = new Map();
|
|
39
|
+
this.dirtyCollections = new Set();
|
|
33
40
|
// bind all public methods to prevent problems with `this`
|
|
34
|
-
(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', '
|
|
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');
|
|
35
42
|
}
|
|
36
43
|
/** Get all the storage keys */
|
|
37
44
|
getAllKeys() {
|
|
@@ -91,35 +98,30 @@ class OnyxCache {
|
|
|
91
98
|
// since it will either be set to a non nullish value or removed from the cache completely.
|
|
92
99
|
this.nullishStorageKeys.delete(key);
|
|
93
100
|
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
101
|
+
const oldValue = this.storageMap[key];
|
|
94
102
|
if (value === null || value === undefined) {
|
|
95
103
|
delete this.storageMap[key];
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
delete this.collectionData[collectionKey][key];
|
|
104
|
+
if (collectionKey && oldValue !== undefined) {
|
|
105
|
+
this.dirtyCollections.add(collectionKey);
|
|
99
106
|
}
|
|
100
107
|
return undefined;
|
|
101
108
|
}
|
|
102
109
|
this.storageMap[key] = value;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!this.collectionData[collectionKey]) {
|
|
106
|
-
this.collectionData[collectionKey] = {};
|
|
107
|
-
}
|
|
108
|
-
this.collectionData[collectionKey][key] = value;
|
|
110
|
+
if (collectionKey && oldValue !== value) {
|
|
111
|
+
this.dirtyCollections.add(collectionKey);
|
|
109
112
|
}
|
|
110
113
|
return value;
|
|
111
114
|
}
|
|
112
115
|
/** Forget the cached value for the given key */
|
|
113
116
|
drop(key) {
|
|
114
117
|
delete this.storageMap[key];
|
|
115
|
-
// Remove from collection data cache if this is a collection member
|
|
116
118
|
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
117
|
-
if (collectionKey
|
|
118
|
-
|
|
119
|
+
if (collectionKey) {
|
|
120
|
+
this.dirtyCollections.add(collectionKey);
|
|
119
121
|
}
|
|
120
|
-
// If this is a collection key, clear its
|
|
122
|
+
// If this is a collection key, clear its snapshot
|
|
121
123
|
if (OnyxKeys_1.default.isCollectionKey(key)) {
|
|
122
|
-
|
|
124
|
+
this.collectionSnapshots.delete(key);
|
|
123
125
|
}
|
|
124
126
|
this.storageKeys.delete(key);
|
|
125
127
|
OnyxKeys_1.default.deregisterMemberKey(key);
|
|
@@ -132,31 +134,45 @@ class OnyxCache {
|
|
|
132
134
|
if (typeof data !== 'object' || Array.isArray(data)) {
|
|
133
135
|
throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
|
|
134
136
|
}
|
|
135
|
-
|
|
136
|
-
shouldRemoveNestedNulls: true,
|
|
137
|
-
objectRemovalMode: 'replace',
|
|
138
|
-
}).result);
|
|
137
|
+
const affectedCollections = new Set();
|
|
139
138
|
for (const [key, value] of Object.entries(data)) {
|
|
140
139
|
this.addKey(key);
|
|
141
140
|
const collectionKey = OnyxKeys_1.default.getCollectionKey(key);
|
|
142
|
-
if (value ===
|
|
141
|
+
if (value === undefined) {
|
|
142
|
+
this.addNullishStorageKey(key);
|
|
143
|
+
// undefined means "no change" — skip storageMap modification
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (value === null) {
|
|
143
147
|
this.addNullishStorageKey(key);
|
|
144
|
-
|
|
145
|
-
if (collectionKey
|
|
146
|
-
|
|
148
|
+
delete this.storageMap[key];
|
|
149
|
+
if (collectionKey) {
|
|
150
|
+
affectedCollections.add(collectionKey);
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
else {
|
|
150
154
|
this.nullishStorageKeys.delete(key);
|
|
151
|
-
//
|
|
155
|
+
// Per-key merge instead of spreading the entire storageMap
|
|
156
|
+
const existing = this.storageMap[key];
|
|
157
|
+
const merged = utils_1.default.fastMerge(existing, value, {
|
|
158
|
+
shouldRemoveNestedNulls: true,
|
|
159
|
+
objectRemovalMode: 'replace',
|
|
160
|
+
}).result;
|
|
161
|
+
// fastMerge is reference-stable: returns the original target when
|
|
162
|
+
// nothing changed, so a simple === check detects no-ops.
|
|
163
|
+
if (merged === existing) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
this.storageMap[key] = merged;
|
|
152
167
|
if (collectionKey) {
|
|
153
|
-
|
|
154
|
-
this.collectionData[collectionKey] = {};
|
|
155
|
-
}
|
|
156
|
-
this.collectionData[collectionKey][key] = this.storageMap[key];
|
|
168
|
+
affectedCollections.add(collectionKey);
|
|
157
169
|
}
|
|
158
170
|
}
|
|
159
171
|
}
|
|
172
|
+
// Mark affected collections as dirty — snapshots will be lazily rebuilt on next read
|
|
173
|
+
for (const collectionKey of affectedCollections) {
|
|
174
|
+
this.dirtyCollections.add(collectionKey);
|
|
175
|
+
}
|
|
160
176
|
}
|
|
161
177
|
/**
|
|
162
178
|
* Check whether the given task is already running
|
|
@@ -186,9 +202,12 @@ class OnyxCache {
|
|
|
186
202
|
this.pendingPromises.set(taskName, returnPromise);
|
|
187
203
|
return returnPromise;
|
|
188
204
|
}
|
|
189
|
-
/** Check if the value has changed */
|
|
205
|
+
/** Check if the value has changed. Uses reference equality as a fast path, falls back to deep equality. */
|
|
190
206
|
hasValueChanged(key, value) {
|
|
191
|
-
const currentValue = this.
|
|
207
|
+
const currentValue = this.storageMap[key];
|
|
208
|
+
if (currentValue === value) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
192
211
|
return !(0, fast_equals_1.deepEqual)(currentValue, value);
|
|
193
212
|
}
|
|
194
213
|
/**
|
|
@@ -257,24 +276,91 @@ class OnyxCache {
|
|
|
257
276
|
*/
|
|
258
277
|
setCollectionKeys(collectionKeys) {
|
|
259
278
|
OnyxKeys_1.default.setCollectionKeys(collectionKeys);
|
|
260
|
-
// Initialize
|
|
279
|
+
// Initialize frozen snapshots for collection keys
|
|
261
280
|
for (const collectionKey of collectionKeys) {
|
|
262
|
-
if (this.
|
|
281
|
+
if (!this.collectionSnapshots.has(collectionKey)) {
|
|
282
|
+
this.collectionSnapshots.set(collectionKey, Object.freeze({}));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Rebuilds the frozen collection snapshot from current storageMap references.
|
|
288
|
+
* Uses the indexed collection->members map for O(collectionMembers) instead of O(totalKeys).
|
|
289
|
+
* Returns the previous snapshot reference when all member references are identical,
|
|
290
|
+
* preventing unnecessary re-renders in useSyncExternalStore.
|
|
291
|
+
*
|
|
292
|
+
* @param collectionKey - The collection key to rebuild
|
|
293
|
+
*/
|
|
294
|
+
rebuildCollectionSnapshot(collectionKey) {
|
|
295
|
+
const previousSnapshot = this.collectionSnapshots.get(collectionKey);
|
|
296
|
+
const members = {};
|
|
297
|
+
let hasMemberChanges = false;
|
|
298
|
+
// Use the indexed forward lookup for O(collectionMembers) iteration.
|
|
299
|
+
// Falls back to scanning all storageKeys if the index isn't populated yet.
|
|
300
|
+
const memberKeys = OnyxKeys_1.default.getMembersOfCollection(collectionKey);
|
|
301
|
+
const keysToScan = memberKeys !== null && memberKeys !== void 0 ? memberKeys : this.storageKeys;
|
|
302
|
+
const needsPrefixCheck = !memberKeys;
|
|
303
|
+
for (const key of keysToScan) {
|
|
304
|
+
// When using the fallback path (scanning all storageKeys instead of the indexed
|
|
305
|
+
// forward lookup), skip keys that don't belong to this collection.
|
|
306
|
+
if (needsPrefixCheck && OnyxKeys_1.default.getCollectionKey(key) !== collectionKey) {
|
|
263
307
|
continue;
|
|
264
308
|
}
|
|
265
|
-
this.
|
|
309
|
+
const val = this.storageMap[key];
|
|
310
|
+
// Skip null/undefined values — they represent deleted or unset keys
|
|
311
|
+
// and should not be included in the frozen collection snapshot.
|
|
312
|
+
if (val !== undefined && val !== null) {
|
|
313
|
+
members[key] = val;
|
|
314
|
+
// Check if this member's reference changed from the old snapshot
|
|
315
|
+
if (!hasMemberChanges && (!previousSnapshot || previousSnapshot[key] !== val)) {
|
|
316
|
+
hasMemberChanges = true;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
266
319
|
}
|
|
320
|
+
// Check if any members were removed from the previous snapshot.
|
|
321
|
+
// We can't rely on count comparison alone — if one key is removed and another added,
|
|
322
|
+
// the counts match but the snapshot content is different.
|
|
323
|
+
if (!hasMemberChanges && previousSnapshot) {
|
|
324
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
325
|
+
for (const key in previousSnapshot) {
|
|
326
|
+
if (!(key in members)) {
|
|
327
|
+
hasMemberChanges = true;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// If nothing actually changed, reuse the old snapshot reference.
|
|
333
|
+
// This is critical: useSyncExternalStore uses === to detect changes,
|
|
334
|
+
// so returning the same reference prevents unnecessary re-renders.
|
|
335
|
+
if (!hasMemberChanges && previousSnapshot) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
Object.freeze(members);
|
|
339
|
+
this.collectionSnapshots.set(collectionKey, members);
|
|
267
340
|
}
|
|
268
341
|
/**
|
|
269
|
-
* Get all data for a collection key
|
|
342
|
+
* Get all data for a collection key.
|
|
343
|
+
* Returns a frozen snapshot with structural sharing — safe to return by reference.
|
|
344
|
+
* Lazily rebuilds the snapshot if the collection was modified since the last read.
|
|
270
345
|
*/
|
|
271
346
|
getCollectionData(collectionKey) {
|
|
272
|
-
|
|
273
|
-
|
|
347
|
+
if (this.dirtyCollections.has(collectionKey)) {
|
|
348
|
+
this.rebuildCollectionSnapshot(collectionKey);
|
|
349
|
+
this.dirtyCollections.delete(collectionKey);
|
|
350
|
+
}
|
|
351
|
+
const snapshot = this.collectionSnapshots.get(collectionKey);
|
|
352
|
+
if (utils_1.default.isEmptyObject(snapshot)) {
|
|
353
|
+
// We check storageKeys.size (not collection-specific keys) to distinguish
|
|
354
|
+
// "init complete, this collection is genuinely empty" from "init not done yet."
|
|
355
|
+
// During init, setAllKeys loads ALL keys at once — so if any key exists,
|
|
356
|
+
// the full storage picture is loaded and an empty collection is truly empty.
|
|
357
|
+
// Returning undefined before init prevents subscribers from seeing a false empty state.
|
|
358
|
+
if (this.storageKeys.size > 0) {
|
|
359
|
+
return FROZEN_EMPTY_COLLECTION;
|
|
360
|
+
}
|
|
274
361
|
return undefined;
|
|
275
362
|
}
|
|
276
|
-
|
|
277
|
-
return Object.assign({}, cachedCollection);
|
|
363
|
+
return snapshot;
|
|
278
364
|
}
|
|
279
365
|
}
|
|
280
366
|
const instance = new OnyxCache();
|
package/dist/utils.js
CHANGED
|
@@ -109,7 +109,17 @@ function mergeObject(target, source, options, metadata, basePath) {
|
|
|
109
109
|
}
|
|
110
110
|
/** Checks whether the given object is an object and not null/undefined. */
|
|
111
111
|
function isEmptyObject(obj) {
|
|
112
|
-
|
|
112
|
+
if (typeof obj !== 'object') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
// Use for-in loop to avoid an unnecessary array allocation from Object.keys()
|
|
116
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
117
|
+
for (const key in obj) {
|
|
118
|
+
if (Object.hasOwn(obj, key)) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return true;
|
|
113
123
|
}
|
|
114
124
|
/**
|
|
115
125
|
* Checks whether the given value can be merged. It has to be an object, but not an array, RegExp or Date.
|