react-native-onyx 1.0.62 → 1.0.64
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 +59 -15
- 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 +20 -3
- package/lib/storage/__mocks__/index.js +4 -0
- package/lib/storage/providers/IDBKeyVal.js +37 -10
- package/lib/storage/providers/SQLiteStorage.js +17 -0
- package/package.json +7 -2
package/lib/Onyx.js
CHANGED
|
@@ -844,6 +844,20 @@ function remove(key) {
|
|
|
844
844
|
return Storage.removeItem(key);
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
/**
|
|
848
|
+
* @private
|
|
849
|
+
* @returns {Promise<void>}
|
|
850
|
+
*/
|
|
851
|
+
function reportStorageQuota() {
|
|
852
|
+
return Storage.getDatabaseSize()
|
|
853
|
+
.then(({bytesUsed, bytesRemaining}) => {
|
|
854
|
+
Logger.logInfo(`Storage Quota Check -- bytesUsed: ${bytesUsed} bytesRemaining: ${bytesRemaining}`);
|
|
855
|
+
})
|
|
856
|
+
.catch((dbSizeError) => {
|
|
857
|
+
Logger.logAlert(`Unable to get database size. Error: ${dbSizeError}`);
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
|
|
847
861
|
/**
|
|
848
862
|
* If we fail to set or merge we must handle this by
|
|
849
863
|
* evicting some data from Onyx and then retrying to do
|
|
@@ -856,7 +870,7 @@ function remove(key) {
|
|
|
856
870
|
* @return {Promise}
|
|
857
871
|
*/
|
|
858
872
|
function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
859
|
-
Logger.logInfo(`
|
|
873
|
+
Logger.logInfo(`Failed to save to storage. Error: ${error}. onyxMethod: ${onyxMethod.name}`);
|
|
860
874
|
|
|
861
875
|
if (error && Str.startsWith(error.message, 'Failed to execute \'put\' on \'IDBObjectStore\'')) {
|
|
862
876
|
Logger.logAlert('Attempted to set invalid data set in Onyx. Please ensure all data is serializable.');
|
|
@@ -865,14 +879,17 @@ function evictStorageAndRetry(error, onyxMethod, ...args) {
|
|
|
865
879
|
|
|
866
880
|
// Find the first key that we can remove that has no subscribers in our blocklist
|
|
867
881
|
const keyForRemoval = _.find(recentlyAccessedKeys, key => !evictionBlocklist[key]);
|
|
868
|
-
|
|
869
882
|
if (!keyForRemoval) {
|
|
883
|
+
// If we have no acceptable keys to remove then we are possibly trying to save mission critical data. If this is the case,
|
|
884
|
+
// 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
|
|
885
|
+
// will allow this write to be skipped.
|
|
870
886
|
Logger.logAlert('Out of storage. But found no acceptable keys to remove.');
|
|
871
|
-
|
|
887
|
+
return reportStorageQuota();
|
|
872
888
|
}
|
|
873
889
|
|
|
874
890
|
// Remove the least recently viewed key that is not currently being accessed and retry.
|
|
875
891
|
Logger.logInfo(`Out of storage. Evicting least recently accessed key (${keyForRemoval}) and retrying.`);
|
|
892
|
+
reportStorageQuota();
|
|
876
893
|
return remove(keyForRemoval)
|
|
877
894
|
.then(() => onyxMethod(...args));
|
|
878
895
|
}
|
|
@@ -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;
|
|
@@ -13,7 +13,15 @@ import {
|
|
|
13
13
|
import _ from 'underscore';
|
|
14
14
|
import fastMerge from '../../fastMerge';
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
// We don't want to initialize the store while the JS bundle loads as idb-keyval will try to use global.indexedDB
|
|
17
|
+
// which might not be available in certain environments that load the bundle (e.g. electron main process).
|
|
18
|
+
let customStoreInstance;
|
|
19
|
+
const getCustomStore = () => {
|
|
20
|
+
if (!customStoreInstance) {
|
|
21
|
+
customStoreInstance = createStore('OnyxDB', 'keyvaluepairs');
|
|
22
|
+
}
|
|
23
|
+
return customStoreInstance;
|
|
24
|
+
};
|
|
17
25
|
|
|
18
26
|
const provider = {
|
|
19
27
|
/**
|
|
@@ -22,7 +30,7 @@ const provider = {
|
|
|
22
30
|
* @param {*} value
|
|
23
31
|
* @return {Promise<void>}
|
|
24
32
|
*/
|
|
25
|
-
setItem: (key, value) => set(key, value,
|
|
33
|
+
setItem: (key, value) => set(key, value, getCustomStore()),
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* Get multiple key-value pairs for the give array of keys in a batch.
|
|
@@ -30,7 +38,7 @@ const provider = {
|
|
|
30
38
|
* @param {String[]} keysParam
|
|
31
39
|
* @return {Promise<Array<[key, value]>>}
|
|
32
40
|
*/
|
|
33
|
-
multiGet: keysParam => getMany(keysParam,
|
|
41
|
+
multiGet: keysParam => getMany(keysParam, getCustomStore())
|
|
34
42
|
.then(values => _.map(values, (value, index) => [keysParam[index], value])),
|
|
35
43
|
|
|
36
44
|
/**
|
|
@@ -38,7 +46,7 @@ const provider = {
|
|
|
38
46
|
* @param {Array<[key, value]>} pairs
|
|
39
47
|
* @return {Promise<void>}
|
|
40
48
|
*/
|
|
41
|
-
multiMerge: pairs =>
|
|
49
|
+
multiMerge: pairs => getCustomStore()('readwrite', (store) => {
|
|
42
50
|
// Note: we are using the manual store transaction here, to fit the read and update
|
|
43
51
|
// of the items in one transaction to achieve best performance.
|
|
44
52
|
|
|
@@ -70,33 +78,33 @@ const provider = {
|
|
|
70
78
|
* @param {Array<[key, value]>} pairs
|
|
71
79
|
* @return {Promise<void>}
|
|
72
80
|
*/
|
|
73
|
-
multiSet: pairs => setMany(pairs,
|
|
81
|
+
multiSet: pairs => setMany(pairs, getCustomStore()),
|
|
74
82
|
|
|
75
83
|
/**
|
|
76
84
|
* Clear everything from storage and also stops the SyncQueue from adding anything more to storage
|
|
77
85
|
* @returns {Promise<void>}
|
|
78
86
|
*/
|
|
79
|
-
clear: () => clear(
|
|
87
|
+
clear: () => clear(getCustomStore()),
|
|
80
88
|
|
|
81
89
|
/**
|
|
82
90
|
* Returns all keys available in storage
|
|
83
91
|
* @returns {Promise<String[]>}
|
|
84
92
|
*/
|
|
85
|
-
getAllKeys: () => keys(
|
|
93
|
+
getAllKeys: () => keys(getCustomStore()),
|
|
86
94
|
|
|
87
95
|
/**
|
|
88
96
|
* Get the value of a given key or return `null` if it's not available in storage
|
|
89
97
|
* @param {String} key
|
|
90
98
|
* @return {Promise<*>}
|
|
91
99
|
*/
|
|
92
|
-
getItem: key => get(key,
|
|
100
|
+
getItem: key => get(key, getCustomStore()),
|
|
93
101
|
|
|
94
102
|
/**
|
|
95
103
|
* Remove given key and it's value from storage
|
|
96
104
|
* @param {String} key
|
|
97
105
|
* @returns {Promise<void>}
|
|
98
106
|
*/
|
|
99
|
-
removeItem: key => del(key,
|
|
107
|
+
removeItem: key => del(key, getCustomStore()),
|
|
100
108
|
|
|
101
109
|
/**
|
|
102
110
|
* Remove given keys and their values from storage
|
|
@@ -104,7 +112,26 @@ const provider = {
|
|
|
104
112
|
* @param {Array} keysParam
|
|
105
113
|
* @returns {Promise}
|
|
106
114
|
*/
|
|
107
|
-
removeItems: keysParam => delMany(keysParam,
|
|
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
|
+
},
|
|
108
135
|
};
|
|
109
136
|
|
|
110
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.64",
|
|
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": {
|