@rb2bv/cache-handler
Version:
Next.js self-hosting simplified.
518 lines (511 loc) • 12.6 kB
JavaScript
;
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __commonJS = (cb, mod) => function __require() {
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
};
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 __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
// If the importer is in node compatibility mode or this is not an ESM
// file that has been converted to a CommonJS file using a Babel-
// compatible transform (i.e. "__esModule" has not been set), then set
// "default" to the CommonJS "module.exports" for node compatibility.
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
mod
));
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// ../../node_modules/cluster-key-slot/lib/index.js
var require_lib = __commonJS({
"../../node_modules/cluster-key-slot/lib/index.js"(exports2, module2) {
"use strict";
var lookup = [
0,
4129,
8258,
12387,
16516,
20645,
24774,
28903,
33032,
37161,
41290,
45419,
49548,
53677,
57806,
61935,
4657,
528,
12915,
8786,
21173,
17044,
29431,
25302,
37689,
33560,
45947,
41818,
54205,
50076,
62463,
58334,
9314,
13379,
1056,
5121,
25830,
29895,
17572,
21637,
42346,
46411,
34088,
38153,
58862,
62927,
50604,
54669,
13907,
9842,
5649,
1584,
30423,
26358,
22165,
18100,
46939,
42874,
38681,
34616,
63455,
59390,
55197,
51132,
18628,
22757,
26758,
30887,
2112,
6241,
10242,
14371,
51660,
55789,
59790,
63919,
35144,
39273,
43274,
47403,
23285,
19156,
31415,
27286,
6769,
2640,
14899,
10770,
56317,
52188,
64447,
60318,
39801,
35672,
47931,
43802,
27814,
31879,
19684,
23749,
11298,
15363,
3168,
7233,
60846,
64911,
52716,
56781,
44330,
48395,
36200,
40265,
32407,
28342,
24277,
20212,
15891,
11826,
7761,
3696,
65439,
61374,
57309,
53244,
48923,
44858,
40793,
36728,
37256,
33193,
45514,
41451,
53516,
49453,
61774,
57711,
4224,
161,
12482,
8419,
20484,
16421,
28742,
24679,
33721,
37784,
41979,
46042,
49981,
54044,
58239,
62302,
689,
4752,
8947,
13010,
16949,
21012,
25207,
29270,
46570,
42443,
38312,
34185,
62830,
58703,
54572,
50445,
13538,
9411,
5280,
1153,
29798,
25671,
21540,
17413,
42971,
47098,
34713,
38840,
59231,
63358,
50973,
55100,
9939,
14066,
1681,
5808,
26199,
30326,
17941,
22068,
55628,
51565,
63758,
59695,
39368,
35305,
47498,
43435,
22596,
18533,
30726,
26663,
6336,
2273,
14466,
10403,
52093,
56156,
60223,
64286,
35833,
39896,
43963,
48026,
19061,
23124,
27191,
31254,
2801,
6864,
10931,
14994,
64814,
60687,
56684,
52557,
48554,
44427,
40424,
36297,
31782,
27655,
23652,
19525,
15522,
11395,
7392,
3265,
61215,
65342,
53085,
57212,
44955,
49082,
36825,
40952,
28183,
32310,
20053,
24180,
11923,
16050,
3793,
7920
];
var toUTF8Array = function toUTF8Array2(str) {
var char;
var i = 0;
var p = 0;
var utf8 = [];
var len = str.length;
for (; i < len; i++) {
char = str.charCodeAt(i);
if (char < 128) {
utf8[p++] = char;
} else if (char < 2048) {
utf8[p++] = char >> 6 | 192;
utf8[p++] = char & 63 | 128;
} else if ((char & 64512) === 55296 && i + 1 < str.length && (str.charCodeAt(i + 1) & 64512) === 56320) {
char = 65536 + ((char & 1023) << 10) + (str.charCodeAt(++i) & 1023);
utf8[p++] = char >> 18 | 240;
utf8[p++] = char >> 12 & 63 | 128;
utf8[p++] = char >> 6 & 63 | 128;
utf8[p++] = char & 63 | 128;
} else {
utf8[p++] = char >> 12 | 224;
utf8[p++] = char >> 6 & 63 | 128;
utf8[p++] = char & 63 | 128;
}
}
return utf8;
};
var generate = module2.exports = function generate2(str) {
var char;
var i = 0;
var start = -1;
var result = 0;
var resultHash = 0;
var utf8 = typeof str === "string" ? toUTF8Array(str) : str;
var len = utf8.length;
while (i < len) {
char = utf8[i++];
if (start === -1) {
if (char === 123) {
start = i;
}
} else if (char !== 125) {
resultHash = lookup[(char ^ resultHash >> 8) & 255] ^ resultHash << 8;
} else if (i - 1 !== start) {
return resultHash & 16383;
}
result = lookup[(char ^ result >> 8) & 255] ^ result << 8;
}
return result & 16383;
};
module2.exports.generateMulti = function generateMulti(keys) {
var i = 1;
var len = keys.length;
var base = generate(keys[0]);
while (i < len) {
if (generate(keys[i++]) !== base) return -1;
}
return base;
};
}
});
// src/handlers/experimental-redis-cluster.ts
var experimental_redis_cluster_exports = {};
__export(experimental_redis_cluster_exports, {
default: () => createHandler
});
module.exports = __toCommonJS(experimental_redis_cluster_exports);
var import_cluster_key_slot = __toESM(require_lib(), 1);
// 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/experimental-redis-cluster.ts
function groupKeysBySlot(keys) {
const slotKeysMap = /* @__PURE__ */ new Map();
for (const key of keys) {
const slot = (0, import_cluster_key_slot.default)(key);
const slotKeys = slotKeysMap.get(slot);
if (slotKeys) {
slotKeys.push(key);
} else {
slotKeysMap.set(slot, [key]);
}
}
return slotKeysMap;
}
function createHandler({
cluster,
keyPrefix = "",
sharedTagsKey = "__sharedTags__",
timeoutMs = 5e3,
keyExpirationStrategy = "EXPIREAT",
revalidateTagQuerySize = 100
}) {
const revalidatedTagsKey = keyPrefix + REVALIDATED_TAGS_KEY;
return {
name: "experimental-redis-cluster",
async get(key, { implicitTags }) {
const result = await cluster.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 cluster.hmGet(
getTimeoutRedisCommandOptions(timeoutMs),
revalidatedTagsKey,
Array.from(combinedTags)
);
for (const timeString of revalidationTimes) {
if (timeString && Number.parseInt(timeString, 10) > cacheValue.lastModified) {
await cluster.unlink(getTimeoutRedisCommandOptions(timeoutMs), keyPrefix + key);
return null;
}
}
return cacheValue;
},
async set(key, cacheHandlerValue) {
const options = getTimeoutRedisCommandOptions(timeoutMs);
let setOperation;
let expireOperation;
switch (keyExpirationStrategy) {
case "EXAT": {
setOperation = cluster.set(
options,
keyPrefix + key,
JSON.stringify(cacheHandlerValue),
typeof cacheHandlerValue.lifespan?.expireAt === "number" ? {
EXAT: cacheHandlerValue.lifespan.expireAt
} : void 0
);
break;
}
case "EXPIREAT": {
setOperation = cluster.set(options, keyPrefix + key, JSON.stringify(cacheHandlerValue));
expireOperation = cacheHandlerValue.lifespan ? cluster.expireAt(options, keyPrefix + key, cacheHandlerValue.lifespan.expireAt) : void 0;
break;
}
default: {
throw new Error(`Invalid keyExpirationStrategy: ${keyExpirationStrategy}`);
}
}
const setTagsOperation = cacheHandlerValue.tags.length > 0 ? cluster.hSet(options, keyPrefix + sharedTagsKey, key, JSON.stringify(cacheHandlerValue.tags)) : void 0;
await Promise.all([setOperation, expireOperation, setTagsOperation]);
},
async revalidateTag(tag) {
if (isImplicitTag(tag)) {
await cluster.hSet(getTimeoutRedisCommandOptions(timeoutMs), revalidatedTagsKey, tag, Date.now());
}
const tagsMap = /* @__PURE__ */ new Map();
let cursor = 0;
const hScanOptions = { COUNT: revalidateTagQuerySize };
do {
const remoteTagsPortion = await cluster.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 slotKeysMap = groupKeysBySlot(keysToDelete);
const unlinkPromises = [];
for (const [slot, keys] of slotKeysMap) {
const targetMasterNode = cluster.slots[slot]?.master;
const client = await targetMasterNode?.client;
if (keys.length === 0 || !client) {
continue;
}
const unlinkPromisesForSlot = client.unlink(getTimeoutRedisCommandOptions(timeoutMs), keys);
if (unlinkPromisesForSlot) {
unlinkPromises.push(unlinkPromisesForSlot);
}
}
const updateTagsOperation = cluster.hDel(
{ isolated: true, ...getTimeoutRedisCommandOptions(timeoutMs) },
keyPrefix + sharedTagsKey,
tagsToDelete
);
await Promise.allSettled([...unlinkPromises, updateTagsOperation]);
},
async delete(key) {
await cluster.unlink(getTimeoutRedisCommandOptions(timeoutMs), key);
}
};
}