@rb2bv/cache-handler
Version:
Next.js self-hosting simplified.
170 lines (164 loc) • 5.42 kB
JavaScript
;
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);
}
};
}