UNPKG

@rb2bv/cache-handler

Version:
169 lines (163 loc) 5.95 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/handlers/redis-strings.ts var redis_strings_exports = {}; __export(redis_strings_exports, { default: () => createHandler }); module.exports = __toCommonJS(redis_strings_exports); // src/constants.ts var MAX_INT32 = 2 ** 31 - 1; var REVALIDATED_TAGS_KEY = "__revalidated_tags__"; // src/helpers/get-timeout-redis-command-options.ts var import_redis = require("redis"); function getTimeoutRedisCommandOptions(timeoutMs) { if (timeoutMs === 0) { return (0, import_redis.commandOptions)({}); } return (0, import_redis.commandOptions)({ signal: AbortSignal.timeout(timeoutMs) }); } // src/helpers/is-implicit-tag.ts var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_"; function isImplicitTag(tag) { return tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID); } // src/handlers/redis-strings.ts function createHandler({ client, keyPrefix = "", sharedTagsKey = "__sharedTags__", timeoutMs = 5e3, keyExpirationStrategy = "EXPIREAT", revalidateTagQuerySize = 100 }) { function assertClientIsReady() { if (!client.isReady) { throw new Error("Redis client is not ready yet or connection is lost. Keep trying..."); } } const revalidatedTagsKey = keyPrefix + REVALIDATED_TAGS_KEY; return { name: "redis-strings", async get(key, { implicitTags }) { assertClientIsReady(); const result = await client.get(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + key); if (!result) { return null; } const cacheValue = JSON.parse(result); if (!cacheValue) { return null; } const combinedTags = /* @__PURE__ */ new Set([...cacheValue.tags, ...implicitTags]); if (combinedTags.size === 0) { return cacheValue; } const revalidationTimes = await client.hmGet( getTimeoutRedisCommandOptions(timeoutMs), revalidatedTagsKey, Array.from(combinedTags) ); for (const timeString of revalidationTimes) { if (timeString && Number.parseInt(timeString, 10) > cacheValue.lastModified) { await client.unlink(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + key); return null; } } return cacheValue; }, async set(key, cacheHandlerValue) { assertClientIsReady(); const options = getTimeoutRedisCommandOptions(timeoutMs); let setOperation; let expireOperation; switch (keyExpirationStrategy) { case "EXAT": { setOperation = client.set( options, keyPrefix + key, JSON.stringify(cacheHandlerValue), typeof cacheHandlerValue.lifespan?.expireAt === "number" ? { EXAT: cacheHandlerValue.lifespan.expireAt } : void 0 ); break; } case "EXPIREAT": { setOperation = client.set(options, keyPrefix + key, JSON.stringify(cacheHandlerValue)); expireOperation = cacheHandlerValue.lifespan ? client.expireAt(options, keyPrefix + key, cacheHandlerValue.lifespan.expireAt) : void 0; break; } default: { throw new Error(`Invalid keyExpirationStrategy: ${keyExpirationStrategy}`); } } const setTagsOperation = cacheHandlerValue.tags.length > 0 ? client.hSet(options, keyPrefix + sharedTagsKey, key, JSON.stringify(cacheHandlerValue.tags)) : void 0; await Promise.all([setOperation, expireOperation, setTagsOperation]).catch((e) => { console.error("Error setting cache value:", e); throw e; }); }, async revalidateTag(tag) { assertClientIsReady(); if (isImplicitTag(tag)) { await client.hSet(getTimeoutRedisCommandOptions(timeoutMs), revalidatedTagsKey, tag, Date.now()); } const tagsMap = /* @__PURE__ */ new Map(); let cursor = 0; const hScanOptions = { COUNT: revalidateTagQuerySize }; do { const remoteTagsPortion = await client.hScan( getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + sharedTagsKey, cursor, hScanOptions ); for (const { field, value } of remoteTagsPortion.tuples) { tagsMap.set(field, JSON.parse(value)); } cursor = remoteTagsPortion.cursor; } while (cursor !== 0); const keysToDelete = []; const tagsToDelete = []; for (const [key, tags] of tagsMap) { if (tags.includes(tag)) { keysToDelete.push(keyPrefix + key); tagsToDelete.push(key); } } if (keysToDelete.length === 0) { return; } const deleteKeysOperation = client.unlink(getTimeoutRedisCommandOptions(timeoutMs), keysToDelete); const updateTagsOperation = client.hDel( { isolated: true, ...getTimeoutRedisCommandOptions(timeoutMs) }, keyPrefix + sharedTagsKey, tagsToDelete ); await Promise.all([deleteKeysOperation, updateTagsOperation]).catch((e) => { console.error("Error revalidating tag:", e); throw e; }); }, async delete(key) { await client.unlink(getTimeoutRedisCommandOptions(timeoutMs), key); } }; }