UNPKG

@rb2bv/cache-handler

Version:
170 lines (164 loc) 5.42 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-stack.ts var redis_stack_exports = {}; __export(redis_stack_exports, { default: () => createHandler }); module.exports = __toCommonJS(redis_stack_exports); var import_redis2 = require("redis"); var import_node_crypto = require("crypto"); // src/constants.ts var MAX_INT32 = 2 ** 31 - 1; var TIME_ONE_YEAR = 31536e3; 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-stack.ts function createHandler({ client, keyPrefix = "", timeoutMs = 5e3, revalidateTagQuerySize = 100 }) { function assertClientIsReady() { if (!client.isReady) { throw new Error("Redis client is not ready"); } } function sanitizeTag(str) { return str.replace(/[^a-zA-Z0-9]/gi, "_"); } const indexName = `idx:tags-${(0, import_node_crypto.randomBytes)(32).toString("hex")}`; async function createIndexIfNotExists() { try { await client.ft.create( indexName, { "$.tags": { type: import_redis2.SchemaFieldTypes.TEXT, AS: "tag" } }, { ON: "JSON", TEMPORARY: TIME_ONE_YEAR } ); } catch (error) { if (error instanceof import_redis2.ErrorReply && error.message === "Index already exists") { return; } throw error; } } const revalidatedTagsKey = keyPrefix + REVALIDATED_TAGS_KEY; return { name: "redis-stack", async get(key, { implicitTags }) { assertClientIsReady(); const cacheValue = await client.json.get( getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + key ); if (!cacheValue) { return null; } const sanitizedImplicitTags = implicitTags.map(sanitizeTag); const combinedTags = /* @__PURE__ */ new Set([...cacheValue.tags, ...sanitizedImplicitTags]); 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(); cacheHandlerValue.tags = cacheHandlerValue.tags.map(sanitizeTag); const options = getTimeoutRedisCommandOptions(timeoutMs); const setCacheValue = client.json.set( options, keyPrefix + key, ".", cacheHandlerValue ); const expireCacheValue = cacheHandlerValue.lifespan ? client.expireAt(options, keyPrefix + key, cacheHandlerValue.lifespan.expireAt) : void 0; await Promise.all([setCacheValue, expireCacheValue]); }, async revalidateTag(tag) { assertClientIsReady(); await createIndexIfNotExists(); const sanitizedTag = sanitizeTag(tag); if (isImplicitTag(tag)) { await client.hSet( getTimeoutRedisCommandOptions(timeoutMs), revalidatedTagsKey, sanitizedTag, Date.now() ); } let from = 0; const keysToDelete = []; while (true) { const { documents: documentIds } = await client.ft.searchNoContent( getTimeoutRedisCommandOptions(timeoutMs), indexName, `@tag:(${sanitizedTag})`, { LIMIT: { from, size: revalidateTagQuerySize }, TIMEOUT: timeoutMs } ); for (const id of documentIds) { keysToDelete.push(id); } if (documentIds.length < revalidateTagQuerySize) { break; } from += revalidateTagQuerySize; } if (keysToDelete.length === 0) { return; } const options = getTimeoutRedisCommandOptions(timeoutMs); await client.unlink(options, keysToDelete); }, async delete(key) { await client.unlink(getTimeoutRedisCommandOptions(timeoutMs), key); } }; }