@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
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var module$1 = require('module');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var util = require('util');
|
|
6
|
+
var zlib = require('zlib');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
|
|
3
12
|
var __defProp = Object.defineProperty;
|
|
4
13
|
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
14
|
var __esm = (fn, res) => function __init() {
|
|
12
15
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
13
16
|
};
|
|
@@ -16,6 +19,32 @@ var __export = (target, all) => {
|
|
|
16
19
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
20
|
};
|
|
18
21
|
|
|
22
|
+
// node_modules/tsup/assets/cjs_shims.js
|
|
23
|
+
var getImportMetaUrl, importMetaUrl;
|
|
24
|
+
var init_cjs_shims = __esm({
|
|
25
|
+
"node_modules/tsup/assets/cjs_shims.js"() {
|
|
26
|
+
getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
|
|
27
|
+
importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
function nodeRequire(id) {
|
|
31
|
+
try {
|
|
32
|
+
return packageRequire(id);
|
|
33
|
+
} catch {
|
|
34
|
+
return cwdRequire(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var packageRequire, cwdRequire;
|
|
38
|
+
var init_node_require = __esm({
|
|
39
|
+
"src/shared/node-require.ts"() {
|
|
40
|
+
init_cjs_shims();
|
|
41
|
+
packageRequire = module$1.createRequire(importMetaUrl);
|
|
42
|
+
cwdRequire = module$1.createRequire(
|
|
43
|
+
path__default.default.join(process.cwd(), "__resolve-anchor__.js")
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
19
48
|
// src/shared/client/adapter-redis.ts
|
|
20
49
|
var adapter_redis_exports = {};
|
|
21
50
|
__export(adapter_redis_exports, {
|
|
@@ -26,7 +55,7 @@ function adaptRedisV5(client) {
|
|
|
26
55
|
return client;
|
|
27
56
|
}
|
|
28
57
|
function createRedisV5Client(config) {
|
|
29
|
-
const { createClient } =
|
|
58
|
+
const { createClient } = nodeRequire("redis");
|
|
30
59
|
const useTls = config.tls ?? config.url.startsWith("rediss://");
|
|
31
60
|
const opts = {
|
|
32
61
|
url: config.url,
|
|
@@ -41,6 +70,8 @@ function createRedisV5Client(config) {
|
|
|
41
70
|
}
|
|
42
71
|
var init_adapter_redis = __esm({
|
|
43
72
|
"src/shared/client/adapter-redis.ts"() {
|
|
73
|
+
init_cjs_shims();
|
|
74
|
+
init_node_require();
|
|
44
75
|
}
|
|
45
76
|
});
|
|
46
77
|
|
|
@@ -50,7 +81,8 @@ __export(adapter_ioredis_exports, {
|
|
|
50
81
|
adaptCluster: () => adaptCluster,
|
|
51
82
|
adaptIoredis: () => adaptIoredis,
|
|
52
83
|
createIoredisClient: () => createIoredisClient,
|
|
53
|
-
createIoredisCluster: () => createIoredisCluster
|
|
84
|
+
createIoredisCluster: () => createIoredisCluster,
|
|
85
|
+
createIoredisSentinel: () => createIoredisSentinel
|
|
54
86
|
});
|
|
55
87
|
function adapt(client, isClusterClient = false) {
|
|
56
88
|
return {
|
|
@@ -133,6 +165,9 @@ function adapt(client, isClusterClient = false) {
|
|
|
133
165
|
on: (event, listener) => {
|
|
134
166
|
client.on(event, listener);
|
|
135
167
|
return client;
|
|
168
|
+
},
|
|
169
|
+
dispose: () => {
|
|
170
|
+
client.disconnect();
|
|
136
171
|
}
|
|
137
172
|
};
|
|
138
173
|
}
|
|
@@ -143,18 +178,36 @@ function adaptCluster(cluster) {
|
|
|
143
178
|
return adapt(cluster, true);
|
|
144
179
|
}
|
|
145
180
|
function createIoredisClient(config) {
|
|
146
|
-
const IORedis =
|
|
181
|
+
const IORedis = nodeRequire("ioredis");
|
|
147
182
|
const Ctor = IORedis.default ?? IORedis;
|
|
148
183
|
return new Ctor(config.url, {
|
|
149
184
|
password: config.password,
|
|
150
185
|
connectTimeout: config.connectTimeout ?? 1500,
|
|
151
186
|
tls: config.tls ? {} : void 0,
|
|
152
187
|
lazyConnect: true,
|
|
153
|
-
maxRetriesPerRequest: 1
|
|
188
|
+
maxRetriesPerRequest: 1,
|
|
189
|
+
// Batch same-tick commands into one RTT (matches redis@5's default
|
|
190
|
+
// behavior) — makes Promise.all fan-outs a single pipeline.
|
|
191
|
+
enableAutoPipelining: true
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function createIoredisSentinel(config) {
|
|
195
|
+
const IORedis = nodeRequire("ioredis");
|
|
196
|
+
const Ctor = IORedis.default ?? IORedis;
|
|
197
|
+
return new Ctor({
|
|
198
|
+
sentinels: config.sentinels,
|
|
199
|
+
name: config.name,
|
|
200
|
+
password: config.password,
|
|
201
|
+
sentinelPassword: config.sentinelPassword,
|
|
202
|
+
connectTimeout: config.connectTimeout ?? 1500,
|
|
203
|
+
tls: config.tls ? {} : void 0,
|
|
204
|
+
lazyConnect: true,
|
|
205
|
+
maxRetriesPerRequest: 1,
|
|
206
|
+
enableAutoPipelining: true
|
|
154
207
|
});
|
|
155
208
|
}
|
|
156
209
|
function createIoredisCluster(config) {
|
|
157
|
-
const IORedis =
|
|
210
|
+
const IORedis = nodeRequire("ioredis");
|
|
158
211
|
const ClusterCtor = IORedis.Cluster ?? IORedis.default.Cluster;
|
|
159
212
|
return new ClusterCtor(config.nodes, {
|
|
160
213
|
redisOptions: {
|
|
@@ -162,15 +215,26 @@ function createIoredisCluster(config) {
|
|
|
162
215
|
tls: config.tls ? {} : void 0
|
|
163
216
|
},
|
|
164
217
|
enableReadyCheck: true,
|
|
165
|
-
lazyConnect: true
|
|
218
|
+
lazyConnect: true,
|
|
219
|
+
// Same-slot commands issued in the same tick share one pipeline.
|
|
220
|
+
enableAutoPipelining: true
|
|
166
221
|
});
|
|
167
222
|
}
|
|
168
223
|
var init_adapter_ioredis = __esm({
|
|
169
224
|
"src/shared/client/adapter-ioredis.ts"() {
|
|
225
|
+
init_cjs_shims();
|
|
226
|
+
init_node_require();
|
|
170
227
|
}
|
|
171
228
|
});
|
|
172
229
|
|
|
230
|
+
// src/incremental/index.ts
|
|
231
|
+
init_cjs_shims();
|
|
232
|
+
|
|
233
|
+
// src/incremental/handler.ts
|
|
234
|
+
init_cjs_shims();
|
|
235
|
+
|
|
173
236
|
// src/shared/abort.ts
|
|
237
|
+
init_cjs_shims();
|
|
174
238
|
var CacheTimeoutError = class extends Error {
|
|
175
239
|
constructor(opName, ms) {
|
|
176
240
|
super(`[@leejpsd/nextjs-cache-handler] ${opName} timed out after ${ms}ms`);
|
|
@@ -202,7 +266,41 @@ async function withAbortSignal(opName, ms, fn) {
|
|
|
202
266
|
}
|
|
203
267
|
}
|
|
204
268
|
|
|
269
|
+
// src/shared/compress.ts
|
|
270
|
+
init_cjs_shims();
|
|
271
|
+
var gzipAsync = util.promisify(zlib.gzip);
|
|
272
|
+
var gunzipAsync = util.promisify(zlib.gunzip);
|
|
273
|
+
var brotliCompressAsync = util.promisify(zlib.brotliCompress);
|
|
274
|
+
var brotliDecompressAsync = util.promisify(zlib.brotliDecompress);
|
|
275
|
+
var GZIP_MARKER = "__ncgz__:";
|
|
276
|
+
var BROTLI_MARKER = "__ncbr__:";
|
|
277
|
+
var MIN_COMPRESS_BYTES = 1024;
|
|
278
|
+
var BROTLI_OPTS = {
|
|
279
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 }
|
|
280
|
+
};
|
|
281
|
+
async function compressValue(value, algo) {
|
|
282
|
+
if (!algo || Buffer.byteLength(value) < MIN_COMPRESS_BYTES) return value;
|
|
283
|
+
const input = Buffer.from(value);
|
|
284
|
+
if (algo === "gzip") {
|
|
285
|
+
return GZIP_MARKER + (await gzipAsync(input)).toString("base64");
|
|
286
|
+
}
|
|
287
|
+
return BROTLI_MARKER + (await brotliCompressAsync(input, BROTLI_OPTS)).toString("base64");
|
|
288
|
+
}
|
|
289
|
+
async function decompressValue(value) {
|
|
290
|
+
if (value === null) return null;
|
|
291
|
+
if (value.startsWith(GZIP_MARKER)) {
|
|
292
|
+
const buf = Buffer.from(value.slice(GZIP_MARKER.length), "base64");
|
|
293
|
+
return (await gunzipAsync(buf)).toString("utf8");
|
|
294
|
+
}
|
|
295
|
+
if (value.startsWith(BROTLI_MARKER)) {
|
|
296
|
+
const buf = Buffer.from(value.slice(BROTLI_MARKER.length), "base64");
|
|
297
|
+
return (await brotliDecompressAsync(buf)).toString("utf8");
|
|
298
|
+
}
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
|
|
205
302
|
// src/shared/client/index.ts
|
|
303
|
+
init_cjs_shims();
|
|
206
304
|
async function buildClient(source) {
|
|
207
305
|
if (typeof source === "function") {
|
|
208
306
|
return await source();
|
|
@@ -220,6 +318,10 @@ async function buildClient(source) {
|
|
|
220
318
|
const { adaptCluster: adaptCluster2, createIoredisCluster: createIoredisCluster2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
221
319
|
return adaptCluster2(createIoredisCluster2(source));
|
|
222
320
|
}
|
|
321
|
+
case "sentinel": {
|
|
322
|
+
const { adaptIoredis: adaptIoredis2, createIoredisSentinel: createIoredisSentinel2 } = await Promise.resolve().then(() => (init_adapter_ioredis(), adapter_ioredis_exports));
|
|
323
|
+
return adaptIoredis2(createIoredisSentinel2(source));
|
|
324
|
+
}
|
|
223
325
|
default: {
|
|
224
326
|
const exhaustive = source;
|
|
225
327
|
throw new Error(
|
|
@@ -229,28 +331,45 @@ async function buildClient(source) {
|
|
|
229
331
|
}
|
|
230
332
|
}
|
|
231
333
|
var ConnectionManager = class {
|
|
232
|
-
constructor(source, onError) {
|
|
334
|
+
constructor(source, onError, retry = {}) {
|
|
233
335
|
this.source = source;
|
|
234
336
|
this.onError = onError;
|
|
337
|
+
this.baseCooldownMs = retry.baseCooldownMs ?? 1e3;
|
|
338
|
+
this.maxCooldownMs = retry.maxCooldownMs ?? 3e4;
|
|
235
339
|
}
|
|
236
340
|
source;
|
|
237
341
|
onError;
|
|
238
342
|
client = null;
|
|
239
343
|
connecting = null;
|
|
240
|
-
|
|
344
|
+
consecutiveFailures = 0;
|
|
345
|
+
lastFailureAt = 0;
|
|
346
|
+
baseCooldownMs;
|
|
347
|
+
maxCooldownMs;
|
|
348
|
+
cooldownMs() {
|
|
349
|
+
if (this.consecutiveFailures === 0) return 0;
|
|
350
|
+
return Math.min(
|
|
351
|
+
this.maxCooldownMs,
|
|
352
|
+
this.baseCooldownMs * 2 ** (this.consecutiveFailures - 1)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
241
355
|
async getOrConnect() {
|
|
242
356
|
if (this.client?.isOpen) return this.client;
|
|
243
|
-
if (this.failed) return null;
|
|
244
357
|
if (this.connecting) return this.connecting;
|
|
358
|
+
if (Date.now() - this.lastFailureAt < this.cooldownMs()) return null;
|
|
359
|
+
const stale = this.client;
|
|
360
|
+
this.client = null;
|
|
245
361
|
this.connecting = (async () => {
|
|
362
|
+
disposeQuietly(stale);
|
|
246
363
|
try {
|
|
247
364
|
const c = await buildClient(this.source);
|
|
248
365
|
c.on("error", (err) => this.onError(err));
|
|
249
366
|
if (!c.isOpen) await c.connect();
|
|
250
367
|
this.client = c;
|
|
368
|
+
this.consecutiveFailures = 0;
|
|
251
369
|
return c;
|
|
252
370
|
} catch (err) {
|
|
253
|
-
this.
|
|
371
|
+
this.consecutiveFailures += 1;
|
|
372
|
+
this.lastFailureAt = Date.now();
|
|
254
373
|
this.onError(err);
|
|
255
374
|
return null;
|
|
256
375
|
} finally {
|
|
@@ -260,13 +379,24 @@ var ConnectionManager = class {
|
|
|
260
379
|
return this.connecting;
|
|
261
380
|
}
|
|
262
381
|
reset() {
|
|
382
|
+
disposeQuietly(this.client);
|
|
263
383
|
this.client = null;
|
|
264
384
|
this.connecting = null;
|
|
265
|
-
this.
|
|
385
|
+
this.consecutiveFailures = 0;
|
|
386
|
+
this.lastFailureAt = 0;
|
|
266
387
|
}
|
|
267
388
|
};
|
|
389
|
+
function disposeQuietly(client) {
|
|
390
|
+
if (!client) return;
|
|
391
|
+
const c = client;
|
|
392
|
+
try {
|
|
393
|
+
(c.dispose ?? c.destroy ?? c.disconnect)?.call(c);
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
268
397
|
|
|
269
398
|
// src/shared/logger.ts
|
|
399
|
+
init_cjs_shims();
|
|
270
400
|
var defaultLogger = {
|
|
271
401
|
debug: () => {
|
|
272
402
|
},
|
|
@@ -283,13 +413,19 @@ var defaultLogger = {
|
|
|
283
413
|
};
|
|
284
414
|
|
|
285
415
|
// src/shared/memory-fallback.ts
|
|
416
|
+
init_cjs_shims();
|
|
286
417
|
var MAX_TIMER_MS = 2147483647;
|
|
287
418
|
function clampTimerMs(ms) {
|
|
288
419
|
return Math.min(Math.max(0, ms), MAX_TIMER_MS);
|
|
289
420
|
}
|
|
421
|
+
var DEFAULT_MAX_ENTRIES = 1e3;
|
|
290
422
|
var MemoryStore = class {
|
|
291
423
|
kv = /* @__PURE__ */ new Map();
|
|
292
424
|
timers = /* @__PURE__ */ new Map();
|
|
425
|
+
maxEntries;
|
|
426
|
+
constructor(maxEntries = DEFAULT_MAX_ENTRIES) {
|
|
427
|
+
this.maxEntries = Math.max(1, maxEntries);
|
|
428
|
+
}
|
|
293
429
|
set(key, value, ttlSeconds) {
|
|
294
430
|
this.delete(key);
|
|
295
431
|
const expiresAt = Date.now() + ttlSeconds * 1e3;
|
|
@@ -299,6 +435,10 @@ var MemoryStore = class {
|
|
|
299
435
|
timer.unref();
|
|
300
436
|
}
|
|
301
437
|
this.timers.set(key, timer);
|
|
438
|
+
while (this.kv.size > this.maxEntries) {
|
|
439
|
+
const oldest = this.kv.keys().next().value;
|
|
440
|
+
this.delete(oldest);
|
|
441
|
+
}
|
|
302
442
|
}
|
|
303
443
|
get(key) {
|
|
304
444
|
const entry = this.kv.get(key);
|
|
@@ -307,6 +447,8 @@ var MemoryStore = class {
|
|
|
307
447
|
this.delete(key);
|
|
308
448
|
return null;
|
|
309
449
|
}
|
|
450
|
+
this.kv.delete(key);
|
|
451
|
+
this.kv.set(key, entry);
|
|
310
452
|
return entry.value;
|
|
311
453
|
}
|
|
312
454
|
delete(key) {
|
|
@@ -339,6 +481,7 @@ var MemoryStore = class {
|
|
|
339
481
|
};
|
|
340
482
|
|
|
341
483
|
// src/shared/metrics.ts
|
|
484
|
+
init_cjs_shims();
|
|
342
485
|
var counters = /* @__PURE__ */ new Map();
|
|
343
486
|
function bump(event) {
|
|
344
487
|
const existing = counters.get(event.type);
|
|
@@ -367,6 +510,7 @@ function createEmitter(onMetric) {
|
|
|
367
510
|
}
|
|
368
511
|
|
|
369
512
|
// src/shared/namespace.ts
|
|
513
|
+
init_cjs_shims();
|
|
370
514
|
var FALLBACK = "unversioned";
|
|
371
515
|
function resolveBuildNamespace(override) {
|
|
372
516
|
if (override !== void 0) {
|
|
@@ -380,6 +524,7 @@ function buildKey(prefix, namespace, rest, hashTag = false) {
|
|
|
380
524
|
}
|
|
381
525
|
|
|
382
526
|
// src/shared/build-phase.ts
|
|
527
|
+
init_cjs_shims();
|
|
383
528
|
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
384
529
|
function isBuildPhase() {
|
|
385
530
|
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
@@ -392,6 +537,7 @@ function shouldUseRedis(opts) {
|
|
|
392
537
|
}
|
|
393
538
|
|
|
394
539
|
// src/incremental/serialize.ts
|
|
540
|
+
init_cjs_shims();
|
|
395
541
|
function isEnvelope(value) {
|
|
396
542
|
return typeof value === "object" && value !== null && "__type" in value && typeof value.__type === "string";
|
|
397
543
|
}
|
|
@@ -473,7 +619,7 @@ function init(opts) {
|
|
|
473
619
|
logger,
|
|
474
620
|
emit,
|
|
475
621
|
conn,
|
|
476
|
-
memEntries: new MemoryStore(),
|
|
622
|
+
memEntries: new MemoryStore(opts.memoryMaxEntries),
|
|
477
623
|
memTagStates: /* @__PURE__ */ new Map(),
|
|
478
624
|
resolveNs: () => resolveBuildNamespace(opts.buildNamespace)
|
|
479
625
|
};
|
|
@@ -532,7 +678,7 @@ function normalizeTtlSeconds(data, ctx) {
|
|
|
532
678
|
return ONE_YEAR_SEC;
|
|
533
679
|
}
|
|
534
680
|
}
|
|
535
|
-
const revalidate = ctx?.cacheControl?.revalidate;
|
|
681
|
+
const revalidate = ctx?.cacheControl?.revalidate ?? ctx?.revalidate;
|
|
536
682
|
if (revalidate === false || revalidate === void 0) return ONE_YEAR_SEC;
|
|
537
683
|
if (typeof revalidate === "number" && revalidate > 0) {
|
|
538
684
|
return Math.min(Math.max(Math.ceil(revalidate), MIN_TTL_SEC), ONE_YEAR_SEC);
|
|
@@ -570,10 +716,49 @@ async function readTagStates(state, tags) {
|
|
|
570
716
|
var IncrementalRedisCacheHandler = class {
|
|
571
717
|
state;
|
|
572
718
|
revalidatedTags;
|
|
719
|
+
/**
|
|
720
|
+
* Request-scoped memo of Redis entry reads. Next.js instantiates this class
|
|
721
|
+
* per request (and calls resetRequestCache() at request boundaries when it
|
|
722
|
+
* reuses an instance), so entries live at most one request. Bounded by the
|
|
723
|
+
* number of distinct cache keys a single request touches.
|
|
724
|
+
*/
|
|
725
|
+
requestMemo = /* @__PURE__ */ new Map();
|
|
573
726
|
constructor(state, ctx) {
|
|
574
727
|
this.state = state;
|
|
575
728
|
this.revalidatedTags = normalizeTags(ctx?.revalidatedTags);
|
|
576
729
|
}
|
|
730
|
+
/**
|
|
731
|
+
* Redis entry read, deduplicated per request: parallel RSC renders or
|
|
732
|
+
* repeated fetch() calls hitting the same cache key share one GET. The
|
|
733
|
+
* memoized promise never rejects — errors are converted to a miss (with
|
|
734
|
+
* the same timeout metric the inline path emitted before).
|
|
735
|
+
*/
|
|
736
|
+
readEntryDeduped(eKey) {
|
|
737
|
+
const memoized = this.requestMemo.get(eKey);
|
|
738
|
+
if (memoized) {
|
|
739
|
+
this.state.emit({ type: "cache.get.deduped" });
|
|
740
|
+
return memoized;
|
|
741
|
+
}
|
|
742
|
+
const read = withAbortSignal(
|
|
743
|
+
"incremental.get",
|
|
744
|
+
this.state.opts.abortTimeoutMs,
|
|
745
|
+
async () => {
|
|
746
|
+
const client = await this.state.conn.getOrConnect();
|
|
747
|
+
if (!client) return null;
|
|
748
|
+
return await decompressValue(await client.get(eKey));
|
|
749
|
+
}
|
|
750
|
+
).catch((err) => {
|
|
751
|
+
if (err instanceof CacheTimeoutError) {
|
|
752
|
+
this.state.emit({
|
|
753
|
+
type: "redis.timeout",
|
|
754
|
+
meta: { op: "incremental.get" }
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
});
|
|
759
|
+
this.requestMemo.set(eKey, read);
|
|
760
|
+
return read;
|
|
761
|
+
}
|
|
577
762
|
async get(cacheKey, ctx) {
|
|
578
763
|
try {
|
|
579
764
|
const requestedTags = normalizeTags([
|
|
@@ -585,24 +770,11 @@ var IncrementalRedisCacheHandler = class {
|
|
|
585
770
|
const useRedis = !useLocal && gateRedis(this.state);
|
|
586
771
|
let raw = null;
|
|
587
772
|
if (useRedis) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
async () => {
|
|
593
|
-
const client = await this.state.conn.getOrConnect();
|
|
594
|
-
if (!client) return null;
|
|
595
|
-
return await client.get(eKey);
|
|
596
|
-
}
|
|
597
|
-
);
|
|
598
|
-
} catch (err) {
|
|
599
|
-
if (err instanceof CacheTimeoutError) {
|
|
600
|
-
this.state.emit({ type: "redis.timeout", meta: { op: "incremental.get" } });
|
|
601
|
-
}
|
|
602
|
-
raw = null;
|
|
603
|
-
}
|
|
773
|
+
raw = await this.readEntryDeduped(eKey);
|
|
774
|
+
}
|
|
775
|
+
if (raw === null && (useLocal || this.state.opts.fallback !== "never")) {
|
|
776
|
+
raw = this.state.memEntries.get(eKey);
|
|
604
777
|
}
|
|
605
|
-
if (raw === null) raw = this.state.memEntries.get(eKey);
|
|
606
778
|
if (raw === null) {
|
|
607
779
|
this.state.emit({ type: "cache.miss" });
|
|
608
780
|
return null;
|
|
@@ -615,7 +787,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
615
787
|
return null;
|
|
616
788
|
}
|
|
617
789
|
const tagStates = await readTagStates(this.state, tags);
|
|
618
|
-
const isFetch = ctx?.kind === "FETCH";
|
|
790
|
+
const isFetch = ctx?.kind === "FETCH" || ctx?.kindHint?.toLowerCase() === "fetch" || typeof parsed.value === "object" && parsed.value !== null && parsed.value.kind === "FETCH";
|
|
619
791
|
if (tagStates.some((s) => isTagStateExpired(s, parsed.lastModified, isFetch))) {
|
|
620
792
|
this.state.emit({ type: "cache.miss", meta: { reason: "tag-state-expired" } });
|
|
621
793
|
return null;
|
|
@@ -638,6 +810,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
638
810
|
const record = { lastModified: Date.now(), value: data };
|
|
639
811
|
const serialized = serializeCacheRecord(record);
|
|
640
812
|
const eKey = entryKey(this.state, cacheKey);
|
|
813
|
+
this.requestMemo.delete(eKey);
|
|
641
814
|
const useRedis = !useLocal && gateRedis(this.state);
|
|
642
815
|
if (useRedis) {
|
|
643
816
|
try {
|
|
@@ -647,10 +820,19 @@ var IncrementalRedisCacheHandler = class {
|
|
|
647
820
|
async () => {
|
|
648
821
|
const client = await this.state.conn.getOrConnect();
|
|
649
822
|
if (!client) {
|
|
650
|
-
this.state.
|
|
823
|
+
if (this.state.opts.fallback !== "never") {
|
|
824
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
825
|
+
}
|
|
651
826
|
return;
|
|
652
827
|
}
|
|
653
|
-
await client.set(
|
|
828
|
+
await client.set(
|
|
829
|
+
eKey,
|
|
830
|
+
await compressValue(
|
|
831
|
+
serialized,
|
|
832
|
+
this.state.opts.rest.compression
|
|
833
|
+
),
|
|
834
|
+
{ EX: ttl }
|
|
835
|
+
);
|
|
654
836
|
}
|
|
655
837
|
);
|
|
656
838
|
this.state.emit({ type: "cache.set", meta: { backend: "redis" } });
|
|
@@ -661,8 +843,15 @@ var IncrementalRedisCacheHandler = class {
|
|
|
661
843
|
}
|
|
662
844
|
}
|
|
663
845
|
}
|
|
664
|
-
this.state.
|
|
665
|
-
|
|
846
|
+
if (useLocal || this.state.opts.fallback !== "never") {
|
|
847
|
+
this.state.memEntries.set(eKey, serialized, ttl);
|
|
848
|
+
this.state.emit({ type: "cache.set", meta: { backend: "memory" } });
|
|
849
|
+
} else {
|
|
850
|
+
this.state.emit({
|
|
851
|
+
type: "cache.set.failed",
|
|
852
|
+
meta: { reason: "no-fallback" }
|
|
853
|
+
});
|
|
854
|
+
}
|
|
666
855
|
} catch (err) {
|
|
667
856
|
this.state.logger.error("set() error", {
|
|
668
857
|
message: err.message
|
|
@@ -716,6 +905,7 @@ var IncrementalRedisCacheHandler = class {
|
|
|
716
905
|
}
|
|
717
906
|
}
|
|
718
907
|
resetRequestCache() {
|
|
908
|
+
this.requestMemo.clear();
|
|
719
909
|
}
|
|
720
910
|
};
|
|
721
911
|
function createIncrementalCacheHandler(opts) {
|