react-native-onyx 1.0.63 → 1.0.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/web.development.js +54 -9
- package/dist/web.development.js.map +1 -1
- package/dist/web.min.js +1 -1
- package/dist/web.min.js.map +1 -1
- package/lib/Onyx.js +33 -7
- package/lib/storage/__mocks__/index.js +4 -0
- package/lib/storage/providers/IDBKeyVal.js +19 -0
- package/lib/storage/providers/SQLiteStorage.js +17 -0
- package/package.json +7 -2
package/lib/Onyx.js
CHANGED
|
@@ -14,6 +14,7 @@ const METHOD = {
|
|
|
14
14
|
SET: 'set',
|
|
15
15
|
MERGE: 'merge',
|
|
16
16
|
MERGE_COLLECTION: 'mergecollection',
|
|
17
|
+
MULTI_SET: 'multiset',
|
|
17
18
|
CLEAR: 'clear',
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -844,6 +845,20 @@ function remove(key) {
|
|
|
844
845
|
return Storage.removeItem(key);
|
|
845
846
|
}
|
|
846
847
|
|
|
848
|
+
/**
|
|
849
|
+
* @private
|
|
850
|
+
* @returns {Promise<void>}
|
|
851
|
+
*/
|
|
852
|
+
function reportStorageQuota() {
|
|
853
|
+
return Storage.getDatabaseSize()
|
|
854
|
+
.then(({bytesUsed, bytesRemaining}) => {
|
|
855
|
+
Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}`);
|
|
856
|
+
})
|
|
857
|
+
.catch((dbSizeError) => {
|
|
858
|
+
Logger.logAlert(`Unable to get database size. Error: ${dbSizeError}`);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
|
|
847
862
|
/**
|
|
848
863
|
* If we fail to set or merge we must handle this by
|
|
849
864
|
* evicting some data from Onyx and then retrying to do
|
|
@@ -856,7 +871,7 @@ function remove(key) {
|
|
|
856
871
|
* @return {Promise}
|
|
857
872
|
*/
|
|
858
873
|
function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
859
|
-
Logger.logInfo(`
|
|
874
|
+
Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
|
|
860
875
|
|
|
861
876
|
if (error && Str.startsWith(error.message, 'Failed to execute \'put\' on \'IDBObjectStore\'')) {
|
|
862
877
|
Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
|
|
@@ -865,14 +880,17 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
|
865
880
|
|
|
866
881
|
// Find the first key that we can remove that has no subscribers in our blocklist
|
|
867
882
|
const keyForRemoval = _.find(recentlyAccessedKeys, key => !evictionBlocklist[key]);
|
|
868
|
-
|
|
869
883
|
if (!keyForRemoval) {
|
|
884
|
+
// If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
|
|
885
|
+
// 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
|
|
886
|
+
// will allow this write to be skipped.
|
|
870
887
|
Logger.logAlert('Out of storage. But found no acceptable keys to remove.');
|
|
871
|
-
|
|
888
|
+
return reportStorageQuota();
|
|
872
889
|
}
|
|
873
890
|
|
|
874
891
|
// Remove the least recently viewed key that is not currently being accessed and retry.
|
|
875
892
|
Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
|
|
893
|
+
reportStorageQuota();
|
|
876
894
|
return remove(keyForRemoval)
|
|
877
895
|
.then(() => onyxMethod(...args));
|
|
878
896
|
}
|
|
@@ -1268,16 +1286,21 @@ function mergeCollection(collectionKey, collection) {
|
|
|
1268
1286
|
/**
|
|
1269
1287
|
* Insert API responses and lifecycle data into Onyx
|
|
1270
1288
|
*
|
|
1271
|
-
* @param {Array} data An array of objects with shape {onyxMethod: oneOf('set', 'merge', 'mergeCollection'), key: string, value: *}
|
|
1289
|
+
* @param {Array} data An array of objects with shape {onyxMethod: oneOf('set', 'merge', 'mergeCollection', 'multiSet', 'clear'), key: string, value: *}
|
|
1272
1290
|
* @returns {Promise} resolves when all operations are complete
|
|
1273
1291
|
*/
|
|
1274
1292
|
function update(data) {
|
|
1275
1293
|
// First, validate the Onyx object is in the format we expect
|
|
1276
|
-
_.each(data, ({onyxMethod, key}) => {
|
|
1277
|
-
if (!_.contains([METHOD.CLEAR, METHOD.SET, METHOD.MERGE, METHOD.MERGE_COLLECTION], onyxMethod)) {
|
|
1294
|
+
_.each(data, ({onyxMethod, key, value}) => {
|
|
1295
|
+
if (!_.contains([METHOD.CLEAR, METHOD.SET, METHOD.MERGE, METHOD.MERGE_COLLECTION, METHOD.MULTI_SET], onyxMethod)) {
|
|
1278
1296
|
throw new Error(`Invalid onyxMethod ${onyxMethod} in Onyx update.`);
|
|
1279
1297
|
}
|
|
1280
|
-
if (onyxMethod
|
|
1298
|
+
if (onyxMethod === METHOD.MULTI_SET) {
|
|
1299
|
+
// For multiset, we just expect the value to be an object
|
|
1300
|
+
if (!_.isObject(value) || _.isArray(value) || _.isFunction(value)) {
|
|
1301
|
+
throw new Error('Invalid value provided in Onyx multiSet. Onyx multiSet value must be of type object.');
|
|
1302
|
+
}
|
|
1303
|
+
} else if (onyxMethod !== METHOD.CLEAR && !_.isString(key)) {
|
|
1281
1304
|
throw new Error(`Invalid ${typeof key} key provided in Onyx update. Onyx key must be of type string.`);
|
|
1282
1305
|
}
|
|
1283
1306
|
});
|
|
@@ -1296,6 +1319,9 @@ function update(data) {
|
|
|
1296
1319
|
case METHOD.MERGE_COLLECTION:
|
|
1297
1320
|
promises.push(() => mergeCollection(key, value));
|
|
1298
1321
|
break;
|
|
1322
|
+
case METHOD.MULTI_SET:
|
|
1323
|
+
promises.push(() => multiSet(value));
|
|
1324
|
+
break;
|
|
1299
1325
|
case METHOD.CLEAR:
|
|
1300
1326
|
clearPromise = clear();
|
|
1301
1327
|
break;
|
|
@@ -55,6 +55,9 @@ const idbKeyvalMock = {
|
|
|
55
55
|
return Promise.resolve(_.keys(storageMapInternal));
|
|
56
56
|
},
|
|
57
57
|
config() {},
|
|
58
|
+
getDatabaseSize() {
|
|
59
|
+
return Promise.resolve({bytesRemaining: 0, bytesUsed: 99999});
|
|
60
|
+
},
|
|
58
61
|
};
|
|
59
62
|
|
|
60
63
|
const idbKeyvalMockSpy = {
|
|
@@ -74,6 +77,7 @@ const idbKeyvalMockSpy = {
|
|
|
74
77
|
setInitialMockData: jest.fn((data) => {
|
|
75
78
|
storageMapInternal = data;
|
|
76
79
|
}),
|
|
80
|
+
getDatabaseSize: jest.fn(idbKeyvalMock.getDatabaseSize),
|
|
77
81
|
};
|
|
78
82
|
|
|
79
83
|
export default idbKeyvalMockSpy;
|
|
@@ -113,6 +113,25 @@ const provider = {
|
|
|
113
113
|
* @returns {Promise}
|
|
114
114
|
*/
|
|
115
115
|
removeItems: keysParam => delMany(keysParam, getCustomStore()),
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Gets the total bytes of the database file
|
|
119
|
+
* @returns {Promise<number>}
|
|
120
|
+
*/
|
|
121
|
+
getDatabaseSize() {
|
|
122
|
+
if (!window.navigator || !window.navigator.storage) {
|
|
123
|
+
throw new Error('StorageManager browser API unavailable');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return window.navigator.storage.estimate()
|
|
127
|
+
.then(value => ({
|
|
128
|
+
bytesUsed: value.usage,
|
|
129
|
+
bytesRemaining: value.quota - value.usage,
|
|
130
|
+
}))
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
throw new Error(`Unable to estimate web storage quota. Original error: ${error}`);
|
|
133
|
+
});
|
|
134
|
+
},
|
|
116
135
|
};
|
|
117
136
|
|
|
118
137
|
export default provider;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* converting the value to a JSON string
|
|
4
4
|
*/
|
|
5
5
|
import {open} from 'react-native-quick-sqlite';
|
|
6
|
+
import {getFreeDiskStorage} from 'react-native-device-info';
|
|
6
7
|
import _ from 'underscore';
|
|
7
8
|
|
|
8
9
|
const DB_NAME = 'OnyxDB';
|
|
@@ -143,6 +144,22 @@ const provider = {
|
|
|
143
144
|
* Noop on mobile for now.
|
|
144
145
|
*/
|
|
145
146
|
setMemoryOnlyKeys: () => {},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Gets the total bytes of the database file
|
|
150
|
+
* @returns {Promise}
|
|
151
|
+
*/
|
|
152
|
+
getDatabaseSize() {
|
|
153
|
+
return Promise.all([db.executeAsync('PRAGMA page_size;'), db.executeAsync('PRAGMA page_count;'), getFreeDiskStorage()])
|
|
154
|
+
.then(([pageSizeResult, pageCountResult, bytesRemaining]) => {
|
|
155
|
+
const pageSize = pageSizeResult.rows.item(0).page_size;
|
|
156
|
+
const pageCount = pageCountResult.rows.item(0).page_count;
|
|
157
|
+
return {
|
|
158
|
+
bytesUsed: pageSize * pageCount,
|
|
159
|
+
bytesRemaining,
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
},
|
|
146
163
|
};
|
|
147
164
|
|
|
148
165
|
export default provider;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-onyx",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.65",
|
|
4
4
|
"author": "Expensify, Inc.",
|
|
5
5
|
"homepage": "https://expensify.com",
|
|
6
6
|
"description": "State management for React Native",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"prop-types": "^15.7.2",
|
|
68
68
|
"react": "18.2.0",
|
|
69
69
|
"react-native": "0.71.2",
|
|
70
|
+
"react-native-device-info": "^10.3.0",
|
|
70
71
|
"react-native-performance": "^2.0.0",
|
|
71
72
|
"react-native-quick-sqlite": "^8.0.0-beta.2",
|
|
72
73
|
"react-test-renderer": "18.1.0",
|
|
@@ -79,7 +80,8 @@
|
|
|
79
80
|
"idb-keyval": "^6.2.1",
|
|
80
81
|
"react": ">=18.1.0",
|
|
81
82
|
"react-native-performance": "^4.0.0",
|
|
82
|
-
"react-native-quick-sqlite": "^8.0.0-beta.2"
|
|
83
|
+
"react-native-quick-sqlite": "^8.0.0-beta.2",
|
|
84
|
+
"react-native-device-info": "^10.3.0"
|
|
83
85
|
},
|
|
84
86
|
"peerDependenciesMeta": {
|
|
85
87
|
"idb-keyval": {
|
|
@@ -90,6 +92,9 @@
|
|
|
90
92
|
},
|
|
91
93
|
"react-native-quick-sqlite": {
|
|
92
94
|
"optional": true
|
|
95
|
+
},
|
|
96
|
+
"react-native-device-info": {
|
|
97
|
+
"optional": true
|
|
93
98
|
}
|
|
94
99
|
},
|
|
95
100
|
"engines": {
|