react-native-onyx 1.0.1 → 1.0.4

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/lib/Onyx.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import _ from 'underscore';
2
2
  import Str from 'expensify-common/lib/str';
3
3
  import lodashMerge from 'lodash/merge';
4
+ import lodashGet from 'lodash/get';
4
5
  import Storage from './storage';
5
-
6
- import {registerLogger, logInfo, logAlert} from './Logger';
6
+ import * as Logger from './Logger';
7
7
  import cache from './OnyxCache';
8
8
  import createDeferredTask from './createDeferredTask';
9
9
 
@@ -59,7 +59,7 @@ function get(key) {
59
59
  cache.set(key, val);
60
60
  return val;
61
61
  })
62
- .catch(err => logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
62
+ .catch(err => Logger.logInfo(`Unable to get item from persistent storage. Key: ${key} Error: ${err}`));
63
63
 
64
64
  return cache.captureTask(taskName, promise);
65
65
  }
@@ -208,9 +208,10 @@ function addAllSafeEvictionKeysToRecentlyAccessedList() {
208
208
  .then((keys) => {
209
209
  _.each(evictionAllowList, (safeEvictionKey) => {
210
210
  _.each(keys, (key) => {
211
- if (isKeyMatch(safeEvictionKey, key)) {
212
- addLastAccessedKey(key);
211
+ if (!isKeyMatch(safeEvictionKey, key)) {
212
+ return;
213
213
  }
214
+ addLastAccessedKey(key);
214
215
  });
215
216
  });
216
217
  });
@@ -258,7 +259,6 @@ function keysChanged(collectionKey, collection) {
258
259
 
259
260
  if (isSubscribedToCollectionKey) {
260
261
  if (_.isFunction(subscriber.callback)) {
261
- // eslint-disable-next-line no-use-before-define
262
262
  const cachedCollection = getCachedCollection(collectionKey);
263
263
  _.each(collection, (data, dataKey) => {
264
264
  subscriber.callback(cachedCollection[dataKey], dataKey);
@@ -317,29 +317,31 @@ function keyChanged(key, data) {
317
317
 
318
318
  // Find all subscribers that were added with connect() and trigger the callback or setState() with the new data
319
319
  _.each(callbackToStateMapping, (subscriber) => {
320
- if (subscriber && isKeyMatch(subscriber.key, key)) {
321
- if (_.isFunction(subscriber.callback)) {
322
- subscriber.callback(data, key);
323
- }
320
+ if (!subscriber || !isKeyMatch(subscriber.key, key)) {
321
+ return;
322
+ }
324
323
 
325
- if (!subscriber.withOnyxInstance) {
326
- return;
327
- }
324
+ if (_.isFunction(subscriber.callback)) {
325
+ subscriber.callback(data, key);
326
+ }
328
327
 
329
- // Check if we are subscribing to a collection key and add this item as a collection
330
- if (isCollectionKey(subscriber.key)) {
331
- subscriber.withOnyxInstance.setState((prevState) => {
332
- const collection = _.clone(prevState[subscriber.statePropertyName] || {});
333
- collection[key] = data;
334
- return {
335
- [subscriber.statePropertyName]: collection,
336
- };
337
- });
338
- } else {
339
- subscriber.withOnyxInstance.setState({
340
- [subscriber.statePropertyName]: data,
341
- });
342
- }
328
+ if (!subscriber.withOnyxInstance) {
329
+ return;
330
+ }
331
+
332
+ // Check if we are subscribing to a collection key and add this item as a collection
333
+ if (isCollectionKey(subscriber.key)) {
334
+ subscriber.withOnyxInstance.setState((prevState) => {
335
+ const collection = _.clone(prevState[subscriber.statePropertyName] || {});
336
+ collection[key] = data;
337
+ return {
338
+ [subscriber.statePropertyName]: collection,
339
+ };
340
+ });
341
+ } else {
342
+ subscriber.withOnyxInstance.setState({
343
+ [subscriber.statePropertyName]: data,
344
+ });
343
345
  }
344
346
  });
345
347
  }
@@ -404,21 +406,23 @@ function connect(mapping) {
404
406
  deferredInitTask.promise
405
407
  .then(() => {
406
408
  // Check to see if this key is flagged as a safe eviction key and add it to the recentlyAccessedKeys list
407
- if (isSafeEvictionKey(mapping.key)) {
408
- // Try to free some cache whenever we connect to a safe eviction key
409
- cache.removeLeastRecentlyUsedKeys();
410
-
411
- if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
412
- // All React components subscribing to a key flagged as a safe eviction
413
- // key must implement the canEvict property.
414
- if (_.isUndefined(mapping.canEvict)) {
415
- throw new Error(
416
- `Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`
417
- );
418
- }
409
+ if (!isSafeEvictionKey(mapping.key)) {
410
+ return;
411
+ }
419
412
 
420
- addLastAccessedKey(mapping.key);
413
+ // Try to free some cache whenever we connect to a safe eviction key
414
+ cache.removeLeastRecentlyUsedKeys();
415
+
416
+ if (mapping.withOnyxInstance && !isCollectionKey(mapping.key)) {
417
+ // All React components subscribing to a key flagged as a safe eviction
418
+ // key must implement the canEvict property.
419
+ if (_.isUndefined(mapping.canEvict)) {
420
+ throw new Error(
421
+ `Cannot subscribe to safe eviction key '${mapping.key}' without providing a canEvict value.`,
422
+ );
421
423
  }
424
+
425
+ addLastAccessedKey(mapping.key);
422
426
  }
423
427
  })
424
428
  .then(getAllKeys)
@@ -505,10 +509,10 @@ function remove(key) {
505
509
  * @return {Promise}
506
510
  */
507
511
  function evictStorageAndRetry(error, onyxMethod, ...args) {
508
- logInfo(`Handled error: ${error}`);
512
+ Logger.logInfo(`Handled error: ${error}`);
509
513
 
510
514
  if (error && Str.startsWith(error.message, 'Failed to execute \'put\' on \'IDBObjectStore\'')) {
511
- logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
515
+ Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
512
516
  throw error;
513
517
  }
514
518
 
@@ -516,12 +520,12 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
516
520
  const keyForRemoval = _.find(recentlyAccessedKeys, key => !evictionBlocklist[key]);
517
521
 
518
522
  if (!keyForRemoval) {
519
- logAlert('Out of storage. But found no acceptable keys to remove.');
523
+ Logger.logAlert('Out of storage. But found no acceptable keys to remove.');
520
524
  throw error;
521
525
  }
522
526
 
523
527
  // Remove the least recently viewed key that is not currently being accessed and retry.
524
- logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
528
+ Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
525
529
  return remove(keyForRemoval)
526
530
  .then(() => onyxMethod(...args));
527
531
  }
@@ -536,12 +540,11 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
536
540
  */
537
541
  function set(key, value) {
538
542
  // Logging properties only since values could be sensitive things we don't want to log
539
- logInfo(`set() called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''}`);
543
+ Logger.logInfo(`set() called for key: ${key}${_.isObject(value) ? ` properties: ${_.keys(value).join(',')}` : ''}`);
540
544
 
541
545
  // eslint-disable-next-line no-use-before-define
542
546
  if (hasPendingMergeForKey(key)) {
543
- // eslint-disable-next-line max-len
544
- logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
547
+ Logger.logAlert(`Onyx.set() called after Onyx.merge() for key: ${key}. It is recommended to use set() or merge() not both.`);
545
548
  }
546
549
 
547
550
  // Adds the key to cache when it's not available
@@ -680,7 +683,7 @@ function merge(key, value) {
680
683
 
681
684
  return set(key, modifiedData);
682
685
  } catch (error) {
683
- logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
686
+ Logger.logAlert(`An error occurred while applying merge for key: ${key}, Error: ${error}`);
684
687
  }
685
688
 
686
689
  return Promise.resolve();
@@ -706,18 +709,36 @@ function initializeWithDefaultKeyStates() {
706
709
  /**
707
710
  * Clear out all the data in the store
708
711
  *
712
+ * Note that calling Onyx.clear() and then Onyx.set() on a key with a default
713
+ * key state may store an unexpected value in Storage.
714
+ *
715
+ * E.g.
716
+ * Onyx.clear();
717
+ * Onyx.set(ONYXKEYS.DEFAULT_KEY, 'default');
718
+ * Storage.getItem(ONYXKEYS.DEFAULT_KEY)
719
+ * .then((storedValue) => console.log(storedValue));
720
+ * null is logged instead of the expected 'default'
721
+ *
722
+ * Onyx.set() might call Storage.setItem() before Onyx.clear() calls
723
+ * Storage.setItem(). Use Onyx.merge() instead if possible. Onyx.merge() calls
724
+ * Onyx.get(key) before calling Storage.setItem() via Onyx.set().
725
+ * Storage.setItem() from Onyx.clear() will have already finished and the merged
726
+ * value will be saved to storage after the default value.
727
+ *
709
728
  * @returns {Promise<void>}
710
729
  */
711
730
  function clear() {
712
731
  return getAllKeys()
713
732
  .then((keys) => {
714
733
  _.each(keys, (key) => {
715
- keyChanged(key, null);
716
- cache.set(key, null);
734
+ const resetValue = lodashGet(defaultKeyStates, key, null);
735
+ cache.set(key, resetValue);
736
+
737
+ // Optimistically inform subscribers on the next tick
738
+ Promise.resolve().then(() => keyChanged(key, resetValue));
717
739
  });
718
- })
719
- .then(Storage.clear)
720
- .then(initializeWithDefaultKeyStates);
740
+ Storage.clear();
741
+ });
721
742
  }
722
743
 
723
744
  /**
@@ -737,10 +758,11 @@ function clear() {
737
758
  function mergeCollection(collectionKey, collection) {
738
759
  // Confirm all the collection keys belong to the same parent
739
760
  _.each(collection, (data, dataKey) => {
740
- if (!isKeyMatch(collectionKey, dataKey)) {
741
- // eslint-disable-next-line max-len
742
- throw new Error(`Provided collection does not have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
761
+ if (isKeyMatch(collectionKey, dataKey)) {
762
+ return;
743
763
  }
764
+
765
+ throw new Error(`Provided collection doesn't have all its data belonging to the same parent. CollectionKey: ${collectionKey}, DataKey: ${dataKey}`);
744
766
  });
745
767
 
746
768
  return getAllKeys()
@@ -780,6 +802,36 @@ function mergeCollection(collectionKey, collection) {
780
802
  });
781
803
  }
782
804
 
805
+ /**
806
+ * Insert API responses and lifecycle data into Onyx
807
+ *
808
+ * @param {Array} data An array of objects with shape {onyxMethod: oneOf('set', 'merge'), key: string, value: *}
809
+ */
810
+ function update(data) {
811
+ // First, validate the Onyx object is in the format we expect
812
+ _.each(data, ({onyxMethod, key}) => {
813
+ if (!_.contains(['set', 'merge'], onyxMethod)) {
814
+ throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
815
+ }
816
+ if (!_.isString(key)) {
817
+ throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
818
+ }
819
+ });
820
+
821
+ _.each(data, ({onyxMethod, key, value}) => {
822
+ switch (onyxMethod) {
823
+ case 'set':
824
+ set(key, value);
825
+ break;
826
+ case 'merge':
827
+ merge(key, value);
828
+ break;
829
+ default:
830
+ break;
831
+ }
832
+ });
833
+ }
834
+
783
835
  /**
784
836
  * Initialize the store with actions and listening for storage events
785
837
  *
@@ -837,7 +889,7 @@ function init({
837
889
  // Initialize all of our keys with data provided then give green light to any pending connections
838
890
  Promise.all([
839
891
  addAllSafeEvictionKeysToRecentlyAccessedList(),
840
- initializeWithDefaultKeyStates()
892
+ initializeWithDefaultKeyStates(),
841
893
  ])
842
894
  .then(deferredInitTask.resolve);
843
895
 
@@ -856,9 +908,10 @@ const Onyx = {
856
908
  multiSet,
857
909
  merge,
858
910
  mergeCollection,
911
+ update,
859
912
  clear,
860
913
  init,
861
- registerLogger,
914
+ registerLogger: Logger.registerLogger,
862
915
  addToEvictionBlockList,
863
916
  removeFromEvictionBlockList,
864
917
  isSafeEvictionKey,
@@ -883,14 +936,18 @@ function applyDecorators() {
883
936
  mergeCollection = decorate.decorateWithMetrics(mergeCollection, 'Onyx:mergeCollection');
884
937
  getAllKeys = decorate.decorateWithMetrics(getAllKeys, 'Onyx:getAllKeys');
885
938
  initializeWithDefaultKeyStates = decorate.decorateWithMetrics(initializeWithDefaultKeyStates, 'Onyx:defaults');
939
+ update = decorate.decorateWithMetrics(update, 'Onyx:update');
886
940
  /* eslint-enable */
887
941
 
888
942
  // Re-expose decorated methods
943
+ /* eslint-disable rulesdir/prefer-actions-set-data */
889
944
  Onyx.set = set;
890
945
  Onyx.multiSet = multiSet;
891
946
  Onyx.clear = clear;
892
947
  Onyx.merge = merge;
893
948
  Onyx.mergeCollection = mergeCollection;
949
+ Onyx.update = update;
950
+ /* eslint-enable */
894
951
 
895
952
  // Expose stats methods on Onyx
896
953
  Onyx.getMetrics = decorate.getMetrics;
package/lib/OnyxCache.js CHANGED
@@ -1,7 +1,6 @@
1
1
  import _ from 'underscore';
2
2
  import lodashMerge from 'lodash/merge';
3
3
 
4
-
5
4
  const isDefined = _.negate(_.isUndefined);
6
5
 
7
6
  /**
@@ -43,7 +42,7 @@ class OnyxCache {
43
42
  this,
44
43
  'getAllKeys', 'getValue', 'hasCacheForKey', 'addKey', 'set', 'drop', 'merge',
45
44
  'hasPendingTask', 'getTaskPromise', 'captureTask', 'removeLeastRecentlyUsedKeys',
46
- 'setRecentKeysLimit'
45
+ 'setRecentKeysLimit',
47
46
  );
48
47
  }
49
48
 
@@ -171,14 +170,16 @@ class OnyxCache {
171
170
  * Remove keys that don't fall into the range of recently used keys
172
171
  */
173
172
  removeLeastRecentlyUsedKeys() {
174
- if (this.recentKeys.size > this.maxRecentKeysSize) {
175
- // Get the last N keys by doing a negative slice
176
- const recentlyAccessed = [...this.recentKeys].slice(-this.maxRecentKeysSize);
177
- const storageKeys = _.keys(this.storageMap);
178
- const keysToRemove = _.difference(storageKeys, recentlyAccessed);
179
-
180
- _.each(keysToRemove, this.drop);
173
+ if (this.recentKeys.size <= this.maxRecentKeysSize) {
174
+ return;
181
175
  }
176
+
177
+ // Get the last N keys by doing a negative slice
178
+ const recentlyAccessed = [...this.recentKeys].slice(-this.maxRecentKeysSize);
179
+ const storageKeys = _.keys(this.storageMap);
180
+ const keysToRemove = _.difference(storageKeys, recentlyAccessed);
181
+
182
+ _.each(keysToRemove, this.drop);
182
183
  }
183
184
 
184
185
  /**
package/lib/compose.js CHANGED
@@ -25,5 +25,6 @@ export default function compose(...funcs) {
25
25
  return funcs[0];
26
26
  }
27
27
 
28
+ // eslint-disable-next-line rulesdir/prefer-underscore-method
28
29
  return funcs.reduce((a, b) => (...args) => a(b(...args)));
29
30
  }
@@ -23,7 +23,7 @@ function measureMarkToNow(startMark, detail) {
23
23
  performance.measure(`${startMark.name} [${startMark.detail.args.toString()}]`, {
24
24
  start: startMark.startTime,
25
25
  end: performance.now(),
26
- detail: {...startMark.detail, ...detail}
26
+ detail: {...startMark.detail, ...detail},
27
27
  });
28
28
  }
29
29
 
@@ -193,12 +193,12 @@ function printMetrics({raw = false, format = 'console', methods} = {}) {
193
193
  title: methodName,
194
194
  heading: ['start time', 'end time', 'duration', 'args'],
195
195
  leftAlignedCols: [3],
196
- rows: calls.map(call => ([
196
+ rows: _.map(calls, call => ([
197
197
  toDuration(call.startTime - performance.timeOrigin, raw),
198
198
  toDuration((call.startTime + call.duration) - timeOrigin, raw),
199
199
  toDuration(call.duration, raw),
200
- call.detail.args.map(String).join(', ').slice(0, 60), // Restrict cell width to 60 chars max
201
- ]))
200
+ _.map(call.detail.args, String).join(', ').slice(0, 60), // Restrict cell width to 60 chars max
201
+ ])),
202
202
  });
203
203
  })
204
204
  .value();
@@ -206,7 +206,7 @@ function printMetrics({raw = false, format = 'console', methods} = {}) {
206
206
  if (/csv|json|string/i.test(format)) {
207
207
  const allTables = [tableSummary, ...methodCallTables];
208
208
 
209
- return allTables.map((table) => {
209
+ return _.map(allTables, (table) => {
210
210
  switch (format.toLowerCase()) {
211
211
  case 'csv':
212
212
  return table.toCSV();
@@ -219,7 +219,7 @@ function printMetrics({raw = false, format = 'console', methods} = {}) {
219
219
  }
220
220
 
221
221
  const lastComplete = lastCompleteCall && toDuration(
222
- (lastCompleteCall.startTime + lastCompleteCall.duration) - timeOrigin, raw
222
+ (lastCompleteCall.startTime + lastCompleteCall.duration) - timeOrigin, raw,
223
223
  );
224
224
 
225
225
  const mainOutput = [
@@ -227,7 +227,7 @@ function printMetrics({raw = false, format = 'console', methods} = {}) {
227
227
  ` - Total: ${toDuration(totalTime, raw)}`,
228
228
  ` - Last call finished at: ${lastComplete || 'N/A'}`,
229
229
  '',
230
- tableSummary.toString()
230
+ tableSummary.toString(),
231
231
  ];
232
232
 
233
233
  /* eslint-disable no-console */
@@ -10,7 +10,7 @@ import lodashMerge from 'lodash/merge';
10
10
  import SyncQueue from '../../SyncQueue';
11
11
 
12
12
  localforage.config({
13
- name: 'OnyxDB'
13
+ name: 'OnyxDB',
14
14
  });
15
15
 
16
16
  const provider = {
@@ -42,7 +42,7 @@ const provider = {
42
42
  const pairs = _.map(
43
43
  keys,
44
44
  key => localforage.getItem(key)
45
- .then(value => [key, value])
45
+ .then(value => [key, value]),
46
46
  );
47
47
 
48
48
  return Promise.all(pairs);
package/lib/withOnyx.js CHANGED
@@ -122,8 +122,7 @@ export default function (mapOnyxToState) {
122
122
  const key = Str.result(mapping.key, this.props);
123
123
 
124
124
  if (!Onyx.isSafeEvictionKey(key)) {
125
- // eslint-disable-next-line max-len
126
- throw new Error(`canEvict cannot be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`);
125
+ throw new Error(`canEvict can't be used on key '${key}'. This key must explicitly be flagged as safe for removal by adding it to Onyx.init({safeEvictionKeys: []}).`);
127
126
  }
128
127
 
129
128
  if (canEvict) {
@@ -147,6 +146,7 @@ export default function (mapOnyxToState) {
147
146
  connectMappingToOnyx(mapping, statePropertyName) {
148
147
  const key = Str.result(mapping.key, this.props);
149
148
 
149
+ // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs
150
150
  this.activeConnectionIDs[key] = Onyx.connect({
151
151
  ...mapping,
152
152
  key,
package/package.json CHANGED
@@ -1,11 +1,24 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "1.0.1",
3
+ "version": "1.0.4",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",
7
7
  "license": "MIT",
8
8
  "private": false,
9
+ "keywords": [
10
+ "React Native",
11
+ "React",
12
+ "Persistant storage",
13
+ "Pub/Sub"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Expensify/react-native-onyx.git"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/Expensify/react-native-onyx/issues"
21
+ },
9
22
  "files": [
10
23
  "dist/**/*",
11
24
  "lib/**/*",
@@ -22,7 +35,7 @@
22
35
  "lint": "eslint .",
23
36
  "lint-tests": "eslint tests/**",
24
37
  "test": "jest",
25
- "build:web": "webpack --config webpack.config.js",
38
+ "build": "webpack --config webpack.config.js",
26
39
  "build:docs": "node buildDocs.js"
27
40
  },
28
41
  "dependencies": {
@@ -45,7 +58,7 @@
45
58
  "babel-plugin-react-native-web": "^0.13.5",
46
59
  "babel-plugin-transform-class-properties": "^6.24.1",
47
60
  "eslint": "^7.6.0",
48
- "eslint-config-expensify": "^2.0.11",
61
+ "eslint-config-expensify": "^2.0.24",
49
62
  "expensify-common": "git+https://github.com/Expensify/expensify-common.git#427295da130a4eacc184d38693664280d020dffd",
50
63
  "jest": "^26.5.2",
51
64
  "jest-cli": "^26.5.2",
package/web.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  if (process.env.NODE_ENV === 'production') {
9
- module.exports = require('./dist/web.min.js');
9
+ module.exports = require('./dist/web.min');
10
10
  } else {
11
- module.exports = require('./dist/web.development.js');
11
+ module.exports = require('./dist/web.development');
12
12
  }