use-cache-helper
Version:
use-cache-helper provides helper functions to easily manage and scale your redis and database caching strategies.
441 lines • 16.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.deletePaginatedList = exports.generateKeyFromQueryFilters = exports.updateItemScoreFromPaginatedList = exports.removeItemFromPaginatedList = exports.insertToPaginatedList = exports.insertRecordsToPaginatedList = exports.getPaginatedListTotalItems = exports.getPaginatedListByPage = exports.init = exports.set = exports.getOrRefreshDataInPaginatedList = exports.getDefaulItemCacheKeyForPaginatedList = exports.getOrRefresh = void 0;
const helpers_1 = require("./helpers");
const errors_1 = require("./errors");
const store_1 = require("./store");
/**
* Get latest cache data or force refresh before returning the value
* @param {Object} params
* @param {string} params.key - Data cache key
* @param {number} params.expiry - (optional) Expiry in seconds
* @param {boolean} params.forceRefresh - (optional) Force refresh
* @param {boolean} params.parseResult - (optional) Call JSON.parse on resulting data
* @param {function} params.cacheRefreshHandler - (optional) Refresh function
* @returns
*/
const getOrRefresh = async (params) => {
const { forceRefresh, parseResult, key, expiry, cacheRefreshHandler } = params;
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
let res = "";
if (redis) {
res = await redis.get(key);
}
else if (upstashRedis) {
res = await upstashRedis.get(key);
}
else {
(0, helpers_1.checkRedis)();
}
if ((!res || forceRefresh) &&
cacheRefreshHandler &&
typeof cacheRefreshHandler === "function") {
const val = await cacheRefreshHandler();
if (typeof val === "undefined" || val === "undefined") {
return undefined;
}
else if (val === null || val === "null") {
return null;
}
const hasExpiryProvided = typeof expiry === "number" && expiry > 0;
const sanitized = val && typeof val === "object" ? JSON.stringify(val) : `${val || ""}`;
if (redis) {
await redis.set(key, sanitized, hasExpiryProvided ? "EX" : undefined, hasExpiryProvided ? expiry : undefined);
}
else if (upstashRedis) {
await upstashRedis.set(key, sanitized, hasExpiryProvided
? {
ex: expiry,
}
: {});
}
return val;
}
if (typeof res === "undefined" || res === "undefined") {
return undefined;
}
if (parseResult && (res === null || res === "null")) {
return null;
}
if (parseResult && res && typeof res === "string") {
try {
return JSON.parse(res);
}
catch (err) {
return res;
}
}
return res;
};
exports.getOrRefresh = getOrRefresh;
/**
* Retrieve the formatted default cache key for an item in a paginated list.
* @param {string} listKey - Your list's cache key
* @param {string} itemId - Your item id
* @returns {string}
*/
const getDefaulItemCacheKeyForPaginatedList = (listKey, itemId) => {
return `${listKey}:id:${itemId}`;
};
exports.getDefaulItemCacheKeyForPaginatedList = getDefaulItemCacheKeyForPaginatedList;
/**
* Get latest cache data of 'key'.
* Wraps the `getOrRefresh` function and enables you to update the score of an item in the list.
* @param {Object} params
* @param {string} params.listKey - Your list's cache key
* @param {string} params.id - Item ID
* @param {string} params.key - (optional) Data cache key
* @param {number} params.expiry - (optional) Expiry in seconds
* @param {boolean} params.forceRefresh - (optional) Force refresh
* @param {boolean} params.parseResult - (optional) Call JSON.parse on resulting data
* @param {number} params.score - (optional) Determines the new score of the cache data. Update LRU score.
* @param {boolean} params.updateScoreInPaginatedList - (optional) If set to true, thn apply new score.
* @param {function} params.cacheRefreshHandler - (optional) Refresh function
* @returns {Object} IGetOrRefreshReturnValue
*/
const getOrRefreshDataInPaginatedList = async (params) => {
const { updateScoreInPaginatedList, score, listKey, key, id: itemId, } = params;
const cacheKey = !key
? (0, exports.getDefaulItemCacheKeyForPaginatedList)(listKey, itemId)
: key;
const val = await (0, exports.getOrRefresh)({ ...params, key: cacheKey });
const id = itemId ? itemId : val && val["id"];
if (typeof val !== "undefined" &&
!!val &&
typeof score === "number" &&
score > 0 &&
typeof id === "string" &&
!!id &&
updateScoreInPaginatedList) {
// update score in the list for LRU algorithm
// so you can evict least recently used data
await (0, exports.updateItemScoreFromPaginatedList)({
id,
score,
key: listKey,
});
}
return val;
};
exports.getOrRefreshDataInPaginatedList = getOrRefreshDataInPaginatedList;
/**
* Set redis cache.
* Object data will be sanitzed with JSON.stringify()
* @param {Object} params
* @param {string} params.key - Data cache key
* @param {string | number | Object} params.value - Your data for the cache key
* @param {number} expiry - (optional) Expiry in seconds. No expiry set by default.
* @returns {string} 'OK' | 'Error'
*/
const set = async (params) => {
try {
const { key, value, expiry } = params;
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
const isObject = typeof value === "object" && !!value;
const hasExpiryProvided = typeof expiry === "number" && expiry > 0;
if (typeof value === "undefined") {
throw new errors_1.UseCacheError("set(): value should not be undefined.");
}
(0, helpers_1.checkRedis)();
const valueToCache = isObject ? JSON.stringify(value) : `${value}`;
if (redis) {
const res = await redis.set(key, valueToCache, hasExpiryProvided ? "EX" : undefined, hasExpiryProvided ? expiry : undefined);
return res;
}
else if (upstashRedis) {
const res = await upstashRedis.set(key, valueToCache, hasExpiryProvided
? {
ex: expiry,
}
: {});
return res;
}
else {
throw new errors_1.UseCacheError("Redis instance missing");
}
}
catch (err) {
const errMessage = err?.message || "";
return errMessage;
}
};
exports.set = set;
/**
* Required initial function to run from the start of your app
* @param params
* @param {Object} params.redis - (optional) Your ioredis instance
* @param {Object} params.upstashRedis - (optional) Your @upstash/redis instance
* @param {number} params.maxPaginatedItems - (optional) Maximum number of paginated items before it starts evicting data.
*/
const init = (params) => {
store_1.store.redis = params.redis;
store_1.store.upstashRedis = params.upstashRedis;
if (params?.maxPaginatedItems > 0) {
store_1.store.maxPaginatedItems = params.maxPaginatedItems;
}
(0, helpers_1.checkRedis)();
};
exports.init = init;
/**
* Get paginated list by page.
* Always in ascending order.
* @param {Object} params
* @param {string} params.key - Your list's cache key
* @param {number} params.page - Target page
* @param {number} params.sizePerPage - Total items in a single page
* @param {boolean} params.ascendingOrder - (optional) Fetch list in ascending order. High to low by default.
* @returns {Object} IGetPaginatedListByPageParams
*/
const getPaginatedListByPage = async (params) => {
const { page, sizePerPage, key, ascendingOrder = false } = params;
const start = (page - 1) * sizePerPage;
const end = start + (sizePerPage - 1);
const items = [];
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
let res = [];
(0, helpers_1.checkRedis)();
if (redis && !ascendingOrder) {
res = await redis.zrevrange(key, start, end, "WITHSCORES");
}
else if (redis) {
res = await redis.zrange(key, start, end, "WITHSCORES");
}
else if (upstashRedis) {
res = await upstashRedis.zrange(key, start, end, {
withScores: true,
rev: !ascendingOrder,
});
}
if (res?.length) {
for (let i = 0; i < res.length; i += 2) {
const score = parseFloat(res[i + 1]);
const id = `${res[i] || ""}`;
if (id?.length > 0 && typeof score === "number") {
items.push({ id, score });
}
}
}
else {
return [];
}
return items
.filter((item) => !!item && item?.id?.length > 0)
.sort((a, b) => {
const scoreA = ascendingOrder
? a.score ?? Infinity
: a.score ?? -Infinity;
const scoreB = ascendingOrder
? b.score ?? Infinity
: b.score ?? -Infinity;
if (ascendingOrder) {
return scoreA - scoreB;
}
return scoreB - scoreA;
})
.map((item) => item?.id);
};
exports.getPaginatedListByPage = getPaginatedListByPage;
/**
* Fetch total items in a list
* @param {string} key - Your list's cache key
* @returns
*/
const getPaginatedListTotalItems = async (key) => {
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
(0, helpers_1.checkRedis)();
if (redis) {
const count = await redis.zcard(key);
return count;
}
else {
const count = await upstashRedis.zcard(key);
return count;
}
};
exports.getPaginatedListTotalItems = getPaginatedListTotalItems;
/**
* Automatically insert ID data from your array of objects.
* Use non-zero & non-negative scores.
* Each payload will be cac
* @param {Object} params
* @param {string} params.listKey - Your list's cache key
* @param {Array} params.listData - Your list data in array form.
* @param {string} params.cacheDataPrefix - Prefix cache to your data. Example: `users:${id}`
* @param {boolean} params.cachePayload - If set to true, each payload in the list will be cached.
* @param {number} params.cachePayloadExpiry - Expiry for each payload cache, unit in seconds.
* @returns {string} - 'OK' | 'Error'
*/
const insertRecordsToPaginatedList = async (params) => {
(0, helpers_1.checkRedis)();
const { listData, listKey, cacheDataPrefix, cachePayload, cachePayloadExpiry, } = params;
if (listData?.length > 0) {
const invalidScores = listData.filter((d) => typeof d?.score !== "number");
if (invalidScores?.length > 0 || invalidScores?.length) {
throw new errors_1.UseCacheError("insertRecordsToPaginatedList() invalid score");
}
for (let i = 0; i < listData.length; i++) {
const payload = listData[i];
const { id, score } = payload;
await (0, exports.insertToPaginatedList)({
score,
id,
key: listKey,
});
if (cachePayload) {
await (0, exports.set)({
key: typeof cacheDataPrefix === "string" && !!cacheDataPrefix
? `${cacheDataPrefix}${id}`
: (0, exports.getDefaulItemCacheKeyForPaginatedList)(listKey, id),
value: payload,
...(typeof cachePayloadExpiry === "number" &&
cachePayloadExpiry > 0 && { expiry: cachePayloadExpiry }),
});
}
}
return "OK";
}
return "Error";
};
exports.insertRecordsToPaginatedList = insertRecordsToPaginatedList;
/**
* Insert an item to the list using the item ID.
* Use non-zero & non-negative scores.
* @param {Object} params
* @param {string} params.key - Your list's cache key
* @param {string} params.id - Data id
* @param {number} params.score - (optional) Score order of the item in the paginated list, determining its placement.
* @returns {string}
*/
const insertToPaginatedList = async (params) => {
const { score, key, id } = params;
const total = await (0, exports.getPaginatedListTotalItems)(key); // get count
const maxPaginatedItems = store_1.store.maxPaginatedItems;
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
const scoreToUse = typeof score !== "number" ? Date.now() : score;
(0, helpers_1.checkRedis)();
// if number of items limit reached
// evict least recently used data
if (total >= maxPaginatedItems && redis) {
await redis.zpopmin(key);
}
else if (total >= maxPaginatedItems && upstashRedis) {
await upstashRedis.zpopmin(key);
}
if (redis) {
const response = await redis.zadd(key, scoreToUse, id);
if (response > 0) {
return "OK";
}
}
else if (upstashRedis) {
await upstashRedis.zadd(key, { incr: true }, { score: scoreToUse, member: id });
return "OK";
}
return "Error";
};
exports.insertToPaginatedList = insertToPaginatedList;
/**
* Remove item from the list.
* @param {Object} params
* @param {string} params.key - Your list's cache key
* @param {string} params.id - Item ID
* @returns {string} 'OK' | 'Error'
*/
const removeItemFromPaginatedList = async (params) => {
const { id, key } = params;
if (!id) {
throw new errors_1.UseCacheError("removeItemFromPaginatedList(): Invalid id.");
}
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
if (redis) {
const response = await redis.zrem(key, id);
if (response > 0) {
return "OK";
}
}
else if (upstashRedis) {
const response = await upstashRedis.zrem(key, id);
if (response > 0) {
return "OK";
}
}
return "Error";
};
exports.removeItemFromPaginatedList = removeItemFromPaginatedList;
/**
* Update the score of an item in the paginated list to move it up or down in the order.
* @param {Object} params
* @param {string} params.key - Your list's cache key
* @param {string} params.id - Item ID*
* @param {number} params.score - Score order of the item in the paginated list, determining its placement.
* @returns
*/
const updateItemScoreFromPaginatedList = async (params) => {
const { score, id, key } = params;
if (!id) {
throw new errors_1.UseCacheError("updateItemScoreFromPaginatedList(): Invalid id.");
}
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
if (redis) {
const response = await redis.zadd(key, score, id);
if (response > 0) {
return "OK";
}
}
else if (upstashRedis) {
await upstashRedis.zadd(key, { incr: true }, { score, member: id });
return "OK";
}
return "Error";
};
exports.updateItemScoreFromPaginatedList = updateItemScoreFromPaginatedList;
/**
* Generate a string key for your cache based on the formatted filter properties of your database query.
* {"limit" : 1 , "team" : "team-id" } => "limit1Teamteamid"
* @param filters
* @returns
*/
const generateKeyFromQueryFilters = (filters) => {
if (typeof filters === "object" && filters) {
let result = "";
for (const key in filters) {
if (Object.prototype.hasOwnProperty.call(filters, key)) {
const val = `${filters[key] || ""}`;
result += `${key.charAt(0).toUpperCase() + key.slice(1)}${val ? val.charAt(0).toUpperCase() + val.slice(1) : ""}`;
}
}
return result;
}
return "";
};
exports.generateKeyFromQueryFilters = generateKeyFromQueryFilters;
/**
* Hard delete paginated list and items
* @param {string} listKey
* @returns {string | number} Status from upstash redis or ioredis
*/
const deletePaginatedList = async (listKey) => {
const redis = store_1.store.redis;
const upstashRedis = store_1.store.upstashRedis;
if (redis) {
const res = await upstashRedis.del(listKey);
return res;
}
else if (upstashRedis) {
const res = await upstashRedis.del(listKey);
return res;
}
else {
(0, helpers_1.checkRedis)();
return "Error";
}
};
exports.deletePaginatedList = deletePaginatedList;
//# sourceMappingURL=features.js.map