@leejpsd/nextjs-cache-handler 0.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +51 -0
- package/LICENSE +21 -0
- package/README.md +259 -0
- package/dist/cache-components/index.cjs +1053 -0
- package/dist/cache-components/index.cjs.map +1 -0
- package/dist/cache-components/index.d.cts +85 -0
- package/dist/cache-components/index.d.ts +85 -0
- package/dist/cache-components/index.js +1049 -0
- package/dist/cache-components/index.js.map +1 -0
- package/dist/incremental/index.cjs +734 -0
- package/dist/incremental/index.cjs.map +1 -0
- package/dist/incremental/index.d.cts +131 -0
- package/dist/incremental/index.d.ts +131 -0
- package/dist/incremental/index.js +731 -0
- package/dist/incremental/index.js.map +1 -0
- package/dist/index.cjs +1408 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +1397 -0
- package/dist/index.js.map +1 -0
- package/dist/ops/index.cjs +19 -0
- package/dist/ops/index.cjs.map +1 -0
- package/dist/ops/index.d.cts +22 -0
- package/dist/ops/index.d.ts +22 -0
- package/dist/ops/index.js +17 -0
- package/dist/ops/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +130 -0
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -0
- package/dist/shared/client/adapter-ioredis.d.cts +22 -0
- package/dist/shared/client/adapter-ioredis.d.ts +22 -0
- package/dist/shared/client/adapter-ioredis.js +125 -0
- package/dist/shared/client/adapter-ioredis.js.map +1 -0
- package/dist/shared/client/adapter-redis.cjs +32 -0
- package/dist/shared/client/adapter-redis.cjs.map +1 -0
- package/dist/shared/client/adapter-redis.d.cts +19 -0
- package/dist/shared/client/adapter-redis.d.ts +19 -0
- package/dist/shared/client/adapter-redis.js +29 -0
- package/dist/shared/client/adapter-redis.js.map +1 -0
- package/dist/types-CgriT7My.d.cts +193 -0
- package/dist/types-CgriT7My.d.ts +193 -0
- package/package.json +178 -0
|
@@ -0,0 +1,1053 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __esm = (fn, res) => function __init() {
|
|
14
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
15
|
+
};
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/shared/client/adapter-redis.ts
|
|
22
|
+
var adapter_redis_exports = {};
|
|
23
|
+
__export(adapter_redis_exports, {
|
|
24
|
+
adaptRedisV5: () => adaptRedisV5,
|
|
25
|
+
createRedisV5Client: () => createRedisV5Client
|
|
26
|
+
});
|
|
27
|
+
function adaptRedisV5(client) {
|
|
28
|
+
return client;
|
|
29
|
+
}
|
|
30
|
+
function createRedisV5Client(config) {
|
|
31
|
+
const { createClient } = __require("redis");
|
|
32
|
+
const useTls = config.tls ?? config.url.startsWith("rediss://");
|
|
33
|
+
const opts = {
|
|
34
|
+
url: config.url,
|
|
35
|
+
socket: {
|
|
36
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
37
|
+
reconnectStrategy: false,
|
|
38
|
+
...useTls ? { tls: true } : {}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (config.password !== void 0) opts.password = config.password;
|
|
42
|
+
return createClient(opts);
|
|
43
|
+
}
|
|
44
|
+
var init_adapter_redis = __esm({
|
|
45
|
+
"src/shared/client/adapter-redis.ts"() {
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// src/shared/client/adapter-ioredis.ts
|
|
50
|
+
var adapter_ioredis_exports = {};
|
|
51
|
+
__export(adapter_ioredis_exports, {
|
|
52
|
+
adaptCluster: () => adaptCluster,
|
|
53
|
+
adaptIoredis: () => adaptIoredis,
|
|
54
|
+
createIoredisClient: () => createIoredisClient,
|
|
55
|
+
createIoredisCluster: () => createIoredisCluster
|
|
56
|
+
});
|
|
57
|
+
function adapt(client, isClusterClient = false) {
|
|
58
|
+
return {
|
|
59
|
+
get isOpen() {
|
|
60
|
+
return client.status === "ready";
|
|
61
|
+
},
|
|
62
|
+
connect: async () => {
|
|
63
|
+
if (client.status !== "ready" && client.status !== "connecting") {
|
|
64
|
+
await client.connect();
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
get: (k) => client.get(k),
|
|
68
|
+
set: (k, v, opts) => opts?.EX !== void 0 ? client.set(k, v, "EX", opts.EX) : client.set(k, v),
|
|
69
|
+
del: async (keys) => {
|
|
70
|
+
const arr = Array.isArray(keys) ? keys : [keys];
|
|
71
|
+
if (arr.length === 0) return 0;
|
|
72
|
+
return client.del(...arr);
|
|
73
|
+
},
|
|
74
|
+
sAdd: async (k, m) => {
|
|
75
|
+
const arr = Array.isArray(m) ? m : [m];
|
|
76
|
+
if (arr.length === 0) return 0;
|
|
77
|
+
return client.sadd(k, ...arr);
|
|
78
|
+
},
|
|
79
|
+
sMembers: (k) => client.smembers(k),
|
|
80
|
+
expire: async (k, s) => {
|
|
81
|
+
const r = await client.expire(k, s);
|
|
82
|
+
return r;
|
|
83
|
+
},
|
|
84
|
+
mGet: async (keys) => {
|
|
85
|
+
if (keys.length === 0) return [];
|
|
86
|
+
return client.mget(...keys);
|
|
87
|
+
},
|
|
88
|
+
eval: (script, opts) => client.eval(
|
|
89
|
+
script,
|
|
90
|
+
opts.keys.length,
|
|
91
|
+
...opts.keys,
|
|
92
|
+
...opts.arguments
|
|
93
|
+
),
|
|
94
|
+
evalSha: (sha, opts) => client.evalsha(
|
|
95
|
+
sha,
|
|
96
|
+
opts.keys.length,
|
|
97
|
+
...opts.keys,
|
|
98
|
+
...opts.arguments
|
|
99
|
+
),
|
|
100
|
+
scriptLoad: async (script) => await client.script("LOAD", script),
|
|
101
|
+
scanIterator: async function* ({ MATCH, COUNT }) {
|
|
102
|
+
if (isClusterClient) {
|
|
103
|
+
const cluster = client;
|
|
104
|
+
for (const node of cluster.nodes("master")) {
|
|
105
|
+
let cursor = "0";
|
|
106
|
+
do {
|
|
107
|
+
const [next, batch] = await node.scan(
|
|
108
|
+
cursor,
|
|
109
|
+
"MATCH",
|
|
110
|
+
MATCH,
|
|
111
|
+
"COUNT",
|
|
112
|
+
COUNT ?? 100
|
|
113
|
+
);
|
|
114
|
+
cursor = next;
|
|
115
|
+
if (batch.length > 0) yield batch;
|
|
116
|
+
} while (cursor !== "0");
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const redis = client;
|
|
120
|
+
let cursor = "0";
|
|
121
|
+
do {
|
|
122
|
+
const [next, batch] = await redis.scan(
|
|
123
|
+
cursor,
|
|
124
|
+
"MATCH",
|
|
125
|
+
MATCH,
|
|
126
|
+
"COUNT",
|
|
127
|
+
COUNT ?? 100
|
|
128
|
+
);
|
|
129
|
+
cursor = next;
|
|
130
|
+
if (batch.length > 0) yield batch;
|
|
131
|
+
} while (cursor !== "0");
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
ping: () => client.ping(),
|
|
135
|
+
on: (event, listener) => {
|
|
136
|
+
client.on(event, listener);
|
|
137
|
+
return client;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function adaptIoredis(client) {
|
|
142
|
+
return adapt(client, false);
|
|
143
|
+
}
|
|
144
|
+
function adaptCluster(cluster) {
|
|
145
|
+
return adapt(cluster, true);
|
|
146
|
+
}
|
|
147
|
+
function createIoredisClient(config) {
|
|
148
|
+
const IORedis = __require("ioredis");
|
|
149
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
150
|
+
return new Ctor(config.url, {
|
|
151
|
+
password: config.password,
|
|
152
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
153
|
+
tls: config.tls ? {} : void 0,
|
|
154
|
+
lazyConnect: true,
|
|
155
|
+
maxRetriesPerRequest: 1
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function createIoredisCluster(config) {
|
|
159
|
+
const IORedis = __require("ioredis");
|
|
160
|
+
const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
|
|
161
|
+
return new ClusterCtor(config.nodes, {
|
|
162
|
+
redisOptions: {
|
|
163
|
+
password: config.password,
|
|
164
|
+
tls: config.tls ? {} : void 0
|
|
165
|
+
},
|
|
166
|
+
enableReadyCheck: true,
|
|
167
|
+
lazyConnect: true
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
var init_adapter_ioredis = __esm({
|
|
171
|
+
"src/shared/client/adapter-ioredis.ts"() {
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// src/shared/abort.ts
|
|
176
|
+
var CacheTimeoutError = class extends Error {
|
|
177
|
+
constructor(opName, ms) {
|
|
178
|
+
super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
|
|
179
|
+
this.opName = opName;
|
|
180
|
+
this.ms = ms;
|
|
181
|
+
}
|
|
182
|
+
opName;
|
|
183
|
+
ms;
|
|
184
|
+
name = "CacheTimeoutError";
|
|
185
|
+
};
|
|
186
|
+
async function withAbortSignal(opName, ms, fn) {
|
|
187
|
+
const ctrl = new AbortController();
|
|
188
|
+
const timer = setTimeout(() => ctrl.abort(), ms);
|
|
189
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
190
|
+
timer.unref();
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
194
|
+
const onAbort = () => {
|
|
195
|
+
ctrl.signal.removeEventListener("abort", onAbort);
|
|
196
|
+
reject(new CacheTimeoutError(opName, ms));
|
|
197
|
+
};
|
|
198
|
+
if (ctrl.signal.aborted) onAbort();
|
|
199
|
+
else ctrl.signal.addEventListener("abort", onAbort, { once: true });
|
|
200
|
+
});
|
|
201
|
+
return await Promise.race([fn(ctrl.signal), timeoutPromise]);
|
|
202
|
+
} finally {
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// src/shared/client/index.ts
|
|
208
|
+
async function buildClient(source) {
|
|
209
|
+
if (typeof source === "function") {
|
|
210
|
+
return await source();
|
|
211
|
+
}
|
|
212
|
+
switch (source.type) {
|
|
213
|
+
case "redis": {
|
|
214
|
+
const { adaptRedisV5: adaptRedisV52, createRedisV5Client: createRedisV5Client2 } = await Promise.resolve().then(() => (init_adapter_redis(), adapter_redis_exports));
|
|
215
|
+
return adaptRedisV52(createRedisV5Client2(source));
|
|
216
|
+
}
|
|
217
|
+
case "ioredis": {
|
|
218
|
+
const { adaptIoredis: adaptIoredis2, createIoredisClient: createIoredisClient2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
219
|
+
return adaptIoredis2(createIoredisClient2(source));
|
|
220
|
+
}
|
|
221
|
+
case "cluster": {
|
|
222
|
+
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
223
|
+
return adaptCluster2(createIoredisCluster2(source));
|
|
224
|
+
}
|
|
225
|
+
default: {
|
|
226
|
+
const exhaustive = source;
|
|
227
|
+
throw new Error(
|
|
228
|
+
`[next-cache] unknown client config: ${JSON.stringify(exhaustive)}`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
var ConnectionManager = class {
|
|
234
|
+
constructor(source, onError) {
|
|
235
|
+
this.source = source;
|
|
236
|
+
this.onError = onError;
|
|
237
|
+
}
|
|
238
|
+
source;
|
|
239
|
+
onError;
|
|
240
|
+
client = null;
|
|
241
|
+
connecting = null;
|
|
242
|
+
failed = false;
|
|
243
|
+
async getOrConnect() {
|
|
244
|
+
if (this.client?.isOpen) return this.client;
|
|
245
|
+
if (this.failed) return null;
|
|
246
|
+
if (this.connecting) return this.connecting;
|
|
247
|
+
this.connecting = (async () => {
|
|
248
|
+
try {
|
|
249
|
+
const c = await buildClient(this.source);
|
|
250
|
+
c.on("error", (err) => this.onError(err));
|
|
251
|
+
if (!c.isOpen) await c.connect();
|
|
252
|
+
this.client = c;
|
|
253
|
+
return c;
|
|
254
|
+
} catch (err) {
|
|
255
|
+
this.failed = true;
|
|
256
|
+
this.onError(err);
|
|
257
|
+
return null;
|
|
258
|
+
} finally {
|
|
259
|
+
this.connecting = null;
|
|
260
|
+
}
|
|
261
|
+
})();
|
|
262
|
+
return this.connecting;
|
|
263
|
+
}
|
|
264
|
+
reset() {
|
|
265
|
+
this.client = null;
|
|
266
|
+
this.connecting = null;
|
|
267
|
+
this.failed = false;
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/shared/logger.ts
|
|
272
|
+
var defaultLogger = {
|
|
273
|
+
debug: () => {
|
|
274
|
+
},
|
|
275
|
+
info: () => {
|
|
276
|
+
},
|
|
277
|
+
warn: (message, meta) => {
|
|
278
|
+
if (meta) console.warn(`[next-cache] ${message}`, meta);
|
|
279
|
+
else console.warn(`[next-cache] ${message}`);
|
|
280
|
+
},
|
|
281
|
+
error: (message, meta) => {
|
|
282
|
+
if (meta) console.error(`[next-cache] ${message}`, meta);
|
|
283
|
+
else console.error(`[next-cache] ${message}`);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/shared/lua/set-with-tags.lua
|
|
288
|
+
var set_with_tags_default = `-- set-with-tags.lua
|
|
289
|
+
--
|
|
290
|
+
-- Atomically write a cache entry and update the tag indices that point at it.
|
|
291
|
+
-- Replaces the non-atomic SET \u2192 SADD \u2192 EXPIRE sequence in the reference
|
|
292
|
+
-- implementation, which had a race window where another instance's hard
|
|
293
|
+
-- invalidate could leave dangling tag members.
|
|
294
|
+
--
|
|
295
|
+
-- KEYS[1] : entry key (e.g. "next-cache:entry:abc123")
|
|
296
|
+
-- KEYS[2..N] : tag set keys (e.g. "next-cache:tag:posts")
|
|
297
|
+
-- ARGV[1] : serialized entry payload
|
|
298
|
+
-- ARGV[2] : entry TTL in seconds
|
|
299
|
+
-- ARGV[3] : tag TTL in seconds (typically same as entry TTL)
|
|
300
|
+
-- ARGV[4] : tag count (number of KEYS after KEYS[1])
|
|
301
|
+
--
|
|
302
|
+
-- Returns: 1 on success.
|
|
303
|
+
|
|
304
|
+
local entryKey = KEYS[1]
|
|
305
|
+
local payload = ARGV[1]
|
|
306
|
+
local entryTtl = tonumber(ARGV[2])
|
|
307
|
+
local tagTtl = tonumber(ARGV[3])
|
|
308
|
+
local tagCount = tonumber(ARGV[4])
|
|
309
|
+
|
|
310
|
+
redis.call('SET', entryKey, payload, 'EX', entryTtl)
|
|
311
|
+
|
|
312
|
+
for i = 1, tagCount do
|
|
313
|
+
local tagKey = KEYS[i + 1]
|
|
314
|
+
redis.call('SADD', tagKey, entryKey)
|
|
315
|
+
-- EXPIRE on every SADD: ensures the tag set never outlives its entries
|
|
316
|
+
-- and survives at least as long as the longest associated entry.
|
|
317
|
+
redis.call('EXPIRE', tagKey, tagTtl)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
return 1
|
|
321
|
+
`;
|
|
322
|
+
|
|
323
|
+
// src/shared/lua/revalidate-hard.lua
|
|
324
|
+
var revalidate_hard_default = "-- revalidate-hard.lua\n--\n-- Atomically remove every entry referenced by a tag and stamp the tag's\n-- expiration marker. Used by `updateTags(tags, { expire: 0 })` \u2014 the hard\n-- invalidation path triggered by `updateTag()` server actions in Next.js 16.\n--\n-- KEYS[1] : tag set key (e.g. \"next-cache:tag:posts\")\n-- KEYS[2] : tag-expiration marker key (e.g. \"next-cache:tag-expiration:posts\")\n-- ARGV[1] : current timestamp in milliseconds\n-- ARGV[2] : marker TTL in seconds (default: 1 day so cross-deployment\n-- readers can still observe the invalidation)\n--\n-- Returns: number of entries that were deleted.\n\nlocal tagKey = KEYS[1]\nlocal markerKey = KEYS[2]\nlocal nowMs = ARGV[1]\nlocal markerTtl = tonumber(ARGV[2])\n\nlocal entries = redis.call('SMEMBERS', tagKey)\nlocal deleted = 0\n\nif #entries > 0 then\n -- DEL accepts varargs; unpack the entry list.\n redis.call('DEL', unpack(entries))\n deleted = #entries\nend\n\n-- Drop the tag set itself; on next SET it'll be recreated by set-with-tags.\nredis.call('DEL', tagKey)\n\n-- Stamp the invalidation timestamp. Readers compare this against an entry's\n-- timestamp inside `getExpiration` to decide whether soft-tag-only entries\n-- should be considered stale.\nredis.call('SET', markerKey, nowMs, 'EX', markerTtl)\n\nreturn deleted\n";
|
|
325
|
+
|
|
326
|
+
// src/shared/lua/refresh-tag-lock.lua
|
|
327
|
+
var refresh_tag_lock_default = `-- refresh-tag-lock.lua
|
|
328
|
+
--
|
|
329
|
+
-- Single-flight lock acquisition for background refresh.
|
|
330
|
+
-- Reserved for v0.2's stampede-protection feature. Currently unused by the
|
|
331
|
+
-- handler runtime, but bundled so v0.2 can ship without a major bump.
|
|
332
|
+
--
|
|
333
|
+
-- KEYS[1] : lock key (e.g. "next-cache:lock:abc123")
|
|
334
|
+
-- ARGV[1] : owner identifier (instance id; written for observability)
|
|
335
|
+
-- ARGV[2] : lock TTL in seconds (10s recommended \u2014 long enough for refresh
|
|
336
|
+
-- to complete, short enough to recover from a crashed owner)
|
|
337
|
+
--
|
|
338
|
+
-- Returns: 1 if lock acquired, 0 if another owner already holds it.
|
|
339
|
+
|
|
340
|
+
local lockKey = KEYS[1]
|
|
341
|
+
local owner = ARGV[1]
|
|
342
|
+
local ttl = tonumber(ARGV[2])
|
|
343
|
+
|
|
344
|
+
if redis.call('GET', lockKey) then
|
|
345
|
+
return 0
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
redis.call('SET', lockKey, owner, 'EX', ttl)
|
|
349
|
+
return 1
|
|
350
|
+
`;
|
|
351
|
+
|
|
352
|
+
// src/shared/lua/index.ts
|
|
353
|
+
var SCRIPTS = {
|
|
354
|
+
setWithTags: set_with_tags_default,
|
|
355
|
+
revalidateHard: revalidate_hard_default,
|
|
356
|
+
refreshTagLock: refresh_tag_lock_default
|
|
357
|
+
};
|
|
358
|
+
var shaCache = /* @__PURE__ */ new Map();
|
|
359
|
+
function sha1(body) {
|
|
360
|
+
return crypto.createHash("sha1").update(body).digest("hex");
|
|
361
|
+
}
|
|
362
|
+
function isNoScriptError(err) {
|
|
363
|
+
if (!err || typeof err !== "object") return false;
|
|
364
|
+
const msg = err.message ?? "";
|
|
365
|
+
return msg.includes("NOSCRIPT");
|
|
366
|
+
}
|
|
367
|
+
async function execLuaScript(client, name, keys, args) {
|
|
368
|
+
const body = SCRIPTS[name];
|
|
369
|
+
let cachedSha = shaCache.get(name);
|
|
370
|
+
if (cachedSha === void 0) {
|
|
371
|
+
cachedSha = sha1(body);
|
|
372
|
+
shaCache.set(name, cachedSha);
|
|
373
|
+
}
|
|
374
|
+
if (typeof client.evalSha === "function") {
|
|
375
|
+
try {
|
|
376
|
+
return await client.evalSha(cachedSha, {
|
|
377
|
+
keys,
|
|
378
|
+
arguments: args
|
|
379
|
+
});
|
|
380
|
+
} catch (err) {
|
|
381
|
+
if (!isNoScriptError(err)) {
|
|
382
|
+
throw err;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return await client.eval(body, { keys, arguments: args });
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// src/shared/memory-fallback.ts
|
|
390
|
+
var MAX_TIMER_MS = 2147483647;
|
|
391
|
+
function clampTimerMs(ms) {
|
|
392
|
+
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
393
|
+
}
|
|
394
|
+
var MemoryStore = class {
|
|
395
|
+
kv = /* @__PURE__ */ new Map();
|
|
396
|
+
timers = /* @__PURE__ */ new Map();
|
|
397
|
+
set(key, value, ttlSeconds) {
|
|
398
|
+
this.delete(key);
|
|
399
|
+
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
400
|
+
this.kv.set(key, { value, expiresAt });
|
|
401
|
+
const timer = setTimeout(() => this.delete(key), clampTimerMs(ttlSeconds * 1e3));
|
|
402
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
403
|
+
timer.unref();
|
|
404
|
+
}
|
|
405
|
+
this.timers.set(key, timer);
|
|
406
|
+
}
|
|
407
|
+
get(key) {
|
|
408
|
+
const entry = this.kv.get(key);
|
|
409
|
+
if (!entry) return null;
|
|
410
|
+
if (Date.now() > entry.expiresAt) {
|
|
411
|
+
this.delete(key);
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
return entry.value;
|
|
415
|
+
}
|
|
416
|
+
delete(key) {
|
|
417
|
+
const timer = this.timers.get(key);
|
|
418
|
+
if (timer !== void 0) {
|
|
419
|
+
clearTimeout(timer);
|
|
420
|
+
this.timers.delete(key);
|
|
421
|
+
}
|
|
422
|
+
return this.kv.delete(key);
|
|
423
|
+
}
|
|
424
|
+
has(key) {
|
|
425
|
+
return this.get(key) !== null;
|
|
426
|
+
}
|
|
427
|
+
keys() {
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
const live = [];
|
|
430
|
+
for (const [key, entry] of this.kv.entries()) {
|
|
431
|
+
if (entry.expiresAt >= now) live.push(key);
|
|
432
|
+
}
|
|
433
|
+
return live;
|
|
434
|
+
}
|
|
435
|
+
size() {
|
|
436
|
+
return this.keys().length;
|
|
437
|
+
}
|
|
438
|
+
clear() {
|
|
439
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
440
|
+
this.timers.clear();
|
|
441
|
+
this.kv.clear();
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
var MemorySetStore = class {
|
|
445
|
+
sets = /* @__PURE__ */ new Map();
|
|
446
|
+
expiresAt = /* @__PURE__ */ new Map();
|
|
447
|
+
timers = /* @__PURE__ */ new Map();
|
|
448
|
+
sAdd(key, member) {
|
|
449
|
+
const members = Array.isArray(member) ? member : [member];
|
|
450
|
+
let set = this.sets.get(key);
|
|
451
|
+
if (!set) {
|
|
452
|
+
set = /* @__PURE__ */ new Set();
|
|
453
|
+
this.sets.set(key, set);
|
|
454
|
+
}
|
|
455
|
+
let added = 0;
|
|
456
|
+
for (const m of members) {
|
|
457
|
+
if (!set.has(m)) {
|
|
458
|
+
set.add(m);
|
|
459
|
+
added += 1;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return added;
|
|
463
|
+
}
|
|
464
|
+
sMembers(key) {
|
|
465
|
+
if (this.isExpired(key)) {
|
|
466
|
+
this.delete(key);
|
|
467
|
+
return [];
|
|
468
|
+
}
|
|
469
|
+
const set = this.sets.get(key);
|
|
470
|
+
return set ? Array.from(set) : [];
|
|
471
|
+
}
|
|
472
|
+
expire(key, ttlSeconds) {
|
|
473
|
+
if (!this.sets.has(key)) return false;
|
|
474
|
+
const prev = this.timers.get(key);
|
|
475
|
+
if (prev !== void 0) clearTimeout(prev);
|
|
476
|
+
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
477
|
+
this.expiresAt.set(key, expiresAt);
|
|
478
|
+
const timer = setTimeout(() => this.delete(key), clampTimerMs(ttlSeconds * 1e3));
|
|
479
|
+
if (typeof timer === "object" && "unref" in timer) {
|
|
480
|
+
timer.unref();
|
|
481
|
+
}
|
|
482
|
+
this.timers.set(key, timer);
|
|
483
|
+
return true;
|
|
484
|
+
}
|
|
485
|
+
delete(key) {
|
|
486
|
+
const timer = this.timers.get(key);
|
|
487
|
+
if (timer !== void 0) {
|
|
488
|
+
clearTimeout(timer);
|
|
489
|
+
this.timers.delete(key);
|
|
490
|
+
}
|
|
491
|
+
this.expiresAt.delete(key);
|
|
492
|
+
return this.sets.delete(key);
|
|
493
|
+
}
|
|
494
|
+
keys() {
|
|
495
|
+
const live = [];
|
|
496
|
+
for (const key of this.sets.keys()) {
|
|
497
|
+
if (!this.isExpired(key)) live.push(key);
|
|
498
|
+
}
|
|
499
|
+
return live;
|
|
500
|
+
}
|
|
501
|
+
clear() {
|
|
502
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
503
|
+
this.timers.clear();
|
|
504
|
+
this.expiresAt.clear();
|
|
505
|
+
this.sets.clear();
|
|
506
|
+
}
|
|
507
|
+
isExpired(key) {
|
|
508
|
+
const at = this.expiresAt.get(key);
|
|
509
|
+
return at !== void 0 && Date.now() > at;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// src/shared/metrics.ts
|
|
514
|
+
var counters = /* @__PURE__ */ new Map();
|
|
515
|
+
function bump(event) {
|
|
516
|
+
const existing = counters.get(event.type);
|
|
517
|
+
if (existing) {
|
|
518
|
+
existing.count += 1;
|
|
519
|
+
if (event.ms !== void 0) existing.totalMs += event.ms;
|
|
520
|
+
existing.lastTs = Date.now();
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
counters.set(event.type, {
|
|
524
|
+
count: 1,
|
|
525
|
+
totalMs: event.ms ?? 0,
|
|
526
|
+
lastTs: Date.now()
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
function createEmitter(onMetric) {
|
|
530
|
+
return (event) => {
|
|
531
|
+
bump(event);
|
|
532
|
+
if (onMetric) {
|
|
533
|
+
try {
|
|
534
|
+
onMetric(event);
|
|
535
|
+
} catch {
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/shared/namespace.ts
|
|
542
|
+
var FALLBACK = "unversioned";
|
|
543
|
+
function resolveBuildNamespace(override) {
|
|
544
|
+
if (override !== void 0) {
|
|
545
|
+
return typeof override === "function" ? override() : override;
|
|
546
|
+
}
|
|
547
|
+
return process.env.DEPLOYMENT_VERSION ?? process.env.GIT_HASH ?? process.env.NEXT_DEPLOYMENT_ID ?? FALLBACK;
|
|
548
|
+
}
|
|
549
|
+
function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
550
|
+
const ns = hashTag ? `{${namespace}}` : namespace;
|
|
551
|
+
return `${prefix}${ns}:${rest}`;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/shared/build-phase.ts
|
|
555
|
+
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
556
|
+
function isBuildPhase() {
|
|
557
|
+
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
558
|
+
}
|
|
559
|
+
function shouldUseRedis(opts) {
|
|
560
|
+
if (opts.fallback === "always") return false;
|
|
561
|
+
const detector = opts.isBuildPhase ?? isBuildPhase;
|
|
562
|
+
if (detector()) return false;
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/cache-components/serialize.ts
|
|
567
|
+
async function readStreamFully(stream) {
|
|
568
|
+
const reader = stream.getReader();
|
|
569
|
+
const chunks = [];
|
|
570
|
+
try {
|
|
571
|
+
while (true) {
|
|
572
|
+
const { done, value } = await reader.read();
|
|
573
|
+
if (done) break;
|
|
574
|
+
if (value) chunks.push(value);
|
|
575
|
+
}
|
|
576
|
+
} finally {
|
|
577
|
+
reader.releaseLock();
|
|
578
|
+
}
|
|
579
|
+
return Buffer.concat(chunks.map((c) => Buffer.from(c)));
|
|
580
|
+
}
|
|
581
|
+
function bufferToStream(buf) {
|
|
582
|
+
return new ReadableStream({
|
|
583
|
+
start(controller) {
|
|
584
|
+
controller.enqueue(new Uint8Array(buf));
|
|
585
|
+
controller.close();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
function encodeEnvelope(buf, meta) {
|
|
590
|
+
const envelope = {
|
|
591
|
+
value: buf.toString("base64"),
|
|
592
|
+
...meta
|
|
593
|
+
};
|
|
594
|
+
return JSON.stringify(envelope);
|
|
595
|
+
}
|
|
596
|
+
function decodeEnvelope(raw) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = JSON.parse(raw);
|
|
599
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.value !== "string") {
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
return parsed;
|
|
603
|
+
} catch {
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// src/cache-components/swr.ts
|
|
609
|
+
var SECOND_MS = 1e3;
|
|
610
|
+
function partitionEntry(entry, now = Date.now()) {
|
|
611
|
+
const ageMs = now - entry.timestamp;
|
|
612
|
+
if (ageMs < 0) {
|
|
613
|
+
return { freshness: "fresh", ageMs };
|
|
614
|
+
}
|
|
615
|
+
const revalidateMs = Math.max(0, entry.revalidate) * SECOND_MS;
|
|
616
|
+
const rawExpireMs = entry.expire === 0 ? Number.POSITIVE_INFINITY : Math.max(0, entry.expire) * SECOND_MS;
|
|
617
|
+
const expireMs = Math.max(rawExpireMs, revalidateMs);
|
|
618
|
+
if (ageMs <= revalidateMs) {
|
|
619
|
+
return { freshness: "fresh", ageMs };
|
|
620
|
+
}
|
|
621
|
+
if (ageMs <= expireMs) {
|
|
622
|
+
return { freshness: "stale", ageMs };
|
|
623
|
+
}
|
|
624
|
+
return { freshness: "expired", ageMs };
|
|
625
|
+
}
|
|
626
|
+
function shouldServeStale(result, staleWhileRevalidate) {
|
|
627
|
+
return result.freshness === "fresh" || result.freshness === "stale" && staleWhileRevalidate;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/cache-components/handler.ts
|
|
631
|
+
var DEFAULT_KEY_PREFIX = "next-cache:";
|
|
632
|
+
var ENTRY_SUFFIX = "entry:";
|
|
633
|
+
var TAG_SUFFIX = "tag:";
|
|
634
|
+
var TAG_EXP_SUFFIX = "tag-expiration:";
|
|
635
|
+
var DEFAULT_ABORT_MS = 1500;
|
|
636
|
+
var ONE_YEAR_SEC = 60 * 60 * 24 * 365;
|
|
637
|
+
var FALLBACK_TTL_SEC = 3600;
|
|
638
|
+
var TAG_EXP_MARKER_TTL_SEC = 60 * 60 * 24 * 7;
|
|
639
|
+
function gateRedis(state) {
|
|
640
|
+
const fb = state.opts.fallback;
|
|
641
|
+
const ibp = state.opts.rest.isBuildPhase;
|
|
642
|
+
return ibp ? shouldUseRedis({ fallback: fb, isBuildPhase: ibp }) : shouldUseRedis({ fallback: fb });
|
|
643
|
+
}
|
|
644
|
+
function init(opts) {
|
|
645
|
+
const logger = opts.logger ?? defaultLogger;
|
|
646
|
+
const emit = createEmitter(opts.onMetric);
|
|
647
|
+
const conn = new ConnectionManager(opts.client, (err) => {
|
|
648
|
+
logger.warn("Redis client error", { message: err.message });
|
|
649
|
+
emit({ type: "redis.connect.failed", meta: { message: err.message } });
|
|
650
|
+
});
|
|
651
|
+
return {
|
|
652
|
+
opts: {
|
|
653
|
+
abortTimeoutMs: opts.abortTimeoutMs ?? DEFAULT_ABORT_MS,
|
|
654
|
+
fallback: opts.fallback ?? "auto",
|
|
655
|
+
staleWhileRevalidate: opts.staleWhileRevalidate ?? true,
|
|
656
|
+
hashTag: opts.hashTag ?? false,
|
|
657
|
+
keyPrefix: opts.keyPrefix ?? DEFAULT_KEY_PREFIX,
|
|
658
|
+
rest: opts
|
|
659
|
+
},
|
|
660
|
+
logger,
|
|
661
|
+
emit,
|
|
662
|
+
conn,
|
|
663
|
+
memEntries: new MemoryStore(),
|
|
664
|
+
memTags: new MemorySetStore(),
|
|
665
|
+
memTagExp: /* @__PURE__ */ new Map(),
|
|
666
|
+
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
667
|
+
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
function entryKey(state, cacheKey) {
|
|
671
|
+
return buildKey(
|
|
672
|
+
`${state.opts.keyPrefix}${ENTRY_SUFFIX}`,
|
|
673
|
+
state.resolveNs(),
|
|
674
|
+
cacheKey,
|
|
675
|
+
state.opts.hashTag
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
function tagKey(state, tag) {
|
|
679
|
+
return buildKey(
|
|
680
|
+
`${state.opts.keyPrefix}${TAG_SUFFIX}`,
|
|
681
|
+
state.resolveNs(),
|
|
682
|
+
tag,
|
|
683
|
+
state.opts.hashTag
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
function tagExpKey(state, tag) {
|
|
687
|
+
return buildKey(
|
|
688
|
+
`${state.opts.keyPrefix}${TAG_EXP_SUFFIX}`,
|
|
689
|
+
state.resolveNs(),
|
|
690
|
+
tag,
|
|
691
|
+
state.opts.hashTag
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
function resolveTtlSeconds(entry) {
|
|
695
|
+
if (Number.isFinite(entry.expire) && entry.expire > 0) {
|
|
696
|
+
return Math.max(60, Math.ceil(entry.expire));
|
|
697
|
+
}
|
|
698
|
+
if (Number.isFinite(entry.revalidate) && entry.revalidate > 0) {
|
|
699
|
+
return Math.max(60, Math.ceil(entry.revalidate));
|
|
700
|
+
}
|
|
701
|
+
if (entry.expire === Number.POSITIVE_INFINITY) {
|
|
702
|
+
return ONE_YEAR_SEC;
|
|
703
|
+
}
|
|
704
|
+
return FALLBACK_TTL_SEC;
|
|
705
|
+
}
|
|
706
|
+
async function getImpl(state, cacheKey, softTags) {
|
|
707
|
+
const start = Date.now();
|
|
708
|
+
const useRedis = gateRedis(state);
|
|
709
|
+
if (!useRedis && state.opts.fallback === "never") {
|
|
710
|
+
state.emit({ type: "cache.miss", meta: { reason: "no-redis" } });
|
|
711
|
+
return void 0;
|
|
712
|
+
}
|
|
713
|
+
const eKey = entryKey(state, cacheKey);
|
|
714
|
+
let envelope = null;
|
|
715
|
+
let raw = null;
|
|
716
|
+
if (useRedis) {
|
|
717
|
+
try {
|
|
718
|
+
raw = await withAbortSignal(
|
|
719
|
+
"cacheHandlers.get",
|
|
720
|
+
state.opts.abortTimeoutMs,
|
|
721
|
+
async () => {
|
|
722
|
+
const client = await state.conn.getOrConnect();
|
|
723
|
+
if (!client) return null;
|
|
724
|
+
return await client.get(eKey);
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
} catch (err) {
|
|
728
|
+
if (err instanceof CacheTimeoutError) {
|
|
729
|
+
state.emit({ type: "redis.timeout", meta: { op: "get" } });
|
|
730
|
+
} else {
|
|
731
|
+
state.logger.warn("get() Redis error, falling back", {
|
|
732
|
+
message: err.message
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
raw = null;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (raw === null && state.opts.fallback !== "never") {
|
|
739
|
+
raw = state.memEntries.get(eKey);
|
|
740
|
+
if (raw !== null) state.emit({ type: "fallback.activated", meta: { op: "get" } });
|
|
741
|
+
}
|
|
742
|
+
if (raw === null) {
|
|
743
|
+
state.emit({ type: "cache.miss", ms: Date.now() - start });
|
|
744
|
+
return void 0;
|
|
745
|
+
}
|
|
746
|
+
envelope = decodeEnvelope(raw);
|
|
747
|
+
if (!envelope) {
|
|
748
|
+
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
749
|
+
return void 0;
|
|
750
|
+
}
|
|
751
|
+
const partition = partitionEntry(envelope, Date.now());
|
|
752
|
+
if (!shouldServeStale(partition, state.opts.staleWhileRevalidate)) {
|
|
753
|
+
if (useRedis) {
|
|
754
|
+
try {
|
|
755
|
+
await withAbortSignal(
|
|
756
|
+
"cacheHandlers.get.evict",
|
|
757
|
+
state.opts.abortTimeoutMs,
|
|
758
|
+
async () => {
|
|
759
|
+
const c = await state.conn.getOrConnect();
|
|
760
|
+
if (c) await c.del(eKey);
|
|
761
|
+
}
|
|
762
|
+
);
|
|
763
|
+
} catch {
|
|
764
|
+
}
|
|
765
|
+
} else {
|
|
766
|
+
state.memEntries.delete(eKey);
|
|
767
|
+
}
|
|
768
|
+
state.emit({
|
|
769
|
+
type: "cache.miss",
|
|
770
|
+
meta: { reason: "expired", freshness: partition.freshness }
|
|
771
|
+
});
|
|
772
|
+
return void 0;
|
|
773
|
+
}
|
|
774
|
+
if (softTags.length > 0) {
|
|
775
|
+
let mostRecent = 0;
|
|
776
|
+
for (const t of softTags) {
|
|
777
|
+
const ts = state.localTagTimestamps.get(t);
|
|
778
|
+
if (ts !== void 0 && ts > mostRecent) mostRecent = ts;
|
|
779
|
+
}
|
|
780
|
+
if (mostRecent > envelope.timestamp) {
|
|
781
|
+
state.emit({
|
|
782
|
+
type: "cache.miss",
|
|
783
|
+
meta: { reason: "soft-tag-invalidated" }
|
|
784
|
+
});
|
|
785
|
+
return void 0;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
state.emit({
|
|
789
|
+
type: partition.freshness === "stale" ? "cache.stale" : "cache.hit",
|
|
790
|
+
ms: Date.now() - start,
|
|
791
|
+
meta: { freshness: partition.freshness }
|
|
792
|
+
});
|
|
793
|
+
return {
|
|
794
|
+
value: bufferToStream(Buffer.from(envelope.value, "base64")),
|
|
795
|
+
tags: envelope.tags,
|
|
796
|
+
stale: envelope.stale,
|
|
797
|
+
timestamp: envelope.timestamp,
|
|
798
|
+
expire: envelope.expire,
|
|
799
|
+
revalidate: envelope.revalidate
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
async function setImpl(state, cacheKey, pendingEntry) {
|
|
803
|
+
let entry;
|
|
804
|
+
try {
|
|
805
|
+
entry = await pendingEntry;
|
|
806
|
+
} catch (err) {
|
|
807
|
+
state.logger.warn("pendingEntry rejected, skipping set()", {
|
|
808
|
+
message: err.message
|
|
809
|
+
});
|
|
810
|
+
state.emit({ type: "cache.set.failed", meta: { reason: "pending-rejected" } });
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
let buf;
|
|
814
|
+
try {
|
|
815
|
+
buf = await readStreamFully(entry.value);
|
|
816
|
+
} catch (err) {
|
|
817
|
+
state.logger.warn("stream errored mid-read, discarding entry", {
|
|
818
|
+
message: err.message
|
|
819
|
+
});
|
|
820
|
+
state.emit({ type: "cache.set.failed", meta: { reason: "partial-stream" } });
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
const eKey = entryKey(state, cacheKey);
|
|
824
|
+
const ttl = resolveTtlSeconds(entry);
|
|
825
|
+
const payload = encodeEnvelope(buf, {
|
|
826
|
+
tags: entry.tags,
|
|
827
|
+
stale: entry.stale,
|
|
828
|
+
timestamp: entry.timestamp,
|
|
829
|
+
expire: entry.expire,
|
|
830
|
+
revalidate: entry.revalidate
|
|
831
|
+
});
|
|
832
|
+
const useRedis = gateRedis(state);
|
|
833
|
+
if (!useRedis) {
|
|
834
|
+
state.emit({ type: "build_phase.skip", meta: { op: "set" } });
|
|
835
|
+
if (state.opts.fallback !== "never") {
|
|
836
|
+
state.memEntries.set(eKey, payload, ttl);
|
|
837
|
+
for (const t of entry.tags) {
|
|
838
|
+
state.memTags.sAdd(tagKey(state, t), eKey);
|
|
839
|
+
state.memTags.expire(tagKey(state, t), ttl);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
state.emit({ type: "cache.set", meta: { backend: "memory" } });
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
await withAbortSignal(
|
|
847
|
+
"cacheHandlers.set",
|
|
848
|
+
state.opts.abortTimeoutMs,
|
|
849
|
+
async () => {
|
|
850
|
+
const client = await state.conn.getOrConnect();
|
|
851
|
+
if (!client) {
|
|
852
|
+
if (state.opts.fallback !== "never") {
|
|
853
|
+
state.memEntries.set(eKey, payload, ttl);
|
|
854
|
+
}
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (entry.tags.length === 0) {
|
|
858
|
+
await client.set(eKey, payload, { EX: ttl });
|
|
859
|
+
} else {
|
|
860
|
+
await luaSetWithTags(client, state, eKey, payload, ttl, entry.tags);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
);
|
|
864
|
+
state.emit({ type: "cache.set", meta: { backend: "redis" } });
|
|
865
|
+
} catch (err) {
|
|
866
|
+
if (err instanceof CacheTimeoutError) {
|
|
867
|
+
state.emit({ type: "redis.timeout", meta: { op: "set" } });
|
|
868
|
+
} else {
|
|
869
|
+
state.logger.warn("set() failed (best-effort)", {
|
|
870
|
+
message: err.message
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
state.emit({ type: "cache.set.failed", meta: { reason: "redis-error" } });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async function luaSetWithTags(client, state, eKey, payload, ttl, tags) {
|
|
877
|
+
const tagKeys = tags.map((t) => tagKey(state, t));
|
|
878
|
+
await execLuaScript(
|
|
879
|
+
client,
|
|
880
|
+
"setWithTags",
|
|
881
|
+
[eKey, ...tagKeys],
|
|
882
|
+
[payload, String(ttl), String(ttl), String(tags.length)]
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
async function refreshTagsImpl(state) {
|
|
886
|
+
if (!gateRedis(state)) {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
try {
|
|
890
|
+
await withAbortSignal(
|
|
891
|
+
"cacheHandlers.refreshTags",
|
|
892
|
+
state.opts.abortTimeoutMs,
|
|
893
|
+
async () => {
|
|
894
|
+
const client = await state.conn.getOrConnect();
|
|
895
|
+
if (!client) return;
|
|
896
|
+
const pattern = `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}*`;
|
|
897
|
+
const seen = [];
|
|
898
|
+
for await (const chunk of client.scanIterator({
|
|
899
|
+
MATCH: pattern,
|
|
900
|
+
COUNT: 100
|
|
901
|
+
})) {
|
|
902
|
+
const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
|
|
903
|
+
seen.push(...keys);
|
|
904
|
+
if (seen.length >= 500) break;
|
|
905
|
+
}
|
|
906
|
+
if (seen.length === 0) return;
|
|
907
|
+
const values = await client.mGet(seen);
|
|
908
|
+
for (let i = 0; i < seen.length; i += 1) {
|
|
909
|
+
const v = values[i];
|
|
910
|
+
if (!v) continue;
|
|
911
|
+
const ts = Number.parseInt(v, 10);
|
|
912
|
+
if (!Number.isFinite(ts)) continue;
|
|
913
|
+
const key = seen[i];
|
|
914
|
+
if (key === void 0) continue;
|
|
915
|
+
const tag = stripExpPrefix(state, key);
|
|
916
|
+
if (tag) state.localTagTimestamps.set(tag, ts);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
);
|
|
920
|
+
} catch (err) {
|
|
921
|
+
state.logger.warn("refreshTags() error swallowed", {
|
|
922
|
+
message: err.message
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function stripExpPrefix(state, fullKey) {
|
|
927
|
+
const ns = state.resolveNs();
|
|
928
|
+
const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
|
|
929
|
+
return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : null;
|
|
930
|
+
}
|
|
931
|
+
async function getExpirationImpl(state, tags) {
|
|
932
|
+
if (tags.length === 0) return 0;
|
|
933
|
+
let max = 0;
|
|
934
|
+
for (const t of tags) {
|
|
935
|
+
const local = state.localTagTimestamps.get(t);
|
|
936
|
+
if (local !== void 0 && local > max) max = local;
|
|
937
|
+
const mem = state.memTagExp.get(t);
|
|
938
|
+
if (mem !== void 0 && mem > max) max = mem;
|
|
939
|
+
}
|
|
940
|
+
if (max > 0) {
|
|
941
|
+
state.emit({ type: "tag.expiration.read", meta: { source: "local", count: tags.length } });
|
|
942
|
+
return max;
|
|
943
|
+
}
|
|
944
|
+
if (!gateRedis(state)) {
|
|
945
|
+
return 0;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
return await withAbortSignal(
|
|
949
|
+
"cacheHandlers.getExpiration",
|
|
950
|
+
state.opts.abortTimeoutMs,
|
|
951
|
+
async () => {
|
|
952
|
+
const client = await state.conn.getOrConnect();
|
|
953
|
+
if (!client) return 0;
|
|
954
|
+
const keys = tags.map((t) => tagExpKey(state, t));
|
|
955
|
+
const values = await client.mGet(keys);
|
|
956
|
+
let m = 0;
|
|
957
|
+
for (const v of values) {
|
|
958
|
+
if (!v) continue;
|
|
959
|
+
const n = Number.parseInt(v, 10);
|
|
960
|
+
if (Number.isFinite(n) && n > m) m = n;
|
|
961
|
+
}
|
|
962
|
+
return m;
|
|
963
|
+
}
|
|
964
|
+
);
|
|
965
|
+
} catch {
|
|
966
|
+
return 0;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
async function updateTagsImpl(state, tags, durations) {
|
|
970
|
+
if (tags.length === 0) return;
|
|
971
|
+
const isHardExpire = durations?.expire === 0;
|
|
972
|
+
const now = Date.now();
|
|
973
|
+
for (const t of tags) {
|
|
974
|
+
state.localTagTimestamps.set(t, now);
|
|
975
|
+
state.memTagExp.set(t, now);
|
|
976
|
+
}
|
|
977
|
+
if (!gateRedis(state)) {
|
|
978
|
+
if (isHardExpire) {
|
|
979
|
+
for (const t of tags) {
|
|
980
|
+
const tk = tagKey(state, t);
|
|
981
|
+
const members = state.memTags.sMembers(tk);
|
|
982
|
+
for (const m of members) state.memEntries.delete(m);
|
|
983
|
+
state.memTags.delete(tk);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
state.emit({
|
|
987
|
+
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
988
|
+
meta: { count: tags.length, backend: "memory" }
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
try {
|
|
993
|
+
await withAbortSignal(
|
|
994
|
+
"cacheHandlers.updateTags",
|
|
995
|
+
state.opts.abortTimeoutMs,
|
|
996
|
+
async () => {
|
|
997
|
+
const client = await state.conn.getOrConnect();
|
|
998
|
+
if (!client) {
|
|
999
|
+
if (isHardExpire) {
|
|
1000
|
+
throw new Error(
|
|
1001
|
+
"[next-cache] hard updateTags requested but Redis unavailable"
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
for (const t of tags) {
|
|
1007
|
+
if (isHardExpire) {
|
|
1008
|
+
await execLuaScript(
|
|
1009
|
+
client,
|
|
1010
|
+
"revalidateHard",
|
|
1011
|
+
[tagKey(state, t), tagExpKey(state, t)],
|
|
1012
|
+
[String(now), String(TAG_EXP_MARKER_TTL_SEC)]
|
|
1013
|
+
);
|
|
1014
|
+
} else {
|
|
1015
|
+
await client.set(tagExpKey(state, t), String(now), {
|
|
1016
|
+
EX: TAG_EXP_MARKER_TTL_SEC
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
);
|
|
1022
|
+
state.emit({
|
|
1023
|
+
type: isHardExpire ? "tag.invalidate.hard" : "tag.invalidate.soft",
|
|
1024
|
+
meta: { count: tags.length, backend: "redis" }
|
|
1025
|
+
});
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
if (err instanceof CacheTimeoutError) {
|
|
1028
|
+
state.emit({ type: "redis.timeout", meta: { op: "updateTags" } });
|
|
1029
|
+
}
|
|
1030
|
+
if (isHardExpire) {
|
|
1031
|
+
throw err;
|
|
1032
|
+
}
|
|
1033
|
+
state.logger.warn("updateTags() soft path failed (best-effort)", {
|
|
1034
|
+
message: err.message
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
function createCacheComponentsHandler(opts) {
|
|
1039
|
+
const state = init(opts);
|
|
1040
|
+
return {
|
|
1041
|
+
get: (cacheKey, softTags) => getImpl(state, cacheKey, softTags),
|
|
1042
|
+
set: (cacheKey, pendingEntry) => setImpl(state, cacheKey, pendingEntry),
|
|
1043
|
+
refreshTags: () => refreshTagsImpl(state),
|
|
1044
|
+
getExpiration: (tags) => getExpirationImpl(state, tags),
|
|
1045
|
+
updateTags: (tags, durations) => updateTagsImpl(state, tags, durations)
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
exports.createCacheComponentsHandler = createCacheComponentsHandler;
|
|
1050
|
+
exports.partitionEntry = partitionEntry;
|
|
1051
|
+
exports.shouldServeStale = shouldServeStale;
|
|
1052
|
+
//# sourceMappingURL=index.cjs.map
|
|
1053
|
+
//# sourceMappingURL=index.cjs.map
|