@trieb.work/nextjs-turbo-redis-cache 1.16.2 → 1.18.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/.github/workflows/ci.yml +11 -2
- package/.github/workflows/release.yml +8 -157
- package/ARCHITECTURE.md +86 -47
- package/README.md +91 -25
- package/dist/index.d.mts +40 -1
- package/dist/index.d.ts +40 -1
- package/dist/index.js +264 -96
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +261 -95
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -7
- package/release.config.cjs +6 -25
- package/src/CacheComponentsHandler.ts +141 -111
- package/src/RedisStringsHandler.ts +37 -25
- package/src/SyncedMap.ts +16 -2
- package/src/index.ts +7 -0
- package/src/utils/cacheTtl.ts +71 -0
- package/src/utils/compareAndUnlink.ts +29 -0
- package/src/utils/redisConnection.ts +17 -0
- package/src/utils/tagRevalidation.ts +162 -0
- package/test/README.md +13 -13
- package/test/nextjs-test-projects/next-app-15-4-11/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/next.config.ts +2 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-0-11-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-2-6/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/next.config.ts +2 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/pnpm-lock.yaml +1 -1
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-2-6-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-3-0/README.md +36 -0
- package/test/nextjs-test-projects/next-app-16-3-0/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0/next.config.ts +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0/package.json +28 -0
- package/test/nextjs-test-projects/next-app-16-3-0/pnpm-lock.yaml +4284 -0
- package/test/nextjs-test-projects/next-app-16-3-0/postcss.config.mjs +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidatePath/route.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidateTag/route.ts +20 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/favicon.ico +0 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/globals.css +26 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/layout.tsx +59 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/page.tsx +755 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/revalidation-interface.tsx +267 -0
- package/test/nextjs-test-projects/next-app-16-3-0/src/app/update-tag-test/page.tsx +25 -0
- package/test/nextjs-test-projects/next-app-16-3-0/tsconfig.json +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/README.md +36 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/cache-handler.js +3 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/next.config.ts +15 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/package.json +28 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/pnpm-lock.yaml +4284 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/postcss.config.mjs +7 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/file.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/globe.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/next.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/vercel.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/public/window.svg +1 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-static-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-cachelife/route.ts +24 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/cached-with-tag/route.ts +21 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/revalidate/route.ts +34 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/expire-matrix/route.ts +23 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidate-tag/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/api/revalidated-fetch/route.ts +19 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/cachelife-short/page.tsx +110 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/page.tsx +112 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/revalidate-durations/page.tsx +106 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/runtime-data-suspense/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/stale-while-revalidate/page.tsx +130 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/tag-invalidation/page.tsx +127 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-nondeterministic/page.tsx +110 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/cache-lab/use-cache-remote/page.tsx +85 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/favicon.ico +0 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/globals.css +26 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/layout.tsx +57 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/page.tsx +755 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/revalidation-interface.tsx +267 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/src/app/update-tag-test/page.tsx +22 -0
- package/test/nextjs-test-projects/next-app-16-3-0-cache-components/tsconfig.json +34 -0
- package/test/nextjs-test-projects/next-pages-16-2-6/pnpm-lock.yaml +4 -4
- package/test/nextjs-test-projects/next-pages-16-3-0/README.md +16 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/eslint.config.mjs +18 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/next.config.ts +7 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/package.json +26 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/pnpm-lock.yaml +3939 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/api/revalidate.ts +24 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/index.tsx +11 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/isr/[slug].tsx +49 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/src/pages/static-forever.tsx +20 -0
- package/test/nextjs-test-projects/next-pages-16-3-0/tsconfig.json +34 -0
- package/test/playwright/cache-lab.spec.ts +75 -0
- package/test/vitest/integration/cache-components/cache-components.integration.test.ts +194 -36
- package/test/vitest/integration/nextjs-cache-handler.integration.test.ts +67 -34
- package/test/vitest/integration/pages-router.integration.test.ts +16 -6
- package/test/vitest/unit/SyncedMap-orphan-cleanup.test.ts +111 -0
- package/test/vitest/unit/cache-components-update-tags.test.ts +350 -0
- package/test/vitest/unit/cache-ttl.test.ts +67 -0
- package/test/vitest/unit/compare-and-unlink.test.ts +22 -0
- package/test/vitest/unit/index.test.ts +17 -0
- package/test/vitest/unit/pages-router-kinds.test.ts +11 -0
- package/test/vitest/unit/redis-connection.test.ts +39 -0
- package/test/vitest/unit/tag-revalidation.test.ts +178 -0
- package/CHANGELOG.md +0 -404
package/dist/index.js
CHANGED
|
@@ -36,7 +36,9 @@ __export(index_exports, {
|
|
|
36
36
|
default: () => index_default,
|
|
37
37
|
getRedisCacheComponentsHandler: () => getRedisCacheComponentsHandler,
|
|
38
38
|
jsonCacheValueSerializer: () => jsonCacheValueSerializer,
|
|
39
|
-
redisCacheHandler: () => redisCacheHandler
|
|
39
|
+
redisCacheHandler: () => redisCacheHandler,
|
|
40
|
+
resolveCacheEntryTtlSeconds: () => resolveCacheEntryTtlSeconds,
|
|
41
|
+
shouldDeferRedisConnection: () => shouldDeferRedisConnection
|
|
40
42
|
});
|
|
41
43
|
module.exports = __toCommonJS(index_exports);
|
|
42
44
|
|
|
@@ -64,6 +66,12 @@ function debugVerbose(color, ...args) {
|
|
|
64
66
|
}
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
// src/utils/redisConnection.ts
|
|
70
|
+
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
71
|
+
function shouldDeferRedisConnection() {
|
|
72
|
+
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
73
|
+
}
|
|
74
|
+
|
|
67
75
|
// src/SyncedMap.ts
|
|
68
76
|
var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
|
|
69
77
|
var SyncedMap = class {
|
|
@@ -89,6 +97,10 @@ var SyncedMap = class {
|
|
|
89
97
|
});
|
|
90
98
|
}
|
|
91
99
|
async setup() {
|
|
100
|
+
if (shouldDeferRedisConnection()) {
|
|
101
|
+
this.setupLockResolve();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
92
104
|
let setupPromises = [];
|
|
93
105
|
if (!this.customizedSync?.withoutRedisHashmap) {
|
|
94
106
|
setupPromises.push(this.initialSync());
|
|
@@ -119,7 +131,9 @@ var SyncedMap = class {
|
|
|
119
131
|
}
|
|
120
132
|
cursor = remoteItems.cursor;
|
|
121
133
|
} while (cursor !== 0);
|
|
122
|
-
|
|
134
|
+
if (!this.customizedSync?.withoutOrphanCleanup) {
|
|
135
|
+
await this.cleanupKeysNotInRedis();
|
|
136
|
+
}
|
|
123
137
|
} catch (error) {
|
|
124
138
|
console.error("Error during initial sync:", error);
|
|
125
139
|
throw error;
|
|
@@ -531,6 +545,37 @@ var jsonCacheValueSerializer = {
|
|
|
531
545
|
}
|
|
532
546
|
};
|
|
533
547
|
|
|
548
|
+
// src/utils/cacheTtl.ts
|
|
549
|
+
function resolveCacheEntryTtlSeconds(ctx, data, options) {
|
|
550
|
+
const expire = ctx.cacheControl?.expire;
|
|
551
|
+
if (Number.isFinite(expire) && expire > 0) {
|
|
552
|
+
return Math.max(1, Math.ceil(expire));
|
|
553
|
+
}
|
|
554
|
+
const revalidate = resolveRevalidateValue(ctx, data);
|
|
555
|
+
if (revalidate === false) {
|
|
556
|
+
return options.estimateExpireAge(options.defaultStaleAge);
|
|
557
|
+
}
|
|
558
|
+
if (revalidate && Number.isSafeInteger(revalidate) && revalidate > 0) {
|
|
559
|
+
return options.estimateExpireAge(revalidate);
|
|
560
|
+
}
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
function resolveRevalidateValue(ctx, data) {
|
|
564
|
+
if (data?.kind === "FETCH" && data.revalidate !== void 0) {
|
|
565
|
+
return data.revalidate;
|
|
566
|
+
}
|
|
567
|
+
if (ctx.revalidate !== void 0) {
|
|
568
|
+
return ctx.revalidate;
|
|
569
|
+
}
|
|
570
|
+
if (ctx.cacheControl?.revalidate !== void 0) {
|
|
571
|
+
return ctx.cacheControl.revalidate;
|
|
572
|
+
}
|
|
573
|
+
if (data?.revalidate !== void 0) {
|
|
574
|
+
return data.revalidate;
|
|
575
|
+
}
|
|
576
|
+
return void 0;
|
|
577
|
+
}
|
|
578
|
+
|
|
534
579
|
// src/RedisStringsHandler.ts
|
|
535
580
|
function redisErrorHandler(debugInfo, redisCommandResult) {
|
|
536
581
|
const beforeTimestamp = performance.now();
|
|
@@ -608,6 +653,7 @@ var RedisStringsHandler = class {
|
|
|
608
653
|
this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
|
|
609
654
|
this.getTimeoutMs = getTimeoutMs;
|
|
610
655
|
this.valueSerializer = valueSerializer;
|
|
656
|
+
this.redisConnectionDeferred = shouldDeferRedisConnection();
|
|
611
657
|
try {
|
|
612
658
|
this.client = (0, import_redis.createClient)({
|
|
613
659
|
url: redisUrl,
|
|
@@ -646,12 +692,14 @@ var RedisStringsHandler = class {
|
|
|
646
692
|
}, 500);
|
|
647
693
|
}
|
|
648
694
|
});
|
|
649
|
-
this.
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
695
|
+
if (!this.redisConnectionDeferred) {
|
|
696
|
+
this.client.connect().then(() => {
|
|
697
|
+
debug("green", "Redis client connected.");
|
|
698
|
+
}).catch((error) => {
|
|
699
|
+
console.error("Failed to connect Redis client:", error);
|
|
700
|
+
throw error;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
655
703
|
} catch (error) {
|
|
656
704
|
console.error("Failed to initialize Redis client");
|
|
657
705
|
throw error;
|
|
@@ -673,6 +721,7 @@ var RedisStringsHandler = class {
|
|
|
673
721
|
database,
|
|
674
722
|
querySize: revalidateTagQuerySize,
|
|
675
723
|
filterKeys,
|
|
724
|
+
customizedSync: { withoutOrphanCleanup: true },
|
|
676
725
|
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
677
726
|
});
|
|
678
727
|
this.inMemoryDeduplicationCache = new SyncedMap({
|
|
@@ -742,11 +791,17 @@ var RedisStringsHandler = class {
|
|
|
742
791
|
}
|
|
743
792
|
this.clientReadyCalls = 0;
|
|
744
793
|
if (!this.client.isReady) {
|
|
794
|
+
if (this.redisConnectionDeferred) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
745
797
|
throw new Error(
|
|
746
798
|
"assertClientIsReady: Redis client is not ready yet or connection is lost."
|
|
747
799
|
);
|
|
748
800
|
}
|
|
749
801
|
}
|
|
802
|
+
isClientUnavailable() {
|
|
803
|
+
return this.redisConnectionDeferred || !this.client.isReady;
|
|
804
|
+
}
|
|
750
805
|
async get(key, ctx) {
|
|
751
806
|
try {
|
|
752
807
|
if (!SUPPORTED_GET_KINDS.includes(ctx.kind)) {
|
|
@@ -760,6 +815,9 @@ var RedisStringsHandler = class {
|
|
|
760
815
|
}
|
|
761
816
|
debug("green", "RedisStringsHandler.get() called with", key, ctx);
|
|
762
817
|
await this.assertClientIsReady();
|
|
818
|
+
if (this.isClientUnavailable()) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
763
821
|
const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
|
|
764
822
|
const redisGetOperation = clientGet(
|
|
765
823
|
(0, import_redis.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
|
|
@@ -893,6 +951,9 @@ var RedisStringsHandler = class {
|
|
|
893
951
|
);
|
|
894
952
|
}
|
|
895
953
|
await this.assertClientIsReady();
|
|
954
|
+
if (this.isClientUnavailable()) {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
896
957
|
if (data?.kind === "APP_PAGE" || data?.kind === "APP_ROUTE") {
|
|
897
958
|
const tags = data.headers["x-next-cache-tags"]?.split(",");
|
|
898
959
|
ctx.tags = [...ctx.tags || [], ...tags || []];
|
|
@@ -915,18 +976,14 @@ var RedisStringsHandler = class {
|
|
|
915
976
|
serializedCacheEntry
|
|
916
977
|
);
|
|
917
978
|
}
|
|
918
|
-
const
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
// matters for those older, non-discriminated shapes.
|
|
923
|
-
data?.revalidate
|
|
924
|
-
);
|
|
925
|
-
const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
|
|
979
|
+
const expireAt = resolveCacheEntryTtlSeconds(ctx, data, {
|
|
980
|
+
estimateExpireAge: this.estimateExpireAge,
|
|
981
|
+
defaultStaleAge: this.defaultStaleAge
|
|
982
|
+
});
|
|
926
983
|
const setOperation = redisErrorHandler(
|
|
927
984
|
"RedisStringsHandler.set(), operation: set " + this.keyPrefix + " " + key,
|
|
928
985
|
this.client.set(this.keyPrefix + key, serializedCacheEntry, {
|
|
929
|
-
EX: expireAt
|
|
986
|
+
...expireAt ? { EX: expireAt } : {}
|
|
930
987
|
})
|
|
931
988
|
);
|
|
932
989
|
debug(
|
|
@@ -989,6 +1046,9 @@ var RedisStringsHandler = class {
|
|
|
989
1046
|
);
|
|
990
1047
|
const tags = new Set([tagOrTags || []].flat());
|
|
991
1048
|
await this.assertClientIsReady();
|
|
1049
|
+
if (this.isClientUnavailable()) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
992
1052
|
const keysToDelete = /* @__PURE__ */ new Set();
|
|
993
1053
|
for (const tag of tags) {
|
|
994
1054
|
if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
|
|
@@ -1127,6 +1187,88 @@ var CachedHandler = class {
|
|
|
1127
1187
|
|
|
1128
1188
|
// src/CacheComponentsHandler.ts
|
|
1129
1189
|
var import_redis2 = require("redis");
|
|
1190
|
+
|
|
1191
|
+
// src/utils/compareAndUnlink.ts
|
|
1192
|
+
var COMPARE_AND_UNLINK = `
|
|
1193
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
1194
|
+
return redis.call('UNLINK', KEYS[1])
|
|
1195
|
+
end
|
|
1196
|
+
return 0
|
|
1197
|
+
`;
|
|
1198
|
+
async function compareAndUnlink(client, key, expected) {
|
|
1199
|
+
const deleted = await client.eval(COMPARE_AND_UNLINK, {
|
|
1200
|
+
keys: [key],
|
|
1201
|
+
arguments: [expected]
|
|
1202
|
+
});
|
|
1203
|
+
return deleted === 1 || deleted === 1n;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
// src/utils/tagRevalidation.ts
|
|
1207
|
+
function normalizeTagManifest(stored, now = Date.now()) {
|
|
1208
|
+
if (typeof stored === "number" && Number.isFinite(stored)) {
|
|
1209
|
+
return { expired: Math.min(stored, now) };
|
|
1210
|
+
}
|
|
1211
|
+
if (stored && typeof stored === "object") {
|
|
1212
|
+
const rec = stored;
|
|
1213
|
+
const stale = typeof rec.stale === "number" && Number.isFinite(rec.stale) ? rec.stale : typeof rec.last === "number" && Number.isFinite(rec.last) ? rec.last : void 0;
|
|
1214
|
+
const expired = typeof rec.expired === "number" && Number.isFinite(rec.expired) ? rec.expired : typeof rec.pending === "number" && Number.isFinite(rec.pending) ? rec.pending : void 0;
|
|
1215
|
+
return {
|
|
1216
|
+
...stale !== void 0 ? { stale } : {},
|
|
1217
|
+
...expired !== void 0 ? { expired } : {}
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
return {};
|
|
1221
|
+
}
|
|
1222
|
+
function applyTagUpdate(existing, durations, now) {
|
|
1223
|
+
if (durations) {
|
|
1224
|
+
const updates = { ...existing };
|
|
1225
|
+
updates.stale = now;
|
|
1226
|
+
if (durations.expire !== void 0) {
|
|
1227
|
+
updates.expired = now + durations.expire * 1e3;
|
|
1228
|
+
}
|
|
1229
|
+
return updates;
|
|
1230
|
+
}
|
|
1231
|
+
return {
|
|
1232
|
+
...existing,
|
|
1233
|
+
expired: now
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
function persistableTagManifest(entry, now) {
|
|
1237
|
+
if (typeof entry.expired === "number" && entry.expired <= now && (entry.stale === void 0 || entry.stale <= entry.expired)) {
|
|
1238
|
+
return entry.expired;
|
|
1239
|
+
}
|
|
1240
|
+
return entry;
|
|
1241
|
+
}
|
|
1242
|
+
function areTagsExpired(tags, entryTimestamp, now, getEntry) {
|
|
1243
|
+
for (const tag of tags) {
|
|
1244
|
+
const expiredAt = getEntry(tag)?.expired;
|
|
1245
|
+
if (typeof expiredAt === "number" && expiredAt <= now && expiredAt > entryTimestamp) {
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return false;
|
|
1250
|
+
}
|
|
1251
|
+
function areTagsStale(tags, entryTimestamp, getEntry) {
|
|
1252
|
+
for (const tag of tags) {
|
|
1253
|
+
const staleAt = getEntry(tag)?.stale ?? 0;
|
|
1254
|
+
if (typeof staleAt === "number" && staleAt > entryTimestamp) {
|
|
1255
|
+
return true;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
return false;
|
|
1259
|
+
}
|
|
1260
|
+
function maxExpiredTimestamp(tags, getEntry) {
|
|
1261
|
+
let max = 0;
|
|
1262
|
+
for (const tag of tags) {
|
|
1263
|
+
const expired = getEntry(tag)?.expired || 0;
|
|
1264
|
+
if (expired > max) {
|
|
1265
|
+
max = expired;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return max;
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/CacheComponentsHandler.ts
|
|
1130
1272
|
var REVALIDATED_TAGS_KEY2 = "__cacheComponents_revalidated_tags__";
|
|
1131
1273
|
var SHARED_TAGS_KEY = "__cacheComponents_sharedTags__";
|
|
1132
1274
|
var killContainerOnErrorCount2 = 0;
|
|
@@ -1184,6 +1326,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1184
1326
|
this.getTimeoutMs = getTimeoutMs;
|
|
1185
1327
|
this.redisGetDeduplication = redisGetDeduplication;
|
|
1186
1328
|
this.inMemoryCachingTime = inMemoryCachingTime;
|
|
1329
|
+
this.redisConnectionDeferred = shouldDeferRedisConnection();
|
|
1187
1330
|
this.client = (0, import_redis2.createClient)({
|
|
1188
1331
|
url: redisUrl,
|
|
1189
1332
|
pingInterval: 1e4,
|
|
@@ -1219,18 +1362,20 @@ var RedisCacheComponentsHandler = class {
|
|
|
1219
1362
|
}, 500);
|
|
1220
1363
|
}
|
|
1221
1364
|
});
|
|
1222
|
-
this.
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1365
|
+
if (!this.redisConnectionDeferred) {
|
|
1366
|
+
this.client.connect().then(() => {
|
|
1367
|
+
debug("green", "RedisCacheComponentsHandler client connected.");
|
|
1368
|
+
}).catch(() => {
|
|
1369
|
+
this.client.connect().catch((error) => {
|
|
1370
|
+
console.error(
|
|
1371
|
+
"Failed to connect RedisCacheComponentsHandler client:",
|
|
1372
|
+
error
|
|
1373
|
+
);
|
|
1374
|
+
this.client.disconnect();
|
|
1375
|
+
throw error;
|
|
1376
|
+
});
|
|
1232
1377
|
});
|
|
1233
|
-
}
|
|
1378
|
+
}
|
|
1234
1379
|
const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY2 && key !== SHARED_TAGS_KEY;
|
|
1235
1380
|
this.revalidatedTagsMap = new SyncedMap({
|
|
1236
1381
|
client: this.client,
|
|
@@ -1239,6 +1384,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1239
1384
|
database,
|
|
1240
1385
|
querySize: revalidateTagQuerySize,
|
|
1241
1386
|
filterKeys,
|
|
1387
|
+
customizedSync: { withoutOrphanCleanup: true },
|
|
1242
1388
|
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
1243
1389
|
});
|
|
1244
1390
|
this.sharedTagsMap = new SyncedMap({
|
|
@@ -1276,6 +1422,13 @@ var RedisCacheComponentsHandler = class {
|
|
|
1276
1422
|
}
|
|
1277
1423
|
}
|
|
1278
1424
|
async assertClientIsReady() {
|
|
1425
|
+
if (this.redisConnectionDeferred) {
|
|
1426
|
+
await Promise.all([
|
|
1427
|
+
this.revalidatedTagsMap.waitUntilReady(),
|
|
1428
|
+
this.sharedTagsMap.waitUntilReady()
|
|
1429
|
+
]);
|
|
1430
|
+
return;
|
|
1431
|
+
}
|
|
1279
1432
|
if (!this.client.isReady && !this.client.isOpen) {
|
|
1280
1433
|
await this.client.connect().catch((error) => {
|
|
1281
1434
|
console.error(
|
|
@@ -1290,57 +1443,76 @@ var RedisCacheComponentsHandler = class {
|
|
|
1290
1443
|
this.sharedTagsMap.waitUntilReady()
|
|
1291
1444
|
]);
|
|
1292
1445
|
}
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
for (const tag of tags) {
|
|
1296
|
-
const ts = this.revalidatedTagsMap.get(tag);
|
|
1297
|
-
if (ts && ts > max) {
|
|
1298
|
-
max = ts;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
return max;
|
|
1446
|
+
isClientUnavailable() {
|
|
1447
|
+
return this.redisConnectionDeferred || !this.client.isReady;
|
|
1302
1448
|
}
|
|
1303
|
-
|
|
1449
|
+
tagManifestFor(tag, now = Date.now()) {
|
|
1450
|
+
return normalizeTagManifest(this.revalidatedTagsMap.get(tag), now);
|
|
1451
|
+
}
|
|
1452
|
+
async get(cacheKey, _softTags) {
|
|
1453
|
+
void _softTags;
|
|
1304
1454
|
const redisKey = `${this.keyPrefix}${cacheKey}`;
|
|
1305
1455
|
try {
|
|
1306
1456
|
await this.assertClientIsReady();
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1457
|
+
if (this.isClientUnavailable()) {
|
|
1458
|
+
return void 0;
|
|
1459
|
+
}
|
|
1460
|
+
const readSerialized = (useDedup) => redisErrorHandler(
|
|
1461
|
+
"RedisCacheComponentsHandler.get(), operation: get" + (useDedup ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
|
|
1462
|
+
(useDedup ? this.deduplicatedRedisGet(cacheKey) : this.redisGet)(
|
|
1311
1463
|
(0, import_redis2.commandOptions)({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
|
|
1312
1464
|
redisKey
|
|
1313
1465
|
)
|
|
1314
1466
|
);
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1467
|
+
let usedDedup = this.redisGetDeduplication;
|
|
1468
|
+
let serialized = await readSerialized(usedDedup);
|
|
1469
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1470
|
+
if (!serialized) {
|
|
1471
|
+
return void 0;
|
|
1472
|
+
}
|
|
1473
|
+
const stored = JSON.parse(serialized);
|
|
1474
|
+
const now = Date.now();
|
|
1475
|
+
const tags = stored.tags || [];
|
|
1476
|
+
const expiryTime = stored.timestamp + stored.expire * 1e3;
|
|
1477
|
+
const ttlExpired = Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime;
|
|
1478
|
+
const tagsExpired = areTagsExpired(
|
|
1479
|
+
tags,
|
|
1480
|
+
stored.timestamp,
|
|
1481
|
+
now,
|
|
1482
|
+
(tag) => this.tagManifestFor(tag, now)
|
|
1483
|
+
);
|
|
1484
|
+
if (ttlExpired || tagsExpired) {
|
|
1485
|
+
if (usedDedup) {
|
|
1486
|
+
usedDedup = false;
|
|
1487
|
+
serialized = await readSerialized(false);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
const dropped = await compareAndUnlink(
|
|
1491
|
+
this.client,
|
|
1492
|
+
redisKey,
|
|
1493
|
+
serialized
|
|
1494
|
+
);
|
|
1495
|
+
if (dropped) {
|
|
1496
|
+
await this.sharedTagsMap.delete(cacheKey).catch(() => {
|
|
1497
|
+
});
|
|
1498
|
+
}
|
|
1499
|
+
return void 0;
|
|
1500
|
+
}
|
|
1501
|
+
const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
|
|
1502
|
+
const entry = {
|
|
1503
|
+
...stored,
|
|
1504
|
+
value: bufferToReadableStream(valueBuffer)
|
|
1505
|
+
};
|
|
1506
|
+
if (areTagsStale(
|
|
1507
|
+
tags,
|
|
1508
|
+
stored.timestamp,
|
|
1509
|
+
(tag) => this.tagManifestFor(tag, now)
|
|
1510
|
+
)) {
|
|
1511
|
+
entry.revalidate = -1;
|
|
1512
|
+
}
|
|
1513
|
+
return entry;
|
|
1338
1514
|
}
|
|
1339
|
-
|
|
1340
|
-
return {
|
|
1341
|
-
...stored,
|
|
1342
|
-
value: bufferToReadableStream(valueBuffer)
|
|
1343
|
-
};
|
|
1515
|
+
return void 0;
|
|
1344
1516
|
} catch (error) {
|
|
1345
1517
|
console.error(
|
|
1346
1518
|
"RedisCacheComponentsHandler.get() Error occurred while getting cache entry. Returning undefined so site can continue to serve content while cache is disabled. The original error was:",
|
|
@@ -1353,6 +1525,9 @@ var RedisCacheComponentsHandler = class {
|
|
|
1353
1525
|
async set(cacheKey, pendingEntry) {
|
|
1354
1526
|
try {
|
|
1355
1527
|
await this.assertClientIsReady();
|
|
1528
|
+
if (this.isClientUnavailable()) {
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1356
1531
|
const entry = await pendingEntry;
|
|
1357
1532
|
const [storeStream] = entry.value.tee();
|
|
1358
1533
|
const buffer = await streamToBuffer(storeStream);
|
|
@@ -1416,7 +1591,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1416
1591
|
async getExpiration(tags) {
|
|
1417
1592
|
try {
|
|
1418
1593
|
await this.assertClientIsReady();
|
|
1419
|
-
return
|
|
1594
|
+
return maxExpiredTimestamp(tags || [], (tag) => this.tagManifestFor(tag));
|
|
1420
1595
|
} catch (error) {
|
|
1421
1596
|
console.error(
|
|
1422
1597
|
"RedisCacheComponentsHandler.getExpiration() Error occurred while getting expiration for tags. The original error was:",
|
|
@@ -1425,37 +1600,28 @@ var RedisCacheComponentsHandler = class {
|
|
|
1425
1600
|
return 0;
|
|
1426
1601
|
}
|
|
1427
1602
|
}
|
|
1428
|
-
async updateTags(tags,
|
|
1603
|
+
async updateTags(tags, durations) {
|
|
1429
1604
|
try {
|
|
1430
|
-
void _durations;
|
|
1431
1605
|
await this.assertClientIsReady();
|
|
1432
|
-
|
|
1433
|
-
const tagsSet = new Set(tags || []);
|
|
1434
|
-
for (const tag of tagsSet) {
|
|
1435
|
-
await this.revalidatedTagsMap.set(tag, now);
|
|
1436
|
-
}
|
|
1437
|
-
const keysToDelete = /* @__PURE__ */ new Set();
|
|
1438
|
-
for (const [key, storedTags] of this.sharedTagsMap.entries()) {
|
|
1439
|
-
if (storedTags.some((tag) => tagsSet.has(tag))) {
|
|
1440
|
-
keysToDelete.add(key);
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
if (keysToDelete.size === 0) {
|
|
1606
|
+
if (this.isClientUnavailable()) {
|
|
1444
1607
|
return;
|
|
1445
1608
|
}
|
|
1446
|
-
const
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
this.
|
|
1451
|
-
|
|
1609
|
+
const now = Date.now();
|
|
1610
|
+
const updated = new Set(tags || []);
|
|
1611
|
+
for (const tag of updated) {
|
|
1612
|
+
const next = applyTagUpdate(this.tagManifestFor(tag), durations, now);
|
|
1613
|
+
await this.revalidatedTagsMap.set(
|
|
1614
|
+
tag,
|
|
1615
|
+
persistableTagManifest(next, now)
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1452
1618
|
if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
|
|
1453
|
-
for (const key of
|
|
1454
|
-
|
|
1619
|
+
for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
|
|
1620
|
+
if (sharedTags.some((tag) => updated.has(tag))) {
|
|
1621
|
+
await this.inMemoryDeduplicationCache.delete(key, true);
|
|
1622
|
+
}
|
|
1455
1623
|
}
|
|
1456
1624
|
}
|
|
1457
|
-
const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
|
|
1458
|
-
await deleteTagsOperation;
|
|
1459
1625
|
} catch (error) {
|
|
1460
1626
|
console.error(
|
|
1461
1627
|
"RedisCacheComponentsHandler.updateTags() Error occurred while updating tags. The original error was:",
|
|
@@ -1499,6 +1665,8 @@ var index_default = CachedHandler;
|
|
|
1499
1665
|
bufferAndMapReviver,
|
|
1500
1666
|
getRedisCacheComponentsHandler,
|
|
1501
1667
|
jsonCacheValueSerializer,
|
|
1502
|
-
redisCacheHandler
|
|
1668
|
+
redisCacheHandler,
|
|
1669
|
+
resolveCacheEntryTtlSeconds,
|
|
1670
|
+
shouldDeferRedisConnection
|
|
1503
1671
|
});
|
|
1504
1672
|
//# sourceMappingURL=index.js.map
|