react-native-onyx 2.0.21 → 2.0.23
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 +49 -64
- package/dist/DevTools.js +0 -1
- package/dist/Onyx.d.ts +49 -258
- package/dist/Onyx.js +192 -1165
- package/dist/OnyxCache.d.ts +14 -15
- package/dist/OnyxUtils.d.ts +320 -0
- package/dist/OnyxUtils.js +1061 -0
- package/dist/PerformanceUtils.d.ts +3 -5
- package/dist/index.d.ts +3 -2
- package/dist/storage/InstanceSync/index.d.ts +14 -0
- package/dist/storage/InstanceSync/index.js +20 -0
- package/dist/storage/InstanceSync/index.web.d.ts +27 -0
- package/dist/storage/InstanceSync/index.web.js +59 -0
- package/dist/storage/__mocks__/index.d.ts +15 -13
- package/dist/storage/__mocks__/index.js +43 -81
- package/dist/storage/index.d.ts +6 -2
- package/dist/storage/index.js +170 -2
- package/dist/storage/platforms/index.d.ts +2 -0
- package/dist/storage/{NativeStorage.js → platforms/index.js} +2 -2
- package/dist/storage/platforms/index.native.d.ts +2 -0
- package/dist/storage/{index.native.js → platforms/index.native.js} +2 -2
- package/dist/storage/providers/{IDBKeyVal.js → IDBKeyValProvider.js} +23 -19
- package/dist/storage/providers/MemoryOnlyProvider.d.ts +9 -0
- package/dist/storage/providers/MemoryOnlyProvider.js +124 -0
- package/dist/storage/providers/NoopProvider.js +85 -0
- package/dist/storage/providers/SQLiteProvider.d.ts +3 -0
- package/dist/storage/providers/{SQLiteStorage.js → SQLiteProvider.js} +17 -11
- package/dist/storage/providers/types.d.ts +17 -14
- package/dist/types.d.ts +128 -55
- package/dist/types.js +2 -0
- package/dist/useOnyx.js +11 -10
- package/dist/utils.d.ts +2 -2
- package/dist/utils.js +29 -16
- package/dist/withOnyx.js +6 -5
- package/package.json +1 -1
- package/dist/storage/NativeStorage.d.ts +0 -2
- package/dist/storage/WebStorage.d.ts +0 -3
- package/dist/storage/WebStorage.js +0 -62
- package/dist/storage/index.native.d.ts +0 -2
- /package/dist/storage/providers/{IDBKeyVal.d.ts → IDBKeyValProvider.d.ts} +0 -0
- /package/dist/storage/providers/{SQLiteStorage.d.ts → NoopProvider.d.ts} +0 -0
|
@@ -0,0 +1,1061 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
/* eslint-disable no-continue */
|
|
30
|
+
const fast_equals_1 = require("fast-equals");
|
|
31
|
+
const underscore_1 = __importDefault(require("underscore"));
|
|
32
|
+
const Logger = __importStar(require("./Logger"));
|
|
33
|
+
const OnyxCache_1 = __importDefault(require("./OnyxCache"));
|
|
34
|
+
const Str = __importStar(require("./Str"));
|
|
35
|
+
const PerformanceUtils = __importStar(require("./PerformanceUtils"));
|
|
36
|
+
const storage_1 = __importDefault(require("./storage"));
|
|
37
|
+
const utils_1 = __importDefault(require("./utils"));
|
|
38
|
+
const batch_1 = __importDefault(require("./batch"));
|
|
39
|
+
const DevTools_1 = __importDefault(require("./DevTools"));
|
|
40
|
+
// Method constants
|
|
41
|
+
const METHOD = {
|
|
42
|
+
SET: 'set',
|
|
43
|
+
MERGE: 'merge',
|
|
44
|
+
MERGE_COLLECTION: 'mergecollection',
|
|
45
|
+
MULTI_SET: 'multiset',
|
|
46
|
+
CLEAR: 'clear',
|
|
47
|
+
};
|
|
48
|
+
// Key/value store of Onyx key and arrays of values to merge
|
|
49
|
+
const mergeQueue = {};
|
|
50
|
+
const mergeQueuePromise = {};
|
|
51
|
+
// Holds a mapping of all the react components that want their state subscribed to a store key
|
|
52
|
+
const callbackToStateMapping = {};
|
|
53
|
+
// Keeps a copy of the values of the onyx collection keys as a map for faster lookups
|
|
54
|
+
let onyxCollectionKeyMap = new Map();
|
|
55
|
+
// Holds a list of keys that have been directly subscribed to or recently modified from least to most recent
|
|
56
|
+
let recentlyAccessedKeys = [];
|
|
57
|
+
// Holds a list of keys that are safe to remove when we reach max storage. If a key does not match with
|
|
58
|
+
// whatever appears in this list it will NEVER be a candidate for eviction.
|
|
59
|
+
let evictionAllowList = [];
|
|
60
|
+
// Holds a map of keys and connectionID arrays whose keys will never be automatically evicted as
|
|
61
|
+
// long as we have at least one subscriber that returns false for the canEvict property.
|
|
62
|
+
const evictionBlocklist = {};
|
|
63
|
+
// Optional user-provided key value states set when Onyx initializes or clears
|
|
64
|
+
let defaultKeyStates = {};
|
|
65
|
+
let batchUpdatesPromise = null;
|
|
66
|
+
let batchUpdatesQueue = [];
|
|
67
|
+
/**
|
|
68
|
+
* Getter - returns the merge queue.
|
|
69
|
+
*
|
|
70
|
+
* @returns {Object} The callback to state mapping.
|
|
71
|
+
*/
|
|
72
|
+
function getMergeQueue() {
|
|
73
|
+
return mergeQueue;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Getter - returns the merge queue promise.
|
|
77
|
+
*
|
|
78
|
+
* @returns {Object} The callback to state mapping.
|
|
79
|
+
*/
|
|
80
|
+
function getMergeQueuePromise() {
|
|
81
|
+
return mergeQueuePromise;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Getter - returns the callback to state mapping.
|
|
85
|
+
*
|
|
86
|
+
* @returns {Object} The callback to state mapping.
|
|
87
|
+
*/
|
|
88
|
+
function getCallbackToStateMapping() {
|
|
89
|
+
return callbackToStateMapping;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Getter - returns the default key states.
|
|
93
|
+
*
|
|
94
|
+
* @returns {Object} The callback to state mapping.
|
|
95
|
+
*/
|
|
96
|
+
function getDefaultKeyStates() {
|
|
97
|
+
return defaultKeyStates;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Sets the initial values for the Onyx store
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} keys - `ONYXKEYS` constants object from Onyx.init()
|
|
103
|
+
* @param {Object} initialKeyStates - initial data to set when `init()` and `clear()` are called
|
|
104
|
+
* @param {Array<String>} safeEvictionKeys - This is an array of keys (individual or collection patterns) that when provided to Onyx are flagged as "safe" for removal.
|
|
105
|
+
*/
|
|
106
|
+
function initStoreValues(keys, initialKeyStates, safeEvictionKeys) {
|
|
107
|
+
// We need the value of the collection keys later for checking if a
|
|
108
|
+
// key is a collection. We store it in a map for faster lookup.
|
|
109
|
+
const collectionValues = underscore_1.default.values(keys.COLLECTION);
|
|
110
|
+
onyxCollectionKeyMap = underscore_1.default.reduce(collectionValues, (acc, val) => {
|
|
111
|
+
acc.set(val, true);
|
|
112
|
+
return acc;
|
|
113
|
+
}, new Map());
|
|
114
|
+
// Set our default key states to use when initializing and clearing Onyx data
|
|
115
|
+
defaultKeyStates = initialKeyStates;
|
|
116
|
+
DevTools_1.default.initState(initialKeyStates);
|
|
117
|
+
// Let Onyx know about which keys are safe to evict
|
|
118
|
+
evictionAllowList = safeEvictionKeys;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Sends an action to DevTools extension
|
|
122
|
+
*
|
|
123
|
+
* @param {string} method - Onyx method from METHOD
|
|
124
|
+
* @param {string} key - Onyx key that was changed
|
|
125
|
+
* @param {any} value - contains the change that was made by the method
|
|
126
|
+
* @param {any} mergedValue - (optional) value that was written in the storage after a merge method was executed.
|
|
127
|
+
*/
|
|
128
|
+
function sendActionToDevTools(method, key, value, mergedValue = undefined) {
|
|
129
|
+
DevTools_1.default.registerAction(utils_1.default.formatActionName(method, key), value, key ? { [key]: mergedValue || value } : value);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* We are batching together onyx updates. This helps with use cases where we schedule onyx updates after each other.
|
|
133
|
+
* This happens for example in the Onyx.update function, where we process API responses that might contain a lot of
|
|
134
|
+
* update operations. Instead of calling the subscribers for each update operation, we batch them together which will
|
|
135
|
+
* cause react to schedule the updates at once instead of after each other. This is mainly a performance optimization.
|
|
136
|
+
* @returns {Promise}
|
|
137
|
+
*/
|
|
138
|
+
function maybeFlushBatchUpdates() {
|
|
139
|
+
if (batchUpdatesPromise) {
|
|
140
|
+
return batchUpdatesPromise;
|
|
141
|
+
}
|
|
142
|
+
batchUpdatesPromise = new Promise((resolve) => {
|
|
143
|
+
/* We use (setTimeout, 0) here which should be called once native module calls are flushed (usually at the end of the frame)
|
|
144
|
+
* We may investigate if (setTimeout, 1) (which in React Native is equal to requestAnimationFrame) works even better
|
|
145
|
+
* then the batch will be flushed on next frame.
|
|
146
|
+
*/
|
|
147
|
+
setTimeout(() => {
|
|
148
|
+
const updatesCopy = batchUpdatesQueue;
|
|
149
|
+
batchUpdatesQueue = [];
|
|
150
|
+
batchUpdatesPromise = null;
|
|
151
|
+
(0, batch_1.default)(() => {
|
|
152
|
+
updatesCopy.forEach((applyUpdates) => {
|
|
153
|
+
applyUpdates();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
resolve();
|
|
157
|
+
}, 0);
|
|
158
|
+
});
|
|
159
|
+
return batchUpdatesPromise;
|
|
160
|
+
}
|
|
161
|
+
function batchUpdates(updates) {
|
|
162
|
+
batchUpdatesQueue.push(updates);
|
|
163
|
+
return maybeFlushBatchUpdates();
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Uses a selector function to return a simplified version of sourceData
|
|
167
|
+
* @param {Mixed} sourceData
|
|
168
|
+
* @param {Function} selector Function that takes sourceData and returns a simplified version of it
|
|
169
|
+
* @param {Object} [withOnyxInstanceState]
|
|
170
|
+
* @returns {Mixed}
|
|
171
|
+
*/
|
|
172
|
+
const getSubsetOfData = (sourceData, selector, withOnyxInstanceState) => selector(sourceData, withOnyxInstanceState);
|
|
173
|
+
/**
|
|
174
|
+
* Takes a collection of items (eg. {testKey_1:{a:'a'}, testKey_2:{b:'b'}})
|
|
175
|
+
* and runs it through a reducer function to return a subset of the data according to a selector.
|
|
176
|
+
* The resulting collection will only contain items that are returned by the selector.
|
|
177
|
+
* @param {Object} collection
|
|
178
|
+
* @param {String|Function} selector (see method docs for getSubsetOfData() for full details)
|
|
179
|
+
* @param {Object} [withOnyxInstanceState]
|
|
180
|
+
* @returns {Object}
|
|
181
|
+
*/
|
|
182
|
+
const reduceCollectionWithSelector = (collection, selector, withOnyxInstanceState) => underscore_1.default.reduce(collection, (finalCollection, item, key) => {
|
|
183
|
+
// eslint-disable-next-line no-param-reassign
|
|
184
|
+
finalCollection[key] = getSubsetOfData(item, selector, withOnyxInstanceState);
|
|
185
|
+
return finalCollection;
|
|
186
|
+
}, {});
|
|
187
|
+
/**
|
|
188
|
+
* Get some data from the store
|
|
189
|
+
*
|
|
190
|
+
* @private
|
|
191
|
+
* @param {string} key
|
|
192
|
+
* @returns {Promise<*>}
|
|
193
|
+
*/
|
|
194
|
+
function get(key) {
|
|
195
|
+
// When we already have the value in cache - resolve right away
|
|
196
|
+
if (OnyxCache_1.default.hasCacheForKey(key)) {
|
|
197
|
+
return Promise.resolve(OnyxCache_1.default.getValue(key));
|
|
198
|
+
}
|
|
199
|
+
const taskName = `get:${key}`;
|
|
200
|
+
// When a value retrieving task for this key is still running hook to it
|
|
201
|
+
if (OnyxCache_1.default.hasPendingTask(taskName)) {
|
|
202
|
+
return OnyxCache_1.default.getTaskPromise(taskName);
|
|
203
|
+
}
|
|
204
|
+
// Otherwise retrieve the value from storage and capture a promise to aid concurrent usages
|
|
205
|
+
const promise = storage_1.default.getItem(key)
|
|
206
|
+
.then((val) => {
|
|
207
|
+
OnyxCache_1.default.set(key, val);
|
|
208
|
+
return val;
|
|
209
|
+
})
|
|
210
|
+
.catch((err) => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
|
|
211
|
+
return OnyxCache_1.default.captureTask(taskName, promise);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Returns current key names stored in persisted storage
|
|
215
|
+
* @private
|
|
216
|
+
* @returns {Promise<Set<Key>>}
|
|
217
|
+
*/
|
|
218
|
+
function getAllKeys() {
|
|
219
|
+
// When we've already read stored keys, resolve right away
|
|
220
|
+
const storedKeys = OnyxCache_1.default.getAllKeys();
|
|
221
|
+
if (storedKeys.size > 0) {
|
|
222
|
+
return Promise.resolve(storedKeys);
|
|
223
|
+
}
|
|
224
|
+
const taskName = 'getAllKeys';
|
|
225
|
+
// When a value retrieving task for all keys is still running hook to it
|
|
226
|
+
if (OnyxCache_1.default.hasPendingTask(taskName)) {
|
|
227
|
+
return OnyxCache_1.default.getTaskPromise(taskName);
|
|
228
|
+
}
|
|
229
|
+
// Otherwise retrieve the keys from storage and capture a promise to aid concurrent usages
|
|
230
|
+
const promise = storage_1.default.getAllKeys().then((keys) => {
|
|
231
|
+
OnyxCache_1.default.setAllKeys(keys);
|
|
232
|
+
// return the updated set of keys
|
|
233
|
+
return OnyxCache_1.default.getAllKeys();
|
|
234
|
+
});
|
|
235
|
+
return OnyxCache_1.default.captureTask(taskName, promise);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Checks to see if the a subscriber's supplied key
|
|
239
|
+
* is associated with a collection of keys.
|
|
240
|
+
*
|
|
241
|
+
* @param {String} key
|
|
242
|
+
* @returns {Boolean}
|
|
243
|
+
*/
|
|
244
|
+
function isCollectionKey(key) {
|
|
245
|
+
return onyxCollectionKeyMap.has(key);
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* @param {String} collectionKey
|
|
249
|
+
* @param {String} key
|
|
250
|
+
* @returns {Boolean}
|
|
251
|
+
*/
|
|
252
|
+
function isCollectionMemberKey(collectionKey, key) {
|
|
253
|
+
return Str.startsWith(key, collectionKey) && key.length > collectionKey.length;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Splits a collection member key into the collection key part and the ID part.
|
|
257
|
+
* @param {String} key - The collection member key to split.
|
|
258
|
+
* @returns {Array<String>} A tuple where the first element is the collection part and the second element is the ID part.
|
|
259
|
+
*/
|
|
260
|
+
function splitCollectionMemberKey(key) {
|
|
261
|
+
const underscoreIndex = key.indexOf('_');
|
|
262
|
+
if (underscoreIndex === -1) {
|
|
263
|
+
throw new Error(`Invalid ${key} key provided, only collection keys are allowed.`);
|
|
264
|
+
}
|
|
265
|
+
return [key.substring(0, underscoreIndex + 1), key.substring(underscoreIndex + 1)];
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Checks to see if a provided key is the exact configured key of our connected subscriber
|
|
269
|
+
* or if the provided key is a collection member key (in case our configured key is a "collection key")
|
|
270
|
+
*
|
|
271
|
+
* @private
|
|
272
|
+
* @param {String} configKey
|
|
273
|
+
* @param {String} key
|
|
274
|
+
* @return {Boolean}
|
|
275
|
+
*/
|
|
276
|
+
function isKeyMatch(configKey, key) {
|
|
277
|
+
return isCollectionKey(configKey) ? Str.startsWith(key, configKey) : configKey === key;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Checks to see if this key has been flagged as
|
|
281
|
+
* safe for removal.
|
|
282
|
+
*
|
|
283
|
+
* @private
|
|
284
|
+
* @param {String} testKey
|
|
285
|
+
* @returns {Boolean}
|
|
286
|
+
*/
|
|
287
|
+
function isSafeEvictionKey(testKey) {
|
|
288
|
+
return underscore_1.default.some(evictionAllowList, (key) => isKeyMatch(key, testKey));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Tries to get a value from the cache. If the value is not present in cache it will return the default value or undefined.
|
|
292
|
+
* If the requested key is a collection, it will return an object with all the collection members.
|
|
293
|
+
*
|
|
294
|
+
* @param {String} key
|
|
295
|
+
* @param {Object} mapping
|
|
296
|
+
* @returns {Mixed}
|
|
297
|
+
*/
|
|
298
|
+
function tryGetCachedValue(key, mapping = {}) {
|
|
299
|
+
let val = OnyxCache_1.default.getValue(key);
|
|
300
|
+
if (isCollectionKey(key)) {
|
|
301
|
+
const allCacheKeys = OnyxCache_1.default.getAllKeys();
|
|
302
|
+
// It is possible we haven't loaded all keys yet so we do not know if the
|
|
303
|
+
// collection actually exists.
|
|
304
|
+
if (allCacheKeys.size === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
const matchingKeys = [];
|
|
308
|
+
allCacheKeys.forEach((k) => {
|
|
309
|
+
if (!k.startsWith(key)) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
matchingKeys.push(k);
|
|
313
|
+
});
|
|
314
|
+
const values = underscore_1.default.reduce(matchingKeys, (finalObject, matchedKey) => {
|
|
315
|
+
const cachedValue = OnyxCache_1.default.getValue(matchedKey);
|
|
316
|
+
if (cachedValue) {
|
|
317
|
+
// This is permissible because we're in the process of constructing the final object in a reduce function.
|
|
318
|
+
// eslint-disable-next-line no-param-reassign
|
|
319
|
+
finalObject[matchedKey] = cachedValue;
|
|
320
|
+
}
|
|
321
|
+
return finalObject;
|
|
322
|
+
}, {});
|
|
323
|
+
val = values;
|
|
324
|
+
}
|
|
325
|
+
if (mapping.selector) {
|
|
326
|
+
const state = mapping.withOnyxInstance ? mapping.withOnyxInstance.state : undefined;
|
|
327
|
+
if (isCollectionKey(key)) {
|
|
328
|
+
return reduceCollectionWithSelector(val, mapping.selector, state);
|
|
329
|
+
}
|
|
330
|
+
return getSubsetOfData(val, mapping.selector, state);
|
|
331
|
+
}
|
|
332
|
+
return val;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Remove a key from the recently accessed key list.
|
|
336
|
+
*
|
|
337
|
+
* @private
|
|
338
|
+
* @param {String} key
|
|
339
|
+
*/
|
|
340
|
+
function removeLastAccessedKey(key) {
|
|
341
|
+
recentlyAccessedKeys = underscore_1.default.without(recentlyAccessedKeys, key);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Add a key to the list of recently accessed keys. The least
|
|
345
|
+
* recently accessed key should be at the head and the most
|
|
346
|
+
* recently accessed key at the tail.
|
|
347
|
+
*
|
|
348
|
+
* @private
|
|
349
|
+
* @param {String} key
|
|
350
|
+
*/
|
|
351
|
+
function addLastAccessedKey(key) {
|
|
352
|
+
// Only specific keys belong in this list since we cannot remove an entire collection.
|
|
353
|
+
if (isCollectionKey(key) || !isSafeEvictionKey(key)) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
removeLastAccessedKey(key);
|
|
357
|
+
recentlyAccessedKeys.push(key);
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Removes a key previously added to this list
|
|
361
|
+
* which will enable it to be deleted again.
|
|
362
|
+
*
|
|
363
|
+
* @private
|
|
364
|
+
* @param {String} key
|
|
365
|
+
* @param {Number} connectionID
|
|
366
|
+
*/
|
|
367
|
+
function removeFromEvictionBlockList(key, connectionID) {
|
|
368
|
+
evictionBlocklist[key] = underscore_1.default.without(evictionBlocklist[key] || [], connectionID);
|
|
369
|
+
// Remove the key if there are no more subscribers
|
|
370
|
+
if (evictionBlocklist[key].length === 0) {
|
|
371
|
+
delete evictionBlocklist[key];
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Keys added to this list can never be deleted.
|
|
376
|
+
*
|
|
377
|
+
* @private
|
|
378
|
+
* @param {String} key
|
|
379
|
+
* @param {Number} connectionID
|
|
380
|
+
*/
|
|
381
|
+
function addToEvictionBlockList(key, connectionID) {
|
|
382
|
+
removeFromEvictionBlockList(key, connectionID);
|
|
383
|
+
if (!evictionBlocklist[key]) {
|
|
384
|
+
evictionBlocklist[key] = [];
|
|
385
|
+
}
|
|
386
|
+
evictionBlocklist[key].push(connectionID);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Take all the keys that are safe to evict and add them to
|
|
390
|
+
* the recently accessed list when initializing the app. This
|
|
391
|
+
* enables keys that have not recently been accessed to be
|
|
392
|
+
* removed.
|
|
393
|
+
*
|
|
394
|
+
* @private
|
|
395
|
+
* @returns {Promise}
|
|
396
|
+
*/
|
|
397
|
+
function addAllSafeEvictionKeysToRecentlyAccessedList() {
|
|
398
|
+
return getAllKeys().then((keys) => {
|
|
399
|
+
underscore_1.default.each(evictionAllowList, (safeEvictionKey) => {
|
|
400
|
+
keys.forEach((key) => {
|
|
401
|
+
if (!isKeyMatch(safeEvictionKey, key)) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
addLastAccessedKey(key);
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* @private
|
|
411
|
+
* @param {String} collectionKey
|
|
412
|
+
* @returns {Object}
|
|
413
|
+
*/
|
|
414
|
+
function getCachedCollection(collectionKey) {
|
|
415
|
+
const collectionMemberKeys = [];
|
|
416
|
+
OnyxCache_1.default.getAllKeys().forEach((storedKey) => {
|
|
417
|
+
if (!isCollectionMemberKey(collectionKey, storedKey)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
collectionMemberKeys.push(storedKey);
|
|
421
|
+
});
|
|
422
|
+
return underscore_1.default.reduce(collectionMemberKeys, (prev, curr) => {
|
|
423
|
+
const cachedValue = OnyxCache_1.default.getValue(curr);
|
|
424
|
+
if (!cachedValue) {
|
|
425
|
+
return prev;
|
|
426
|
+
}
|
|
427
|
+
// eslint-disable-next-line no-param-reassign
|
|
428
|
+
prev[curr] = cachedValue;
|
|
429
|
+
return prev;
|
|
430
|
+
}, {});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* When a collection of keys change, search for any callbacks matching the collection key and trigger those callbacks
|
|
434
|
+
*
|
|
435
|
+
* @private
|
|
436
|
+
* @param {String} collectionKey
|
|
437
|
+
* @param {Object} partialCollection - a partial collection of grouped member keys
|
|
438
|
+
* @param {boolean} [notifyRegularSubscibers=true]
|
|
439
|
+
* @param {boolean} [notifyWithOnyxSubscibers=true]
|
|
440
|
+
*/
|
|
441
|
+
function keysChanged(collectionKey, partialCollection, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
|
|
442
|
+
// We are iterating over all subscribers similar to keyChanged(). However, we are looking for subscribers who are subscribing to either a collection key or
|
|
443
|
+
// individual collection key member for the collection that is being updated. It is important to note that the collection parameter cane be a PARTIAL collection
|
|
444
|
+
// and does not represent all of the combined keys and values for a collection key. It is just the "new" data that was merged in via mergeCollection().
|
|
445
|
+
const stateMappingKeys = underscore_1.default.keys(callbackToStateMapping);
|
|
446
|
+
for (let i = 0; i < stateMappingKeys.length; i++) {
|
|
447
|
+
const subscriber = callbackToStateMapping[stateMappingKeys[i]];
|
|
448
|
+
if (!subscriber) {
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
// Skip iteration if we do not have a collection key or a collection member key on this subscriber
|
|
452
|
+
if (!Str.startsWith(subscriber.key, collectionKey)) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* e.g. Onyx.connect({key: ONYXKEYS.COLLECTION.REPORT, callback: ...});
|
|
457
|
+
*/
|
|
458
|
+
const isSubscribedToCollectionKey = subscriber.key === collectionKey;
|
|
459
|
+
/**
|
|
460
|
+
* e.g. Onyx.connect({key: `${ONYXKEYS.COLLECTION.REPORT}{reportID}`, callback: ...});
|
|
461
|
+
*/
|
|
462
|
+
const isSubscribedToCollectionMemberKey = isCollectionMemberKey(collectionKey, subscriber.key);
|
|
463
|
+
// We prepare the "cached collection" which is the entire collection + the new partial data that
|
|
464
|
+
// was merged in via mergeCollection().
|
|
465
|
+
const cachedCollection = getCachedCollection(collectionKey);
|
|
466
|
+
// Regular Onyx.connect() subscriber found.
|
|
467
|
+
if (underscore_1.default.isFunction(subscriber.callback)) {
|
|
468
|
+
if (!notifyRegularSubscibers) {
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
// If they are subscribed to the collection key and using waitForCollectionCallback then we'll
|
|
472
|
+
// send the whole cached collection.
|
|
473
|
+
if (isSubscribedToCollectionKey) {
|
|
474
|
+
if (subscriber.waitForCollectionCallback) {
|
|
475
|
+
subscriber.callback(cachedCollection);
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
// If they are not using waitForCollectionCallback then we notify the subscriber with
|
|
479
|
+
// the new merged data but only for any keys in the partial collection.
|
|
480
|
+
const dataKeys = underscore_1.default.keys(partialCollection);
|
|
481
|
+
for (let j = 0; j < dataKeys.length; j++) {
|
|
482
|
+
const dataKey = dataKeys[j];
|
|
483
|
+
subscriber.callback(cachedCollection[dataKey], dataKey);
|
|
484
|
+
}
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
// And if the subscriber is specifically only tracking a particular collection member key then we will
|
|
488
|
+
// notify them with the cached data for that key only.
|
|
489
|
+
if (isSubscribedToCollectionMemberKey) {
|
|
490
|
+
subscriber.callback(cachedCollection[subscriber.key], subscriber.key);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
// React component subscriber found.
|
|
496
|
+
if (subscriber.withOnyxInstance) {
|
|
497
|
+
if (!notifyWithOnyxSubscibers) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
// We are subscribed to a collection key so we must update the data in state with the new
|
|
501
|
+
// collection member key values from the partial update.
|
|
502
|
+
if (isSubscribedToCollectionKey) {
|
|
503
|
+
// If the subscriber has a selector, then the component's state must only be updated with the data
|
|
504
|
+
// returned by the selector.
|
|
505
|
+
if (subscriber.selector) {
|
|
506
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
507
|
+
const previousData = prevState[subscriber.statePropertyName];
|
|
508
|
+
const newData = reduceCollectionWithSelector(cachedCollection, subscriber.selector, subscriber.withOnyxInstance.state);
|
|
509
|
+
if (!(0, fast_equals_1.deepEqual)(previousData, newData)) {
|
|
510
|
+
return {
|
|
511
|
+
[subscriber.statePropertyName]: newData,
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
519
|
+
const finalCollection = underscore_1.default.clone(prevState[subscriber.statePropertyName] || {});
|
|
520
|
+
const dataKeys = underscore_1.default.keys(partialCollection);
|
|
521
|
+
for (let j = 0; j < dataKeys.length; j++) {
|
|
522
|
+
const dataKey = dataKeys[j];
|
|
523
|
+
finalCollection[dataKey] = cachedCollection[dataKey];
|
|
524
|
+
}
|
|
525
|
+
PerformanceUtils.logSetStateCall(subscriber, prevState[subscriber.statePropertyName], finalCollection, 'keysChanged', collectionKey);
|
|
526
|
+
return {
|
|
527
|
+
[subscriber.statePropertyName]: finalCollection,
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
// If a React component is only interested in a single key then we can set the cached value directly to the state name.
|
|
533
|
+
if (isSubscribedToCollectionMemberKey) {
|
|
534
|
+
// However, we only want to update this subscriber if the partial data contains a change.
|
|
535
|
+
// Otherwise, we would update them with a value they already have and trigger an unnecessary re-render.
|
|
536
|
+
const dataFromCollection = partialCollection[subscriber.key];
|
|
537
|
+
if (underscore_1.default.isUndefined(dataFromCollection)) {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
// If the subscriber has a selector, then the component's state must only be updated with the data
|
|
541
|
+
// returned by the selector and the state should only change when the subset of data changes from what
|
|
542
|
+
// it was previously.
|
|
543
|
+
if (subscriber.selector) {
|
|
544
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
545
|
+
const prevData = prevState[subscriber.statePropertyName];
|
|
546
|
+
const newData = getSubsetOfData(cachedCollection[subscriber.key], subscriber.selector, subscriber.withOnyxInstance.state);
|
|
547
|
+
if (!(0, fast_equals_1.deepEqual)(prevData, newData)) {
|
|
548
|
+
PerformanceUtils.logSetStateCall(subscriber, prevData, newData, 'keysChanged', collectionKey);
|
|
549
|
+
return {
|
|
550
|
+
[subscriber.statePropertyName]: newData,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
});
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
558
|
+
const data = cachedCollection[subscriber.key];
|
|
559
|
+
const previousData = prevState[subscriber.statePropertyName];
|
|
560
|
+
// Avoids triggering unnecessary re-renders when feeding empty objects
|
|
561
|
+
if (utils_1.default.isEmptyObject(data) && utils_1.default.isEmptyObject(previousData)) {
|
|
562
|
+
return null;
|
|
563
|
+
}
|
|
564
|
+
if (data === previousData) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
PerformanceUtils.logSetStateCall(subscriber, previousData, data, 'keysChanged', collectionKey);
|
|
568
|
+
return {
|
|
569
|
+
[subscriber.statePropertyName]: data,
|
|
570
|
+
};
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* When a key change happens, search for any callbacks matching the key or collection key and trigger those callbacks
|
|
578
|
+
*
|
|
579
|
+
* @example
|
|
580
|
+
* keyChanged(key, value, subscriber => subscriber.initWithStoredValues === false)
|
|
581
|
+
*
|
|
582
|
+
* @private
|
|
583
|
+
* @param {String} key
|
|
584
|
+
* @param {*} data
|
|
585
|
+
* @param {*} prevData
|
|
586
|
+
* @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
|
|
587
|
+
* @param {boolean} [notifyRegularSubscibers=true]
|
|
588
|
+
* @param {boolean} [notifyWithOnyxSubscibers=true]
|
|
589
|
+
*/
|
|
590
|
+
function keyChanged(key, data, prevData, canUpdateSubscriber = () => true, notifyRegularSubscibers = true, notifyWithOnyxSubscibers = true) {
|
|
591
|
+
// Add or remove this key from the recentlyAccessedKeys lists
|
|
592
|
+
if (!underscore_1.default.isNull(data)) {
|
|
593
|
+
addLastAccessedKey(key);
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
removeLastAccessedKey(key);
|
|
597
|
+
}
|
|
598
|
+
// We are iterating over all subscribers to see if they are interested in the key that has just changed. If the subscriber's key is a collection key then we will
|
|
599
|
+
// notify them if the key that changed is a collection member. Or if it is a regular key notify them when there is an exact match. Depending on whether the subscriber
|
|
600
|
+
// was connected via withOnyx we will call setState() directly on the withOnyx instance. If it is a regular connection we will pass the data to the provided callback.
|
|
601
|
+
const stateMappingKeys = underscore_1.default.keys(callbackToStateMapping);
|
|
602
|
+
for (let i = 0; i < stateMappingKeys.length; i++) {
|
|
603
|
+
const subscriber = callbackToStateMapping[stateMappingKeys[i]];
|
|
604
|
+
if (!subscriber || !isKeyMatch(subscriber.key, key) || !canUpdateSubscriber(subscriber)) {
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
// Subscriber is a regular call to connect() and provided a callback
|
|
608
|
+
if (underscore_1.default.isFunction(subscriber.callback)) {
|
|
609
|
+
if (!notifyRegularSubscibers) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
if (isCollectionKey(subscriber.key) && subscriber.waitForCollectionCallback) {
|
|
613
|
+
const cachedCollection = getCachedCollection(subscriber.key);
|
|
614
|
+
cachedCollection[key] = data;
|
|
615
|
+
subscriber.callback(cachedCollection);
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
subscriber.callback(data, key);
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Subscriber connected via withOnyx() HOC
|
|
622
|
+
if (subscriber.withOnyxInstance) {
|
|
623
|
+
if (!notifyWithOnyxSubscibers) {
|
|
624
|
+
continue;
|
|
625
|
+
}
|
|
626
|
+
// Check if we are subscribing to a collection key and overwrite the collection member key value in state
|
|
627
|
+
if (isCollectionKey(subscriber.key)) {
|
|
628
|
+
// If the subscriber has a selector, then the consumer of this data must only be given the data
|
|
629
|
+
// returned by the selector and only when the selected data has changed.
|
|
630
|
+
if (subscriber.selector) {
|
|
631
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
632
|
+
const prevWithOnyxData = prevState[subscriber.statePropertyName];
|
|
633
|
+
const newWithOnyxData = {
|
|
634
|
+
[key]: getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state),
|
|
635
|
+
};
|
|
636
|
+
const prevDataWithNewData = Object.assign(Object.assign({}, prevWithOnyxData), newWithOnyxData);
|
|
637
|
+
if (!(0, fast_equals_1.deepEqual)(prevWithOnyxData, prevDataWithNewData)) {
|
|
638
|
+
PerformanceUtils.logSetStateCall(subscriber, prevWithOnyxData, newWithOnyxData, 'keyChanged', key);
|
|
639
|
+
return {
|
|
640
|
+
[subscriber.statePropertyName]: prevDataWithNewData,
|
|
641
|
+
};
|
|
642
|
+
}
|
|
643
|
+
return null;
|
|
644
|
+
});
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
648
|
+
const collection = prevState[subscriber.statePropertyName] || {};
|
|
649
|
+
const newCollection = Object.assign(Object.assign({}, collection), { [key]: data });
|
|
650
|
+
PerformanceUtils.logSetStateCall(subscriber, collection, newCollection, 'keyChanged', key);
|
|
651
|
+
return {
|
|
652
|
+
[subscriber.statePropertyName]: newCollection,
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// If the subscriber has a selector, then the component's state must only be updated with the data
|
|
658
|
+
// returned by the selector and only if the selected data has changed.
|
|
659
|
+
if (subscriber.selector) {
|
|
660
|
+
subscriber.withOnyxInstance.setStateProxy(() => {
|
|
661
|
+
const previousValue = getSubsetOfData(prevData, subscriber.selector, subscriber.withOnyxInstance.state);
|
|
662
|
+
const newValue = getSubsetOfData(data, subscriber.selector, subscriber.withOnyxInstance.state);
|
|
663
|
+
if (!(0, fast_equals_1.deepEqual)(previousValue, newValue)) {
|
|
664
|
+
return {
|
|
665
|
+
[subscriber.statePropertyName]: newValue,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
return null;
|
|
669
|
+
});
|
|
670
|
+
continue;
|
|
671
|
+
}
|
|
672
|
+
// If we did not match on a collection key then we just set the new data to the state property
|
|
673
|
+
subscriber.withOnyxInstance.setStateProxy((prevState) => {
|
|
674
|
+
const prevWithOnyxData = prevState[subscriber.statePropertyName];
|
|
675
|
+
// Avoids triggering unnecessary re-renders when feeding empty objects
|
|
676
|
+
if (utils_1.default.isEmptyObject(data) && utils_1.default.isEmptyObject(prevWithOnyxData)) {
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
if (prevWithOnyxData === data) {
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
PerformanceUtils.logSetStateCall(subscriber, prevData, data, 'keyChanged', key);
|
|
683
|
+
return {
|
|
684
|
+
[subscriber.statePropertyName]: data,
|
|
685
|
+
};
|
|
686
|
+
});
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
console.error('Warning: Found a matching subscriber to a key that changed, but no callback or withOnyxInstance could be found.');
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
693
|
+
* Sends the data obtained from the keys to the connection. It either:
|
|
694
|
+
* - sets state on the withOnyxInstances
|
|
695
|
+
* - triggers the callback function
|
|
696
|
+
*
|
|
697
|
+
* @private
|
|
698
|
+
* @param {Object} mapping
|
|
699
|
+
* @param {Object} [mapping.withOnyxInstance]
|
|
700
|
+
* @param {String} [mapping.statePropertyName]
|
|
701
|
+
* @param {Function} [mapping.callback]
|
|
702
|
+
* @param {String} [mapping.selector]
|
|
703
|
+
* @param {*|null} val
|
|
704
|
+
* @param {String|undefined} matchedKey
|
|
705
|
+
* @param {Boolean} isBatched
|
|
706
|
+
*/
|
|
707
|
+
function sendDataToConnection(mapping, val, matchedKey, isBatched) {
|
|
708
|
+
// If the mapping no longer exists then we should not send any data.
|
|
709
|
+
// This means our subscriber disconnected or withOnyx wrapped component unmounted.
|
|
710
|
+
if (!callbackToStateMapping[mapping.connectionID]) {
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
if (mapping.withOnyxInstance) {
|
|
714
|
+
let newData = val;
|
|
715
|
+
// If the mapping has a selector, then the component's state must only be updated with the data
|
|
716
|
+
// returned by the selector.
|
|
717
|
+
if (mapping.selector) {
|
|
718
|
+
if (isCollectionKey(mapping.key)) {
|
|
719
|
+
newData = reduceCollectionWithSelector(val, mapping.selector, mapping.withOnyxInstance.state);
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
newData = getSubsetOfData(val, mapping.selector, mapping.withOnyxInstance.state);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
PerformanceUtils.logSetStateCall(mapping, null, newData, 'sendDataToConnection');
|
|
726
|
+
if (isBatched) {
|
|
727
|
+
batchUpdates(() => {
|
|
728
|
+
mapping.withOnyxInstance.setWithOnyxState(mapping.statePropertyName, newData);
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
mapping.withOnyxInstance.setWithOnyxState(mapping.statePropertyName, newData);
|
|
733
|
+
}
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
if (underscore_1.default.isFunction(mapping.callback)) {
|
|
737
|
+
mapping.callback(val, matchedKey);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* We check to see if this key is flagged as safe for eviction and add it to the recentlyAccessedKeys list so that when we
|
|
742
|
+
* run out of storage the least recently accessed key can be removed.
|
|
743
|
+
*
|
|
744
|
+
* @private
|
|
745
|
+
* @param {Object} mapping
|
|
746
|
+
*/
|
|
747
|
+
function addKeyToRecentlyAccessedIfNeeded(mapping) {
|
|
748
|
+
if (!isSafeEvictionKey(mapping.key)) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
// Try to free some cache whenever we connect to a safe eviction key
|
|
752
|
+
OnyxCache_1.default.removeLeastRecentlyUsedKeys();
|
|
753
|
+
if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
|
|
754
|
+
// All React components subscribing to a key flagged as a safe eviction key must implement the canEvict property.
|
|
755
|
+
if (underscore_1.default.isUndefined(mapping.canEvict)) {
|
|
756
|
+
throw new Error(`Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`);
|
|
757
|
+
}
|
|
758
|
+
addLastAccessedKey(mapping.key);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Gets the data for a given an array of matching keys, combines them into an object, and sends the result back to the subscriber.
|
|
763
|
+
*
|
|
764
|
+
* @private
|
|
765
|
+
* @param {Array} matchingKeys
|
|
766
|
+
* @param {Object} mapping
|
|
767
|
+
*/
|
|
768
|
+
function getCollectionDataAndSendAsObject(matchingKeys, mapping) {
|
|
769
|
+
// Keys that are not in the cache
|
|
770
|
+
const missingKeys = [];
|
|
771
|
+
// Tasks that are pending
|
|
772
|
+
const pendingTasks = [];
|
|
773
|
+
// Keys for the tasks that are pending
|
|
774
|
+
const pendingKeys = [];
|
|
775
|
+
// We are going to combine all the data from the matching keys into a single object
|
|
776
|
+
const data = {};
|
|
777
|
+
/**
|
|
778
|
+
* We are going to iterate over all the matching keys and check if we have the data in the cache.
|
|
779
|
+
* If we do then we add it to the data object. If we do not then we check if there is a pending task
|
|
780
|
+
* for the key. If there is then we add the promise to the pendingTasks array and the key to the pendingKeys
|
|
781
|
+
* array. If there is no pending task then we add the key to the missingKeys array.
|
|
782
|
+
*
|
|
783
|
+
* These missingKeys will be later to use to multiGet the data from the storage.
|
|
784
|
+
*/
|
|
785
|
+
matchingKeys.forEach((key) => {
|
|
786
|
+
const cacheValue = OnyxCache_1.default.getValue(key);
|
|
787
|
+
if (cacheValue) {
|
|
788
|
+
data[key] = cacheValue;
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const pendingKey = `get:${key}`;
|
|
792
|
+
if (OnyxCache_1.default.hasPendingTask(pendingKey)) {
|
|
793
|
+
pendingTasks.push(OnyxCache_1.default.getTaskPromise(pendingKey));
|
|
794
|
+
pendingKeys.push(key);
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
missingKeys.push(key);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
Promise.all(pendingTasks)
|
|
801
|
+
// We are going to wait for all the pending tasks to resolve and then add the data to the data object.
|
|
802
|
+
.then((values) => {
|
|
803
|
+
values.forEach((value, index) => {
|
|
804
|
+
data[pendingKeys[index]] = value;
|
|
805
|
+
});
|
|
806
|
+
return Promise.resolve();
|
|
807
|
+
})
|
|
808
|
+
// We are going to get the missing keys using multiGet from the storage.
|
|
809
|
+
.then(() => {
|
|
810
|
+
if (missingKeys.length === 0) {
|
|
811
|
+
return Promise.resolve();
|
|
812
|
+
}
|
|
813
|
+
return storage_1.default.multiGet(missingKeys);
|
|
814
|
+
})
|
|
815
|
+
// We are going to add the data from the missing keys to the data object and also merge it to the cache.
|
|
816
|
+
.then((values) => {
|
|
817
|
+
if (!values || values.length === 0) {
|
|
818
|
+
return Promise.resolve();
|
|
819
|
+
}
|
|
820
|
+
// temp object is used to merge the missing data into the cache
|
|
821
|
+
const temp = {};
|
|
822
|
+
values.forEach((value) => {
|
|
823
|
+
data[value[0]] = value[1];
|
|
824
|
+
temp[value[0]] = value[1];
|
|
825
|
+
});
|
|
826
|
+
OnyxCache_1.default.merge(temp);
|
|
827
|
+
return Promise.resolve();
|
|
828
|
+
})
|
|
829
|
+
// We are going to send the data to the subscriber.
|
|
830
|
+
.finally(() => {
|
|
831
|
+
sendDataToConnection(mapping, data, undefined, true);
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Schedules an update that will be appended to the macro task queue (so it doesn't update the subscribers immediately).
|
|
836
|
+
*
|
|
837
|
+
* @example
|
|
838
|
+
* scheduleSubscriberUpdate(key, value, subscriber => subscriber.initWithStoredValues === false)
|
|
839
|
+
*
|
|
840
|
+
* @param {String} key
|
|
841
|
+
* @param {*} value
|
|
842
|
+
* @param {*} prevValue
|
|
843
|
+
* @param {Function} [canUpdateSubscriber] only subscribers that pass this truth test will be updated
|
|
844
|
+
* @returns {Promise}
|
|
845
|
+
*/
|
|
846
|
+
function scheduleSubscriberUpdate(key, value, prevValue, canUpdateSubscriber = () => true) {
|
|
847
|
+
const promise = Promise.resolve().then(() => keyChanged(key, value, prevValue, canUpdateSubscriber, true, false));
|
|
848
|
+
batchUpdates(() => keyChanged(key, value, prevValue, canUpdateSubscriber, false, true));
|
|
849
|
+
return Promise.all([maybeFlushBatchUpdates(), promise]);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* This method is similar to notifySubscribersOnNextTick but it is built for working specifically with collections
|
|
853
|
+
* so that keysChanged() is triggered for the collection and not keyChanged(). If this was not done, then the
|
|
854
|
+
* subscriber callbacks receive the data in a different format than they normally expect and it breaks code.
|
|
855
|
+
*
|
|
856
|
+
* @param {String} key
|
|
857
|
+
* @param {*} value
|
|
858
|
+
* @returns {Promise}
|
|
859
|
+
*/
|
|
860
|
+
function scheduleNotifyCollectionSubscribers(key, value) {
|
|
861
|
+
const promise = Promise.resolve().then(() => keysChanged(key, value, true, false));
|
|
862
|
+
batchUpdates(() => keysChanged(key, value, false, true));
|
|
863
|
+
return Promise.all([maybeFlushBatchUpdates(), promise]);
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* Remove a key from Onyx and update the subscribers
|
|
867
|
+
*
|
|
868
|
+
* @private
|
|
869
|
+
* @param {String} key
|
|
870
|
+
* @return {Promise}
|
|
871
|
+
*/
|
|
872
|
+
function remove(key) {
|
|
873
|
+
const prevValue = OnyxCache_1.default.getValue(key, false);
|
|
874
|
+
OnyxCache_1.default.drop(key);
|
|
875
|
+
scheduleSubscriberUpdate(key, null, prevValue);
|
|
876
|
+
return storage_1.default.removeItem(key);
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* @private
|
|
880
|
+
* @returns {Promise<void>}
|
|
881
|
+
*/
|
|
882
|
+
function reportStorageQuota() {
|
|
883
|
+
return storage_1.default.getDatabaseSize()
|
|
884
|
+
.then(({ bytesUsed, bytesRemaining }) => {
|
|
885
|
+
Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}`);
|
|
886
|
+
})
|
|
887
|
+
.catch((dbSizeError) => {
|
|
888
|
+
Logger.logAlert(`Unable to get database size. Error: ${dbSizeError}`);
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* If we fail to set or merge we must handle this by
|
|
893
|
+
* evicting some data from Onyx and then retrying to do
|
|
894
|
+
* whatever it is we attempted to do.
|
|
895
|
+
*
|
|
896
|
+
* @private
|
|
897
|
+
* @param {Error} error
|
|
898
|
+
* @param {Function} onyxMethod
|
|
899
|
+
* @param {...any} args
|
|
900
|
+
* @return {Promise}
|
|
901
|
+
*/
|
|
902
|
+
function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
903
|
+
Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
|
|
904
|
+
if (error && Str.startsWith(error.message, "Failed to execute 'put' on 'IDBObjectStore'")) {
|
|
905
|
+
Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
// Find the first key that we can remove that has no subscribers in our blocklist
|
|
909
|
+
const keyForRemoval = underscore_1.default.find(recentlyAccessedKeys, (key) => !evictionBlocklist[key]);
|
|
910
|
+
if (!keyForRemoval) {
|
|
911
|
+
// If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
|
|
912
|
+
// then we should stop retrying as there is not much the user can do to fix this. Instead of getting them stuck in an infinite loop we
|
|
913
|
+
// will allow this write to be skipped.
|
|
914
|
+
Logger.logAlert('Out of storage. But found no acceptable keys to remove.');
|
|
915
|
+
return reportStorageQuota();
|
|
916
|
+
}
|
|
917
|
+
// Remove the least recently viewed key that is not currently being accessed and retry.
|
|
918
|
+
Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
|
|
919
|
+
reportStorageQuota();
|
|
920
|
+
return remove(keyForRemoval).then(() => onyxMethod(...args));
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* Notifys subscribers and writes current value to cache
|
|
924
|
+
*
|
|
925
|
+
* @param {String} key
|
|
926
|
+
* @param {*} value
|
|
927
|
+
* @param {Boolean} hasChanged
|
|
928
|
+
* @param {Boolean} wasRemoved
|
|
929
|
+
* @returns {Promise}
|
|
930
|
+
*/
|
|
931
|
+
function broadcastUpdate(key, value, hasChanged, wasRemoved = false) {
|
|
932
|
+
const prevValue = OnyxCache_1.default.getValue(key, false);
|
|
933
|
+
// Update subscribers if the cached value has changed, or when the subscriber specifically requires
|
|
934
|
+
// all updates regardless of value changes (indicated by initWithStoredValues set to false).
|
|
935
|
+
if (hasChanged && !wasRemoved) {
|
|
936
|
+
OnyxCache_1.default.set(key, value);
|
|
937
|
+
}
|
|
938
|
+
else {
|
|
939
|
+
OnyxCache_1.default.addToAccessedKeys(key);
|
|
940
|
+
}
|
|
941
|
+
return scheduleSubscriberUpdate(key, value, prevValue, (subscriber) => hasChanged || subscriber.initWithStoredValues === false);
|
|
942
|
+
}
|
|
943
|
+
/**
|
|
944
|
+
* @param {String} key
|
|
945
|
+
* @returns {Boolean}
|
|
946
|
+
*/
|
|
947
|
+
function hasPendingMergeForKey(key) {
|
|
948
|
+
return Boolean(mergeQueue[key]);
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* Removes a key from storage if the value is null.
|
|
952
|
+
* Otherwise removes all nested null values in objects and returns the object
|
|
953
|
+
* @param {String} key
|
|
954
|
+
* @param {Mixed} value
|
|
955
|
+
* @returns {Mixed} The value without null values and a boolean "wasRemoved", which indicates if the key got removed completely
|
|
956
|
+
*/
|
|
957
|
+
function removeNullValues(key, value) {
|
|
958
|
+
if (underscore_1.default.isNull(value)) {
|
|
959
|
+
remove(key);
|
|
960
|
+
return { value, wasRemoved: true };
|
|
961
|
+
}
|
|
962
|
+
// We can remove all null values in an object by merging it with itself
|
|
963
|
+
// utils.fastMerge recursively goes through the object and removes all null values
|
|
964
|
+
// Passing two identical objects as source and target to fastMerge will not change it, but only remove the null values
|
|
965
|
+
return { value: utils_1.default.removeNestedNullValues(value), wasRemoved: false };
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Storage expects array like: [["@MyApp_user", value_1], ["@MyApp_key", value_2]]
|
|
969
|
+
* This method transforms an object like {'@MyApp_user': myUserValue, '@MyApp_key': myKeyValue}
|
|
970
|
+
* to an array of key-value pairs in the above format and removes key-value pairs that are being set to null
|
|
971
|
+
* @private
|
|
972
|
+
* @param {Record} data
|
|
973
|
+
* @return {Array} an array of key - value pairs <[key, value]>
|
|
974
|
+
*/
|
|
975
|
+
function prepareKeyValuePairsForStorage(data) {
|
|
976
|
+
const keyValuePairs = [];
|
|
977
|
+
underscore_1.default.forEach(data, (value, key) => {
|
|
978
|
+
const { value: valueAfterRemoving, wasRemoved } = removeNullValues(key, value);
|
|
979
|
+
if (wasRemoved)
|
|
980
|
+
return;
|
|
981
|
+
keyValuePairs.push([key, valueAfterRemoving]);
|
|
982
|
+
});
|
|
983
|
+
return keyValuePairs;
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Merges an array of changes with an existing value
|
|
987
|
+
*
|
|
988
|
+
* @private
|
|
989
|
+
* @param {*} existingValue
|
|
990
|
+
* @param {Array<*>} changes Array of changes that should be applied to the existing value
|
|
991
|
+
* @param {Boolean} shouldRemoveNullObjectValues
|
|
992
|
+
* @returns {*}
|
|
993
|
+
*/
|
|
994
|
+
function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
|
|
995
|
+
const lastChange = underscore_1.default.last(changes);
|
|
996
|
+
if (underscore_1.default.isArray(lastChange)) {
|
|
997
|
+
return lastChange;
|
|
998
|
+
}
|
|
999
|
+
if (underscore_1.default.some(changes, underscore_1.default.isObject)) {
|
|
1000
|
+
// Object values are then merged one after the other
|
|
1001
|
+
return underscore_1.default.reduce(changes, (modifiedData, change) => utils_1.default.fastMerge(modifiedData, change, shouldRemoveNullObjectValues), existingValue || {});
|
|
1002
|
+
}
|
|
1003
|
+
// If we have anything else we can't merge it so we'll
|
|
1004
|
+
// simply return the last value that was queued
|
|
1005
|
+
return lastChange;
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Merge user provided default key value pairs.
|
|
1009
|
+
* @private
|
|
1010
|
+
* @returns {Promise}
|
|
1011
|
+
*/
|
|
1012
|
+
function initializeWithDefaultKeyStates() {
|
|
1013
|
+
return storage_1.default.multiGet(underscore_1.default.keys(defaultKeyStates)).then((pairs) => {
|
|
1014
|
+
const existingDataAsObject = underscore_1.default.object(pairs);
|
|
1015
|
+
const merged = utils_1.default.fastMerge(existingDataAsObject, defaultKeyStates);
|
|
1016
|
+
OnyxCache_1.default.merge(merged);
|
|
1017
|
+
underscore_1.default.each(merged, (val, key) => keyChanged(key, val, existingDataAsObject));
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
const OnyxUtils = {
|
|
1021
|
+
METHOD,
|
|
1022
|
+
getMergeQueue,
|
|
1023
|
+
getMergeQueuePromise,
|
|
1024
|
+
getCallbackToStateMapping,
|
|
1025
|
+
getDefaultKeyStates,
|
|
1026
|
+
initStoreValues,
|
|
1027
|
+
sendActionToDevTools,
|
|
1028
|
+
maybeFlushBatchUpdates,
|
|
1029
|
+
batchUpdates,
|
|
1030
|
+
get,
|
|
1031
|
+
getAllKeys,
|
|
1032
|
+
isCollectionKey,
|
|
1033
|
+
isCollectionMemberKey,
|
|
1034
|
+
splitCollectionMemberKey,
|
|
1035
|
+
isKeyMatch,
|
|
1036
|
+
isSafeEvictionKey,
|
|
1037
|
+
tryGetCachedValue,
|
|
1038
|
+
removeLastAccessedKey,
|
|
1039
|
+
addLastAccessedKey,
|
|
1040
|
+
removeFromEvictionBlockList,
|
|
1041
|
+
addToEvictionBlockList,
|
|
1042
|
+
addAllSafeEvictionKeysToRecentlyAccessedList,
|
|
1043
|
+
getCachedCollection,
|
|
1044
|
+
keysChanged,
|
|
1045
|
+
keyChanged,
|
|
1046
|
+
sendDataToConnection,
|
|
1047
|
+
addKeyToRecentlyAccessedIfNeeded,
|
|
1048
|
+
getCollectionDataAndSendAsObject,
|
|
1049
|
+
scheduleSubscriberUpdate,
|
|
1050
|
+
scheduleNotifyCollectionSubscribers,
|
|
1051
|
+
remove,
|
|
1052
|
+
reportStorageQuota,
|
|
1053
|
+
evictStorageAndRetry,
|
|
1054
|
+
broadcastUpdate,
|
|
1055
|
+
hasPendingMergeForKey,
|
|
1056
|
+
removeNullValues,
|
|
1057
|
+
prepareKeyValuePairsForStorage,
|
|
1058
|
+
applyMerge,
|
|
1059
|
+
initializeWithDefaultKeyStates,
|
|
1060
|
+
};
|
|
1061
|
+
exports.default = OnyxUtils;
|