@kwiz/common
Version:
KWIZ common utilities and helpers for M365 platform
383 lines • 14.7 kB
JavaScript
import { sizeOf } from "../helpers/collections.base";
import { isDebug } from "../helpers/debug";
import { flatted } from "../helpers/flatted";
import { jsonParse } from "../helpers/json";
import { getGlobal } from "../helpers/objects";
import { isDate, isNullOrEmptyString, isNullOrUndefined, isNumber } from "../helpers/typecheckers";
import { ConsoleLogger } from "./consolelogger";
let logger = ConsoleLogger.get("utils/localstoragecache");
export const keyPrefix = "kw$_";
export const LOCAL_STORAGE_PREFIX = "kwizcom-localstorage-cache";
export const LOCAL_STORGAGE_EXPIRATIONS_KEY = LOCAL_STORAGE_PREFIX + "-expirations";
export const DEFAULT_EXPIRATION = 20 * 60 * 1000; // 20 minutes;
/** When caching logic changes (serialization methods, format, schema), the MODULE_REVISION should be incremented
* and all client side apps will need to be rebuilt */
export const MODULE_REVISION = "1";
/** key (no prefix) is kept in lower case. not case sensitive */
function _getCache() {
let _cache = getGlobal("common_utils_localstoragecache_module_cache");
if (!_cache.purgeCalled) {
//issue 7081 - purge all orphans/expired items
_cache.purgeCalled = true;
//clear expired cache items.
(globalThis || window).setTimeout(() => {
purgeCache();
if (isDebug()) {
let size = _getStoredSize();
logger.debug(`Size of items in local storage: ${size}KB`);
}
}, 5000);
}
return _cache;
}
var _supportsLocalStorage = null;
function _parseExpiration(exp) {
var expirationDate;
if (isNumber(exp) && exp > 0) {
expirationDate = new Date();
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + exp);
}
else if (exp instanceof Date) {
expirationDate = exp;
}
else if (exp) {
var tempexp = exp;
var seconds = typeof (tempexp.seconds) === "number" ? tempexp.seconds : undefined;
var minutes = typeof (tempexp.minutes) === "number" ? tempexp.minutes : undefined;
var hours = typeof (tempexp.hours) === "number" ? tempexp.hours : undefined;
var days = typeof (tempexp.days) === "number" ? tempexp.days : undefined;
var months = typeof (tempexp.months) === "number" ? tempexp.months : undefined;
var years = typeof (tempexp.years) === "number" ? tempexp.years : undefined;
if (seconds || minutes || hours || days || months || years) {
expirationDate = new Date();
if (seconds) {
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + (seconds * 1000));
}
if (minutes) {
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + (minutes * 60 * 1000));
}
if (hours) {
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + (hours * 60 * 60 * 1000));
}
if (days) {
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + (days * 24 * 60 * 60 * 1000));
}
if (months) {
expirationDate.setMonth(expirationDate.getMonth() + months);
}
if (years) {
expirationDate.setFullYear(expirationDate.getFullYear() + years);
}
}
}
if (!expirationDate) {
expirationDate = new Date();
expirationDate.setMilliseconds(expirationDate.getMilliseconds() + DEFAULT_EXPIRATION);
}
return expirationDate;
}
function _getCacheExpirations() {
let _cache = _getCache();
if (isNullOrUndefined(_cache.expirations)) {
_cache.expirations = jsonParse(_getItem(LOCAL_STORGAGE_EXPIRATIONS_KEY));
//ISSUE: 1525 - expire the cache if it was built with a different version number so that the cache
//is compatible with the current build
if (!isNullOrUndefined(_cache.expirations) && _cache.expirations.build !== MODULE_REVISION) {
logger.log(`Purging cache because of build number change`);
purgeCache(true);
_cache.expirations = null;
}
if (isNullOrUndefined(_cache.expirations)) {
_cache.expirations = {
build: MODULE_REVISION
};
}
}
return _cache.expirations;
}
function _saveCacheExpirations() {
let _cache = _getCache();
if (!isNullOrUndefined(_cache.expirations) && sizeOf(_cache.expirations) > 0) {
_setItem(LOCAL_STORGAGE_EXPIRATIONS_KEY, JSON.stringify(_cache.expirations));
}
else {
_removeItem(LOCAL_STORGAGE_EXPIRATIONS_KEY);
}
}
function _setCacheExpiration(keyWithPrefix, expireDate) {
var expirations = _getCacheExpirations();
expirations[keyWithPrefix] = expireDate.toISOString();
_saveCacheExpirations();
}
function _isKeyExpired(keyWithPrefix) {
var expirations = _getCacheExpirations();
if (expirations && expirations[keyWithPrefix]) {
var now = new Date();
var eDate = new Date(expirations[keyWithPrefix]);
if (now > eDate) {
try {
delete expirations[keyWithPrefix];
}
catch (ex) {
expirations[keyWithPrefix] = undefined; // undefined variables are removed when passed to JSON.stringify
}
_saveCacheExpirations();
//has a date, it is expired.
return true;
}
//has a date, it is not expired yet.
return false;
}
//has no date or not in expirations at all - say it is expired...
return true;
}
function _getItem(key) {
try {
return localStorage.getItem(key);
}
catch {
}
return null;
}
function _setItem(key, value) {
try {
localStorage.setItem(key, value);
return true;
}
catch {
}
return false;
}
function _removeItem(key) {
try {
localStorage.removeItem(key);
return true;
}
catch {
}
return false;
}
/**Get the size (KB) of all entries in local storage. Only returns the size for entries with kwizcom key prefix. */
function _getStoredSize() {
let keys = getCacheKeys();
let total = 0;
let length = 0;
let useBlob = 'Blob' in (globalThis || window);
keys.forEach((key) => {
let v = _getItem(`${keyPrefix}${key}`);
if (!isNullOrEmptyString(v)) {
if (useBlob) {
length = (new Blob([v + key])).size;
}
else {
length = ((v.length + key.length) * 2);
}
}
total += length;
});
return Number((total / 1024).toFixed(2));
}
export function isLocalStorageSupported() {
if (_supportsLocalStorage !== null) {
return _supportsLocalStorage;
}
var result;
try {
_setItem(LOCAL_STORAGE_PREFIX, LOCAL_STORAGE_PREFIX);
result = _getItem(LOCAL_STORAGE_PREFIX) === LOCAL_STORAGE_PREFIX;
_removeItem(LOCAL_STORAGE_PREFIX);
_supportsLocalStorage = result;
}
catch (ex) {
_supportsLocalStorage = false;
}
return _supportsLocalStorage;
}
//#region exported methods
export function getCacheItem(key, options) {
key = key.toLowerCase();
let keyWithPrefix = keyPrefix + key;
let _cache = _getCache();
if (typeof (_cache[key]) !== "undefined"
&& _cache[key] !== null) {
let isExpired = _isKeyExpired(keyWithPrefix);
if (!isExpired) {
return _cache[key];
}
//else remove it from cache
removeCacheItem(key);
}
if (isLocalStorageSupported()) {
var value = _getItem(keyWithPrefix);
if (isNullOrUndefined(value)) {
return null;
}
let isExpired = _isKeyExpired(keyWithPrefix);
if (!isExpired) {
let valueAsT = options && options.useFlatted ? flatted.parse(value) : jsonParse(value);
if (valueAsT !== null) {
_cache[key] = valueAsT;
return valueAsT;
}
else {
_cache[key] = value;
return value;
}
}
//else remove it from cache
removeCacheItem(key);
}
return null;
}
export function setCacheItem(key, value, expiration, options) {
if (isLocalStorageSupported()) {
key = key.toLowerCase();
removeCacheItem(key);
var val = null;
try {
if (options && options.useFlatted)
val = flatted.stringify(value);
else
val = JSON.stringify(value);
}
catch (ex) {
logger.debug(`Object cannot be stored in local storage: ${ex && ex.message || ex} ${key}`);
return; //this put [object] in cache for me if object can't be stringified!
}
let keyWithPrefix = keyPrefix + key;
var expireDate = _parseExpiration(expiration);
let saved = _setItem(keyWithPrefix, val);
if (saved) {
_setCacheExpiration(keyWithPrefix, expireDate);
}
let _cache = _getCache();
_cache[key] = value;
}
}
export function removeCacheItem(keyNoPrefix) {
keyNoPrefix = keyNoPrefix.toLowerCase();
let _cache = _getCache();
delete _cache[keyNoPrefix];
let keyWithPrefix = keyPrefix + keyNoPrefix;
if (isLocalStorageSupported()) {
_removeItem(keyNoPrefix); //in case we have an old one
_removeItem(keyWithPrefix);
}
}
export function removeCacheItems(keys) {
keys.forEach((key) => {
removeCacheItem(key);
});
}
export function getCacheKeys() {
let keys = [];
if (isLocalStorageSupported()) {
keys = Object.keys(localStorage).filter((key) => {
return key.startsWith(keyPrefix);
}).map((key) => {
return key.substring(keyPrefix.length);
});
}
return keys;
}
/** remove expired cache keys created by this utility.
* to remove all keys (non-expired too) send removeAll=true
*/
function purgeCache(removeAll) {
if (!isLocalStorageSupported())
return;
var cacheExpirationsKeys = [
LOCAL_STORGAGE_EXPIRATIONS_KEY,
"kwizcom-aplfe-caching-expirations", // old clean up
"localStorageExpirations" // old clean up
];
let now = new Date();
let nonExpiredKeys = [];
//get all expiration keys (key/expiration date/time)
for (let j = 0; j < cacheExpirationsKeys.length; j++) {
try {
let expirations = null;
let cacheExpirationsKey = cacheExpirationsKeys[j];
let removeAllForKey = removeAll || cacheExpirationsKey !== LOCAL_STORGAGE_EXPIRATIONS_KEY;
if (cacheExpirationsKey === "localStorageExpirations") {
//old format - load expirations from this one as well
expirations = _getItem(cacheExpirationsKey); // "key1^11/18/2011 5pm|key2^3/10/2012 3pm"
if (expirations) {
let arr = expirations.split("|"); // ["key1^11/18/2011 5pm","key2^3/10/2012 3pm"]
for (let i = 0; i < arr.length; i++) {
try {
let key_expiration_format = arr[i]; // "key1^11/18/2011 5pm"
let key = key_expiration_format.split("^")[0];
//old keys - remove all, all the time
_removeItem(key); //remove key from cache
}
catch (e) { }
}
}
}
else {
//new format
expirations = cacheExpirationsKey === LOCAL_STORGAGE_EXPIRATIONS_KEY ? _getCacheExpirations() : jsonParse(_getItem(cacheExpirationsKey));
if (expirations) {
let expirationKeys = Object.keys(expirations);
logger.group(() => {
expirationKeys.forEach(keyWithPrefix => {
try {
let shouldRemoveKey = removeAllForKey || !keyWithPrefix.startsWith(keyPrefix);
if (!shouldRemoveKey) {
//check specific key expiration
let expirationDate = new Date(expirations[keyWithPrefix]);
if (!isDate(expirationDate) || expirationDate < now) {
shouldRemoveKey = true;
delete expirations[keyWithPrefix];
logger.info(`purging key ${keyWithPrefix}`);
}
else {
nonExpiredKeys.push(keyWithPrefix);
}
}
if (shouldRemoveKey)
_removeItem(keyWithPrefix);
}
catch (e) {
logger.warn(`failed to remove key ${keyWithPrefix}`);
}
});
}, "Checking expired items", true);
}
}
if (cacheExpirationsKey === LOCAL_STORGAGE_EXPIRATIONS_KEY)
_saveCacheExpirations();
else //older keys - just remove them.
_removeItem(cacheExpirationsKey);
}
catch (e) {
logger.warn(`something went terribly wrong ${e}`);
}
}
logger.group(() => {
logger.table(nonExpiredKeys);
//cleanup orphans
//loop on all keys
//if stats with: jsr_, kwfs| or keyPrefix - and not in nonExpiredKeys, it is an orphan. Remove it.
let localStorageKeys = Object.keys(localStorage);
for (let keyIdx = 0; keyIdx < localStorageKeys.length; keyIdx++) {
let key = localStorageKeys[keyIdx];
if (key.startsWith("jsr_") || key.startsWith("kwfs|")) {
logger.log(`removing old key ${key}`);
_removeItem(key); //old key
}
else if (key.startsWith(keyPrefix) && !nonExpiredKeys.includes(key)) //orphan!
{
logger.log(`removing orphan key ${key}`);
_removeItem(key);
}
}
}, "Expired keys", true);
}
/** cleanup - remove all local storage keys created by this utility */
export function clearCache() {
return purgeCache(true);
}
//#endregion
//# sourceMappingURL=localstoragecache.js.map