@aws-amplify/cache
Version:
Cache category of aws-amplify
688 lines • 30.6 kB
JavaScript
"use strict";
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var core_1 = require("@aws-amplify/core");
var async_storage_1 = tslib_1.__importDefault(require("@react-native-async-storage/async-storage"));
exports.AsyncStorage = async_storage_1.default;
var StorageCache_1 = require("./StorageCache");
var Utils_1 = require("./Utils");
var logger = new core_1.ConsoleLogger('AsyncStorageCache');
/*
* Customized cache which based on the AsyncStorage with LRU implemented
*/
var AsyncStorageCache = /** @class */ (function (_super) {
tslib_1.__extends(AsyncStorageCache, _super);
/**
* initialize the cache
*
* @param {Object} config - the configuration of the cache
*/
function AsyncStorageCache(config) {
var _this = this;
var cache_config = config
? Object.assign({}, Utils_1.defaultConfig, config)
: Utils_1.defaultConfig;
_this = _super.call(this, cache_config) || this;
_this.getItem = _this.getItem.bind(_this);
_this.setItem = _this.setItem.bind(_this);
_this.removeItem = _this.removeItem.bind(_this);
logger.debug('Using AsyncStorageCache');
return _this;
}
/**
* decrease current size of the cache
* @private
* @param amount - the amount of the cache size which needs to be decreased
*/
AsyncStorageCache.prototype._decreaseCurSizeInBytes = function (amount) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var curSize;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCacheCurSize()];
case 1:
curSize = _a.sent();
return [4 /*yield*/, async_storage_1.default.setItem(this.cacheCurSizeKey, (curSize - amount).toString())];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* increase current size of the cache
* @private
* @param amount - the amount of the cache szie which need to be increased
*/
AsyncStorageCache.prototype._increaseCurSizeInBytes = function (amount) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var curSize;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCacheCurSize()];
case 1:
curSize = _a.sent();
return [4 /*yield*/, async_storage_1.default.setItem(this.cacheCurSizeKey, (curSize + amount).toString())];
case 2:
_a.sent();
return [2 /*return*/];
}
});
});
};
/**
* update the visited time if item has been visited
* @private
* @param item - the item which need to be refreshed
* @param prefixedKey - the key of the item
*
* @return the refreshed item
*/
AsyncStorageCache.prototype._refreshItem = function (item, prefixedKey) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
item.visitedTime = Utils_1.getCurrTime();
return [4 /*yield*/, async_storage_1.default.setItem(prefixedKey, JSON.stringify(item))];
case 1:
_a.sent();
return [2 /*return*/, item];
}
});
});
};
/**
* check wether item is expired
* @private
* @param key - the key of the item
*
* @return true if the item is expired.
*/
AsyncStorageCache.prototype._isExpired = function (key) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var text, item;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, async_storage_1.default.getItem(key)];
case 1:
text = _a.sent();
item = JSON.parse(text);
if (Utils_1.getCurrTime() >= item.expires) {
return [2 /*return*/, true];
}
return [2 /*return*/, false];
}
});
});
};
/**
* delete item from cache
* @private
* @param prefixedKey - the key of the item
* @param size - optional, the byte size of the item
*/
AsyncStorageCache.prototype._removeItem = function (prefixedKey, size) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var itemSize, _a, _b, _c, removeItemError_1;
return tslib_1.__generator(this, function (_d) {
switch (_d.label) {
case 0:
if (!size) return [3 /*break*/, 1];
_a = size;
return [3 /*break*/, 3];
case 1:
_c = (_b = JSON).parse;
return [4 /*yield*/, async_storage_1.default.getItem(prefixedKey)];
case 2:
_a = _c.apply(_b, [_d.sent()]).byteSize;
_d.label = 3;
case 3:
itemSize = _a;
// first try to update the current size of the cache
return [4 /*yield*/, this._decreaseCurSizeInBytes(itemSize)];
case 4:
// first try to update the current size of the cache
_d.sent();
_d.label = 5;
case 5:
_d.trys.push([5, 7, , 9]);
return [4 /*yield*/, async_storage_1.default.removeItem(prefixedKey)];
case 6:
_d.sent();
return [3 /*break*/, 9];
case 7:
removeItemError_1 = _d.sent();
// if some error happened, we need to rollback the current size
return [4 /*yield*/, this._increaseCurSizeInBytes(itemSize)];
case 8:
// if some error happened, we need to rollback the current size
_d.sent();
logger.error("Failed to remove item: " + removeItemError_1);
return [3 /*break*/, 9];
case 9: return [2 /*return*/];
}
});
});
};
/**
* put item into cache
* @private
* @param prefixedKey - the key of the item
* @param itemData - the value of the item
* @param itemSizeInBytes - the byte size of the item
*/
AsyncStorageCache.prototype._setItem = function (prefixedKey, item) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var setItemErr_1;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
// first try to update the current size of the cache.
return [4 /*yield*/, this._increaseCurSizeInBytes(item.byteSize)];
case 1:
// first try to update the current size of the cache.
_a.sent();
_a.label = 2;
case 2:
_a.trys.push([2, 4, , 6]);
return [4 /*yield*/, async_storage_1.default.setItem(prefixedKey, JSON.stringify(item))];
case 3:
_a.sent();
return [3 /*break*/, 6];
case 4:
setItemErr_1 = _a.sent();
// if some error happened, we need to rollback the current size
return [4 /*yield*/, this._decreaseCurSizeInBytes(item.byteSize)];
case 5:
// if some error happened, we need to rollback the current size
_a.sent();
logger.error("Failed to set item " + setItemErr_1);
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
/**
* total space needed when poping out items
* @private
* @param itemSize
*
* @return total space needed
*/
AsyncStorageCache.prototype._sizeToPop = function (itemSize) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var spaceItemNeed, cacheThresholdSpace;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, this.getCacheCurSize()];
case 1:
spaceItemNeed = (_a.sent()) + itemSize - this.config.capacityInBytes;
cacheThresholdSpace = (1 - this.config.warningThreshold) * this.config.capacityInBytes;
return [2 /*return*/, spaceItemNeed > cacheThresholdSpace
? spaceItemNeed
: cacheThresholdSpace];
}
});
});
};
/**
* see whether cache is full
* @private
* @param itemSize
*
* @return true if cache is full
*/
AsyncStorageCache.prototype._isCacheFull = function (itemSize) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var _a;
return tslib_1.__generator(this, function (_b) {
switch (_b.label) {
case 0:
_a = itemSize;
return [4 /*yield*/, this.getCacheCurSize()];
case 1: return [2 /*return*/, (_a + (_b.sent()) > this.config.capacityInBytes)];
}
});
});
};
/**
* scan the storage and find out all the keys owned by this cache
* also clean the expired keys while scanning
* @private
* @return array of keys
*/
AsyncStorageCache.prototype._findValidKeys = function () {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var keys, keyInCache, i, key;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
keys = [];
return [4 /*yield*/, async_storage_1.default.getAllKeys()];
case 1:
keyInCache = _a.sent();
i = 0;
_a.label = 2;
case 2:
if (!(i < keyInCache.length)) return [3 /*break*/, 7];
key = keyInCache[i];
if (!(key.indexOf(this.config.keyPrefix) === 0 &&
key !== this.cacheCurSizeKey)) return [3 /*break*/, 6];
return [4 /*yield*/, this._isExpired(key)];
case 3:
if (!_a.sent()) return [3 /*break*/, 5];
return [4 /*yield*/, this._removeItem(key)];
case 4:
_a.sent();
return [3 /*break*/, 6];
case 5:
keys.push(key);
_a.label = 6;
case 6:
i += 1;
return [3 /*break*/, 2];
case 7: return [2 /*return*/, keys];
}
});
});
};
/**
* get all the items we have, sort them by their priority,
* if priority is same, sort them by their last visited time
* pop out items from the low priority (5 is the lowest)
* @private
* @param keys - all the keys in this cache
* @param sizeToPop - the total size of the items which needed to be poped out
*/
AsyncStorageCache.prototype._popOutItems = function (keys, sizeToPop) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var items, remainedSize, i, val, item, i;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
items = [];
remainedSize = sizeToPop;
i = 0;
_a.label = 1;
case 1:
if (!(i < keys.length)) return [3 /*break*/, 4];
return [4 /*yield*/, async_storage_1.default.getItem(keys[i])];
case 2:
val = _a.sent();
if (val != null) {
item = JSON.parse(val);
items.push(item);
}
_a.label = 3;
case 3:
i += 1;
return [3 /*break*/, 1];
case 4:
// first compare priority
// then compare visited time
items.sort(function (a, b) {
if (a.priority > b.priority) {
return -1;
}
else if (a.priority < b.priority) {
return 1;
}
else {
if (a.visitedTime < b.visitedTime) {
return -1;
}
else
return 1;
}
});
i = 0;
_a.label = 5;
case 5:
if (!(i < items.length)) return [3 /*break*/, 8];
// pop out items until we have enough room for new item
return [4 /*yield*/, this._removeItem(items[i].key, items[i].byteSize)];
case 6:
// pop out items until we have enough room for new item
_a.sent();
remainedSize -= items[i].byteSize;
if (remainedSize <= 0) {
return [2 /*return*/];
}
_a.label = 7;
case 7:
i += 1;
return [3 /*break*/, 5];
case 8: return [2 /*return*/];
}
});
});
};
/**
* Set item into cache. You can put number, string, boolean or object.
* The cache will first check whether has the same key.
* If it has, it will delete the old item and then put the new item in
* The cache will pop out items if it is full
* You can specify the cache item options. The cache will abort and output a warning:
* If the key is invalid
* If the size of the item exceeds itemMaxSize.
* If the value is undefined
* If incorrect cache item configuration
* If error happened with browser storage
*
* @param {String} key - the key of the item
* @param {Object} value - the value of the item
* @param {Object} [options] - optional, the specified meta-data
* @return {Prmoise}
*/
AsyncStorageCache.prototype.setItem = function (key, value, options) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var prefixedKey, cacheItemOptions, item, val, validKeys, sizeToPop, e_1;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
logger.debug("Set item: key is " + key + ", value is " + value + " with options: " + options);
prefixedKey = this.config.keyPrefix + key;
// invalid keys
if (prefixedKey === this.config.keyPrefix ||
prefixedKey === this.cacheCurSizeKey) {
logger.warn("Invalid key: should not be empty or 'CurSize'");
return [2 /*return*/];
}
if (typeof value === 'undefined') {
logger.warn("The value of item should not be undefined!");
return [2 /*return*/];
}
cacheItemOptions = {
priority: options && options.priority !== undefined
? options.priority
: this.config.defaultPriority,
expires: options && options.expires !== undefined
? options.expires
: this.config.defaultTTL + Utils_1.getCurrTime(),
};
if (cacheItemOptions.priority < 1 || cacheItemOptions.priority > 5) {
logger.warn("Invalid parameter: priority due to out or range. It should be within 1 and 5.");
return [2 /*return*/];
}
item = this.fillCacheItem(prefixedKey, value, cacheItemOptions);
// check wether this item is too big;
if (item.byteSize > this.config.itemMaxSize) {
logger.warn("Item with key: " + key + " you are trying to put into is too big!");
return [2 /*return*/];
}
_a.label = 1;
case 1:
_a.trys.push([1, 12, , 13]);
return [4 /*yield*/, async_storage_1.default.getItem(prefixedKey)];
case 2:
val = _a.sent();
if (!val) return [3 /*break*/, 4];
return [4 /*yield*/, this._removeItem(prefixedKey, JSON.parse(val).byteSize)];
case 3:
_a.sent();
_a.label = 4;
case 4: return [4 /*yield*/, this._isCacheFull(item.byteSize)];
case 5:
if (!_a.sent()) return [3 /*break*/, 10];
return [4 /*yield*/, this._findValidKeys()];
case 6:
validKeys = _a.sent();
return [4 /*yield*/, this._isCacheFull(item.byteSize)];
case 7:
if (!_a.sent()) return [3 /*break*/, 10];
return [4 /*yield*/, this._sizeToPop(item.byteSize)];
case 8:
sizeToPop = _a.sent();
return [4 /*yield*/, this._popOutItems(validKeys, sizeToPop)];
case 9:
_a.sent();
_a.label = 10;
case 10:
// put item in the cache
return [4 /*yield*/, this._setItem(prefixedKey, item)];
case 11:
// put item in the cache
_a.sent();
return [3 /*break*/, 13];
case 12:
e_1 = _a.sent();
logger.warn("setItem failed! " + e_1);
return [3 /*break*/, 13];
case 13: return [2 /*return*/];
}
});
});
};
/**
* Get item from cache. It will return null if item doesn’t exist or it has been expired.
* If you specified callback function in the options,
* then the function will be executed if no such item in the cache
* and finally put the return value into cache.
* Please make sure the callback function will return the value you want to put into the cache.
* The cache will abort output a warning:
* If the key is invalid
* If error happened with AsyncStorage
*
* @param {String} key - the key of the item
* @param {Object} [options] - the options of callback function
* @return {Promise} - return a promise resolves to be the value of the item
*/
AsyncStorageCache.prototype.getItem = function (key, options) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var ret, prefixedKey, item, val, e_2;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
logger.debug("Get item: key is " + key + " with options " + options);
ret = null;
prefixedKey = this.config.keyPrefix + key;
if (prefixedKey === this.config.keyPrefix ||
prefixedKey === this.cacheCurSizeKey) {
logger.warn("Invalid key: should not be empty or 'CurSize'");
return [2 /*return*/, null];
}
_a.label = 1;
case 1:
_a.trys.push([1, 8, , 9]);
return [4 /*yield*/, async_storage_1.default.getItem(prefixedKey)];
case 2:
ret = _a.sent();
if (!(ret != null)) return [3 /*break*/, 7];
return [4 /*yield*/, this._isExpired(prefixedKey)];
case 3:
if (!_a.sent()) return [3 /*break*/, 5];
// if expired, remove that item and return null
return [4 /*yield*/, this._removeItem(prefixedKey, JSON.parse(ret).byteSize)];
case 4:
// if expired, remove that item and return null
_a.sent();
return [3 /*break*/, 7];
case 5:
item = JSON.parse(ret);
return [4 /*yield*/, this._refreshItem(item, prefixedKey)];
case 6:
item = _a.sent();
return [2 /*return*/, item.data];
case 7:
if (options && options.callback !== undefined) {
val = options.callback();
if (val !== null) {
this.setItem(key, val, options);
}
return [2 /*return*/, val];
}
return [2 /*return*/, null];
case 8:
e_2 = _a.sent();
logger.warn("getItem failed! " + e_2);
return [2 /*return*/, null];
case 9: return [2 /*return*/];
}
});
});
};
/**
* remove item from the cache
* The cache will abort output a warning:
* If error happened with AsyncStorage
* @param {String} key - the key of the item
* @return {Promise}
*/
AsyncStorageCache.prototype.removeItem = function (key) {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var prefixedKey, val, e_3;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
logger.debug("Remove item: key is " + key);
prefixedKey = this.config.keyPrefix + key;
if (prefixedKey === this.config.keyPrefix ||
prefixedKey === this.cacheCurSizeKey) {
return [2 /*return*/];
}
_a.label = 1;
case 1:
_a.trys.push([1, 5, , 6]);
return [4 /*yield*/, async_storage_1.default.getItem(prefixedKey)];
case 2:
val = _a.sent();
if (!val) return [3 /*break*/, 4];
return [4 /*yield*/, this._removeItem(prefixedKey, JSON.parse(val).byteSize)];
case 3:
_a.sent();
_a.label = 4;
case 4: return [3 /*break*/, 6];
case 5:
e_3 = _a.sent();
logger.warn("removeItem failed! " + e_3);
return [3 /*break*/, 6];
case 6: return [2 /*return*/];
}
});
});
};
/**
* clear the entire cache
* The cache will abort output a warning:
* If error happened with AsyncStorage
* @return {Promise}
*/
AsyncStorageCache.prototype.clear = function () {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var keys, keysToRemove, i, i, e_4;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
logger.debug("Clear Cache");
_a.label = 1;
case 1:
_a.trys.push([1, 7, , 8]);
return [4 /*yield*/, async_storage_1.default.getAllKeys()];
case 2:
keys = _a.sent();
keysToRemove = [];
for (i = 0; i < keys.length; i += 1) {
if (keys[i].indexOf(this.config.keyPrefix) === 0) {
keysToRemove.push(keys[i]);
}
}
i = 0;
_a.label = 3;
case 3:
if (!(i < keysToRemove.length)) return [3 /*break*/, 6];
return [4 /*yield*/, async_storage_1.default.removeItem(keysToRemove[i])];
case 4:
_a.sent();
_a.label = 5;
case 5:
i += 1;
return [3 /*break*/, 3];
case 6: return [3 /*break*/, 8];
case 7:
e_4 = _a.sent();
logger.warn("clear failed! " + e_4);
return [3 /*break*/, 8];
case 8: return [2 /*return*/];
}
});
});
};
/**
* return the current size of the cache
* @return {Promise}
*/
AsyncStorageCache.prototype.getCacheCurSize = function () {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var ret;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, async_storage_1.default.getItem(this.cacheCurSizeKey)];
case 1:
ret = _a.sent();
if (!!ret) return [3 /*break*/, 3];
return [4 /*yield*/, async_storage_1.default.setItem(this.cacheCurSizeKey, '0')];
case 2:
_a.sent();
ret = '0';
_a.label = 3;
case 3: return [2 /*return*/, Number(ret)];
}
});
});
};
/**
* Return all the keys in the cache.
* Will return an empty array if error happend.
* @return {Promise}
*/
AsyncStorageCache.prototype.getAllKeys = function () {
return tslib_1.__awaiter(this, void 0, void 0, function () {
var keys, retKeys, i, e_5;
return tslib_1.__generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, async_storage_1.default.getAllKeys()];
case 1:
keys = _a.sent();
retKeys = [];
for (i = 0; i < keys.length; i += 1) {
if (keys[i].indexOf(this.config.keyPrefix) === 0 &&
keys[i] !== this.cacheCurSizeKey) {
retKeys.push(keys[i].substring(this.config.keyPrefix.length));
}
}
return [2 /*return*/, retKeys];
case 2:
e_5 = _a.sent();
logger.warn("getALlkeys failed! " + e_5);
return [2 /*return*/, []];
case 3: return [2 /*return*/];
}
});
});
};
/**
* Return a new instance of cache with customized configuration.
* @param {Object} config - the customized configuration
* @return {Object} - the new instance of Cache
*/
AsyncStorageCache.prototype.createInstance = function (config) {
if (config.keyPrefix === Utils_1.defaultConfig.keyPrefix) {
logger.error('invalid keyPrefix, setting keyPrefix with timeStamp');
config.keyPrefix = Utils_1.getCurrTime.toString();
}
return new AsyncStorageCache(config);
};
return AsyncStorageCache;
}(StorageCache_1.StorageCache));
exports.AsyncStorageCache = AsyncStorageCache;
var instance = new AsyncStorageCache();
exports.Cache = instance;
core_1.Amplify.register(instance);
//# sourceMappingURL=AsyncStorageCache.js.map