UNPKG

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