react-native-onyx 1.0.119 → 1.0.120

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/README.md CHANGED
@@ -405,3 +405,10 @@ To continuously work on Onyx we have to set up a task that copies content to par
405
405
  3. Optional: run `npm run build` (if you're working or want to test on a non react-native project)
406
406
  - `npm link` would actually work outside of `react-native` and it can be used to link Onyx locally for a web only project
407
407
  4. Copy Onyx to consumer project's `node_modules/react-native-onyx`
408
+
409
+ # Automated Tests
410
+
411
+ There are Playwright e2e tests implemented for the web. To run them:
412
+
413
+ - `npm run e2e` to run the e2e tests
414
+ - or `npm run e2e-ui` to run the e2e tests in UI mode
@@ -11,6 +11,119 @@
11
11
  return /******/ (() => { // webpackBootstrap
12
12
  /******/ var __webpack_modules__ = ({
13
13
 
14
+ /***/ "./lib/ActiveClientManager/index.web.js":
15
+ /*!**********************************************!*\
16
+ !*** ./lib/ActiveClientManager/index.web.js ***!
17
+ \**********************************************/
18
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
19
+
20
+ "use strict";
21
+ __webpack_require__.r(__webpack_exports__);
22
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
23
+ /* harmony export */ "init": () => (/* binding */ init),
24
+ /* harmony export */ "isClientTheLeader": () => (/* binding */ isClientTheLeader),
25
+ /* harmony export */ "isReady": () => (/* binding */ isReady),
26
+ /* harmony export */ "subscribeToClientChange": () => (/* binding */ subscribeToClientChange)
27
+ /* harmony export */ });
28
+ /* harmony import */ var _Str__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ../Str */ "./lib/Str.js");
29
+ /* harmony import */ var _broadcast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../broadcast */ "./lib/broadcast/index.web.js");
30
+ /**
31
+ * When you have many tabs in one browser, the data of Onyx is shared between all of them. Since we persist write requests in Onyx, we need to ensure that
32
+ * only one tab is processing those saved requests or we would be duplicating data (or creating errors).
33
+ * This file ensures exactly that by tracking all the clientIDs connected, storing the most recent one last and it considers that last clientID the "leader".
34
+ */
35
+
36
+
37
+
38
+
39
+ const NEW_LEADER_MESSAGE = 'NEW_LEADER';
40
+ const REMOVED_LEADER_MESSAGE = 'REMOVE_LEADER';
41
+
42
+ const clientID = _Str__WEBPACK_IMPORTED_MODULE_0__.guid();
43
+ const subscribers = [];
44
+ let timestamp = null;
45
+
46
+ let activeClientID = null;
47
+ let setIsReady = () => {};
48
+ const isReadyPromise = new Promise((resolve) => {
49
+ setIsReady = resolve;
50
+ });
51
+
52
+ /**
53
+ * Determines when the client is ready. We need to wait both till we saved our ID in onyx AND the init method was called
54
+ * @returns {Promise}
55
+ */
56
+ function isReady() {
57
+ return isReadyPromise;
58
+ }
59
+
60
+ /**
61
+ * Returns a boolean indicating if the current client is the leader.
62
+ *
63
+ * @returns {Boolean}
64
+ */
65
+ function isClientTheLeader() {
66
+ return activeClientID === clientID;
67
+ }
68
+
69
+ /**
70
+ * Subscribes to when the client changes.
71
+ * @param {Function} callback
72
+ */
73
+ function subscribeToClientChange(callback) {
74
+ subscribers.push(callback);
75
+ }
76
+
77
+ /**
78
+ * Subscribe to the broadcast channel to listen for messages from other tabs, so that
79
+ * all tabs agree on who the leader is, which should always be the last tab to open.
80
+ */
81
+ function init() {
82
+ _broadcast__WEBPACK_IMPORTED_MODULE_1__.subscribe((message) => {
83
+ switch (message.data.type) {
84
+ case NEW_LEADER_MESSAGE:{
85
+ // Only update the active leader if the message received was from another
86
+ // tab that initialized after the current one; if the timestamps are the
87
+ // same, it uses the client ID to tie-break
88
+ const isTimestampEqual = timestamp === message.data.timestamp;
89
+ const isTimestampNewer = timestamp > message.data.timestamp;
90
+ if (isClientTheLeader() && (isTimestampNewer || isTimestampEqual && clientID > message.data.clientID)) {
91
+ return;
92
+ }
93
+ activeClientID = message.data.clientID;
94
+
95
+ subscribers.forEach((callback) => callback());
96
+ break;
97
+ }
98
+ case REMOVED_LEADER_MESSAGE:
99
+ activeClientID = clientID;
100
+ timestamp = Date.now();
101
+ _broadcast__WEBPACK_IMPORTED_MODULE_1__.sendMessage({ type: NEW_LEADER_MESSAGE, clientID, timestamp });
102
+ subscribers.forEach((callback) => callback());
103
+ break;
104
+ default:
105
+ break;}
106
+
107
+ });
108
+
109
+ activeClientID = clientID;
110
+ timestamp = Date.now();
111
+
112
+ _broadcast__WEBPACK_IMPORTED_MODULE_1__.sendMessage({ type: NEW_LEADER_MESSAGE, clientID, timestamp });
113
+ setIsReady();
114
+
115
+ window.addEventListener('beforeunload', () => {
116
+ if (!isClientTheLeader()) {
117
+ return;
118
+ }
119
+ _broadcast__WEBPACK_IMPORTED_MODULE_1__.sendMessage({ type: REMOVED_LEADER_MESSAGE, clientID });
120
+ });
121
+ }
122
+
123
+
124
+
125
+ /***/ }),
126
+
14
127
  /***/ "./lib/Logger.js":
