@trieb.work/nextjs-turbo-redis-cache 1.7.0 → 1.8.0-beta.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/.github/workflows/ci.yml +2 -0
- package/CHANGELOG.md +94 -97
- package/README.md +24 -19
- package/dist/index.d.mts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +399 -264
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +399 -264
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/RedisStringsHandler.ts +524 -354
- package/src/SyncedMap.ts +11 -6
package/dist/index.mjs
CHANGED
|
@@ -167,18 +167,22 @@ var SyncedMap = class {
|
|
|
167
167
|
};
|
|
168
168
|
try {
|
|
169
169
|
await this.subscriberClient.connect().catch(async () => {
|
|
170
|
-
|
|
170
|
+
console.error("Failed to connect subscriber client. Retrying...");
|
|
171
|
+
await this.subscriberClient.connect().catch((error) => {
|
|
172
|
+
console.error("Failed to connect subscriber client.", error);
|
|
173
|
+
throw error;
|
|
174
|
+
});
|
|
171
175
|
});
|
|
172
176
|
if ((process.env.SKIP_KEYSPACE_CONFIG_CHECK || "").toUpperCase() !== "TRUE") {
|
|
173
177
|
const keyspaceEventConfig = (await this.subscriberClient.configGet("notify-keyspace-events"))?.["notify-keyspace-events"];
|
|
174
178
|
if (!keyspaceEventConfig.includes("E")) {
|
|
175
179
|
throw new Error(
|
|
176
|
-
|
|
180
|
+
'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'E' for Keyevent events, published with __keyevent@<db>__ prefix. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
|
|
177
181
|
);
|
|
178
182
|
}
|
|
179
183
|
if (!keyspaceEventConfig.includes("A") && !(keyspaceEventConfig.includes("x") && keyspaceEventConfig.includes("e"))) {
|
|
180
184
|
throw new Error(
|
|
181
|
-
|
|
185
|
+
'Keyspace event configuration is set to "' + keyspaceEventConfig + "\" but has to include 'A' or 'x' and 'e' for expired and evicted events. We recommend to set it to 'Exe' like so `redis-cli -h localhost config set notify-keyspace-events Exe`"
|
|
182
186
|
);
|
|
183
187
|
}
|
|
184
188
|
}
|
|
@@ -259,8 +263,6 @@ var SyncedMap = class {
|
|
|
259
263
|
);
|
|
260
264
|
await Promise.all(operations);
|
|
261
265
|
}
|
|
262
|
-
// /api/revalidated-fetch
|
|
263
|
-
// true
|
|
264
266
|
async delete(keys, withoutSyncMessage = false) {
|
|
265
267
|
debugVerbose(
|
|
266
268
|
"SyncedMap.delete() called with keys",
|
|
@@ -414,322 +416,455 @@ var REVALIDATED_TAGS_KEY = "__revalidated_tags__";
|
|
|
414
416
|
function getTimeoutRedisCommandOptions(timeoutMs) {
|
|
415
417
|
return commandOptions({ signal: AbortSignal.timeout(timeoutMs) });
|
|
416
418
|
}
|
|
419
|
+
var killContainerOnErrorCount = 0;
|
|
417
420
|
var RedisStringsHandler = class {
|
|
418
421
|
constructor({
|
|
419
422
|
redisUrl = process.env.REDIS_URL ? process.env.REDIS_URL : process.env.REDISHOST ? `redis://${process.env.REDISHOST}:${process.env.REDISPORT}` : "redis://localhost:6379",
|
|
420
423
|
database = process.env.VERCEL_ENV === "production" ? 0 : 1,
|
|
421
424
|
keyPrefix = process.env.VERCEL_URL || "UNDEFINED_URL_",
|
|
422
425
|
sharedTagsKey = "__sharedTags__",
|
|
423
|
-
timeoutMs = 5e3,
|
|
426
|
+
timeoutMs = process.env.REDIS_COMMAND_TIMEOUT_MS ? Number.parseInt(process.env.REDIS_COMMAND_TIMEOUT_MS) ?? 5e3 : 5e3,
|
|
424
427
|
revalidateTagQuerySize = 250,
|
|
425
428
|
avgResyncIntervalMs = 60 * 60 * 1e3,
|
|
426
429
|
redisGetDeduplication = true,
|
|
427
430
|
inMemoryCachingTime = 1e4,
|
|
428
431
|
defaultStaleAge = 60 * 60 * 24 * 14,
|
|
429
432
|
estimateExpireAge = (staleAge) => process.env.VERCEL_ENV === "production" ? staleAge * 2 : staleAge * 1.2,
|
|
433
|
+
killContainerOnErrorThreshold = process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD ? Number.parseInt(process.env.KILL_CONTAINER_ON_ERROR_THRESHOLD) ?? 0 : 0,
|
|
430
434
|
socketOptions,
|
|
431
435
|
clientOptions
|
|
432
436
|
}) {
|
|
433
|
-
this.
|
|
434
|
-
this.timeoutMs = timeoutMs;
|
|
435
|
-
this.redisGetDeduplication = redisGetDeduplication;
|
|
436
|
-
this.inMemoryCachingTime = inMemoryCachingTime;
|
|
437
|
-
this.defaultStaleAge = defaultStaleAge;
|
|
438
|
-
this.estimateExpireAge = estimateExpireAge;
|
|
437
|
+
this.clientReadyCalls = 0;
|
|
439
438
|
try {
|
|
440
|
-
this.
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
439
|
+
this.keyPrefix = keyPrefix;
|
|
440
|
+
this.timeoutMs = timeoutMs;
|
|
441
|
+
this.redisGetDeduplication = redisGetDeduplication;
|
|
442
|
+
this.inMemoryCachingTime = inMemoryCachingTime;
|
|
443
|
+
this.defaultStaleAge = defaultStaleAge;
|
|
444
|
+
this.estimateExpireAge = estimateExpireAge;
|
|
445
|
+
this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
|
|
446
|
+
try {
|
|
447
|
+
this.client = createClient({
|
|
448
|
+
url: redisUrl,
|
|
449
|
+
pingInterval: 5e3,
|
|
450
|
+
// Useful with Redis deployments that do not use TCP Keep-Alive. Restarts the connection if it is idle for too long.
|
|
451
|
+
...database !== 0 ? { database } : {},
|
|
452
|
+
...socketOptions ? { socket: { connectTimeout: timeoutMs, ...socketOptions } } : { connectTimeout: timeoutMs },
|
|
453
|
+
...clientOptions || {}
|
|
454
|
+
});
|
|
455
|
+
this.client.on("error", (error) => {
|
|
456
|
+
console.error(
|
|
457
|
+
"Redis client error",
|
|
458
|
+
error,
|
|
459
|
+
killContainerOnErrorCount++
|
|
460
|
+
);
|
|
461
|
+
setTimeout(
|
|
462
|
+
() => this.client.connect().catch((error2) => {
|
|
463
|
+
console.error(
|
|
464
|
+
"Failed to reconnect Redis client after connection loss:",
|
|
465
|
+
error2
|
|
466
|
+
);
|
|
467
|
+
}),
|
|
468
|
+
1e3
|
|
469
|
+
);
|
|
470
|
+
if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
|
|
471
|
+
console.error(
|
|
472
|
+
"Redis client error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
473
|
+
error,
|
|
474
|
+
killContainerOnErrorCount++
|
|
475
|
+
);
|
|
476
|
+
this.client.disconnect();
|
|
477
|
+
this.client.quit();
|
|
478
|
+
setTimeout(() => {
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}, 500);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
this.client.connect().then(() => {
|
|
484
|
+
console.info("Redis client connected.");
|
|
485
|
+
}).catch(() => {
|
|
486
|
+
this.client.connect().catch((error) => {
|
|
487
|
+
console.error("Failed to connect Redis client:", error);
|
|
488
|
+
this.client.disconnect();
|
|
489
|
+
throw error;
|
|
490
|
+
});
|
|
491
|
+
});
|
|
492
|
+
} catch (error) {
|
|
493
|
+
console.error("Failed to initialize Redis client");
|
|
494
|
+
throw error;
|
|
495
|
+
}
|
|
496
|
+
const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey;
|
|
497
|
+
this.sharedTagsMap = new SyncedMap({
|
|
498
|
+
client: this.client,
|
|
499
|
+
keyPrefix,
|
|
500
|
+
redisKey: sharedTagsKey,
|
|
501
|
+
database,
|
|
502
|
+
timeoutMs,
|
|
503
|
+
querySize: revalidateTagQuerySize,
|
|
504
|
+
filterKeys,
|
|
505
|
+
resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
445
506
|
});
|
|
446
|
-
this.
|
|
447
|
-
|
|
507
|
+
this.revalidatedTagsMap = new SyncedMap({
|
|
508
|
+
client: this.client,
|
|
509
|
+
keyPrefix,
|
|
510
|
+
redisKey: REVALIDATED_TAGS_KEY,
|
|
511
|
+
database,
|
|
512
|
+
timeoutMs,
|
|
513
|
+
querySize: revalidateTagQuerySize,
|
|
514
|
+
filterKeys,
|
|
515
|
+
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
448
516
|
});
|
|
449
|
-
this.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
517
|
+
this.inMemoryDeduplicationCache = new SyncedMap({
|
|
518
|
+
client: this.client,
|
|
519
|
+
keyPrefix,
|
|
520
|
+
redisKey: "inMemoryDeduplicationCache",
|
|
521
|
+
database,
|
|
522
|
+
timeoutMs,
|
|
523
|
+
querySize: revalidateTagQuerySize,
|
|
524
|
+
filterKeys,
|
|
525
|
+
customizedSync: {
|
|
526
|
+
withoutRedisHashmap: true,
|
|
527
|
+
withoutSetSync: true
|
|
528
|
+
}
|
|
457
529
|
});
|
|
530
|
+
const redisGet = this.client.get.bind(this.client);
|
|
531
|
+
this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
|
|
532
|
+
redisGet,
|
|
533
|
+
inMemoryCachingTime,
|
|
534
|
+
this.inMemoryDeduplicationCache
|
|
535
|
+
);
|
|
536
|
+
this.redisGet = redisGet;
|
|
537
|
+
this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
|
|
458
538
|
} catch (error) {
|
|
459
|
-
console.error(
|
|
539
|
+
console.error(
|
|
540
|
+
"RedisStringsHandler constructor error",
|
|
541
|
+
error,
|
|
542
|
+
killContainerOnErrorCount++
|
|
543
|
+
);
|
|
544
|
+
if (killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= killContainerOnErrorThreshold) {
|
|
545
|
+
console.error(
|
|
546
|
+
"RedisStringsHandler constructor error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
547
|
+
error,
|
|
548
|
+
killContainerOnErrorCount++
|
|
549
|
+
);
|
|
550
|
+
process.exit(1);
|
|
551
|
+
}
|
|
460
552
|
throw error;
|
|
461
553
|
}
|
|
462
|
-
const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY && key !== sharedTagsKey;
|
|
463
|
-
this.sharedTagsMap = new SyncedMap({
|
|
464
|
-
client: this.client,
|
|
465
|
-
keyPrefix,
|
|
466
|
-
redisKey: sharedTagsKey,
|
|
467
|
-
database,
|
|
468
|
-
timeoutMs,
|
|
469
|
-
querySize: revalidateTagQuerySize,
|
|
470
|
-
filterKeys,
|
|
471
|
-
resyncIntervalMs: avgResyncIntervalMs - avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
472
|
-
});
|
|
473
|
-
this.revalidatedTagsMap = new SyncedMap({
|
|
474
|
-
client: this.client,
|
|
475
|
-
keyPrefix,
|
|
476
|
-
redisKey: REVALIDATED_TAGS_KEY,
|
|
477
|
-
database,
|
|
478
|
-
timeoutMs,
|
|
479
|
-
querySize: revalidateTagQuerySize,
|
|
480
|
-
filterKeys,
|
|
481
|
-
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
482
|
-
});
|
|
483
|
-
this.inMemoryDeduplicationCache = new SyncedMap({
|
|
484
|
-
client: this.client,
|
|
485
|
-
keyPrefix,
|
|
486
|
-
redisKey: "inMemoryDeduplicationCache",
|
|
487
|
-
database,
|
|
488
|
-
timeoutMs,
|
|
489
|
-
querySize: revalidateTagQuerySize,
|
|
490
|
-
filterKeys,
|
|
491
|
-
customizedSync: {
|
|
492
|
-
withoutRedisHashmap: true,
|
|
493
|
-
withoutSetSync: true
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
const redisGet = this.client.get.bind(this.client);
|
|
497
|
-
this.redisDeduplicationHandler = new DeduplicatedRequestHandler(
|
|
498
|
-
redisGet,
|
|
499
|
-
inMemoryCachingTime,
|
|
500
|
-
this.inMemoryDeduplicationCache
|
|
501
|
-
);
|
|
502
|
-
this.redisGet = redisGet;
|
|
503
|
-
this.deduplicatedRedisGet = this.redisDeduplicationHandler.deduplicatedFunction;
|
|
504
554
|
}
|
|
505
555
|
resetRequestCache() {
|
|
506
556
|
}
|
|
507
557
|
async assertClientIsReady() {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
558
|
+
if (this.clientReadyCalls > 10) {
|
|
559
|
+
throw new Error(
|
|
560
|
+
"assertClientIsReady called more than 10 times without being ready."
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
await Promise.race([
|
|
564
|
+
Promise.all([
|
|
565
|
+
this.sharedTagsMap.waitUntilReady(),
|
|
566
|
+
this.revalidatedTagsMap.waitUntilReady()
|
|
567
|
+
]),
|
|
568
|
+
new Promise(
|
|
569
|
+
(_, reject) => setTimeout(() => {
|
|
570
|
+
reject(
|
|
571
|
+
new Error(
|
|
572
|
+
"assertClientIsReady: Timeout waiting for Redis maps to be ready"
|
|
573
|
+
)
|
|
574
|
+
);
|
|
575
|
+
}, this.timeoutMs * 5)
|
|
576
|
+
)
|
|
511
577
|
]);
|
|
578
|
+
this.clientReadyCalls = 0;
|
|
512
579
|
if (!this.client.isReady) {
|
|
513
|
-
throw new Error(
|
|
580
|
+
throw new Error(
|
|
581
|
+
"assertClientIsReady: Redis client is not ready yet or connection is lost."
|
|
582
|
+
);
|
|
514
583
|
}
|
|
515
584
|
}
|
|
516
585
|
async get(key, ctx) {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
586
|
+
try {
|
|
587
|
+
if (ctx.kind !== "APP_ROUTE" && ctx.kind !== "APP_PAGE" && ctx.kind !== "FETCH") {
|
|
588
|
+
console.warn(
|
|
589
|
+
"RedisStringsHandler.get() called with",
|
|
590
|
+
key,
|
|
591
|
+
ctx,
|
|
592
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
593
|
+
ctx?.kind
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
debug("green", "RedisStringsHandler.get() called with", key, ctx);
|
|
597
|
+
await this.assertClientIsReady();
|
|
598
|
+
const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
|
|
599
|
+
const serializedCacheEntry = await clientGet(
|
|
600
|
+
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
601
|
+
this.keyPrefix + key
|
|
524
602
|
);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const serializedCacheEntry = await clientGet(
|
|
530
|
-
getTimeoutRedisCommandOptions(this.timeoutMs),
|
|
531
|
-
this.keyPrefix + key
|
|
532
|
-
);
|
|
533
|
-
debug(
|
|
534
|
-
"green",
|
|
535
|
-
"RedisStringsHandler.get() finished with result (serializedCacheEntry)",
|
|
536
|
-
serializedCacheEntry?.substring(0, 200)
|
|
537
|
-
);
|
|
538
|
-
if (!serializedCacheEntry) {
|
|
539
|
-
return null;
|
|
540
|
-
}
|
|
541
|
-
const cacheEntry = JSON.parse(
|
|
542
|
-
serializedCacheEntry,
|
|
543
|
-
bufferReviver
|
|
544
|
-
);
|
|
545
|
-
debug(
|
|
546
|
-
"green",
|
|
547
|
-
"RedisStringsHandler.get() finished with result (cacheEntry)",
|
|
548
|
-
JSON.stringify(cacheEntry).substring(0, 200)
|
|
549
|
-
);
|
|
550
|
-
if (!cacheEntry) {
|
|
551
|
-
return null;
|
|
552
|
-
}
|
|
553
|
-
if (!cacheEntry?.tags) {
|
|
554
|
-
console.warn(
|
|
555
|
-
"RedisStringsHandler.get() called with",
|
|
556
|
-
key,
|
|
557
|
-
ctx,
|
|
558
|
-
"cacheEntry is mall formed (missing tags)"
|
|
603
|
+
debug(
|
|
604
|
+
"green",
|
|
605
|
+
"RedisStringsHandler.get() finished with result (serializedCacheEntry)",
|
|
606
|
+
serializedCacheEntry?.substring(0, 200)
|
|
559
607
|
);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
"cacheEntry is mall formed (missing value)"
|
|
608
|
+
if (!serializedCacheEntry) {
|
|
609
|
+
return null;
|
|
610
|
+
}
|
|
611
|
+
const cacheEntry = JSON.parse(
|
|
612
|
+
serializedCacheEntry,
|
|
613
|
+
bufferReviver
|
|
567
614
|
);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
key,
|
|
573
|
-
ctx,
|
|
574
|
-
"cacheEntry is mall formed (missing lastModified)"
|
|
615
|
+
debug(
|
|
616
|
+
"green",
|
|
617
|
+
"RedisStringsHandler.get() finished with result (cacheEntry)",
|
|
618
|
+
JSON.stringify(cacheEntry).substring(0, 200)
|
|
575
619
|
);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
const combinedTags = /* @__PURE__ */ new Set([
|
|
579
|
-
...ctx?.softTags || [],
|
|
580
|
-
...ctx?.tags || []
|
|
581
|
-
]);
|
|
582
|
-
if (combinedTags.size === 0) {
|
|
583
|
-
return cacheEntry;
|
|
620
|
+
if (!cacheEntry) {
|
|
621
|
+
return null;
|
|
584
622
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
623
|
+
if (!cacheEntry?.tags) {
|
|
624
|
+
console.warn(
|
|
625
|
+
"RedisStringsHandler.get() called with",
|
|
626
|
+
key,
|
|
627
|
+
ctx,
|
|
628
|
+
"cacheEntry is mall formed (missing tags)"
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
if (!cacheEntry?.value) {
|
|
632
|
+
console.warn(
|
|
633
|
+
"RedisStringsHandler.get() called with",
|
|
634
|
+
key,
|
|
635
|
+
ctx,
|
|
636
|
+
"cacheEntry is mall formed (missing value)"
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
if (!cacheEntry?.lastModified) {
|
|
640
|
+
console.warn(
|
|
641
|
+
"RedisStringsHandler.get() called with",
|
|
642
|
+
key,
|
|
643
|
+
ctx,
|
|
644
|
+
"cacheEntry is mall formed (missing lastModified)"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (ctx.kind === "FETCH") {
|
|
648
|
+
const combinedTags = /* @__PURE__ */ new Set([
|
|
649
|
+
...ctx?.softTags || [],
|
|
650
|
+
...ctx?.tags || []
|
|
651
|
+
]);
|
|
652
|
+
if (combinedTags.size === 0) {
|
|
653
|
+
return cacheEntry;
|
|
654
|
+
}
|
|
655
|
+
for (const tag of combinedTags) {
|
|
656
|
+
const revalidationTime = this.revalidatedTagsMap.get(tag);
|
|
657
|
+
if (revalidationTime && revalidationTime > cacheEntry.lastModified) {
|
|
658
|
+
const redisKey = this.keyPrefix + key;
|
|
659
|
+
this.client.unlink(getTimeoutRedisCommandOptions(this.timeoutMs), redisKey).catch((err) => {
|
|
660
|
+
console.error(
|
|
661
|
+
"Error occurred while unlinking stale data. Error was:",
|
|
662
|
+
err
|
|
663
|
+
);
|
|
664
|
+
}).finally(async () => {
|
|
665
|
+
await this.sharedTagsMap.delete(key);
|
|
666
|
+
await this.revalidatedTagsMap.delete(tag);
|
|
667
|
+
});
|
|
668
|
+
debug(
|
|
669
|
+
"green",
|
|
670
|
+
'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
|
|
671
|
+
tag,
|
|
672
|
+
redisKey,
|
|
673
|
+
revalidationTime,
|
|
674
|
+
cacheEntry
|
|
593
675
|
);
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
await this.revalidatedTagsMap.delete(tag);
|
|
597
|
-
});
|
|
598
|
-
debug(
|
|
599
|
-
"green",
|
|
600
|
-
'RedisStringsHandler.get() found revalidation time for tag. Cache entry is stale and will be deleted and "null" will be returned.',
|
|
601
|
-
tag,
|
|
602
|
-
redisKey,
|
|
603
|
-
revalidationTime,
|
|
604
|
-
cacheEntry
|
|
605
|
-
);
|
|
606
|
-
return null;
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
607
678
|
}
|
|
608
679
|
}
|
|
680
|
+
return cacheEntry;
|
|
681
|
+
} catch (error) {
|
|
682
|
+
console.error(
|
|
683
|
+
"RedisStringsHandler.get() Error occurred while getting cache entry. Returning null so site can continue to serve content while cache is disabled. The original error was:",
|
|
684
|
+
error,
|
|
685
|
+
killContainerOnErrorCount++
|
|
686
|
+
);
|
|
687
|
+
if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
|
|
688
|
+
console.error(
|
|
689
|
+
"RedisStringsHandler get() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
690
|
+
error,
|
|
691
|
+
killContainerOnErrorCount
|
|
692
|
+
);
|
|
693
|
+
this.client.disconnect();
|
|
694
|
+
this.client.quit();
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
process.exit(1);
|
|
697
|
+
}, 500);
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
609
700
|
}
|
|
610
|
-
return cacheEntry;
|
|
611
701
|
}
|
|
612
702
|
async set(key, data, ctx) {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
703
|
+
try {
|
|
704
|
+
if (data.kind !== "APP_ROUTE" && data.kind !== "APP_PAGE" && data.kind !== "FETCH") {
|
|
705
|
+
console.warn(
|
|
706
|
+
"RedisStringsHandler.set() called with",
|
|
707
|
+
key,
|
|
708
|
+
ctx,
|
|
709
|
+
data,
|
|
710
|
+
" this cache handler is only designed and tested for kind APP_ROUTE and APP_PAGE and not for kind ",
|
|
711
|
+
data?.kind
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
await this.assertClientIsReady();
|
|
715
|
+
if (data.kind === "APP_PAGE" || data.kind === "APP_ROUTE") {
|
|
716
|
+
const tags = data.headers["x-next-cache-tags"]?.split(",");
|
|
717
|
+
ctx.tags = [...ctx.tags || [], ...tags || []];
|
|
718
|
+
}
|
|
719
|
+
const cacheEntry = {
|
|
720
|
+
lastModified: Date.now(),
|
|
721
|
+
tags: ctx?.tags || [],
|
|
722
|
+
value: data
|
|
723
|
+
};
|
|
724
|
+
const serializedCacheEntry = JSON.stringify(cacheEntry, bufferReplacer);
|
|
725
|
+
if (this.redisGetDeduplication) {
|
|
726
|
+
this.redisDeduplicationHandler.seedRequestReturn(
|
|
727
|
+
key,
|
|
728
|
+
serializedCacheEntry
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
const revalidate = (
|
|
732
|
+
// For fetch requests in newest versions, the revalidate context property is never used, and instead the revalidate property of the passed-in data is used
|
|
733
|
+
data.kind === "FETCH" && data.revalidate || ctx.revalidate || ctx.cacheControl?.revalidate || data?.revalidate
|
|
734
|
+
);
|
|
735
|
+
const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
|
|
736
|
+
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
737
|
+
const setOperation = this.client.set(
|
|
738
|
+
options,
|
|
739
|
+
this.keyPrefix + key,
|
|
740
|
+
serializedCacheEntry,
|
|
741
|
+
{
|
|
742
|
+
EX: expireAt
|
|
743
|
+
}
|
|
744
|
+
);
|
|
745
|
+
debug(
|
|
746
|
+
"blue",
|
|
747
|
+
"RedisStringsHandler.set() will set the following serializedCacheEntry",
|
|
748
|
+
this.keyPrefix,
|
|
616
749
|
key,
|
|
617
|
-
ctx,
|
|
618
750
|
data,
|
|
619
|
-
|
|
620
|
-
|
|
751
|
+
ctx,
|
|
752
|
+
serializedCacheEntry?.substring(0, 200),
|
|
753
|
+
expireAt
|
|
621
754
|
);
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
755
|
+
let setTagsOperation;
|
|
756
|
+
if (ctx.tags && ctx.tags.length > 0) {
|
|
757
|
+
const currentTags = this.sharedTagsMap.get(key);
|
|
758
|
+
const currentIsSameAsNew = currentTags?.length === ctx.tags.length && currentTags.every((v) => ctx.tags.includes(v)) && ctx.tags.every((v) => currentTags.includes(v));
|
|
759
|
+
if (!currentIsSameAsNew) {
|
|
760
|
+
setTagsOperation = this.sharedTagsMap.set(
|
|
761
|
+
key,
|
|
762
|
+
structuredClone(ctx.tags)
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
debug(
|
|
767
|
+
"blue",
|
|
768
|
+
"RedisStringsHandler.set() will set the following sharedTagsMap",
|
|
636
769
|
key,
|
|
637
|
-
|
|
770
|
+
ctx.tags
|
|
638
771
|
);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
debug(
|
|
652
|
-
"blue",
|
|
653
|
-
"RedisStringsHandler.set() will set the following serializedCacheEntry",
|
|
654
|
-
this.keyPrefix,
|
|
655
|
-
key,
|
|
656
|
-
data,
|
|
657
|
-
ctx,
|
|
658
|
-
serializedCacheEntry?.substring(0, 200),
|
|
659
|
-
expireAt
|
|
660
|
-
);
|
|
661
|
-
let setTagsOperation;
|
|
662
|
-
if (ctx.tags && ctx.tags.length > 0) {
|
|
663
|
-
const currentTags = this.sharedTagsMap.get(key);
|
|
664
|
-
const currentIsSameAsNew = currentTags?.length === ctx.tags.length && currentTags.every((v) => ctx.tags.includes(v)) && ctx.tags.every((v) => currentTags.includes(v));
|
|
665
|
-
if (!currentIsSameAsNew) {
|
|
666
|
-
setTagsOperation = this.sharedTagsMap.set(
|
|
667
|
-
key,
|
|
668
|
-
structuredClone(ctx.tags)
|
|
772
|
+
await Promise.all([setOperation, setTagsOperation]);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
console.error(
|
|
775
|
+
"RedisStringsHandler.set() Error occurred while setting cache entry. The original error was:",
|
|
776
|
+
error,
|
|
777
|
+
killContainerOnErrorCount++
|
|
778
|
+
);
|
|
779
|
+
if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
|
|
780
|
+
console.error(
|
|
781
|
+
"RedisStringsHandler set() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
782
|
+
error,
|
|
783
|
+
killContainerOnErrorCount
|
|
669
784
|
);
|
|
785
|
+
this.client.disconnect();
|
|
786
|
+
this.client.quit();
|
|
787
|
+
setTimeout(() => {
|
|
788
|
+
process.exit(1);
|
|
789
|
+
}, 500);
|
|
670
790
|
}
|
|
791
|
+
throw error;
|
|
671
792
|
}
|
|
672
|
-
debug(
|
|
673
|
-
"blue",
|
|
674
|
-
"RedisStringsHandler.set() will set the following sharedTagsMap",
|
|
675
|
-
key,
|
|
676
|
-
ctx.tags
|
|
677
|
-
);
|
|
678
|
-
await Promise.all([setOperation, setTagsOperation]);
|
|
679
793
|
}
|
|
680
794
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
681
795
|
async revalidateTag(tagOrTags, ...rest) {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
796
|
+
try {
|
|
797
|
+
debug(
|
|
798
|
+
"red",
|
|
799
|
+
"RedisStringsHandler.revalidateTag() called with",
|
|
800
|
+
tagOrTags,
|
|
801
|
+
rest
|
|
802
|
+
);
|
|
803
|
+
const tags = new Set([tagOrTags || []].flat());
|
|
804
|
+
await this.assertClientIsReady();
|
|
805
|
+
const keysToDelete = /* @__PURE__ */ new Set();
|
|
806
|
+
for (const tag of tags) {
|
|
807
|
+
if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
|
|
808
|
+
const now = Date.now();
|
|
809
|
+
debug(
|
|
810
|
+
"red",
|
|
811
|
+
"RedisStringsHandler.revalidateTag() set revalidation time for tag",
|
|
812
|
+
tag,
|
|
813
|
+
"to",
|
|
814
|
+
now
|
|
815
|
+
);
|
|
816
|
+
await this.revalidatedTagsMap.set(tag, now);
|
|
817
|
+
}
|
|
702
818
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
819
|
+
for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
|
|
820
|
+
if (sharedTags.some((tag) => tags.has(tag))) {
|
|
821
|
+
keysToDelete.add(key);
|
|
822
|
+
}
|
|
707
823
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
824
|
+
debug(
|
|
825
|
+
"red",
|
|
826
|
+
"RedisStringsHandler.revalidateTag() found",
|
|
827
|
+
keysToDelete,
|
|
828
|
+
"keys to delete"
|
|
829
|
+
);
|
|
830
|
+
if (keysToDelete.size === 0) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
const redisKeys = Array.from(keysToDelete);
|
|
834
|
+
const fullRedisKeys = redisKeys.map((key) => this.keyPrefix + key);
|
|
835
|
+
const options = getTimeoutRedisCommandOptions(this.timeoutMs);
|
|
836
|
+
const deleteKeysOperation = this.client.unlink(options, fullRedisKeys);
|
|
837
|
+
if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
|
|
838
|
+
for (const key of keysToDelete) {
|
|
839
|
+
this.inMemoryDeduplicationCache.delete(key);
|
|
840
|
+
}
|
|
725
841
|
}
|
|
842
|
+
const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
|
|
843
|
+
await Promise.all([deleteKeysOperation, deleteTagsOperation]);
|
|
844
|
+
debug(
|
|
845
|
+
"red",
|
|
846
|
+
"RedisStringsHandler.revalidateTag() finished delete operations"
|
|
847
|
+
);
|
|
848
|
+
} catch (error) {
|
|
849
|
+
console.error(
|
|
850
|
+
"RedisStringsHandler.revalidateTag() Error occurred while revalidating tags. The original error was:",
|
|
851
|
+
error,
|
|
852
|
+
killContainerOnErrorCount++
|
|
853
|
+
);
|
|
854
|
+
if (this.killContainerOnErrorThreshold > 0 && killContainerOnErrorCount >= this.killContainerOnErrorThreshold) {
|
|
855
|
+
console.error(
|
|
856
|
+
"RedisStringsHandler revalidateTag() error threshold reached, disconnecting and exiting (please implement a restart process/container watchdog to handle this error)",
|
|
857
|
+
error,
|
|
858
|
+
killContainerOnErrorCount
|
|
859
|
+
);
|
|
860
|
+
this.client.disconnect();
|
|
861
|
+
this.client.quit();
|
|
862
|
+
setTimeout(() => {
|
|
863
|
+
process.exit(1);
|
|
864
|
+
}, 500);
|
|
865
|
+
}
|
|
866
|
+
throw error;
|
|
726
867
|
}
|
|
727
|
-
const deleteTagsOperation = this.sharedTagsMap.delete(redisKeys);
|
|
728
|
-
await Promise.all([deleteKeysOperation, deleteTagsOperation]);
|
|
729
|
-
debug(
|
|
730
|
-
"red",
|
|
731
|
-
"RedisStringsHandler.revalidateTag() finished delete operations"
|
|
732
|
-
);
|
|
733
868
|
}
|
|
734
869
|
};
|
|
735
870
|
|