1 |
|
2 |
|
3 |
|
4 | import { Amplify, ConsoleLogger as Logger } from '@aws-amplify/core';
|
5 | import AsyncStorage from '@react-native-async-storage/async-storage';
|
6 | import { StorageCache } from './StorageCache';
|
7 | import { defaultConfig, getCurrTime } from './Utils';
|
8 | import { ICache } from './types';
|
9 |
|
10 | const logger = new Logger('AsyncStorageCache');
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | export class AsyncStorageCache extends StorageCache implements ICache {
|
16 | |
17 |
|
18 |
|
19 |
|
20 |
|
21 | constructor(config?) {
|
22 | const cache_config = config
|
23 | ? Object.assign({}, defaultConfig, config)
|
24 | : defaultConfig;
|
25 | super(cache_config);
|
26 | this.getItem = this.getItem.bind(this);
|
27 | this.setItem = this.setItem.bind(this);
|
28 | this.removeItem = this.removeItem.bind(this);
|
29 | logger.debug('Using AsyncStorageCache');
|
30 | }
|
31 |
|
32 | |
33 |
|
34 |
|
35 |
|
36 |
|
37 | async _decreaseCurSizeInBytes(amount) {
|
38 | const curSize = await this.getCacheCurSize();
|
39 | await AsyncStorage.setItem(
|
40 | this.cacheCurSizeKey,
|
41 | (curSize - amount).toString()
|
42 | );
|
43 | }
|
44 |
|
45 | |
46 |
|
47 |
|
48 |
|
49 |
|
50 | async _increaseCurSizeInBytes(amount) {
|
51 | const curSize = await this.getCacheCurSize();
|
52 | await AsyncStorage.setItem(
|
53 | this.cacheCurSizeKey,
|
54 | (curSize + amount).toString()
|
55 | );
|
56 | }
|
57 |
|
58 | |
59 |
|
60 |
|
61 |
|
62 |
|
63 |
|
64 |
|
65 |
|
66 | async _refreshItem(item, prefixedKey) {
|
67 | item.visitedTime = getCurrTime();
|
68 | await AsyncStorage.setItem(prefixedKey, JSON.stringify(item));
|
69 | return item;
|
70 | }
|
71 |
|
72 | |
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 |
|
79 | async _isExpired(key) {
|
80 | const text = await AsyncStorage.getItem(key);
|
81 | const item = JSON.parse(text);
|
82 | if (getCurrTime() >= item.expires) {
|
83 | return true;
|
84 | }
|
85 | return false;
|
86 | }
|
87 |
|
88 | |
89 |
|
90 |
|
91 |
|
92 |
|
93 |
|
94 | async _removeItem(prefixedKey, size?) {
|
95 | const itemSize = size
|
96 | ? size
|
97 | : JSON.parse(await AsyncStorage.getItem(prefixedKey)).byteSize;
|
98 |
|
99 | await this._decreaseCurSizeInBytes(itemSize);
|
100 |
|
101 |
|
102 | try {
|
103 | await AsyncStorage.removeItem(prefixedKey);
|
104 | } catch (removeItemError) {
|
105 |
|
106 | await this._increaseCurSizeInBytes(itemSize);
|
107 | logger.error(`Failed to remove item: ${removeItemError}`);
|
108 | }
|
109 | }
|
110 |
|
111 | |
112 |
|
113 |
|
114 |
|
115 |
|
116 |
|
117 |
|
118 | async _setItem(prefixedKey, item) {
|
119 |
|
120 | await this._increaseCurSizeInBytes(item.byteSize);
|
121 |
|
122 |
|
123 | try {
|
124 | await AsyncStorage.setItem(prefixedKey, JSON.stringify(item));
|
125 | } catch (setItemErr) {
|
126 |
|
127 | await this._decreaseCurSizeInBytes(item.byteSize);
|
128 | logger.error(`Failed to set item ${setItemErr}`);
|
129 | }
|
130 | }
|
131 |
|
132 | |
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
139 | async _sizeToPop(itemSize) {
|
140 | const spaceItemNeed =
|
141 | (await this.getCacheCurSize()) + itemSize - this.config.capacityInBytes;
|
142 | const cacheThresholdSpace =
|
143 | (1 - this.config.warningThreshold) * this.config.capacityInBytes;
|
144 | return spaceItemNeed > cacheThresholdSpace
|
145 | ? spaceItemNeed
|
146 | : cacheThresholdSpace;
|
147 | }
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | async _isCacheFull(itemSize) {
|
157 | return (
|
158 | itemSize + (await this.getCacheCurSize()) > this.config.capacityInBytes
|
159 | );
|
160 | }
|
161 |
|
162 | |
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 | async _findValidKeys() {
|
169 | const keys = [];
|
170 | let keyInCache = [];
|
171 |
|
172 | keyInCache = await AsyncStorage.getAllKeys();
|
173 |
|
174 | for (let i = 0; i < keyInCache.length; i += 1) {
|
175 | const key = keyInCache[i];
|
176 | if (
|
177 | key.indexOf(this.config.keyPrefix) === 0 &&
|
178 | key !== this.cacheCurSizeKey
|
179 | ) {
|
180 | if (await this._isExpired(key)) {
|
181 | await this._removeItem(key);
|
182 | } else {
|
183 | keys.push(key);
|
184 | }
|
185 | }
|
186 | }
|
187 | return keys;
|
188 | }
|
189 |
|
190 | |
191 |
|
192 |
|
193 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | async _popOutItems(keys, sizeToPop) {
|
199 | const items = [];
|
200 | let remainedSize = sizeToPop;
|
201 | for (let i = 0; i < keys.length; i += 1) {
|
202 | const val = await AsyncStorage.getItem(keys[i]);
|
203 | if (val != null) {
|
204 | const item = JSON.parse(val);
|
205 | items.push(item);
|
206 | }
|
207 | }
|
208 |
|
209 |
|
210 |
|
211 | items.sort((a, b) => {
|
212 | if (a.priority > b.priority) {
|
213 | return -1;
|
214 | } else if (a.priority < b.priority) {
|
215 | return 1;
|
216 | } else {
|
217 | if (a.visitedTime < b.visitedTime) {
|
218 | return -1;
|
219 | } else return 1;
|
220 | }
|
221 | });
|
222 |
|
223 | for (let i = 0; i < items.length; i += 1) {
|
224 |
|
225 | await this._removeItem(items[i].key, items[i].byteSize);
|
226 | remainedSize -= items[i].byteSize;
|
227 | if (remainedSize <= 0) {
|
228 | return;
|
229 | }
|
230 | }
|
231 | }
|
232 |
|
233 | |
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 |
|
249 |
|
250 | async setItem(key, value, options) {
|
251 | logger.debug(
|
252 | `Set item: key is ${key}, value is ${value} with options: ${options}`
|
253 | );
|
254 | const prefixedKey = this.config.keyPrefix + key;
|
255 |
|
256 | if (
|
257 | prefixedKey === this.config.keyPrefix ||
|
258 | prefixedKey === this.cacheCurSizeKey
|
259 | ) {
|
260 | logger.warn(`Invalid key: should not be empty or 'CurSize'`);
|
261 | return;
|
262 | }
|
263 |
|
264 | if (typeof value === 'undefined') {
|
265 | logger.warn(`The value of item should not be undefined!`);
|
266 | return;
|
267 | }
|
268 |
|
269 | const cacheItemOptions = {
|
270 | priority:
|
271 | options && options.priority !== undefined
|
272 | ? options.priority
|
273 | : this.config.defaultPriority,
|
274 | expires:
|
275 | options && options.expires !== undefined
|
276 | ? options.expires
|
277 | : this.config.defaultTTL + getCurrTime(),
|
278 | };
|
279 |
|
280 | if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) {
|
281 | logger.warn(
|
282 | `Invalid parameter: priority due to out or range. It should be within 1 and 5.`
|
283 | );
|
284 | return;
|
285 | }
|
286 |
|
287 | const item = this.fillCacheItem(prefixedKey, value, cacheItemOptions);
|
288 |
|
289 |
|
290 | if (item.byteSize > this.config.itemMaxSize) {
|
291 | logger.warn(
|
292 | `Item with key: ${key} you are trying to put into is too big!`
|
293 | );
|
294 | return;
|
295 | }
|
296 |
|
297 | try {
|
298 |
|
299 | const val = await AsyncStorage.getItem(prefixedKey);
|
300 | if (val) {
|
301 | await this._removeItem(prefixedKey, JSON.parse(val).byteSize);
|
302 | }
|
303 |
|
304 |
|
305 | if (await this._isCacheFull(item.byteSize)) {
|
306 | const validKeys = await this._findValidKeys();
|
307 | if (await this._isCacheFull(item.byteSize)) {
|
308 | const sizeToPop = await this._sizeToPop(item.byteSize);
|
309 | await this._popOutItems(validKeys, sizeToPop);
|
310 | }
|
311 | }
|
312 |
|
313 |
|
314 | await this._setItem(prefixedKey, item);
|
315 | } catch (e) {
|
316 | logger.warn(`setItem failed! ${e}`);
|
317 | }
|
318 | }
|
319 |
|
320 | |
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 |
|
330 |
|
331 |
|
332 |
|
333 |
|
334 | async getItem(key, options) {
|
335 | logger.debug(`Get item: key is ${key} with options ${options}`);
|
336 | let ret = null;
|
337 | const prefixedKey = this.config.keyPrefix + key;
|
338 |
|
339 | if (
|
340 | prefixedKey === this.config.keyPrefix ||
|
341 | prefixedKey === this.cacheCurSizeKey
|
342 | ) {
|
343 | logger.warn(`Invalid key: should not be empty or 'CurSize'`);
|
344 | return null;
|
345 | }
|
346 |
|
347 | try {
|
348 | ret = await AsyncStorage.getItem(prefixedKey);
|
349 | if (ret != null) {
|
350 | if (await this._isExpired(prefixedKey)) {
|
351 |
|
352 | await this._removeItem(prefixedKey, JSON.parse(ret).byteSize);
|
353 | } else {
|
354 |
|
355 | let item = JSON.parse(ret);
|
356 | item = await this._refreshItem(item, prefixedKey);
|
357 | return item.data;
|
358 | }
|
359 | }
|
360 |
|
361 | if (options && options.callback !== undefined) {
|
362 | const val = options.callback();
|
363 | if (val !== null) {
|
364 | this.setItem(key, val, options);
|
365 | }
|
366 | return val;
|
367 | }
|
368 | return null;
|
369 | } catch (e) {
|
370 | logger.warn(`getItem failed! ${e}`);
|
371 | return null;
|
372 | }
|
373 | }
|
374 |
|
375 | |
376 |
|
377 |
|
378 |
|
379 |
|
380 |
|
381 |
|
382 | async removeItem(key) {
|
383 | logger.debug(`Remove item: key is ${key}`);
|
384 | const prefixedKey = this.config.keyPrefix + key;
|
385 |
|
386 | if (
|
387 | prefixedKey === this.config.keyPrefix ||
|
388 | prefixedKey === this.cacheCurSizeKey
|
389 | ) {
|
390 | return;
|
391 | }
|
392 |
|
393 | try {
|
394 | const val = await AsyncStorage.getItem(prefixedKey);
|
395 | if (val) {
|
396 | await this._removeItem(prefixedKey, JSON.parse(val).byteSize);
|
397 | }
|
398 | } catch (e) {
|
399 | logger.warn(`removeItem failed! ${e}`);
|
400 | }
|
401 | }
|
402 |
|
403 | |
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 | async clear() {
|
410 | logger.debug(`Clear Cache`);
|
411 | try {
|
412 | const keys = await AsyncStorage.getAllKeys();
|
413 |
|
414 | const keysToRemove = [];
|
415 | for (let i = 0; i < keys.length; i += 1) {
|
416 | if (keys[i].indexOf(this.config.keyPrefix) === 0) {
|
417 | keysToRemove.push(keys[i]);
|
418 | }
|
419 | }
|
420 |
|
421 |
|
422 | for (let i = 0; i < keysToRemove.length; i += 1) {
|
423 | await AsyncStorage.removeItem(keysToRemove[i]);
|
424 | }
|
425 | } catch (e) {
|
426 | logger.warn(`clear failed! ${e}`);
|
427 | }
|
428 | }
|
429 |
|
430 | |
431 |
|
432 |
|
433 |
|
434 | async getCacheCurSize() {
|
435 | let ret = await AsyncStorage.getItem(this.cacheCurSizeKey);
|
436 | if (!ret) {
|
437 | await AsyncStorage.setItem(this.cacheCurSizeKey, '0');
|
438 | ret = '0';
|
439 | }
|
440 | return Number(ret);
|
441 | }
|
442 |
|
443 | |
444 |
|
445 |
|
446 |
|
447 |
|
448 | async getAllKeys() {
|
449 | try {
|
450 | const keys = await AsyncStorage.getAllKeys();
|
451 |
|
452 | const retKeys = [];
|
453 | for (let i = 0; i < keys.length; i += 1) {
|
454 | if (
|
455 | keys[i].indexOf(this.config.keyPrefix) === 0 &&
|
456 | keys[i] !== this.cacheCurSizeKey
|
457 | ) {
|
458 | retKeys.push(keys[i].substring(this.config.keyPrefix.length));
|
459 | }
|
460 | }
|
461 | return retKeys;
|
462 | } catch (e) {
|
463 | logger.warn(`getALlkeys failed! ${e}`);
|
464 | return [];
|
465 | }
|
466 | }
|
467 |
|
468 | |
469 |
|
470 |
|
471 |
|
472 |
|
473 | createInstance(config): ICache {
|
474 | if (config.keyPrefix === defaultConfig.keyPrefix) {
|
475 | logger.error('invalid keyPrefix, setting keyPrefix with timeStamp');
|
476 | config.keyPrefix = getCurrTime.toString();
|
477 | }
|
478 | return new AsyncStorageCache(config);
|
479 | }
|
480 | }
|
481 |
|
482 | const instance: ICache = new AsyncStorageCache();
|
483 | export { AsyncStorage, instance as Cache };
|
484 |
|
485 | Amplify.register(instance);
|