UNPKG

13.6 kBPlain TextView Raw
1/*
2 * Copyright 2017-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
5 * the License. A copy of the License is located at
6 *
7 * http://aws.amazon.com/apache2.0/
8 *
9 * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
10 * CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions
11 * and limitations under the License.
12 */
13
14import { StorageCache } from './StorageCache';
15import { defaultConfig, getCurrTime } from './Utils';
16import AsyncStorage from '@react-native-async-storage/async-storage';
17import { ICache } from './types';
18import { ConsoleLogger as Logger } from '@aws-amplify/core';
19
20const logger = new Logger('AsyncStorageCache');
21
22/*
23 * Customized cache which based on the AsyncStorage with LRU implemented
24 */
25export class AsyncStorageCache extends StorageCache implements ICache {
26 /**
27 * initialize the cache
28 *
29 * @param {Object} config - the configuration of the cache
30 */
31 constructor(config?) {
32 const cache_config = config
33 ? Object.assign({}, defaultConfig, config)
34 : defaultConfig;
35 super(cache_config);
36 this.getItem = this.getItem.bind(this);
37 this.setItem = this.setItem.bind(this);
38 this.removeItem = this.removeItem.bind(this);
39 logger.debug('Using AsyncStorageCache');
40 }
41
42 /**
43 * decrease current size of the cache
44 * @private
45 * @param amount - the amount of the cache size which needs to be decreased
46 */
47 async _decreaseCurSizeInBytes(amount) {
48 const curSize = await this.getCacheCurSize();
49 await AsyncStorage.setItem(
50 this.cacheCurSizeKey,
51 (curSize - amount).toString()
52 );
53 }
54
55 /**
56 * increase current size of the cache
57 * @private
58 * @param amount - the amount of the cache szie which need to be increased
59 */
60 async _increaseCurSizeInBytes(amount) {
61 const curSize = await this.getCacheCurSize();
62 await AsyncStorage.setItem(
63 this.cacheCurSizeKey,
64 (curSize + amount).toString()
65 );
66 }
67
68 /**
69 * update the visited time if item has been visited
70 * @private
71 * @param item - the item which need to be refreshed
72 * @param prefixedKey - the key of the item
73 *
74 * @return the refreshed item
75 */
76 async _refreshItem(item, prefixedKey) {
77 item.visitedTime = getCurrTime();
78 await AsyncStorage.setItem(prefixedKey, JSON.stringify(item));
79 return item;
80 }
81
82 /**
83 * check wether item is expired
84 * @private
85 * @param key - the key of the item
86 *
87 * @return true if the item is expired.
88 */
89 async _isExpired(key) {
90 const text = await AsyncStorage.getItem(key);
91 const item = JSON.parse(text);
92 if (getCurrTime() >= item.expires) {
93 return true;
94 }
95 return false;
96 }
97
98 /**
99 * delete item from cache
100 * @private
101 * @param prefixedKey - the key of the item
102 * @param size - optional, the byte size of the item
103 */
104 async _removeItem(prefixedKey, size?) {
105 const itemSize = size
106 ? size
107 : JSON.parse(await AsyncStorage.getItem(prefixedKey)).byteSize;
108 // first try to update the current size of the cache
109 await this._decreaseCurSizeInBytes(itemSize);
110
111 // try to remove the item from cache
112 try {
113 await AsyncStorage.removeItem(prefixedKey);
114 } catch (removeItemError) {
115 // if some error happened, we need to rollback the current size
116 await this._increaseCurSizeInBytes(itemSize);
117 logger.error(`Failed to remove item: ${removeItemError}`);
118 }
119 }
120
121 /**
122 * put item into cache
123 * @private
124 * @param prefixedKey - the key of the item
125 * @param itemData - the value of the item
126 * @param itemSizeInBytes - the byte size of the item
127 */
128 async _setItem(prefixedKey, item) {
129 // first try to update the current size of the cache.
130 await this._increaseCurSizeInBytes(item.byteSize);
131
132 // try to add the item into cache
133 try {
134 await AsyncStorage.setItem(prefixedKey, JSON.stringify(item));
135 } catch (setItemErr) {
136 // if some error happened, we need to rollback the current size
137 await this._decreaseCurSizeInBytes(item.byteSize);
138 logger.error(`Failed to set item ${setItemErr}`);
139 }
140 }
141
142 /**
143 * total space needed when poping out items
144 * @private
145 * @param itemSize
146 *
147 * @return total space needed
148 */
149 async _sizeToPop(itemSize) {
150 const spaceItemNeed =
151 (await this.getCacheCurSize()) + itemSize - this.config.capacityInBytes;
152 const cacheThresholdSpace =
153 (1 - this.config.warningThreshold) * this.config.capacityInBytes;
154 return spaceItemNeed > cacheThresholdSpace
155 ? spaceItemNeed
156 : cacheThresholdSpace;
157 }
158
159 /**
160 * see whether cache is full
161 * @private
162 * @param itemSize
163 *
164 * @return true if cache is full
165 */
166 async _isCacheFull(itemSize) {
167 return (
168 itemSize + (await this.getCacheCurSize()) > this.config.capacityInBytes
169 );
170 }
171
172 /**
173 * scan the storage and find out all the keys owned by this cache
174 * also clean the expired keys while scanning
175 * @private
176 * @return array of keys
177 */
178 async _findValidKeys() {
179 const keys = [];
180 let keyInCache = [];
181
182 keyInCache = await AsyncStorage.getAllKeys();
183
184 for (let i = 0; i < keyInCache.length; i += 1) {
185 const key = keyInCache[i];
186 if (
187 key.indexOf(this.config.keyPrefix) === 0 &&
188 key !== this.cacheCurSizeKey
189 ) {
190 if (await this._isExpired(key)) {
191 await this._removeItem(key);
192 } else {
193 keys.push(key);
194 }
195 }
196 }
197 return keys;
198 }
199
200 /**
201 * get all the items we have, sort them by their priority,
202 * if priority is same, sort them by their last visited time
203 * pop out items from the low priority (5 is the lowest)
204 * @private
205 * @param keys - all the keys in this cache
206 * @param sizeToPop - the total size of the items which needed to be poped out
207 */
208 async _popOutItems(keys, sizeToPop) {
209 const items = [];
210 let remainedSize = sizeToPop;
211 for (let i = 0; i < keys.length; i += 1) {
212 const val = await AsyncStorage.getItem(keys[i]);
213 if (val != null) {
214 const item = JSON.parse(val);
215 items.push(item);
216 }
217 }
218
219 // first compare priority
220 // then compare visited time
221 items.sort((a, b) => {
222 if (a.priority > b.priority) {
223 return -1;
224 } else if (a.priority < b.priority) {
225 return 1;
226 } else {
227 if (a.visitedTime < b.visitedTime) {
228 return -1;
229 } else return 1;
230 }
231 });
232
233 for (let i = 0; i < items.length; i += 1) {
234 // pop out items until we have enough room for new item
235 await this._removeItem(items[i].key, items[i].byteSize);
236 remainedSize -= items[i].byteSize;
237 if (remainedSize <= 0) {
238 return;
239 }
240 }
241 }
242
243 /**
244 * Set item into cache. You can put number, string, boolean or object.
245 * The cache will first check whether has the same key.
246 * If it has, it will delete the old item and then put the new item in
247 * The cache will pop out items if it is full
248 * You can specify the cache item options. The cache will abort and output a warning:
249 * If the key is invalid
250 * If the size of the item exceeds itemMaxSize.
251 * If the value is undefined
252 * If incorrect cache item configuration
253 * If error happened with browser storage
254 *
255 * @param {String} key - the key of the item
256 * @param {Object} value - the value of the item
257 * @param {Object} [options] - optional, the specified meta-data
258 * @return {Prmoise}
259 */
260 async setItem(key, value, options) {
261 logger.debug(
262 `Set item: key is ${key}, value is ${value} with options: ${options}`
263 );
264 const prefixedKey = this.config.keyPrefix + key;
265 // invalid keys
266 if (
267 prefixedKey === this.config.keyPrefix ||
268 prefixedKey === this.cacheCurSizeKey
269 ) {
270 logger.warn(`Invalid key: should not be empty or 'CurSize'`);
271 return;
272 }
273
274 if (typeof value === 'undefined') {
275 logger.warn(`The value of item should not be undefined!`);
276 return;
277 }
278
279 const cacheItemOptions = {
280 priority:
281 options && options.priority !== undefined
282 ? options.priority
283 : this.config.defaultPriority,
284 expires:
285 options && options.expires !== undefined
286 ? options.expires
287 : this.config.defaultTTL + getCurrTime(),
288 };
289
290 if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) {
291 logger.warn(
292 `Invalid parameter: priority due to out or range. It should be within 1 and 5.`
293 );
294 return;
295 }
296
297 const item = this.fillCacheItem(prefixedKey, value, cacheItemOptions);
298
299 // check wether this item is too big;
300 if (item.byteSize > this.config.itemMaxSize) {
301 logger.warn(
302 `Item with key: ${key} you are trying to put into is too big!`
303 );
304 return;
305 }
306
307 try {
308 // first look into the storage, if it exists, delete it.
309 const val = await AsyncStorage.getItem(prefixedKey);
310 if (val) {
311 await this._removeItem(prefixedKey, JSON.parse(val).byteSize);
312 }
313
314 // check whether the cache is full
315 if (await this._isCacheFull(item.byteSize)) {
316 const validKeys = await this._findValidKeys();
317 if (await this._isCacheFull(item.byteSize)) {
318 const sizeToPop = await this._sizeToPop(item.byteSize);
319 await this._popOutItems(validKeys, sizeToPop);
320 }
321 }
322
323 // put item in the cache
324 await this._setItem(prefixedKey, item);
325 } catch (e) {
326 logger.warn(`setItem failed! ${e}`);
327 }
328 }
329
330 /**
331 * Get item from cache. It will return null if item doesn’t exist or it has been expired.
332 * If you specified callback function in the options,
333 * then the function will be executed if no such item in the cache
334 * and finally put the return value into cache.
335 * Please make sure the callback function will return the value you want to put into the cache.
336 * The cache will abort output a warning:
337 * If the key is invalid
338 * If error happened with AsyncStorage
339 *
340 * @param {String} key - the key of the item
341 * @param {Object} [options] - the options of callback function
342 * @return {Promise} - return a promise resolves to be the value of the item
343 */
344 async getItem(key, options) {
345 logger.debug(`Get item: key is ${key} with options ${options}`);
346 let ret = null;
347 const prefixedKey = this.config.keyPrefix + key;
348
349 if (
350 prefixedKey === this.config.keyPrefix ||
351 prefixedKey === this.cacheCurSizeKey
352 ) {
353 logger.warn(`Invalid key: should not be empty or 'CurSize'`);
354 return null;
355 }
356
357 try {
358 ret = await AsyncStorage.getItem(prefixedKey);
359 if (ret != null) {
360 if (await this._isExpired(prefixedKey)) {
361 // if expired, remove that item and return null
362 await this._removeItem(prefixedKey, JSON.parse(ret).byteSize);
363 } else {
364 // if not expired, great, return the value and refresh it
365 let item = JSON.parse(ret);
366 item = await this._refreshItem(item, prefixedKey);
367 return item.data;
368 }
369 }
370
371 if (options && options.callback !== undefined) {
372 const val = options.callback();
373 if (val !== null) {
374 this.setItem(key, val, options);
375 }
376 return val;
377 }
378 return null;
379 } catch (e) {
380 logger.warn(`getItem failed! ${e}`);
381 return null;
382 }
383 }
384
385 /**
386 * remove item from the cache
387 * The cache will abort output a warning:
388 * If error happened with AsyncStorage
389 * @param {String} key - the key of the item
390 * @return {Promise}
391 */
392 async removeItem(key) {
393 logger.debug(`Remove item: key is ${key}`);
394 const prefixedKey = this.config.keyPrefix + key;
395
396 if (
397 prefixedKey === this.config.keyPrefix ||
398 prefixedKey === this.cacheCurSizeKey
399 ) {
400 return;
401 }
402
403 try {
404 const val = await AsyncStorage.getItem(prefixedKey);
405 if (val) {
406 await this._removeItem(prefixedKey, JSON.parse(val).byteSize);
407 }
408 } catch (e) {
409 logger.warn(`removeItem failed! ${e}`);
410 }
411 }
412
413 /**
414 * clear the entire cache
415 * The cache will abort output a warning:
416 * If error happened with AsyncStorage
417 * @return {Promise}
418 */
419 async clear() {
420 logger.debug(`Clear Cache`);
421 try {
422 const keys = await AsyncStorage.getAllKeys();
423
424 const keysToRemove = [];
425 for (let i = 0; i < keys.length; i += 1) {
426 if (keys[i].indexOf(this.config.keyPrefix) === 0) {
427 keysToRemove.push(keys[i]);
428 }
429 }
430
431 // can be improved
432 for (let i = 0; i < keysToRemove.length; i += 1) {
433 await AsyncStorage.removeItem(keysToRemove[i]);
434 }
435 } catch (e) {
436 logger.warn(`clear failed! ${e}`);
437 }
438 }
439
440 /**
441 * return the current size of the cache
442 * @return {Promise}
443 */
444 async getCacheCurSize() {
445 let ret = await AsyncStorage.getItem(this.cacheCurSizeKey);
446 if (!ret) {
447 await AsyncStorage.setItem(this.cacheCurSizeKey, '0');
448 ret = '0';
449 }
450 return Number(ret);
451 }
452
453 /**
454 * Return all the keys in the cache.
455 * Will return an empty array if error happend.
456 * @return {Promise}
457 */
458 async getAllKeys() {
459 try {
460 const keys = await AsyncStorage.getAllKeys();
461
462 const retKeys = [];
463 for (let i = 0; i < keys.length; i += 1) {
464 if (
465 keys[i].indexOf(this.config.keyPrefix) === 0 &&
466 keys[i] !== this.cacheCurSizeKey
467 ) {
468 retKeys.push(keys[i].substring(this.config.keyPrefix.length));
469 }
470 }
471 return retKeys;
472 } catch (e) {
473 logger.warn(`getALlkeys failed! ${e}`);
474 return [];
475 }
476 }
477
478 /**
479 * Return a new instance of cache with customized configuration.
480 * @param {Object} config - the customized configuration
481 * @return {Object} - the new instance of Cache
482 */
483 createInstance(config): ICache {
484 if (config.keyPrefix === defaultConfig.keyPrefix) {
485 logger.error('invalid keyPrefix, setting keyPrefix with timeStamp');
486 config.keyPrefix = getCurrTime.toString();
487 }
488 return new AsyncStorageCache(config);
489 }
490}
491
492const instance: ICache = new AsyncStorageCache();
493export { AsyncStorage, instance as Cache };
494export default instance;