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