@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.mjs
CHANGED
|
@@ -22,6 +22,12 @@ function debugVerbose(color, ...args) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// src/utils/redisConnection.ts
|
|
26
|
+
var PHASE_PRODUCTION_BUILD = "phase-production-build";
|
|
27
|
+
function shouldDeferRedisConnection() {
|
|
28
|
+
return process.env.NEXT_PHASE === PHASE_PRODUCTION_BUILD;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
// src/SyncedMap.ts
|
|
26
32
|
var SYNC_CHANNEL_SUFFIX = ":sync-channel:";
|
|
27
33
|
var SyncedMap = class {
|
|
@@ -47,6 +53,10 @@ var SyncedMap = class {
|
|
|
47
53
|
});
|
|
48
54
|
}
|
|
49
55
|
async setup() {
|
|
56
|
+
if (shouldDeferRedisConnection()) {
|
|
57
|
+
this.setupLockResolve();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
50
60
|
let setupPromises = [];
|
|
51
61
|
if (!this.customizedSync?.withoutRedisHashmap) {
|
|
52
62
|
setupPromises.push(this.initialSync());
|
|
@@ -77,7 +87,9 @@ var SyncedMap = class {
|
|
|
77
87
|
}
|
|
78
88
|
cursor = remoteItems.cursor;
|
|
79
89
|
} while (cursor !== 0);
|
|
80
|
-
|
|
90
|
+
if (!this.customizedSync?.withoutOrphanCleanup) {
|
|
91
|
+
await this.cleanupKeysNotInRedis();
|
|
92
|
+
}
|
|
81
93
|
} catch (error) {
|
|
82
94
|
console.error("Error during initial sync:", error);
|
|
83
95
|
throw error;
|
|
@@ -489,6 +501,37 @@ var jsonCacheValueSerializer = {
|
|
|
489
501
|
}
|
|
490
502
|
};
|
|
491
503
|
|
|
504
|
+
// src/utils/cacheTtl.ts
|
|
505
|
+
function resolveCacheEntryTtlSeconds(ctx, data, options) {
|
|
506
|
+
const expire = ctx.cacheControl?.expire;
|
|
507
|
+
if (Number.isFinite(expire) && expire > 0) {
|
|
508
|
+
return Math.max(1, Math.ceil(expire));
|
|
509
|
+
}
|
|
510
|
+
const revalidate = resolveRevalidateValue(ctx, data);
|
|
511
|
+
if (revalidate === false) {
|
|
512
|
+
return options.estimateExpireAge(options.defaultStaleAge);
|
|
513
|
+
}
|
|
514
|
+
if (revalidate && Number.isSafeInteger(revalidate) && revalidate > 0) {
|
|
515
|
+
return options.estimateExpireAge(revalidate);
|
|
516
|
+
}
|
|
517
|
+
return void 0;
|
|
518
|
+
}
|
|
519
|
+
function resolveRevalidateValue(ctx, data) {
|
|
520
|
+
if (data?.kind === "FETCH" && data.revalidate !== void 0) {
|
|
521
|
+
return data.revalidate;
|
|
522
|
+
}
|
|
523
|
+
if (ctx.revalidate !== void 0) {
|
|
524
|
+
return ctx.revalidate;
|
|
525
|
+
}
|
|
526
|
+
if (ctx.cacheControl?.revalidate !== void 0) {
|
|
527
|
+
return ctx.cacheControl.revalidate;
|
|
528
|
+
}
|
|
529
|
+
if (data?.revalidate !== void 0) {
|
|
530
|
+
return data.revalidate;
|
|
531
|
+
}
|
|
532
|
+
return void 0;
|
|
533
|
+
}
|
|
534
|
+
|
|
492
535
|
// src/RedisStringsHandler.ts
|
|
493
536
|
function redisErrorHandler(debugInfo, redisCommandResult) {
|
|
494
537
|
const beforeTimestamp = performance.now();
|
|
@@ -566,6 +609,7 @@ var RedisStringsHandler = class {
|
|
|
566
609
|
this.killContainerOnErrorThreshold = killContainerOnErrorThreshold;
|
|
567
610
|
this.getTimeoutMs = getTimeoutMs;
|
|
568
611
|
this.valueSerializer = valueSerializer;
|
|
612
|
+
this.redisConnectionDeferred = shouldDeferRedisConnection();
|
|
569
613
|
try {
|
|
570
614
|
this.client = createClient({
|
|
571
615
|
url: redisUrl,
|
|
@@ -604,12 +648,14 @@ var RedisStringsHandler = class {
|
|
|
604
648
|
}, 500);
|
|
605
649
|
}
|
|
606
650
|
});
|
|
607
|
-
this.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
651
|
+
if (!this.redisConnectionDeferred) {
|
|
652
|
+
this.client.connect().then(() => {
|
|
653
|
+
debug("green", "Redis client connected.");
|
|
654
|
+
}).catch((error) => {
|
|
655
|
+
console.error("Failed to connect Redis client:", error);
|
|
656
|
+
throw error;
|
|
657
|
+
});
|
|
658
|
+
}
|
|
613
659
|
} catch (error) {
|
|
614
660
|
console.error("Failed to initialize Redis client");
|
|
615
661
|
throw error;
|
|
@@ -631,6 +677,7 @@ var RedisStringsHandler = class {
|
|
|
631
677
|
database,
|
|
632
678
|
querySize: revalidateTagQuerySize,
|
|
633
679
|
filterKeys,
|
|
680
|
+
customizedSync: { withoutOrphanCleanup: true },
|
|
634
681
|
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
635
682
|
});
|
|
636
683
|
this.inMemoryDeduplicationCache = new SyncedMap({
|
|
@@ -700,11 +747,17 @@ var RedisStringsHandler = class {
|
|
|
700
747
|
}
|
|
701
748
|
this.clientReadyCalls = 0;
|
|
702
749
|
if (!this.client.isReady) {
|
|
750
|
+
if (this.redisConnectionDeferred) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
703
753
|
throw new Error(
|
|
704
754
|
"assertClientIsReady: Redis client is not ready yet or connection is lost."
|
|
705
755
|
);
|
|
706
756
|
}
|
|
707
757
|
}
|
|
758
|
+
isClientUnavailable() {
|
|
759
|
+
return this.redisConnectionDeferred || !this.client.isReady;
|
|
760
|
+
}
|
|
708
761
|
async get(key, ctx) {
|
|
709
762
|
try {
|
|
710
763
|
if (!SUPPORTED_GET_KINDS.includes(ctx.kind)) {
|
|
@@ -718,6 +771,9 @@ var RedisStringsHandler = class {
|
|
|
718
771
|
}
|
|
719
772
|
debug("green", "RedisStringsHandler.get() called with", key, ctx);
|
|
720
773
|
await this.assertClientIsReady();
|
|
774
|
+
if (this.isClientUnavailable()) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
721
777
|
const clientGet = this.redisGetDeduplication ? this.deduplicatedRedisGet(key) : this.redisGet;
|
|
722
778
|
const redisGetOperation = clientGet(
|
|
723
779
|
commandOptions({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
|
|
@@ -851,6 +907,9 @@ var RedisStringsHandler = class {
|
|
|
851
907
|
);
|
|
852
908
|
}
|
|
853
909
|
await this.assertClientIsReady();
|
|
910
|
+
if (this.isClientUnavailable()) {
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
854
913
|
if (data?.kind === "APP_PAGE" || data?.kind === "APP_ROUTE") {
|
|
855
914
|
const tags = data.headers["x-next-cache-tags"]?.split(",");
|
|
856
915
|
ctx.tags = [...ctx.tags || [], ...tags || []];
|
|
@@ -873,18 +932,14 @@ var RedisStringsHandler = class {
|
|
|
873
932
|
serializedCacheEntry
|
|
874
933
|
);
|
|
875
934
|
}
|
|
876
|
-
const
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
// matters for those older, non-discriminated shapes.
|
|
881
|
-
data?.revalidate
|
|
882
|
-
);
|
|
883
|
-
const expireAt = revalidate && Number.isSafeInteger(revalidate) && revalidate > 0 ? this.estimateExpireAge(revalidate) : this.estimateExpireAge(this.defaultStaleAge);
|
|
935
|
+
const expireAt = resolveCacheEntryTtlSeconds(ctx, data, {
|
|
936
|
+
estimateExpireAge: this.estimateExpireAge,
|
|
937
|
+
defaultStaleAge: this.defaultStaleAge
|
|
938
|
+
});
|
|
884
939
|
const setOperation = redisErrorHandler(
|
|
885
940
|
"RedisStringsHandler.set(), operation: set " + this.keyPrefix + " " + key,
|
|
886
941
|
this.client.set(this.keyPrefix + key, serializedCacheEntry, {
|
|
887
|
-
EX: expireAt
|
|
942
|
+
...expireAt ? { EX: expireAt } : {}
|
|
888
943
|
})
|
|
889
944
|
);
|
|
890
945
|
debug(
|
|
@@ -947,6 +1002,9 @@ var RedisStringsHandler = class {
|
|
|
947
1002
|
);
|
|
948
1003
|
const tags = new Set([tagOrTags || []].flat());
|
|
949
1004
|
await this.assertClientIsReady();
|
|
1005
|
+
if (this.isClientUnavailable()) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
950
1008
|
const keysToDelete = /* @__PURE__ */ new Set();
|
|
951
1009
|
for (const tag of tags) {
|
|
952
1010
|
if (tag.startsWith(NEXT_CACHE_IMPLICIT_TAG_ID)) {
|
|
@@ -1085,6 +1143,88 @@ var CachedHandler = class {
|
|
|
1085
1143
|
|
|
1086
1144
|
// src/CacheComponentsHandler.ts
|
|
1087
1145
|
import { commandOptions as commandOptions2, createClient as createClient2 } from "redis";
|
|
1146
|
+
|
|
1147
|
+
// src/utils/compareAndUnlink.ts
|
|
1148
|
+
var COMPARE_AND_UNLINK = `
|
|
1149
|
+
if redis.call('GET', KEYS[1]) == ARGV[1] then
|
|
1150
|
+
return redis.call('UNLINK', KEYS[1])
|
|
1151
|
+
end
|
|
1152
|
+
return 0
|
|
1153
|
+
`;
|
|
1154
|
+
async function compareAndUnlink(client, key, expected) {
|
|
1155
|
+
const deleted = await client.eval(COMPARE_AND_UNLINK, {
|
|
1156
|
+
keys: [key],
|
|
1157
|
+
arguments: [expected]
|
|
1158
|
+
});
|
|
1159
|
+
return deleted === 1 || deleted === 1n;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/utils/tagRevalidation.ts
|
|
1163
|
+
function normalizeTagManifest(stored, now = Date.now()) {
|
|
1164
|
+
if (typeof stored === "number" && Number.isFinite(stored)) {
|
|
1165
|
+
return { expired: Math.min(stored, now) };
|
|
1166
|
+
}
|
|
1167
|
+
if (stored && typeof stored === "object") {
|
|
1168
|
+
const rec = stored;
|
|
1169
|
+
const stale = typeof rec.stale === "number" && Number.isFinite(rec.stale) ? rec.stale : typeof rec.last === "number" && Number.isFinite(rec.last) ? rec.last : void 0;
|
|
1170
|
+
const expired = typeof rec.expired === "number" && Number.isFinite(rec.expired) ? rec.expired : typeof rec.pending === "number" && Number.isFinite(rec.pending) ? rec.pending : void 0;
|
|
1171
|
+
return {
|
|
1172
|
+
...stale !== void 0 ? { stale } : {},
|
|
1173
|
+
...expired !== void 0 ? { expired } : {}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
return {};
|
|
1177
|
+
}
|
|
1178
|
+
function applyTagUpdate(existing, durations, now) {
|
|
1179
|
+
if (durations) {
|
|
1180
|
+
const updates = { ...existing };
|
|
1181
|
+
updates.stale = now;
|
|
1182
|
+
if (durations.expire !== void 0) {
|
|
1183
|
+
updates.expired = now + durations.expire * 1e3;
|
|
1184
|
+
}
|
|
1185
|
+
return updates;
|
|
1186
|
+
}
|
|
1187
|
+
return {
|
|
1188
|
+
...existing,
|
|
1189
|
+
expired: now
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
function persistableTagManifest(entry, now) {
|
|
1193
|
+
if (typeof entry.expired === "number" && entry.expired <= now && (entry.stale === void 0 || entry.stale <= entry.expired)) {
|
|
1194
|
+
return entry.expired;
|
|
1195
|
+
}
|
|
1196
|
+
return entry;
|
|
1197
|
+
}
|
|
1198
|
+
function areTagsExpired(tags, entryTimestamp, now, getEntry) {
|
|
1199
|
+
for (const tag of tags) {
|
|
1200
|
+
const expiredAt = getEntry(tag)?.expired;
|
|
1201
|
+
if (typeof expiredAt === "number" && expiredAt <= now && expiredAt > entryTimestamp) {
|
|
1202
|
+
return true;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
return false;
|
|
1206
|
+
}
|
|
1207
|
+
function areTagsStale(tags, entryTimestamp, getEntry) {
|
|
1208
|
+
for (const tag of tags) {
|
|
1209
|
+
const staleAt = getEntry(tag)?.stale ?? 0;
|
|
1210
|
+
if (typeof staleAt === "number" && staleAt > entryTimestamp) {
|
|
1211
|
+
return true;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
function maxExpiredTimestamp(tags, getEntry) {
|
|
1217
|
+
let max = 0;
|
|
1218
|
+
for (const tag of tags) {
|
|
1219
|
+
const expired = getEntry(tag)?.expired || 0;
|
|
1220
|
+
if (expired > max) {
|
|
1221
|
+
max = expired;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
return max;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// src/CacheComponentsHandler.ts
|
|
1088
1228
|
var REVALIDATED_TAGS_KEY2 = "__cacheComponents_revalidated_tags__";
|
|
1089
1229
|
var SHARED_TAGS_KEY = "__cacheComponents_sharedTags__";
|
|
1090
1230
|
var killContainerOnErrorCount2 = 0;
|
|
@@ -1142,6 +1282,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1142
1282
|
this.getTimeoutMs = getTimeoutMs;
|
|
1143
1283
|
this.redisGetDeduplication = redisGetDeduplication;
|
|
1144
1284
|
this.inMemoryCachingTime = inMemoryCachingTime;
|
|
1285
|
+
this.redisConnectionDeferred = shouldDeferRedisConnection();
|
|
1145
1286
|
this.client = createClient2({
|
|
1146
1287
|
url: redisUrl,
|
|
1147
1288
|
pingInterval: 1e4,
|
|
@@ -1177,18 +1318,20 @@ var RedisCacheComponentsHandler = class {
|
|
|
1177
1318
|
}, 500);
|
|
1178
1319
|
}
|
|
1179
1320
|
});
|
|
1180
|
-
this.
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1321
|
+
if (!this.redisConnectionDeferred) {
|
|
1322
|
+
this.client.connect().then(() => {
|
|
1323
|
+
debug("green", "RedisCacheComponentsHandler client connected.");
|
|
1324
|
+
}).catch(() => {
|
|
1325
|
+
this.client.connect().catch((error) => {
|
|
1326
|
+
console.error(
|
|
1327
|
+
"Failed to connect RedisCacheComponentsHandler client:",
|
|
1328
|
+
error
|
|
1329
|
+
);
|
|
1330
|
+
this.client.disconnect();
|
|
1331
|
+
throw error;
|
|
1332
|
+
});
|
|
1190
1333
|
});
|
|
1191
|
-
}
|
|
1334
|
+
}
|
|
1192
1335
|
const filterKeys = (key) => key !== REVALIDATED_TAGS_KEY2 && key !== SHARED_TAGS_KEY;
|
|
1193
1336
|
this.revalidatedTagsMap = new SyncedMap({
|
|
1194
1337
|
client: this.client,
|
|
@@ -1197,6 +1340,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1197
1340
|
database,
|
|
1198
1341
|
querySize: revalidateTagQuerySize,
|
|
1199
1342
|
filterKeys,
|
|
1343
|
+
customizedSync: { withoutOrphanCleanup: true },
|
|
1200
1344
|
resyncIntervalMs: avgResyncIntervalMs + avgResyncIntervalMs / 10 + Math.random() * (avgResyncIntervalMs / 10)
|
|
1201
1345
|
});
|
|
1202
1346
|
this.sharedTagsMap = new SyncedMap({
|
|
@@ -1234,6 +1378,13 @@ var RedisCacheComponentsHandler = class {
|
|
|
1234
1378
|
}
|
|
1235
1379
|
}
|
|
1236
1380
|
async assertClientIsReady() {
|
|
1381
|
+
if (this.redisConnectionDeferred) {
|
|
1382
|
+
await Promise.all([
|
|
1383
|
+
this.revalidatedTagsMap.waitUntilReady(),
|
|
1384
|
+
this.sharedTagsMap.waitUntilReady()
|
|
1385
|
+
]);
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1237
1388
|
if (!this.client.isReady && !this.client.isOpen) {
|
|
1238
1389
|
await this.client.connect().catch((error) => {
|
|
1239
1390
|
console.error(
|
|
@@ -1248,57 +1399,76 @@ var RedisCacheComponentsHandler = class {
|
|
|
1248
1399
|
this.sharedTagsMap.waitUntilReady()
|
|
1249
1400
|
]);
|
|
1250
1401
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
for (const tag of tags) {
|
|
1254
|
-
const ts = this.revalidatedTagsMap.get(tag);
|
|
1255
|
-
if (ts && ts > max) {
|
|
1256
|
-
max = ts;
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
return max;
|
|
1402
|
+
isClientUnavailable() {
|
|
1403
|
+
return this.redisConnectionDeferred || !this.client.isReady;
|
|
1260
1404
|
}
|
|
1261
|
-
|
|
1405
|
+
tagManifestFor(tag, now = Date.now()) {
|
|
1406
|
+
return normalizeTagManifest(this.revalidatedTagsMap.get(tag), now);
|
|
1407
|
+
}
|
|
1408
|
+
async get(cacheKey, _softTags) {
|
|
1409
|
+
void _softTags;
|
|
1262
1410
|
const redisKey = `${this.keyPrefix}${cacheKey}`;
|
|
1263
1411
|
try {
|
|
1264
1412
|
await this.assertClientIsReady();
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1413
|
+
if (this.isClientUnavailable()) {
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
const readSerialized = (useDedup) => redisErrorHandler(
|
|
1417
|
+
"RedisCacheComponentsHandler.get(), operation: get" + (useDedup ? " deduplicated" : "") + " " + this.getTimeoutMs + "ms " + redisKey,
|
|
1418
|
+
(useDedup ? this.deduplicatedRedisGet(cacheKey) : this.redisGet)(
|
|
1269
1419
|
commandOptions2({ signal: AbortSignal.timeout(this.getTimeoutMs) }),
|
|
1270
1420
|
redisKey
|
|
1271
1421
|
)
|
|
1272
1422
|
);
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1423
|
+
let usedDedup = this.redisGetDeduplication;
|
|
1424
|
+
let serialized = await readSerialized(usedDedup);
|
|
1425
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1426
|
+
if (!serialized) {
|
|
1427
|
+
return void 0;
|
|
1428
|
+
}
|
|
1429
|
+
const stored = JSON.parse(serialized);
|
|
1430
|
+
const now = Date.now();
|
|
1431
|
+
const tags = stored.tags || [];
|
|
1432
|
+
const expiryTime = stored.timestamp + stored.expire * 1e3;
|
|
1433
|
+
const ttlExpired = Number.isFinite(stored.expire) && stored.expire > 0 && now > expiryTime;
|
|
1434
|
+
const tagsExpired = areTagsExpired(
|
|
1435
|
+
tags,
|
|
1436
|
+
stored.timestamp,
|
|
1437
|
+
now,
|
|
1438
|
+
(tag) => this.tagManifestFor(tag, now)
|
|
1439
|
+
);
|
|
1440
|
+
if (ttlExpired || tagsExpired) {
|
|
1441
|
+
if (usedDedup) {
|
|
1442
|
+
usedDedup = false;
|
|
1443
|
+
serialized = await readSerialized(false);
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
const dropped = await compareAndUnlink(
|
|
1447
|
+
this.client,
|
|
1448
|
+
redisKey,
|
|
1449
|
+
serialized
|
|
1450
|
+
);
|
|
1451
|
+
if (dropped) {
|
|
1452
|
+
await this.sharedTagsMap.delete(cacheKey).catch(() => {
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
return void 0;
|
|
1456
|
+
}
|
|
1457
|
+
const valueBuffer = typeof stored.value === "string" ? new Uint8Array(Buffer.from(stored.value, "base64")) : stored.value;
|
|
1458
|
+
const entry = {
|
|
1459
|
+
...stored,
|
|
1460
|
+
value: bufferToReadableStream(valueBuffer)
|
|
1461
|
+
};
|
|
1462
|
+
if (areTagsStale(
|
|
1463
|
+
tags,
|
|
1464
|
+
stored.timestamp,
|
|
1465
|
+
(tag) => this.tagManifestFor(tag, now)
|
|
1466
|
+
)) {
|
|
1467
|
+
entry.revalidate = -1;
|
|
1468
|
+
}
|
|
1469
|
+
return entry;
|
|
1296
1470
|
}
|
|
1297
|
-
|
|
1298
|
-
return {
|
|
1299
|
-
...stored,
|
|
1300
|
-
value: bufferToReadableStream(valueBuffer)
|
|
1301
|
-
};
|
|
1471
|
+
return void 0;
|
|
1302
1472
|
} catch (error) {
|
|
1303
1473
|
console.error(
|
|
1304
1474
|
"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:",
|
|
@@ -1311,6 +1481,9 @@ var RedisCacheComponentsHandler = class {
|
|
|
1311
1481
|
async set(cacheKey, pendingEntry) {
|
|
1312
1482
|
try {
|
|
1313
1483
|
await this.assertClientIsReady();
|
|
1484
|
+
if (this.isClientUnavailable()) {
|
|
1485
|
+
return;
|
|
1486
|
+
}
|
|
1314
1487
|
const entry = await pendingEntry;
|
|
1315
1488
|
const [storeStream] = entry.value.tee();
|
|
1316
1489
|
const buffer = await streamToBuffer(storeStream);
|
|
@@ -1374,7 +1547,7 @@ var RedisCacheComponentsHandler = class {
|
|
|
1374
1547
|
async getExpiration(tags) {
|
|
1375
1548
|
try {
|
|
1376
1549
|
await this.assertClientIsReady();
|
|
1377
|
-
return
|
|
1550
|
+
return maxExpiredTimestamp(tags || [], (tag) => this.tagManifestFor(tag));
|
|
1378
1551
|
} catch (error) {
|
|
1379
1552
|
console.error(
|
|
1380
1553
|
"RedisCacheComponentsHandler.getExpiration() Error occurred while getting expiration for tags. The original error was:",
|
|
@@ -1383,37 +1556,28 @@ var RedisCacheComponentsHandler = class {
|
|
|
1383
1556
|
return 0;
|
|
1384
1557
|
}
|
|
1385
1558
|
}
|
|
1386
|
-
async updateTags(tags,
|
|
1559
|
+
async updateTags(tags, durations) {
|
|
1387
1560
|
try {
|
|
1388
|
-
void _durations;
|
|
1389
1561
|
await this.assertClientIsReady();
|
|
1390
|
-
|
|
1391
|
-
const tagsSet = new Set(tags || []);
|
|
1392
|
-
for (const tag of tagsSet) {
|
|
1393
|
-
await this.revalidatedTagsMap.set(tag, now);
|
|
1394
|
-
}
|
|
1395
|
-
const keysToDelete = /* @__PURE__ */ new Set();
|
|
1396
|
-
for (const [key, storedTags] of this.sharedTagsMap.entries()) {
|
|
1397
|
-
if (storedTags.some((tag) => tagsSet.has(tag))) {
|
|
1398
|
-
keysToDelete.add(key);
|
|
1399
|
-
}
|
|
1400
|
-
}
|
|
1401
|
-
if (keysToDelete.size === 0) {
|
|
1562
|
+
if (this.isClientUnavailable()) {
|
|
1402
1563
|
return;
|
|
1403
1564
|
}
|
|
1404
|
-
const
|
|
1405
|
-
const
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
this.
|
|
1409
|
-
|
|
1565
|
+
const now = Date.now();
|
|
1566
|
+
const updated = new Set(tags || []);
|
|
1567
|
+
for (const tag of updated) {
|
|
1568
|
+
const next = applyTagUpdate(this.tagManifestFor(tag), durations, now);
|
|
1569
|
+
await this.revalidatedTagsMap.set(
|
|
1570
|
+
tag,
|
|
1571
|
+
persistableTagManifest(next, now)
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1410
1574
|
if (this.redisGetDeduplication && this.inMemoryCachingTime > 0) {
|
|
1411
|
-
for (const key of
|
|
1412
|
-
|
|
1575
|
+
for (const [key, sharedTags] of this.sharedTagsMap.entries()) {
|
|
1576
|
+
if (sharedTags.some((tag) => updated.has(tag))) {
|
|
1577
|
+
await this.inMemoryDeduplicationCache.delete(key, true);
|
|
1578
|
+
}
|
|
1413
1579
|
}
|
|
1414
1580
|
}
|
|
1415
|
-
const deleteTagsOperation = this.sharedTagsMap.delete(cacheKeys);
|
|
1416
|
-
await deleteTagsOperation;
|
|
1417
1581
|
} catch (error) {
|
|
1418
1582
|
console.error(
|
|
1419
1583
|
"RedisCacheComponentsHandler.updateTags() Error occurred while updating tags. The original error was:",
|
|
@@ -1457,6 +1621,8 @@ export {
|
|
|
1457
1621
|
index_default as default,
|
|
1458
1622
|
getRedisCacheComponentsHandler,
|
|
1459
1623
|
jsonCacheValueSerializer,
|
|
1460
|
-
redisCacheHandler
|
|
1624
|
+
redisCacheHandler,
|
|
1625
|
+
resolveCacheEntryTtlSeconds,
|
|
1626
|
+
shouldDeferRedisConnection
|
|
1461
1627
|
};
|
|
1462
1628
|
//# sourceMappingURL=index.mjs.map
|