15
128
  /*!***********************!*\
16
129
  !*** ./lib/Logger.js ***!
@@ -79,6 +192,8 @@ __webpack_require__.r(__webpack_exports__);
79
192
  /* harmony import */ var _createDeferredTask__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./createDeferredTask */ "./lib/createDeferredTask.js");
80
193
  /* harmony import */ var _metrics_PerformanceUtils__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./metrics/PerformanceUtils */ "./lib/metrics/PerformanceUtils.js");
81
194
  /* harmony import */ var _storage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./storage */ "./lib/storage/index.web.js");
195
+ /* harmony import */ var _broadcast__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(/*! ./broadcast */ "./lib/broadcast/index.web.js");
196
+ /* harmony import */ var _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./ActiveClientManager */ "./lib/ActiveClientManager/index.web.js");
82
197
  /* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./utils */ "./lib/utils.js");
83
198
  /* harmony import */ var _batch__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./batch */ "./lib/batch.js");
84
199
  /* eslint-disable no-continue */
@@ -93,6 +208,8 @@ __webpack_require__.r(__webpack_exports__);
93
208
 
94
209
 
95
210
 
211
+
212
+
96
213
  // Method constants
97
214
  const METHOD = {
98
215
  SET: 'set',
@@ -102,6 +219,8 @@ const METHOD = {
102
219
  CLEAR: 'clear'
103
220
  };
104
221
 
222
+ const ON_CLEAR = 'on_clear';
223
+
105
224
  // Key/value store of Onyx key and arrays of values to merge
106
225
  const mergeQueue = {};
107
226
  const mergeQueuePromise = {};
@@ -132,6 +251,12 @@ let defaultKeyStates = {};
132
251
  // Connections can be made before `Onyx.init`. They would wait for this task before resolving
133
252
  const deferredInitTask = (0,_createDeferredTask__WEBPACK_IMPORTED_MODULE_2__["default"])();
134
253
 
254
+ // The promise of the clear function, saved so that no writes happen while it's executing
255
+ let isClearing = false;
256
+
257
+ // Callback to be executed after the clear execution ends
258
+ let onClearCallback = null;
259
+
135
260
  let batchUpdatesPromise = null;
136
261
  let batchUpdatesQueue = [];
137
262
 
@@ -1143,6 +1268,15 @@ function removeNullValues(key, value) {
1143
1268
  * @returns {Promise}
1144
1269
  */
1145
1270
  function set(key, value) {
1271
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1272
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.sendMessage({ type: METHOD.SET, key, value });
1273
+ return Promise.resolve();
1274
+ }
1275
+
1276
+ if (isClearing) {
1277
+ return Promise.resolve();
1278
+ }
1279
+
1146
1280
  const valueWithoutNull = removeNullValues(key, value);
1147
1281
 
1148
1282
  if (valueWithoutNull === null) {
@@ -1189,6 +1323,15 @@ function prepareKeyValuePairsForStorage(data) {
1189
1323
  * @returns {Promise}
1190
1324
  */
1191
1325
  function multiSet(data) {
1326
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1327
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.sendMessage({ type: METHOD.MULTI_SET, data });
1328
+ return Promise.resolve();
1329
+ }
1330
+
1331
+ if (isClearing) {
1332
+ return Promise.resolve();
1333
+ }
1334
+
1192
1335
  const keyValuePairs = prepareKeyValuePairsForStorage(data);
1193
1336
 
1194
1337
  const updatePromises = underscore__WEBPACK_IMPORTED_MODULE_1___default().map(data, (val, key) => {
@@ -1259,6 +1402,15 @@ function applyMerge(existingValue, changes, shouldRemoveNullObjectValues) {
1259
1402
  * @returns {Promise}
1260
1403
  */
1261
1404
  function merge(key, changes) {
1405
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1406
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.sendMessage({ type: METHOD.MERGE, key, changes });
1407
+ return Promise.resolve();
1408
+ }
1409
+
1410
+ if (isClearing) {
1411
+ return Promise.resolve();
1412
+ }
1413
+
1262
1414
  // Top-level undefined values are ignored
1263
1415
  // Therefore we need to prevent adding them to the merge queue
1264
1416
  if (underscore__WEBPACK_IMPORTED_MODULE_1___default().isUndefined(changes)) {
@@ -1313,7 +1465,7 @@ function merge(key, changes) {
1313
1465
  const updatePromise = broadcastUpdate(key, modifiedData, hasChanged, 'merge');
1314
1466
 
1315
1467
  // If the value has not changed, calling Storage.setItem() would be redundant and a waste of performance, so return early instead.
1316
- if (!hasChanged) {
1468
+ if (!hasChanged || isClearing) {
1317
1469
  return updatePromise;
1318
1470
  }
1319
1471
 
@@ -1367,6 +1519,17 @@ function initializeWithDefaultKeyStates() {
1367
1519
  * @returns {Promise<void>}
1368
1520
  */
1369
1521
  function clear() {let keysToPreserve = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
1522
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1523
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.sendMessage({ type: METHOD.CLEAR, keysToPreserve });
1524
+ return Promise.resolve();
1525
+ }
1526
+
1527
+ if (isClearing) {
1528
+ return Promise.resolve();
1529
+ }
1530
+
1531
+ isClearing = true;
1532
+
1370
1533
  return getAllKeys().
1371
1534
  then((keys) => {
1372
1535
  const keysToBeClearedFromStorage = [];
@@ -1425,7 +1588,11 @@ function clear() {let keysToPreserve = arguments.length > 0 && arguments[0] !==
1425
1588
 
1426
1589
  // Remove only the items that we want cleared from storage, and reset others to default
1427
1590
  underscore__WEBPACK_IMPORTED_MODULE_1___default().each(keysToBeClearedFromStorage, (key) => _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].drop(key));
1428
- return _storage__WEBPACK_IMPORTED_MODULE_5__["default"].removeItems(keysToBeClearedFromStorage).then(() => _storage__WEBPACK_IMPORTED_MODULE_5__["default"].multiSet(defaultKeyValuePairs)).then(() => Promise.all(updatePromises));
1591
+ return _storage__WEBPACK_IMPORTED_MODULE_5__["default"].removeItems(keysToBeClearedFromStorage).then(() => _storage__WEBPACK_IMPORTED_MODULE_5__["default"].multiSet(defaultKeyValuePairs)).then(() => {
1592
+ isClearing = false;
1593
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.sendMessage({ type: METHOD.CLEAR, keysToPreserve });
1594
+ return Promise.all(updatePromises);
1595
+ });
1429
1596
  });
1430
1597
  }
1431
1598
 
@@ -1575,6 +1742,48 @@ function setMemoryOnlyKeys(keyList) {
1575
1742
  _OnyxCache__WEBPACK_IMPORTED_MODULE_4__["default"].setRecentKeysLimit(Infinity);
1576
1743
  }
1577
1744
 
1745
+ /**
1746
+ * Sets the callback to be called when the clear finishes executing.
1747
+ * @param {Function} callback
1748
+ */
1749
+ function onClear(callback) {
1750
+ onClearCallback = callback;
1751
+ }
1752
+
1753
+ /**
1754
+ * Subscribes to the Broadcast channel and executes actions based on the
1755
+ * types of events.
1756
+ */
1757
+ function subscribeToEvents() {
1758
+ _broadcast__WEBPACK_IMPORTED_MODULE_11__.subscribe((_ref5) => {let { data } = _ref5;
1759
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1760
+ return;
1761
+ }
1762
+ switch (data.type) {
1763
+ case METHOD.CLEAR:
1764
+ clear(data.keysToPreserve);
1765
+ break;
1766
+ case METHOD.SET:
1767
+ set(data.key, data.value);
1768
+ break;
1769
+ case METHOD.MULTI_SET:
1770
+ multiSet(data.key, data.value);
1771
+ break;
1772
+ case METHOD.MERGE:
1773
+ merge(data.key, data.changes);
1774
+ break;
1775
+ case ON_CLEAR:
1776
+ if (!onClearCallback) {
1777
+ break;
1778
+ }
1779
+ onClearCallback();
1780
+ break;
1781
+ default:
1782
+ break;}
1783
+
1784
+ });
1785
+ }
1786
+
1578
1787
  /**
1579
1788
  * Initialize the store with actions and listening for storage events
1580
1789
  *
@@ -1609,6 +1818,15 @@ function init()
1609
1818
 
1610
1819
 
1611
1820
  {let { keys = {}, initialKeyStates = {}, safeEvictionKeys = [], maxCachedKeysCount = 1000, captureMetrics = false, shouldSyncMultipleInstances = Boolean(__webpack_require__.g.localStorage), debugSetState = false } = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
1821
+ _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.init();
1822
+
1823
+ _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isReady().then(() => {
1824
+ if (!_ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader()) {
1825
+ return;
1826
+ }
1827
+ subscribeToEvents();
1828
+ });
1829
+
1612
1830
  if (captureMetrics) {
1613
1831
  // The code here is only bundled and applied when the captureMetrics is set
1614
1832
  // eslint-disable-next-line no-use-before-define
@@ -1670,7 +1888,11 @@ const Onyx = {
1670
1888
  METHOD,
1671
1889
  setMemoryOnlyKeys,
1672
1890
  tryGetCachedValue,
1673
- hasPendingMergeForKey
1891
+ hasPendingMergeForKey,
1892
+ onClear,
1893
+ isClientManagerReady: _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isReady,
1894
+ isClientTheLeader: _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.isClientTheLeader,
1895
+ subscribeToClientChange: _ActiveClientManager__WEBPACK_IMPORTED_MODULE_10__.subscribeToClientChange
1674
1896
  };
1675
1897
 
1676
1898
  /**
@@ -1967,6 +2189,7 @@ const instance = new OnyxCache();
1967
2189
  "use strict";
1968
2190
  __webpack_require__.r(__webpack_exports__);
1969
2191
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2192
+ /* harmony export */ "guid": () => (/* binding */ guid),
1970
2193
  /* harmony export */ "result": () => (/* binding */ result),
1971
2194
  /* harmony export */ "startsWith": () => (/* binding */ startsWith)
1972
2195
  /* harmony export */ });
@@ -2000,6 +2223,21 @@ function result(parameter) {for (var _len = arguments.length, args = new Array(_
2000
2223
  return underscore__WEBPACK_IMPORTED_MODULE_0___default().isFunction(parameter) ? parameter(...args) : parameter;
2001
2224
  }
2002
2225
 
2226
+ /**
2227
+ * A simple GUID generator taken from https://stackoverflow.com/a/32760401/9114791
2228
+ *
2229
+ * @param {String} [prefix] an optional prefix to put in front of the guid
2230
+ * @returns {String}
2231
+ */
2232
+ function guid() {let prefix = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
2233
+ function s4() {
2234
+ return Math.floor((1 + Math.random()) * 0x10000).
2235
+ toString(16).
2236
+ substring(1);
2237
+ }
2238
+ return `${prefix}${s4()}${s4()}-${s4()}-${s4()}-${s4()}-${s4()}${s4()}${s4()}`;
2239
+ }
2240
+
2003
2241
 
2004
2242
 
2005
2243
  /***/ }),
@@ -2021,6 +2259,55 @@ __webpack_require__.r(__webpack_exports__);
2021
2259
 
2022
2260
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (react_dom__WEBPACK_IMPORTED_MODULE_0__.unstable_batchedUpdates);
2023
2261
 
2262
+ /***/ }),
2263
+
2264
+ /***/ "./lib/broadcast/index.web.js":
2265
+ /*!************************************!*\
2266
+ !*** ./lib/broadcast/index.web.js ***!
2267
+ \************************************/
2268
+ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
2269
+
2270
+ "use strict";
2271
+ __webpack_require__.r(__webpack_exports__);
2272
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
2273
+ /* harmony export */ "disconnect": () => (/* binding */ disconnect),
2274
+ /* harmony export */ "sendMessage": () => (/* binding */ sendMessage),
2275
+ /* harmony export */ "subscribe": () => (/* binding */ subscribe)
2276
+ /* harmony export */ });
2277
+ const BROADCAST_ONYX = 'BROADCAST_ONYX';
2278
+
2279
+ const subscriptions = [];
2280
+ const channel = new BroadcastChannel(BROADCAST_ONYX);
2281
+
2282
+ /**
2283
+ * Sends a message to the broadcast channel.
2284
+ * @param {String} message
2285
+ */
2286
+ function sendMessage(message) {
2287
+ channel.postMessage(message);
2288
+ }
2289
+
2290
+ /**
2291
+ * Subscribes to the broadcast channel. Every time a new message
2292
+ * is received, the callback is called.
2293
+ * @param {Function} callback
2294
+ */
2295
+ function subscribe(callback) {
2296
+ subscriptions.push(callback);
2297
+ channel.onmessage = (message) => {
2298
+ subscriptions.forEach((c) => c(message));
2299
+ };
2300
+ }
2301
+
2302
+ /**
2303
+ * Disconnects from the broadcast channel.
2304
+ */
2305
+ function disconnect() {
2306
+ channel.close();
2307
+ }
2308
+
2309
+
2310
+
2024
2311
  /***/ }),
2025
2312
 
2026
2313
  /***/ "./lib/createDeferredTask.js":
@@ -2227,7 +2514,7 @@ const webStorage = {
2227
2514
  this.clear = () => {
2228
2515
  let allKeys;
2229
2516
 
2230
- // They keys must be retreived before storage is cleared or else the list of keys would be empty
2517
+ // The keys must be retrieved before storage is cleared or else the list of keys would be empty
2231
2518
  return _providers_IDBKeyVal__WEBPACK_IMPORTED_MODULE_1__["default"].getAllKeys().
2232
2519
  then((keys) => {
2233
2520
  allKeys = keys;