UNPKG

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
"use strict"; 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