UNPKG

@safaricom-mxl/nextjs-turbo-redis-cache

Version:

Next.js redis cache handler

870 lines (863 loc) 29.6 kB
// src/RedisStringsHandler.ts import { createClient } from "redis"; // src/utils/debug.ts function debug(_color = "none", ..._args) { const _colorCode = { red: "\x1B[31m", blue: "\x1B[34m", green: "\x1B[32m", yellow: "\x1B[33m", cyan: "\x1B[36m", white: "\x1B[37m", none: "" }; const isEnabled = process.env.DEBUG_CACHE_HANDLER; if (isEnabled && isEnabled !== "false" && isEnabled !== "0") { } } function debugVerbose(..._args) { const isEnabled = process.env.DEBUG_CACHE_HANDLER_VERBOSE; if (isEnabled && isEnabled !== "false" && isEnabled !== "0") { } } // src/DeduplicatedRequestHandler.ts var DeduplicatedRequestHandler = class { constructor(fn, cachingTimeMs, inMemoryDeduplicationCache) { // Method to handle deduplicated requests this.deduplicatedFunction = (key) => { debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction() called with", key ); const self = this; const dedupedFn = async (...args) => { debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn called with", key ); if (self.inMemoryDeduplicationCache?.has(key)) { debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ", key, "found key in inMemoryDeduplicationCache" ); const res = await self.inMemoryDeduplicationCache.get(key)?.then((v) => structuredClone(v)); debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ", key, "found key in inMemoryDeduplicationCache and served result from there", JSON.stringify(res).substring(0, 200) ); return res; } const promise = self.fn(...args); self.inMemoryDeduplicationCache.set(key, promise); debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ", key, "did not found key in inMemoryDeduplicationCache. Setting it now and waiting for promise to resolve" ); try { const ts = performance.now(); const result = await promise; debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ", key, "promise resolved (in ", performance.now() - ts, "ms). Returning result", JSON.stringify(result).substring(0, 200) ); return structuredClone(result); } finally { setTimeout(() => { debugVerbose( "DeduplicatedRequestHandler.deduplicatedFunction().dedupedFn ", key, "deleting key from inMemoryDeduplicationCache after ", self.cachingTimeMs, "ms" ); self.inMemoryDeduplicationCache.delete(key); }, self.cachingTimeMs); } }; return dedupedFn; }; this.fn = fn; this.cachingTimeMs = cachingTimeMs; this.inMemoryDeduplicationCache = inMemoryDeduplicationCache; } // Method to manually seed a result into the cache seedRequestReturn(key, value) { const resultPromise = new Promise((res) => res(value)); this.inMemoryDeduplicationCache.set(key, resultPromise); debugVerbose( "DeduplicatedRequestHandler.seedRequestReturn() seeded result ", key, value.substring(0, 200) ); setTimeout(() => { this.inMemoryDeduplicationCache.delete(key); }, this.cachingTimeMs); } }; // src/SyncedMap.ts var SYNC_CHANNEL_SUFFIX = ":sync-channel:"; var SyncedMap = class { constructor(options) { this.client = options.client; this.keyPrefix = options.keyPrefix; this.redisKey = options.redisKey; this.syncChannel = `${options.keyPrefix}${SYNC_CHANNEL_SUFFIX}${options.redisKey}`; this.database = options.database; this.querySize = options.querySize; this.filterKeys = options.filterKeys; this.resyncIntervalMs = options.resyncIntervalMs; this.customizedSync = options.customizedSync; this.map = /* @__PURE__ */ new Map(); this.subscriberClient = this.client.duplicate(); this.setupLock = new Promise((resolve) => { this.setupLockResolve = resolve; }); this.setup().catch((error) => { throw error; }); } async setup() { try { const setupPromises = []; if (!this.customizedSync?.withoutRedisHashmap) { setupPromises.push(this.initialSync()); this.setupPeriodicResync(); } setupPromises.push(this.setupPubSub()); await Promise.all(setupPromises); this.setupLockResolve(); } catch (error) { this.setupLockResolve(); throw error; } } async initialSync() { let cursor = 0; const _hScanOptions = { COUNT: this.querySize }; do { const hScanResult = await redisErrorHandler( "SyncedMap.initialSync(), operation: hScan " + this.syncChannel + " " + this.keyPrefix + " " + this.redisKey + " " + cursor + " " + this.querySize, this.client.hScan( this.keyPrefix + this.redisKey, cursor.toString(), "COUNT", this.querySize.toString() ) ); let nextCursor = 0; if (Array.isArray(hScanResult)) { nextCursor = Number.parseInt(hScanResult[0], 10); const elems = hScanResult[1]; for (let i = 0; i < elems.length; i += 2) { const field = elems[i]; const value = elems[i + 1]; if (this.filterKeys(field)) { try { const parsed = JSON.parse(value); this.map.set(field, parsed); } catch { } } } } else if (hScanResult && hScanResult.tuples) { const obj = hScanResult; nextCursor = typeof obj.cursor === "string" ? Number.parseInt(obj.cursor, 10) : obj.cursor; for (const { field, value } of obj.tuples) { if (this.filterKeys(field)) { try { const parsed = JSON.parse(value); this.map.set(field, parsed); } catch { } } } } cursor = nextCursor; } while (cursor !== 0); await this.cleanupKeysNotInRedis(); } async cleanupKeysNotInRedis() { let cursor = 0; const scanOptions = { COUNT: this.querySize, MATCH: `${this.keyPrefix}*` }; let remoteKeys = []; do { const remoteKeysPortion = await redisErrorHandler( "SyncedMap.cleanupKeysNotInRedis(), operation: scan " + this.keyPrefix, this.client.scan(cursor.toString(), scanOptions) ); let nextCursor = 0; if (Array.isArray(remoteKeysPortion)) { nextCursor = Number.parseInt(remoteKeysPortion[0], 10); const keysPart = remoteKeysPortion[1]; remoteKeys = remoteKeys.concat(keysPart); } else { if (remoteKeysPortion && Array.isArray(remoteKeysPortion.keys)) { remoteKeys = remoteKeys.concat(remoteKeysPortion.keys); } nextCursor = typeof remoteKeysPortion?.cursor === "string" ? Number.parseInt(remoteKeysPortion.cursor, 10) : remoteKeysPortion?.cursor || 0; } cursor = nextCursor; } while (cursor !== 0); const remoteKeysSet = new Set( remoteKeys.map((key) => key.substring(this.keyPrefix.length)) ); const keysToDelete = []; for (const key of this.map.keys()) { const keyStr = key; if (!remoteKeysSet.has(keyStr) && this.filterKeys(keyStr)) { keysToDelete.push(keyStr); } } if (keysToDelete.length > 0) { await this.delete(keysToDelete); } } setupPeriodicResync() { if (this.resyncIntervalMs && this.resyncIntervalMs > 0) { setInterval(() => { this.initialSync().catch((_error) => { }); }, this.resyncIntervalMs); } } async setupPubSub() { const syncHandler = async (message) => { const syncMessage = JSON.parse(message); if (syncMessage.type === "insert") { if (syncMessage.key !== void 0 && syncMessage.value !== void 0) { this.map.set(syncMessage.key, syncMessage.value); } } else if (syncMessage.type === "delete" && syncMessage.keys) { for (const key of syncMessage.keys) { this.map.delete(key); } } }; const keyEventHandler = async (key, message) => { debug( "yellow", "SyncedMap.keyEventHandler() called with message", this.redisKey, message, key ); if (key.startsWith(this.keyPrefix)) { const keyInMap = key.substring(this.keyPrefix.length); if (this.filterKeys(keyInMap)) { debugVerbose( "SyncedMap.keyEventHandler() key matches filter and will be deleted", this.redisKey, message, key ); await this.delete(keyInMap, true); } } else { debugVerbose( "SyncedMap.keyEventHandler() key does not have prefix", this.redisKey, message, key ); } }; await this.subscriberClient.connect().catch(async () => { await new Promise((resolve) => setTimeout(resolve, 1e3)); await this.subscriberClient.connect().catch((error) => { throw error; }); }); if ((process.env.SKIP_KEYSPACE_CONFIG_CHECK || "").toUpperCase() !== "TRUE") { const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"]; if (!keyspaceEventConfig.includes("E")) { throw new Error( 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`" ); } if (!(keyspaceEventConfig.includes("A") || keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) { throw new Error( 'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`" ); } } await Promise.all([ // We use a custom channel for insert/delete For the following reason: // With custom channel we can delete multiple entries in one message. If we would listen to unlink / del we // could get thousands of messages for one revalidateTag (For example revalidateTag("algolia") would send an enormous amount of network packages) // Also we can send the value in the message for insert this.subscriberClient.subscribe(this.syncChannel, syncHandler), // Subscribe to Redis keyevent notifications for evicted and expired keys this.subscriberClient.subscribe( `__keyevent@${this.database}__:evicted`, keyEventHandler ), this.subscriberClient.subscribe( `__keyevent@${this.database}__:expired`, keyEventHandler ) ]); this.subscriberClient.on("error", async (_err) => { try { await this.subscriberClient.disconnect(); this.subscriberClient = this.client.duplicate(); await this.setupPubSub(); } catch (_reconnectError) { } }); } async waitUntilReady() { await this.setupLock; } get(key) { debugVerbose( "SyncedMap.get() called with key", key, JSON.stringify(this.map.get(key))?.substring(0, 100) ); return this.map.get(key); } async set(key, value) { debugVerbose( "SyncedMap.set() called with key", key, JSON.stringify(value)?.substring(0, 100) ); this.map.set(key, value); const operations = []; if (this.customizedSync?.withoutSetSync) { return; } if (!this.customizedSync?.withoutRedisHashmap) { operations.push( redisErrorHandler( "SyncedMap.set(), operation: hSet " + this.syncChannel + " " + this.keyPrefix + " " + key, this.client.hSet( this.keyPrefix + this.redisKey, key, JSON.stringify(value) ) ) ); } const insertMessage = { type: "insert", key, value }; operations.push( redisErrorHandler( "SyncedMap.set(), operation: publish " + this.syncChannel + " " + this.keyPrefix + " " + key, this.client.publish(this.syncChannel, JSON.stringify(insertMessage)) ) ); await Promise.all(operations); } async delete(keys, withoutSyncMessage = false) { debugVerbose( "SyncedMap.delete() called with keys", this.redisKey, keys, withoutSyncMessage ); const keysArray = Array.isArray(keys) ? keys : [keys]; const operations = []; for (const key of keysArray) { this.map.delete(key); } if (!this.customizedSync?.withoutRedisHashmap) { operations.push( redisErrorHandler( "SyncedMap.delete(), operation: hDel " + this.syncChannel + " " + this.keyPrefix + " " + this.redisKey + " " + keysArray, this.client.hDel(this.keyPrefix + this.redisKey, keysArray) ) ); } if (!withoutSyncMessage) { const deletionMessage = { type: "delete", keys: keysArray }; operations.push( redisErrorHandler( "SyncedMap.delete(), operation: publish " + this.syncChannel + " " + this.keyPrefix + " " + keysArray, this.client.publish(this.syncChannel, JSON.stringify(deletionMessage)) ) ); } await Promise.all(operations); debugVerbose( "SyncedMap.delete() finished operations", this.redisKey, keys, operations.length ); } has(key) { return this.map.has(key); } entries() { return this.map.entries(); } async close() { try { if (this.subscriberClient) { await this.subscriberClient.disconnect(); } } catch (_error) { } } }; // src/utils/json.ts function bufferReviver(_, value) { if (value && typeof value === "object" && typeof value.$binary === "string") { try { const base64String = value.$binary; if (/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) { return Buffer.from(base64String, "base64"); } } catch { } } return value; } function bufferReplacer(_, value) { if (Buffer.isBuffer(value)) { return { $binary: value.toString("base64") }; } if (value && typeof value === "object" && value?.type === "Buffer" && Array.isArray(value.data)) { return { $binary: Buffer.from(value.data).toString("base64") }; } return value; } // src/RedisStringsHandler.ts function redisErrorHandler(_debugInfo, redisCommandResult) { const _beforeTimestamp = performance.now(); if (!redisCommandResult || typeof redisCommandResult.catch !== "function") { return redisCommandResult; } return redisCommandResult.catch((error) => { throw error; }); } if (process.env.DEBUG_CACHE_HANDLER) { setInterval(() => { const start = performance.now(); setImmediate(() => { const duration = performance.now() - start; if (duration > 100) { debug( "yellow", `RedisStringsHandler detected an event loop lag of: ${duration.toFixed(2)}ms. If your container is hosted in a cloud provider with container suspension this is normal. If not you should increase the CPU of your container.` ); } }); }, 500); } var NEXT_CACHE_IMPLICIT_TAG_ID = "_N_T_"; var REVALIDATED_TAGS_KEY = "__revalidated_tags__"; var killContainerOnErrorCount = 0; var RedisStringsHandler = class { constructor({ redisUrl = process.env.REDIS_URL ? process.env.REDIS_URL : process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}` : "redis://localhost:6379", database = process.env.VERCEL_ENV === "production" ? 0 : 1, keyPrefix = process.env.VERCEL_URL || "UNDEFINED_URL_", sharedTagsKey = "__sharedTags__", getTimeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS ? Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS, 10) ?? 500 : 500, revalidateTagQuerySize = 250, avgResyncIntervalMs = 60 * 60 * 1e3, redisGetDeduplication = true, inMemoryCachingTime = 1e4, defaultStaleAge = 60 * 60 * 24 * 14, estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2, killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD, 10) ?? 0 : 0, socketOptions, clientOptions }) { this.clientReadyCalls = 0; try { this.keyPrefix = keyPrefix; this.redisGetDeduplication = redisGetDeduplication; this.inMemoryCachingTime = inMemoryCachingTime; this.defaultStaleAge = defaultStaleAge; this.estimateExpireAge = estimateExpireAge; this.killContainerOnErrorThreshold = killContainerOnErrorThreshold; this.getTimeoutMs = getTimeoutMs; this.client = createClient({ url: redisUrl, pingInterval: 1e4, // Useful with Redis deployments that do not use TCP Keep-Alive. Restarts the connection if it is idle for too long. ...database !== 0 ? { database } : {}, ...socketOptions ? { socket: { ...socketOptions } } : {}, ...clientOptions || {} }); this.client.on("error", (_error) => { setTimeout(() => this.client.connect().catch((_error2) => { }), 1e3); if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) { this.client.disconnect(); setTimeout(() => { process.exit(1); }, 500); } }); this.client.connect().then(() => { }).catch(() => { this.client.connect().catch((error) => { this.client.disconnect(); throw error; }); }); const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey; this.sharedTagsMap = new SyncedMap({ client: this.client, keyPrefix, redisKey: sharedTagsKey, database, querySize: revalidateTagQuerySize, filterKeys, resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10) }); this.revalidatedTagsMap = new SyncedMap({ client: this.client, keyPrefix, redisKey: REVALIDATED_TAGS_KEY, database, querySize: revalidateTagQuerySize, filterKeys, resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10) }); this.inMemoryDeduplicationCache = new SyncedMap({ client: this.client, keyPrefix, redisKey: "inMemoryDeduplicationCache", database, querySize: revalidateTagQuerySize, filterKeys, customizedSync: { withoutRedisHashmap: true, withoutSetSync: true } }); const redisGet = (key) => { return this.client.get(key); }; this.redisDeduplicationHandler = new DeduplicatedRequestHandler( redisGet, inMemoryCachingTime, this.inMemoryDeduplicationCache ); this.redisGet = redisGet; this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction; } catch (error) { if (killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= killContainerOnErrorThreshold) { process.exit(1); } throw error; } } resetRequestCache() { } async close() { try { await Promise.all([ this.sharedTagsMap?.close(), this.revalidatedTagsMap?.close(), this.inMemoryDeduplicationCache?.close() ]); if (this.client) { await this.client.disconnect(); } } catch (_error) { } } async assertClientIsReady() { this.clientReadyCalls++; if (this.clientReadyCalls > 10) { throw new Error( "assertClientIsReady called more than 10 times without being ready." ); } const timeoutMs = this.clientReadyCalls === 1 ? 6e4 : 3e4; await Promise.race([ Promise.all([ this.sharedTagsMap.waitUntilReady(), this.revalidatedTagsMap.waitUntilReady() ]), new Promise( (_, reject) => setTimeout(() => { reject( new Error( `assertClientIsReady: Timeout waiting for Redis maps to be ready after ${timeoutMs}ms (attempt ${this.clientReadyCalls})` ) ); }, timeoutMs) ) ]); this.clientReadyCalls = 0; if (!this.client.isReady) { throw new Error( "assertClientIsReady: Redis client is not ready yet or connection is lost." ); } } async get(key, ctx) { try { if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") { } debug("green", "RedisStringsHandler.get() called with", key, ctx); try { await this.assertClientIsReady(); } catch (_readyError) { return null; } const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet; const serializedCacheEntry = await redisErrorHandler( "RedisStringsHandler.get(), operation: get" + (this.redisGetDeduplication ? "deduplicated" : "") + " " + this.getTimeoutMs + "ms " + this.keyPrefix + " " + key, clientGet(this.keyPrefix + key) ); debug( "green", "RedisStringsHandler.get() finished with result (serializedCacheEntry)", serializedCacheEntry?.substring(0, 200) ); if (!serializedCacheEntry) { return null; } const cacheEntry = JSON.parse( serializedCacheEntry, bufferReviver ); debug( "green", "RedisStringsHandler.get() finished with result (cacheEntry)", JSON.stringify(cacheEntry).substring(0, 200) ); if (!cacheEntry) { return null; } if (!cacheEntry?.tags) { } if (!cacheEntry?.value) { } if (!cacheEntry?.lastModified) { } if (ctx.kind === "FETCH") { const combinedTags = /* @__PURE__ */ new Set([ ...ctx?.softTags || [], ...ctx?.tags || [] ]); if (combinedTags.size === 0) { return cacheEntry; } for (const tag of combinedTags) { const revalidationTime = this.revalidatedTagsMap.get(tag); if (revalidationTime && revalidationTime > cacheEntry.lastModified) { const redisKey = this.keyPrefix + key; this.client.unlink(redisKey).catch((_err) => { }).finally(async () => { await this.sharedTagsMap.delete(key); await this.revalidatedTagsMap.delete(tag); }); debug( "green", 'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.', tag, redisKey, revalidationTime, cacheEntry ); return null; } } } return cacheEntry; } catch (_error) { if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) { this.client.disconnect(); setTimeout(() => { process.exit(1); }, 500); } return null; } } async set(key, data, ctx) { try { if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") { } await this.assertClientIsReady(); if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") { const tags = data.headers["x-next-cache-tags"]?.split(","); ctx.tags = [...ctx.tags || [], ...tags || []]; } const cacheEntry = { lastModified: Date.now(), tags: ctx?.tags || [], value: data }; const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer); if (this.redisGetDeduplication) { this.redisDeduplicationHandler.seedRequestReturn( key, serializedCacheEntry ); } const revalidate = ( // For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used data.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || data?.revalidate ); const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge); const setOperation = redisErrorHandler( "RedisStringsHandler.set(), operation: set " + this.keyPrefix + " " + key, this.client.set(this.keyPrefix + key, serializedCacheEntry, { EX: expireAt }) ); debug( "blue", "RedisStringsHandler.set() will set the following serializedCacheEntry", this.keyPrefix, key, data, ctx, serializedCacheEntry?.substring(0, 200), expireAt ); let setTagsOperation; if (ctx.tags && ctx.tags.length > 0) { const currentTags = this.sharedTagsMap.get(key); const currentIsSameAsNew = currentTags?.length === ctx.tags.length && currentTags.every((v) => ctx.tags?.includes(v)) && ctx.tags.every((v) => currentTags.includes(v)); if (!currentIsSameAsNew) { setTagsOperation = this.sharedTagsMap.set( key, structuredClone(ctx.tags) ); } } debug( "blue", "RedisStringsHandler.set() will set the following sharedTagsMap", key, ctx.tags ); await Promise.all([setOperation, setTagsOperation]); } catch (error) { if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) { this.client.disconnect(); setTimeout(() => { process.exit(1); }, 500); } throw error; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any async revalidateTag(tagOrTags, ...rest) { try { debug( "red", "RedisStringsHandler.revalidateTag() called with", tagOrTags, rest ); const tags = new Set([tagOrTags || []].flat()); await this.assertClientIsReady(); const keysToDelete = /* @__PURE__ */ new Set(); for (const tag of tags) { if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) { const now = Date.now(); debug( "red", "RedisStringsHandler.revalidateTag() set revalidation time for tag", tag, "to", now ); await this.revalidatedTagsMap.set(tag, now); } } for (const [key, sharedTags] of this.sharedTagsMap.entries()) { if (sharedTags.some((tag) => tags.has(tag))) { keysToDelete.add(key); } } debug( "red", "RedisStringsHandler.revalidateTag() found", keysToDelete, "keys to delete" ); if (keysToDelete.size === 0) { return; } const redisKeys = Array.from(keysToDelete); const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key); const deleteKeysOperation = redisErrorHandler( "RedisStringsHandler.revalidateTag(), operation: unlink " + this.keyPrefix + " " + fullRedisKeys, this.client.unlink(fullRedisKeys) ); if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) { for (const key of keysToDelete) { this.inMemoryDeduplicationCache.delete(key); } } const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys); await Promise.all([deleteKeysOperation, deleteTagsOperation]); debug( "red", "RedisStringsHandler.revalidateTag() finished delete operations" ); } catch (error) { if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) { this.client.disconnect(); setTimeout(() => { process.exit(1); }, 500); } throw error; } } }; // src/CachedHandler.ts var cachedHandler; var CachedHandler = class { constructor(options) { if (!cachedHandler) { cachedHandler = new RedisStringsHandler(options); } } get(...args) { debugVerbose("CachedHandler.get called with", args); return cachedHandler.get(...args); } set(...args) { debugVerbose("CachedHandler.set called with", args); return cachedHandler.set(...args); } revalidateTag(...args) { debugVerbose("CachedHandler.revalidateTag called with", args); return cachedHandler.revalidateTag(...args); } resetRequestCache(...args) { return cachedHandler.resetRequestCache(...args); } }; // src/index.ts var index_default = CachedHandler; export { RedisStringsHandler, index_default as default }; //# sourceMappingURL=index.mjs.map