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