@leejpsd/nextjs-cache-handler 0.2.0 → 0.3.0
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 +59 -2
- package/README.md +47 -25
- package/dist/cache-components/index.cjs +235 -46
- package/dist/cache-components/index.cjs.map +1 -1
- package/dist/cache-components/index.d.cts +1 -1
- package/dist/cache-components/index.d.ts +1 -1
- package/dist/cache-components/index.js +227 -46
- package/dist/cache-components/index.js.map +1 -1
- package/dist/incremental/index.cjs +231 -41
- package/dist/incremental/index.cjs.map +1 -1
- package/dist/incremental/index.d.cts +42 -3
- package/dist/incremental/index.d.ts +42 -3
- package/dist/incremental/index.js +223 -41
- package/dist/incremental/index.js.map +1 -1
- package/dist/index.cjs +313 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +305 -70
- package/dist/index.js.map +1 -1
- package/dist/ops/index.d.cts +1 -1
- package/dist/ops/index.d.ts +1 -1
- package/dist/otel/index.cjs +72 -0
- package/dist/otel/index.cjs.map +1 -0
- package/dist/otel/index.d.cts +72 -0
- package/dist/otel/index.d.ts +72 -0
- package/dist/otel/index.js +64 -0
- package/dist/otel/index.js.map +1 -0
- package/dist/shared/client/adapter-ioredis.cjs +49 -10
- package/dist/shared/client/adapter-ioredis.cjs.map +1 -1
- package/dist/shared/client/adapter-ioredis.d.cts +5 -2
- package/dist/shared/client/adapter-ioredis.d.ts +5 -2
- package/dist/shared/client/adapter-ioredis.js +43 -11
- package/dist/shared/client/adapter-ioredis.js.map +1 -1
- package/dist/shared/client/adapter-redis.cjs +22 -7
- package/dist/shared/client/adapter-redis.cjs.map +1 -1
- package/dist/shared/client/adapter-redis.d.cts +1 -1
- package/dist/shared/client/adapter-redis.d.ts +1 -1
- package/dist/shared/client/adapter-redis.js +16 -7
- package/dist/shared/client/adapter-redis.js.map +1 -1
- package/dist/{types-Dq1q6qXd.d.cts → types-Bl-rkLKX.d.cts} +37 -1
- package/dist/{types-Dq1q6qXd.d.ts → types-Bl-rkLKX.d.ts} +37 -1
- package/package.json +20 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
+
import path2 from 'path';
|
|
2
|
+
import 'url';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { gzip, gunzip, brotliCompress, brotliDecompress, constants } from 'zlib';
|
|
1
6
|
import { createHash } from 'crypto';
|
|
2
7
|
|
|
3
8
|
var __defProp = Object.defineProperty;
|
|
4
9
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
6
|
-
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
7
|
-
}) : x)(function(x) {
|
|
8
|
-
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
9
|
-
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
10
|
-
});
|
|
11
10
|
var __esm = (fn, res) => function __init() {
|
|
12
11
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
12
|
};
|
|
@@ -15,6 +14,27 @@ var __export = (target, all) => {
|
|
|
15
14
|
for (var name in all)
|
|
16
15
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
16
|
};
|
|
17
|
+
var init_esm_shims = __esm({
|
|
18
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
function nodeRequire(id) {
|
|
22
|
+
try {
|
|
23
|
+
return packageRequire(id);
|
|
24
|
+
} catch {
|
|
25
|
+
return cwdRequire(id);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
var packageRequire, cwdRequire;
|
|
29
|
+
var init_node_require = __esm({
|
|
30
|
+
"src/shared/node-require.ts"() {
|
|
31
|
+
init_esm_shims();
|
|
32
|
+
packageRequire = createRequire(import.meta.url);
|
|
33
|
+
cwdRequire = createRequire(
|
|
34
|
+
path2.join(process.cwd(), "__resolve-anchor__.js")
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
18
38
|
|
|
19
39
|
// src/shared/client/adapter-redis.ts
|
|
20
40
|
var adapter_redis_exports = {};
|
|
@@ -26,7 +46,7 @@ function adaptRedisV5(client) {
|
|
|
26
46
|
return client;
|
|
27
47
|
}
|
|
28
48
|
function createRedisV5Client(config) {
|
|
29
|
-
const { createClient } =
|
|
49
|
+
const { createClient } = nodeRequire("redis");
|
|
30
50
|
const useTls = config.tls ?? config.url.startsWith("rediss://");
|
|
31
51
|
const opts = {
|
|
32
52
|
url: config.url,
|
|
@@ -41,6 +61,8 @@ function createRedisV5Client(config) {
|
|
|
41
61
|
}
|
|
42
62
|
var init_adapter_redis = __esm({
|
|
43
63
|
"src/shared/client/adapter-redis.ts"() {
|
|
64
|
+
init_esm_shims();
|
|
65
|
+
init_node_require();
|
|
44
66
|
}
|
|
45
67
|
});
|
|
46
68
|
|
|
@@ -50,7 +72,8 @@ __export(adapter_ioredis_exports, {
|
|
|
50
72
|
adaptCluster: () => adaptCluster,
|
|
51
73
|
adaptIoredis: () => adaptIoredis,
|
|
52
74
|
createIoredisClient: () => createIoredisClient,
|
|
53
|
-
createIoredisCluster: () => createIoredisCluster
|
|
75
|
+
createIoredisCluster: () => createIoredisCluster,
|
|
76
|
+
createIoredisSentinel: () => createIoredisSentinel
|
|
54
77
|
});
|
|
55
78
|
function adapt(client, isClusterClient = false) {
|
|
56
79
|
return {
|
|
@@ -133,6 +156,9 @@ function adapt(client, isClusterClient = false) {
|
|
|
133
156
|
on: (event, listener) => {
|
|
134
157
|
client.on(event, listener);
|
|
135
158
|
return client;
|
|
159
|
+
},
|
|
160
|
+
dispose: () => {
|
|
161
|
+
client.disconnect();
|
|
136
162
|
}
|
|
137
163
|
};
|
|
138
164
|
}
|
|
@@ -143,18 +169,36 @@ function adaptCluster(cluster) {
|
|
|
143
169
|
return adapt(cluster, true);
|
|
144
170
|
}
|
|
145
171
|
function createIoredisClient(config) {
|
|
146
|
-
const IORedis =
|
|
172
|
+
const IORedis = nodeRequire("ioredis");
|
|
147
173
|
const Ctor = IORedis.default ?? IORedis;
|
|
148
174
|
return new Ctor(config.url, {
|
|
149
175
|
password: config.password,
|
|
150
176
|
connectTimeout: config.connectTimeout ?? 1500,
|
|
151
177
|
tls: config.tls ? {} : void 0,
|
|
152
178
|
lazyConnect: true,
|
|
153
|
-
maxRetriesPerRequest: 1
|
|
179
|
+
maxRetriesPerRequest: 1,
|
|
180
|
+
// Batch same-tick commands into one RTT (matches redis@5's default
|
|
181
|
+
// behavior) — makes Promise.all fan-outs a single pipeline.
|
|
182
|
+
enableAutoPipelining: true
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
function createIoredisSentinel(config) {
|
|
186
|
+
const IORedis = nodeRequire("ioredis");
|
|
187
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
188
|
+
return new Ctor({
|
|
189
|
+
sentinels: config.sentinels,
|
|
190
|
+
name: config.name,
|
|
191
|
+
password: config.password,
|
|
192
|
+
sentinelPassword: config.sentinelPassword,
|
|
193
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
194
|
+
tls: config.tls ? {} : void 0,
|
|
195
|
+
lazyConnect: true,
|
|
196
|
+
maxRetriesPerRequest: 1,
|
|
197
|
+
enableAutoPipelining: true
|
|
154
198
|
});
|
|
155
199
|
}
|
|
156
200
|
function createIoredisCluster(config) {
|
|
157
|
-
const IORedis =
|
|
201
|
+
const IORedis = nodeRequire("ioredis");
|
|
158
202
|
const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
|
|
159
203
|
return new ClusterCtor(config.nodes, {
|
|
160
204
|
redisOptions: {
|
|
@@ -162,15 +206,29 @@ function createIoredisCluster(config) {
|
|
|
162
206
|
tls: config.tls ? {} : void 0
|
|
163
207
|
},
|
|
164
208
|
enableReadyCheck: true,
|
|
165
|
-
lazyConnect: true
|
|
209
|
+
lazyConnect: true,
|
|
210
|
+
// Same-slot commands issued in the same tick share one pipeline.
|
|
211
|
+
enableAutoPipelining: true
|
|
166
212
|
});
|
|
167
213
|
}
|
|
168
214
|
var init_adapter_ioredis = __esm({
|
|
169
215
|
"src/shared/client/adapter-ioredis.ts"() {
|
|
216
|
+
init_esm_shims();
|
|
217
|
+
init_node_require();
|
|
170
218
|
}
|
|
171
219
|
});
|
|
172
220
|
|
|
221
|
+
// src/index.ts
|
|
222
|
+
init_esm_shims();
|
|
223
|
+
|
|
224
|
+
// src/cache-components/index.ts
|
|
225
|
+
init_esm_shims();
|
|
226
|
+
|
|
227
|
+
// src/cache-components/handler.ts
|
|
228
|
+
init_esm_shims();
|
|
229
|
+
|
|
173
230
|
// src/shared/abort.ts
|
|
231
|
+
init_esm_shims();
|
|
174
232
|
var CacheTimeoutError = class extends Error {
|
|
175
233
|
constructor(opName, ms) {
|
|
176
234
|
super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
|
|
@@ -202,7 +260,41 @@ async function withAbortSignal(opName, ms, fn) {
|
|
|
202
260
|
}
|
|
203
261
|
}
|
|
204
262
|
|
|
263
|
+
// src/shared/compress.ts
|
|
264
|
+
init_esm_shims();
|
|
265
|
+
var gzipAsync = promisify(gzip);
|
|
266
|
+
var gunzipAsync = promisify(gunzip);
|
|
267
|
+
var brotliCompressAsync = promisify(brotliCompress);
|
|
268
|
+
var brotliDecompressAsync = promisify(brotliDecompress);
|
|
269
|
+
var GZIP_MARKER = "__ncgz__:";
|
|
270
|
+
var BROTLI_MARKER = "__ncbr__:";
|
|
271
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
272
|
+
var BROTLI_OPTS = {
|
|
273
|
+
params: { [constants.BROTLI_PARAM_QUALITY]: 5 }
|
|
274
|
+
};
|
|
275
|
+
async function compressValue(value, algo) {
|
|
276
|
+
if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
|
|
277
|
+
const input = Buffer.from(value);
|
|
278
|
+
if (algo === "gzip") {
|
|
279
|
+
return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
|
|
280
|
+
}
|
|
281
|
+
return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
|
|
282
|
+
}
|
|
283
|
+
async function decompressValue(value) {
|
|
284
|
+
if (value === null) return null;
|
|
285
|
+
if (value.startsWith(GZIP_MARKER)) {
|
|
286
|
+
const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
|
|
287
|
+
return (await gunzipAsync(buf)).toString("utf8");
|
|
288
|
+
}
|
|
289
|
+
if (value.startsWith(BROTLI_MARKER)) {
|
|
290
|
+
const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
|
|
291
|
+
return (await brotliDecompressAsync(buf)).toString("utf8");
|
|
292
|
+
}
|
|
293
|
+
return value;
|
|
294
|
+
}
|
|
295
|
+
|
|
205
296
|
// src/shared/client/index.ts
|
|
297
|
+
init_esm_shims();
|
|
206
298
|
async function buildClient(source) {
|
|
207
299
|
if (typeof source === "function") {
|
|
208
300
|
return await source();
|
|
@@ -220,6 +312,10 @@ async function buildClient(source) {
|
|
|
220
312
|
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
221
313
|
return adaptCluster2(createIoredisCluster2(source));
|
|
222
314
|
}
|
|
315
|
+
case "sentinel": {
|
|
316
|
+
const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
317
|
+
return adaptIoredis2(createIoredisSentinel2(source));
|
|
318
|
+
}
|
|
223
319
|
default: {
|
|
224
320
|
const exhaustive = source;
|
|
225
321
|
throw new Error(
|
|
@@ -229,28 +325,45 @@ async function buildClient(source) {
|
|
|
229
325
|
}
|
|
230
326
|
}
|
|
231
327
|
var ConnectionManager = class {
|
|
232
|
-
constructor(source, onError) {
|
|
328
|
+
constructor(source, onError, retry = {}) {
|
|
233
329
|
this.source = source;
|
|
234
330
|
this.onError = onError;
|
|
331
|
+
this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
|
|
332
|
+
this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
|
|
235
333
|
}
|
|
236
334
|
source;
|
|
237
335
|
onError;
|
|
238
336
|
client = null;
|
|
239
337
|
connecting = null;
|
|
240
|
-
|
|
338
|
+
consecutiveFailures = 0;
|
|
339
|
+
lastFailureAt = 0;
|
|
340
|
+
baseCooldownMs;
|
|
341
|
+
maxCooldownMs;
|
|
342
|
+
cooldownMs() {
|
|
343
|
+
if (this.consecutiveFailures === 0) return 0;
|
|
344
|
+
return Math.min(
|
|
345
|
+
this.maxCooldownMs,
|
|
346
|
+
this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
241
349
|
async getOrConnect() {
|
|
242
350
|
if (this.client?.isOpen) return this.client;
|
|
243
|
-
if (this.failed) return null;
|
|
244
351
|
if (this.connecting) return this.connecting;
|
|
352
|
+
if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
|
|
353
|
+
const stale = this.client;
|
|
354
|
+
this.client = null;
|
|
245
355
|
this.connecting = (async () => {
|
|
356
|
+
disposeQuietly(stale);
|
|
246
357
|
try {
|
|
247
358
|
const c = await buildClient(this.source);
|
|
248
359
|
c.on("error", (err) => this.onError(err));
|
|
249
360
|
if (!c.isOpen) await c.connect();
|
|
250
361
|
this.client = c;
|
|
362
|
+
this.consecutiveFailures = 0;
|
|
251
363
|
return c;
|
|
252
364
|
} catch (err) {
|
|
253
|
-
this.
|
|
365
|
+
this.consecutiveFailures += 1;
|
|
366
|
+
this.lastFailureAt = Date.now();
|
|
254
367
|
this.onError(err);
|
|
255
368
|
return null;
|
|
256
369
|
} finally {
|
|
@@ -260,13 +373,24 @@ var ConnectionManager = class {
|
|
|
260
373
|
return this.connecting;
|
|
261
374
|
}
|
|
262
375
|
reset() {
|
|
376
|
+
disposeQuietly(this.client);
|
|
263
377
|
this.client = null;
|
|
264
378
|
this.connecting = null;
|
|
265
|
-
this.
|
|
379
|
+
this.consecutiveFailures = 0;
|
|
380
|
+
this.lastFailureAt = 0;
|
|
266
381
|
}
|
|
267
382
|
};
|
|
383
|
+
function disposeQuietly(client) {
|
|
384
|
+
if (!client) return;
|
|
385
|
+
const c = client;
|
|
386
|
+
try {
|
|
387
|
+
(c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
|
|
388
|
+
} catch {
|
|
389
|
+
}
|
|
390
|
+
}
|
|
268
391
|
|
|
269
392
|
// src/shared/logger.ts
|
|
393
|
+
init_esm_shims();
|
|
270
394
|
var defaultLogger = {
|
|
271
395
|
debug: () => {
|
|
272
396
|
},
|
|
@@ -282,6 +406,9 @@ var defaultLogger = {
|
|
|
282
406
|
}
|
|
283
407
|
};
|
|
284
408
|
|
|
409
|
+
// src/shared/lua/index.ts
|
|
410
|
+
init_esm_shims();
|
|
411
|
+
|
|
285
412
|
// src/shared/lua/set-with-tags.lua
|
|
286
413
|
var set_with_tags_default = `-- set-with-tags.lua
|
|
287
414
|
--
|
|
@@ -385,13 +512,19 @@ async function execLuaScript(client, name, keys, args) {
|
|
|
385
512
|
}
|
|
386
513
|
|
|
387
514
|
// src/shared/memory-fallback.ts
|
|
515
|
+
init_esm_shims();
|
|
388
516
|
var MAX_TIMER_MS = 2147483647;
|
|
389
517
|
function clampTimerMs(ms) {
|
|
390
518
|
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
391
519
|
}
|
|
520
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
392
521
|
var MemoryStore = class {
|
|
393
522
|
kv = /* @__PURE__ */ new Map();
|
|
394
523
|
timers = /* @__PURE__ */ new Map();
|
|
524
|
+
maxEntries;
|
|
525
|
+
constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
526
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
527
|
+
}
|
|
395
528
|
set(key, value, ttlSeconds) {
|
|
396
529
|
this.delete(key);
|
|
397
530
|
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
@@ -401,6 +534,10 @@ var MemoryStore = class {
|
|
|
401
534
|
timer.unref();
|
|
402
535
|
}
|
|
403
536
|
this.timers.set(key, timer);
|
|
537
|
+
while (this.kv.size > this.maxEntries) {
|
|
538
|
+
const oldest = this.kv.keys().next().value;
|
|
539
|
+
this.delete(oldest);
|
|
540
|
+
}
|
|
404
541
|
}
|
|
405
542
|
get(key) {
|
|
406
543
|
const entry = this.kv.get(key);
|
|
@@ -409,6 +546,8 @@ var MemoryStore = class {
|
|
|
409
546
|
this.delete(key);
|
|
410
547
|
return null;
|
|
411
548
|
}
|
|
549
|
+
this.kv.delete(key);
|
|
550
|
+
this.kv.set(key, entry);
|
|
412
551
|
return entry.value;
|
|
413
552
|
}
|
|
414
553
|
delete(key) {
|
|
@@ -443,13 +582,19 @@ var MemorySetStore = class {
|
|
|
443
582
|
sets = /* @__PURE__ */ new Map();
|
|
444
583
|
expiresAt = /* @__PURE__ */ new Map();
|
|
445
584
|
timers = /* @__PURE__ */ new Map();
|
|
585
|
+
maxSets;
|
|
586
|
+
constructor(maxSets = DEFAULT_MAX_ENTRIES) {
|
|
587
|
+
this.maxSets = Math.max(1, maxSets);
|
|
588
|
+
}
|
|
446
589
|
sAdd(key, member) {
|
|
447
590
|
const members = Array.isArray(member) ? member : [member];
|
|
448
591
|
let set = this.sets.get(key);
|
|
449
592
|
if (!set) {
|
|
450
593
|
set = /* @__PURE__ */ new Set();
|
|
451
|
-
|
|
594
|
+
} else {
|
|
595
|
+
this.sets.delete(key);
|
|
452
596
|
}
|
|
597
|
+
this.sets.set(key, set);
|
|
453
598
|
let added = 0;
|
|
454
599
|
for (const m of members) {
|
|
455
600
|
if (!set.has(m)) {
|
|
@@ -457,6 +602,10 @@ var MemorySetStore = class {
|
|
|
457
602
|
added += 1;
|
|
458
603
|
}
|
|
459
604
|
}
|
|
605
|
+
while (this.sets.size > this.maxSets) {
|
|
606
|
+
const oldest = this.sets.keys().next().value;
|
|
607
|
+
this.delete(oldest);
|
|
608
|
+
}
|
|
460
609
|
return added;
|
|
461
610
|
}
|
|
462
611
|
sMembers(key) {
|
|
@@ -509,6 +658,7 @@ var MemorySetStore = class {
|
|
|
509
658
|
};
|
|
510
659
|
|
|
511
660
|
// src/shared/metrics.ts
|
|
661
|
+
init_esm_shims();
|
|
512
662
|
var counters = /* @__PURE__ */ new Map();
|
|
513
663
|
function bump(event) {
|
|
514
664
|
const existing = counters.get(event.type);
|
|
@@ -548,6 +698,7 @@ function getMetricSnapshot() {
|
|
|
548
698
|
}
|
|
549
699
|
|
|
550
700
|
// src/shared/namespace.ts
|
|
701
|
+
init_esm_shims();
|
|
551
702
|
var FALLBACK = "unversioned";
|
|
552
703
|
function resolveBuildNamespace(override) {
|
|
553
704
|
if (override !== void 0) {
|
|
@@ -561,6 +712,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
|
561
712
|
}
|
|
562
713
|
|
|
563
714
|
// src/shared/build-phase.ts
|
|
715
|
+
init_esm_shims();
|
|
564
716
|
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
565
717
|
function isBuildPhase() {
|
|
566
718
|
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
@@ -573,6 +725,7 @@ function shouldUseRedis(opts) {
|
|
|
573
725
|
}
|
|
574
726
|
|
|
575
727
|
// src/cache-components/serialize.ts
|
|
728
|
+
init_esm_shims();
|
|
576
729
|
async function readStreamFully(stream) {
|
|
577
730
|
const reader = stream.getReader();
|
|
578
731
|
const chunks = [];
|
|
@@ -615,6 +768,7 @@ function decodeEnvelope(raw) {
|
|
|
615
768
|
}
|
|
616
769
|
|
|
617
770
|
// src/cache-components/swr.ts
|
|
771
|
+
init_esm_shims();
|
|
618
772
|
var SECOND_MS = 1e3;
|
|
619
773
|
function partitionEntry(entry, now = Date.now()) {
|
|
620
774
|
const ageMs = now - entry.timestamp;
|
|
@@ -673,8 +827,8 @@ function init(opts) {
|
|
|
673
827
|
logger,
|
|
674
828
|
emit,
|
|
675
829
|
conn,
|
|
676
|
-
memEntries: new MemoryStore(),
|
|
677
|
-
memTags: new MemorySetStore(),
|
|
830
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
831
|
+
memTags: new MemorySetStore(opts.memoryMaxEntries),
|
|
678
832
|
memTagExp: /* @__PURE__ */ new Map(),
|
|
679
833
|
localTagTimestamps: /* @__PURE__ */ new Map(),
|
|
680
834
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace),
|
|
@@ -791,6 +945,12 @@ async function getImpl(state, cacheKey, softTags) {
|
|
|
791
945
|
state.emit({ type: "cache.miss", ms: Date.now() - start });
|
|
792
946
|
return void 0;
|
|
793
947
|
}
|
|
948
|
+
try {
|
|
949
|
+
raw = await decompressValue(raw);
|
|
950
|
+
} catch {
|
|
951
|
+
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
794
954
|
envelope = decodeEnvelope(raw);
|
|
795
955
|
if (!envelope) {
|
|
796
956
|
state.emit({ type: "cache.miss", meta: { reason: "decode-error" } });
|
|
@@ -919,10 +1079,14 @@ async function setImpl(state, cacheKey, pendingEntry) {
|
|
|
919
1079
|
}
|
|
920
1080
|
return;
|
|
921
1081
|
}
|
|
1082
|
+
const wirePayload = await compressValue(
|
|
1083
|
+
payload,
|
|
1084
|
+
state.opts.rest.compression
|
|
1085
|
+
);
|
|
922
1086
|
if (entry.tags.length === 0) {
|
|
923
|
-
await client.set(eKey,
|
|
1087
|
+
await client.set(eKey, wirePayload, { EX: ttl });
|
|
924
1088
|
} else {
|
|
925
|
-
await luaSetWithTags(client, state, eKey,
|
|
1089
|
+
await luaSetWithTags(client, state, eKey, wirePayload, ttl, entry.tags);
|
|
926
1090
|
}
|
|
927
1091
|
}
|
|
928
1092
|
);
|
|
@@ -958,28 +1122,36 @@ async function refreshTagsImpl(state) {
|
|
|
958
1122
|
async () => {
|
|
959
1123
|
const client = await state.conn.getOrConnect();
|
|
960
1124
|
if (!client) return;
|
|
961
|
-
const
|
|
962
|
-
const
|
|
1125
|
+
const ns = state.opts.hashTag ? `{${state.resolveNs()}}` : state.resolveNs();
|
|
1126
|
+
const pattern = `${escapeMatch(
|
|
1127
|
+
`${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`
|
|
1128
|
+
)}*`;
|
|
1129
|
+
const scanStartMs = Date.now();
|
|
1130
|
+
const fresh = /* @__PURE__ */ new Map();
|
|
963
1131
|
for await (const chunk of client.scanIterator({
|
|
964
1132
|
MATCH: pattern,
|
|
965
1133
|
COUNT: 100
|
|
966
1134
|
})) {
|
|
967
1135
|
const keys = Array.isArray(chunk) ? chunk.map(String) : [String(chunk)];
|
|
968
|
-
|
|
969
|
-
|
|
1136
|
+
if (keys.length === 0) continue;
|
|
1137
|
+
const values = await client.mGet(keys);
|
|
1138
|
+
for (let i = 0; i < keys.length; i += 1) {
|
|
1139
|
+
const v = values[i];
|
|
1140
|
+
if (!v) continue;
|
|
1141
|
+
const ts = Number.parseInt(v, 10);
|
|
1142
|
+
if (!Number.isFinite(ts)) continue;
|
|
1143
|
+
const key = keys[i];
|
|
1144
|
+
if (key === void 0) continue;
|
|
1145
|
+
const tag = stripExpPrefix(state, key);
|
|
1146
|
+
if (tag) fresh.set(tag, ts);
|
|
1147
|
+
}
|
|
970
1148
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (!v) continue;
|
|
976
|
-
const ts = Number.parseInt(v, 10);
|
|
977
|
-
if (!Number.isFinite(ts)) continue;
|
|
978
|
-
const key = seen[i];
|
|
979
|
-
if (key === void 0) continue;
|
|
980
|
-
const tag = stripExpPrefix(state, key);
|
|
981
|
-
if (tag) state.localTagTimestamps.set(tag, ts);
|
|
1149
|
+
for (const [tag, ts] of state.localTagTimestamps) {
|
|
1150
|
+
if (ts >= scanStartMs && (fresh.get(tag) ?? 0) < ts) {
|
|
1151
|
+
fresh.set(tag, ts);
|
|
1152
|
+
}
|
|
982
1153
|
}
|
|
1154
|
+
state.localTagTimestamps = fresh;
|
|
983
1155
|
}
|
|
984
1156
|
);
|
|
985
1157
|
} catch (err) {
|
|
@@ -988,6 +1160,19 @@ async function refreshTagsImpl(state) {
|
|
|
988
1160
|
});
|
|
989
1161
|
}
|
|
990
1162
|
}
|
|
1163
|
+
function escapeMatch(s) {
|
|
1164
|
+
return s.replace(/[\\*?[\]]/g, "\\$&");
|
|
1165
|
+
}
|
|
1166
|
+
var MAX_LOCAL_TAG_ENTRIES = 1e4;
|
|
1167
|
+
function pruneOldestByTimestamp(map, maxEntries) {
|
|
1168
|
+
if (map.size <= maxEntries) return;
|
|
1169
|
+
const byAge = [...map.entries()].sort((a, b) => a[1] - b[1]);
|
|
1170
|
+
const drop = map.size - maxEntries;
|
|
1171
|
+
for (let i = 0; i < drop; i += 1) {
|
|
1172
|
+
const entry = byAge[i];
|
|
1173
|
+
if (entry) map.delete(entry[0]);
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
991
1176
|
function stripExpPrefix(state, fullKey) {
|
|
992
1177
|
const ns = state.resolveNs();
|
|
993
1178
|
const prefix = state.opts.hashTag ? `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}{${ns}}:` : `${state.opts.keyPrefix}${TAG_EXP_SUFFIX}${ns}:`;
|
|
@@ -1039,6 +1224,7 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1039
1224
|
state.localTagTimestamps.set(t, now);
|
|
1040
1225
|
state.memTagExp.set(t, now);
|
|
1041
1226
|
}
|
|
1227
|
+
pruneOldestByTimestamp(state.memTagExp, MAX_LOCAL_TAG_ENTRIES);
|
|
1042
1228
|
if (!gateRedis(state)) {
|
|
1043
1229
|
if (isHardExpire) {
|
|
1044
1230
|
for (const t of tags) {
|
|
@@ -1068,20 +1254,18 @@ async function updateTagsImpl(state, tags, durations) {
|
|
|
1068
1254
|
}
|
|
1069
1255
|
return;
|
|
1070
1256
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1257
|
+
await Promise.all(
|
|
1258
|
+
tags.map(
|
|
1259
|
+
(t) => isHardExpire ? execLuaScript(
|
|
1074
1260
|
client,
|
|
1075
1261
|
"revalidateHard",
|
|
1076
1262
|
[tagKey(state, t), tagExpKey(state, t)],
|
|
1077
1263
|
[String(now), String(TAG_EXP_MARKER_TTL_SEC)]
|
|
1078
|
-
)
|
|
1079
|
-
} else {
|
|
1080
|
-
await client.set(tagExpKey(state, t), String(now), {
|
|
1264
|
+
) : client.set(tagExpKey(state, t), String(now), {
|
|
1081
1265
|
EX: TAG_EXP_MARKER_TTL_SEC
|
|
1082
|
-
})
|
|
1083
|
-
|
|
1084
|
-
|
|
1266
|
+
})
|
|
1267
|
+
)
|
|
1268
|
+
);
|
|
1085
1269
|
}
|
|
1086
1270
|
);
|
|
1087
1271
|
state.emit({
|
|
@@ -1111,7 +1295,14 @@ function createCacheComponentsHandler(opts) {
|
|
|
1111
1295
|
};
|
|
1112
1296
|
}
|
|
1113
1297
|
|
|
1298
|
+
// src/incremental/index.ts
|
|
1299
|
+
init_esm_shims();
|
|
1300
|
+
|
|
1301
|
+
// src/incremental/handler.ts
|
|
1302
|
+
init_esm_shims();
|
|
1303
|
+
|
|
1114
1304
|
// src/incremental/serialize.ts
|
|
1305
|
+
init_esm_shims();
|
|
1115
1306
|
function isEnvelope(value) {
|
|
1116
1307
|
return typeof value === "object" && value !== null && "__type" in value && typeof value.__type === "string";
|
|
1117
1308
|
}
|
|
@@ -1193,7 +1384,7 @@ function init2(opts) {
|
|
|
1193
1384
|
logger,
|
|
1194
1385
|
emit,
|
|
1195
1386
|
conn,
|
|
1196
|
-
memEntries: new MemoryStore(),
|
|
1387
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
1197
1388
|
memTagStates: /* @__PURE__ */ new Map(),
|
|
1198
1389
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
1199
1390
|
};
|
|
@@ -1252,7 +1443,7 @@ function normalizeTtlSeconds(data, ctx) {
|
|
|
1252
1443
|
return ONE_YEAR_SEC2;
|
|
1253
1444
|
}
|
|
1254
1445
|
}
|
|
1255
|
-
const revalidate = ctx?.cacheControl?.revalidate;
|
|
1446
|
+
const revalidate = ctx?.cacheControl?.revalidate ?? ctx?.revalidate;
|
|
1256
1447
|
if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC2;
|
|
1257
1448
|
if (typeof revalidate === "number" && revalidate > 0) {
|
|
1258
1449
|
return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC2);
|
|
@@ -1290,10 +1481,49 @@ async function readTagStates(state, tags) {
|
|
|
1290
1481
|
var IncrementalRedisCacheHandler = class {
|
|
1291
1482
|
state;
|
|
1292
1483
|
revalidatedTags;
|
|
1484
|
+
/**
|
|
1485
|
+
* Request-scoped memo of Redis entry reads. Next.js instantiates this class
|
|
1486
|
+
* per request (and calls resetRequestCache() at request boundaries when it
|
|
1487
|
+
* reuses an instance), so entries live at most one request. Bounded by the
|
|
1488
|
+
* number of distinct cache keys a single request touches.
|
|
1489
|
+
*/
|
|
1490
|
+
requestMemo = /* @__PURE__ */ new Map();
|
|
1293
1491
|
constructor(state, ctx) {
|
|
1294
1492
|
this.state = state;
|
|
1295
1493
|
this.revalidatedTags = normalizeTags(ctx?.revalidatedTags);
|
|
1296
1494
|
}
|
|
1495
|
+
/**
|
|
1496
|
+
* Redis entry read, deduplicated per request: parallel RSC renders or
|
|
1497
|
+
* repeated fetch() calls hitting the same cache key share one GET. The
|
|
1498
|
+
* memoized promise never rejects — errors are converted to a miss (with
|
|
1499
|
+
* the same timeout metric the inline path emitted before).
|
|
1500
|
+
*/
|
|
1501
|
+
readEntryDeduped(eKey) {
|
|
1502
|
+
const memoized = this.requestMemo.get(eKey);
|
|
1503
|
+
if (memoized) {
|
|
1504
|
+
this.state.emit({ type: "cache.get.deduped" });
|
|
1505
|
+
return memoized;
|
|
1506
|
+
}
|
|
1507
|
+
const read = withAbortSignal(
|
|
1508
|
+
"incremental.get",
|
|
1509
|
+
this.state.opts.abortTimeoutMs,
|
|
1510
|
+
async () => {
|
|
1511
|
+
const client = await this.state.conn.getOrConnect();
|
|
1512
|
+
if (!client) return null;
|
|
1513
|
+
return await decompressValue(await client.get(eKey));
|
|
1514
|
+
}
|
|
1515
|
+
).catch((err) => {
|
|
1516
|
+
if (err instanceof CacheTimeoutError) {
|
|
1517
|
+
this.state.emit({
|
|
1518
|
+
type: "redis.timeout",
|
|
1519
|
+
meta: { op: "incremental.get" }
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
return null;
|
|
1523
|
+
});
|
|
1524
|
+
this.requestMemo.set(eKey, read);
|
|
1525
|
+
return read;
|
|
1526
|
+
}
|
|
1297
1527
|
async get(cacheKey, ctx) {
|
|
1298
1528
|
try {
|
|
1299
1529
|
const requestedTags = normalizeTags([
|
|
@@ -1305,24 +1535,11 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1305
1535
|
const useRedis = !useLocal && gateRedis2(this.state);
|
|
1306
1536
|
let raw = null;
|
|
1307
1537
|
if (useRedis) {
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
async () => {
|
|
1313
|
-
const client = await this.state.conn.getOrConnect();
|
|
1314
|
-
if (!client) return null;
|
|
1315
|
-
return await client.get(eKey);
|
|
1316
|
-
}
|
|
1317
|
-
);
|
|
1318
|
-
} catch (err) {
|
|
1319
|
-
if (err instanceof CacheTimeoutError) {
|
|
1320
|
-
this.state.emit({ type: "redis.timeout", meta: { op: "incremental.get" } });
|
|
1321
|
-
}
|
|
1322
|
-
raw = null;
|
|
1323
|
-
}
|
|
1538
|
+
raw = await this.readEntryDeduped(eKey);
|
|
1539
|
+
}
|
|
1540
|
+
if (raw === null && (useLocal || this.state.opts.fallback !== "never")) {
|
|
1541
|
+
raw = this.state.memEntries.get(eKey);
|
|
1324
1542
|
}
|
|
1325
|
-
if (raw === null) raw = this.state.memEntries.get(eKey);
|
|
1326
1543
|
if (raw === null) {
|
|
1327
1544
|
this.state.emit({ type: "cache.miss" });
|
|
1328
1545
|
return null;
|
|
@@ -1335,7 +1552,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1335
1552
|
return null;
|
|
1336
1553
|
}
|
|
1337
1554
|
const tagStates = await readTagStates(this.state, tags);
|
|
1338
|
-
const isFetch = ctx?.kind === "FETCH";
|
|
1555
|
+
const isFetch = ctx?.kind === "FETCH" || ctx?.kindHint?.toLowerCase() === "fetch" || typeof parsed.value === "object" && parsed.value !== null && parsed.value.kind === "FETCH";
|
|
1339
1556
|
if (tagStates.some((s) => isTagStateExpired(s, parsed.lastModified, isFetch))) {
|
|
1340
1557
|
this.state.emit({ type: "cache.miss", meta: { reason: "tag-state-expired" } });
|
|
1341
1558
|
return null;
|
|
@@ -1358,6 +1575,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1358
1575
|
const record = { lastModified: Date.now(), value: data };
|
|
1359
1576
|
const serialized = serializeCacheRecord(record);
|
|
1360
1577
|
const eKey = entryKey2(this.state, cacheKey);
|
|
1578
|
+
this.requestMemo.delete(eKey);
|
|
1361
1579
|
const useRedis = !useLocal && gateRedis2(this.state);
|
|
1362
1580
|
if (useRedis) {
|
|
1363
1581
|
try {
|
|
@@ -1367,10 +1585,19 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1367
1585
|
async () => {
|
|
1368
1586
|
const client = await this.state.conn.getOrConnect();
|
|
1369
1587
|
if (!client) {
|
|
1370
|
-
this.state.
|
|
1588
|
+
if (this.state.opts.fallback !== "never") {
|
|
1589
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
1590
|
+
}
|
|
1371
1591
|
return;
|
|
1372
1592
|
}
|
|
1373
|
-
await client.set(
|
|
1593
|
+
await client.set(
|
|
1594
|
+
eKey,
|
|
1595
|
+
await compressValue(
|
|
1596
|
+
serialized,
|
|
1597
|
+
this.state.opts.rest.compression
|
|
1598
|
+
),
|
|
1599
|
+
{ EX: ttl }
|
|
1600
|
+
);
|
|
1374
1601
|
}
|
|
1375
1602
|
);
|
|
1376
1603
|
this.state.emit({ type: "cache.set", meta: { backend: "redis" } });
|
|
@@ -1381,8 +1608,15 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1381
1608
|
}
|
|
1382
1609
|
}
|
|
1383
1610
|
}
|
|
1384
|
-
this.state.
|
|
1385
|
-
|
|
1611
|
+
if (useLocal || this.state.opts.fallback !== "never") {
|
|
1612
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
1613
|
+
this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
|
|
1614
|
+
} else {
|
|
1615
|
+
this.state.emit({
|
|
1616
|
+
type: "cache.set.failed",
|
|
1617
|
+
meta: { reason: "no-fallback" }
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1386
1620
|
} catch (err) {
|
|
1387
1621
|
this.state.logger.error("set() error", {
|
|
1388
1622
|
message: err.message
|
|
@@ -1436,6 +1670,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
1436
1670
|
}
|
|
1437
1671
|
}
|
|
1438
1672
|
resetRequestCache() {
|
|
1673
|
+
this.requestMemo.clear();
|
|
1439
1674
|
}
|
|
1440
1675
|
};
|
|
1441
1676
|
function createIncrementalCacheHandler(opts) {
|