-
Notifications
You must be signed in to change notification settings - Fork 76
/
Copy pathOnyxCache.js
224 lines (196 loc) · 6.04 KB
/
OnyxCache.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
import _ from 'underscore';
import {deepEqual} from 'fast-equals';
import utils from './utils';
const isDefined = _.negate(_.isUndefined);
/**
* In memory cache providing data by reference
* Encapsulates Onyx cache related functionality
*/
class OnyxCache {
constructor() {
/**
* @private
* Cache of all the storage keys available in persistent storage
* @type {Set<string>}
*/
this.storageKeys = new Set();
/**
* @private
* Unique list of keys maintained in access order (most recent at the end)
* @type {Set<string>}
*/
this.recentKeys = new Set();
/**
* @private
* A map of cached values
* @type {Record<string, *>}
*/
this.storageMap = {};
/**
* @private
* Captured pending tasks for already running storage methods
* Using a map yields better performance on operations such a delete
* https://www.zhenghao.io/posts/object-vs-map
* @type {Map<string, Promise>}
*/
this.pendingPromises = new Map();
// bind all public methods to prevent problems with `this`
_.bindAll(
this,
'getAllKeys', 'getValue', 'hasCacheForKey', 'addKey', 'set', 'drop', 'merge',
'hasPendingTask', 'getTaskPromise', 'captureTask', 'removeLeastRecentlyUsedKeys',
'setRecentKeysLimit',
);
}
/**
* Get all the storage keys
* @returns {string[]}
*/
getAllKeys() {
return Array.from(this.storageKeys);
}
/**
* Get a cached value from storage
* @param {string} key
* @returns {*}
*/
getValue(key) {
this.addToAccessedKeys(key);
return this.storageMap[key];
}
/**
* Check whether cache has data for the given key
* @param {string} key
* @returns {boolean}
*/
hasCacheForKey(key) {
return isDefined(this.storageMap[key]);
}
/**
* Saves a key in the storage keys list
* Serves to keep the result of `getAllKeys` up to date
* @param {string} key
*/
addKey(key) {
this.storageKeys.add(key);
}
/**
* Set's a key value in cache
* Adds the key to the storage keys list as well
* @param {string} key
* @param {*} value
* @returns {*} value - returns the cache value
*/
set(key, value) {
this.addKey(key);
this.addToAccessedKeys(key);
this.storageMap[key] = value;
return value;
}
/**
* Forget the cached value for the given key
* @param {string} key
*/
drop(key) {
delete this.storageMap[key];
this.storageKeys.delete(key);
this.recentKeys.delete(key);
}
/**
* Deep merge data to cache, any non existing keys will be created
* @param {Record<string, *>} data - a map of (cache) key - values
*/
merge(data) {
if (!_.isObject(data) || _.isArray(data)) {
throw new Error('data passed to cache.merge() must be an Object of onyx key/value pairs');
}
// lodash adds a small overhead so we don't use it here
// eslint-disable-next-line prefer-object-spread, rulesdir/prefer-underscore-method
this.storageMap = Object.assign({}, utils.fastMerge(this.storageMap, data));
const storageKeys = this.getAllKeys();
const mergedKeys = _.keys(data);
this.storageKeys = new Set([...storageKeys, ...mergedKeys]);
_.each(mergedKeys, key => this.addToAccessedKeys(key));
}
/**
* Check whether the given task is already running
* @param {string} taskName - unique name given for the task
* @returns {*}
*/
hasPendingTask(taskName) {
return isDefined(this.pendingPromises.get(taskName));
}
/**
* Use this method to prevent concurrent calls for the same thing
* Instead of calling the same task again use the existing promise
* provided from this function
* @template T
* @param {string} taskName - unique name given for the task
* @returns {Promise<T>}
*/
getTaskPromise(taskName) {
return this.pendingPromises.get(taskName);
}
/**
* Capture a promise for a given task so other caller can
* hook up to the promise if it's still pending
* @template T
* @param {string} taskName - unique name for the task
* @param {Promise<T>} promise
* @returns {Promise<T>}
*/
captureTask(taskName, promise) {
const returnPromise = promise.finally(() => {
this.pendingPromises.delete(taskName);
});
this.pendingPromises.set(taskName, returnPromise);
return returnPromise;
}
/**
* @private
* Adds a key to the top of the recently accessed keys
* @param {string} key
*/
addToAccessedKeys(key) {
// Removing and re-adding a key ensures it's at the end of the list
this.recentKeys.delete(key);
this.recentKeys.add(key);
}
/**
* Remove keys that don't fall into the range of recently used keys
*/
removeLeastRecentlyUsedKeys() {
let numKeysToRemove = this.recentKeys.size - this.maxRecentKeysSize;
if (numKeysToRemove <= 0) {
return;
}
const iterator = this.recentKeys.values();
const temp = [];
while (numKeysToRemove > 0) {
const value = iterator.next().value;
temp.push(value);
numKeysToRemove--;
}
for (let i = 0; i < temp.length; ++i) {
delete this.storageMap[temp[i]];
this.recentKeys.delete(temp[i]);
}
}
/**
* Set the recent keys list size
* @param {number} limit
*/
setRecentKeysLimit(limit) {
this.maxRecentKeysSize = limit;
}
/**
* @param {String} key
* @param {*} value
* @returns {Boolean}
*/
hasValueChanged(key, value) {
return !deepEqual(this.storageMap[key], value);
}
}
const instance = new OnyxCache();
export default instance;