UNPKG

13.2 kBPlain TextView Raw
1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4import { Amplify, ConsoleLogger as Logger } from '@aws-amplify/core';
5import AsyncStorage from '@react-native-async-storage/async-storage';
6import { StorageCache } from './StorageCache';
7import { defaultConfig, getCurrTime } from './Utils';
8import { ICache } from './types';
9
10const logger = new Logger('AsyncStorageCache');
11
12/*
13 * Customized cache which based on the AsyncStorage with LRU implemented
14 */
15export class AsyncStorageCache extends StorageCache implements ICache {
16 /**
17 * initialize the cache
18 *
19 * @param {Object} config - the configuration of the cache
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 * decrease current size of the cache
34 * @private
35 * @param amount - the amount of the cache size which needs to be decreased
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 * increase current size of the cache
47 * @private
48 * @param amount - the amount of the cache szie which need to be increased
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 * update the visited time if item has been visited
60 * @private
61 * @param item - the item which need to be refreshed
62 * @param prefixedKey - the key of the item
63 *
64 * @return the refreshed item
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 * check wether item is expired
74 * @private
75 * @param key - the key of the item
76 *
77 * @return true if the item is expired.
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 * delete item from cache
90 * @private
91 * @param prefixedKey - the key of the item
92 * @param size - optional, the byte size of the item
93 */
94 async _removeItem(prefixedKey, size?) {
95 const itemSize = size
96 ? size
97 : JSON.parse(await AsyncStorage.getItem(prefixedKey)).byteSize;
98 // first try to update the current size of the cache
99 await this._decreaseCurSizeInBytes(itemSize);
100
101 // try to remove the item from cache
102 try {
103 await AsyncStorage.removeItem(prefixedKey);
104 } catch (removeItemError) {
105 // if some error happened, we need to rollback the current size
106 await this._increaseCurSizeInBytes(itemSize);
107 logger.error(`Failed to remove item: ${removeItemError}`);
108 }
109 }
110
111 /**
112 * put item into cache
113 * @private
114 * @param prefixedKey - the key of the item
115 * @param itemData - the value of the item
116 * @param itemSizeInBytes - the byte size of the item
117 */
118 async _setItem(prefixedKey, item) {
119 // first try to update the current size of the cache.
120 await this._increaseCurSizeInBytes(item.byteSize);
121
122 // try to add the item into cache
123 try {
124 await AsyncStorage.setItem(prefixedKey, JSON.stringify(item));
125 } catch (setItemErr) {
126 // if some error happened, we need to rollback the current size
127 await this._decreaseCurSizeInBytes(item.byteSize);
128 logger.error(`Failed to set item ${setItemErr}`);
129 }
130 }
131
132 /**
133 * total space needed when poping out items
134 * @private
135 * @param itemSize
136 *
137 * @return total space needed
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 * see whether cache is full
151 * @private
152 * @param itemSize
153 *
154 * @return true if cache is full
155 */
156 async _isCacheFull(itemSize) {
157 return (
158 itemSize + (await this.getCacheCurSize()) > this.config.capacityInBytes
159 );
160 }
161
162 /**
163 * scan the storage and find out all the keys owned by this cache
164 * also clean the expired keys while scanning
165 * @private
166 * @return array of keys
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 * get all the items we have, sort them by their priority,
192 * if priority is same, sort them by their last visited time
193 * pop out items from the low priority (5 is the lowest)
194 * @private
195 * @param keys - all the keys in this cache
196 * @param sizeToPop - the total size of the items which needed to be poped out
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 // first compare priority
210 // then compare visited time
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 // pop out items until we have enough room for new item
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 * Set item into cache. You can put number, string, boolean or object.
235 * The cache will first check whether has the same key.
236 * If it has, it will delete the old item and then put the new item in
237 * The cache will pop out items if it is full
238 * You can specify the cache item options. The cache will abort and output a warning:
239 * If the key is invalid
240 * If the size of the item exceeds itemMaxSize.
241 * If the value is undefined
242 * If incorrect cache item configuration
243 * If error happened with browser storage
244 *
245 * @param {String} key - the key of the item
246 * @param {Object} value - the value of the item
247 * @param {Object} [options] - optional, the specified meta-data
248 * @return {Prmoise}
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 // invalid keys
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 // check wether this item is too big;
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 // first look into the storage, if it exists, delete it.
299 const val = await AsyncStorage.getItem(prefixedKey);
300 if (val) {
301 await this._removeItem(prefixedKey, JSON.parse(val).byteSize);
302 }
303
304 // check whether the cache is full
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 // put item in the cache
314 await this._setItem(prefixedKey, item);
315 } catch (e) {
316 logger.warn(`setItem failed! ${e}`);
317 }
318 }
319
320 /**
321 * Get item from cache. It will return null if item doesn’t exist or it has been expired.
322 * If you specified callback function in the options,
323 * then the function will be executed if no such item in the cache
324 * and finally put the return value into cache.
325 * Please make sure the callback function will return the value you want to put into the cache.
326 * The cache will abort output a warning:
327 * If the key is invalid
328 * If error happened with AsyncStorage
329 *
330 * @param {String} key - the key of the item
331 * @param {Object} [options] - the options of callback function
332 * @return {Promise} - return a promise resolves to be the value of the item
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 // if expired, remove that item and return null
352 await this._removeItem(prefixedKey, JSON.parse(ret).byteSize);
353 } else {
354 // if not expired, great, return the value and refresh it
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 * remove item from the cache
377 * The cache will abort output a warning:
378 * If error happened with AsyncStorage
379 * @param {String} key - the key of the item
380 * @return {Promise}
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 * clear the entire cache
405 * The cache will abort output a warning:
406 * If error happened with AsyncStorage
407 * @return {Promise}
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 // can be improved
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 * return the current size of the cache
432 * @return {Promise}
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 * Return all the keys in the cache.
445 * Will return an empty array if error happend.
446 * @return {Promise}
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 * Return a new instance of cache with customized configuration.
470 * @param {Object} config - the customized configuration
471 * @return {Object} - the new instance of Cache
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
482const instance: ICache = new AsyncStorageCache();
483export { AsyncStorage, instance as Cache };
484
485Amplify.register(instance);