@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
|
@@ -7,7 +7,7 @@ import { CacheEntry } from '../../../src/RedisStringsHandler';
|
|
|
7
7
|
import { revalidate as revalidatedFetchRouteRevalidate } from '../../nextjs-test-projects/next-app-15-4-11/src/app/api/revalidated-fetch/route';
|
|
8
8
|
|
|
9
9
|
// Select which Next.js test app to use. Can be overridden via NEXT_TEST_APP env var
|
|
10
|
-
// Examples: next-app-15-4-11, next-app-16-0-11, next-app-16-2-6
|
|
10
|
+
// Examples: next-app-15-4-11, next-app-16-0-11, next-app-16-2-6, next-app-16-3-0
|
|
11
11
|
const NEXT_TEST_APP = process.env.NEXT_TEST_APP || 'next-app-15-4-11';
|
|
12
12
|
const NEXT_APP_DIR = join(
|
|
13
13
|
__dirname,
|
|
@@ -17,6 +17,8 @@ const NEXT_APP_DIR = join(
|
|
|
17
17
|
NEXT_TEST_APP,
|
|
18
18
|
);
|
|
19
19
|
console.log('NEXT_APP_DIR', NEXT_APP_DIR);
|
|
20
|
+
|
|
21
|
+
const SHARED_TAGS_KEY = '__sharedTags__';
|
|
20
22
|
const NEXT_START_PORT = 3055;
|
|
21
23
|
const NEXT_START_URL = `http://localhost:${NEXT_START_PORT}`;
|
|
22
24
|
|
|
@@ -29,6 +31,20 @@ async function delay(ms: number) {
|
|
|
29
31
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
async function findFetchCacheEntryKey(tag: string): Promise<string> {
|
|
35
|
+
const sharedTags = await redisClient.hGetAll(
|
|
36
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
37
|
+
);
|
|
38
|
+
const matches = Object.entries(sharedTags).filter(
|
|
39
|
+
([key, serializedTags]) =>
|
|
40
|
+
!key.startsWith('/') &&
|
|
41
|
+
(JSON.parse(serializedTags) as string[]).includes(tag),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
expect(matches).toHaveLength(1);
|
|
45
|
+
return matches[0]![0];
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
async function runCommand(cmd: string, args: string[], cwd: string) {
|
|
33
49
|
return new Promise((resolve, reject) => {
|
|
34
50
|
let stderr = '';
|
|
@@ -64,7 +80,7 @@ async function runCommand(cmd: string, args: string[], cwd: string) {
|
|
|
64
80
|
});
|
|
65
81
|
}
|
|
66
82
|
|
|
67
|
-
async function waitForServer(url, timeout = 20000) {
|
|
83
|
+
async function waitForServer(url: string, timeout = 20000) {
|
|
68
84
|
const start = Date.now();
|
|
69
85
|
while (Date.now() - start < timeout) {
|
|
70
86
|
try {
|
|
@@ -226,7 +242,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
226
242
|
expect(keys.length).toBe(0);
|
|
227
243
|
|
|
228
244
|
const hashmap = await redisClient.hGet(
|
|
229
|
-
process.env.VERCEL_URL +
|
|
245
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
230
246
|
'/api/cached-static-fetch',
|
|
231
247
|
);
|
|
232
248
|
expect(hashmap).toBeNull();
|
|
@@ -247,7 +263,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
247
263
|
expect(keys.length).toBe(1);
|
|
248
264
|
|
|
249
265
|
const hashmap = (await redisClient.hGet(
|
|
250
|
-
process.env.VERCEL_URL +
|
|
266
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
251
267
|
'/api/cached-static-fetch',
|
|
252
268
|
)) as string;
|
|
253
269
|
expect(JSON.parse(hashmap)).toEqual([
|
|
@@ -297,6 +313,15 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
297
313
|
const ttl = await redisClient.ttl(
|
|
298
314
|
process.env.VERCEL_URL + '/api/revalidated-fetch',
|
|
299
315
|
);
|
|
316
|
+
|
|
317
|
+
// Next.js 16.3+ passes cacheControl.expire for ISR fetch routes (~1 year).
|
|
318
|
+
// TTL is keyed on expire (SWR-safe), not 2 × revalidate — skip the
|
|
319
|
+
// long-running expiry wait in that case.
|
|
320
|
+
if (NEXT_TEST_APP.includes('16-3')) {
|
|
321
|
+
expect(ttl).toBeGreaterThan(2 * revalidatedFetchRouteRevalidate);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
300
325
|
expect(ttl).toBeLessThan(2 * revalidatedFetchRouteRevalidate);
|
|
301
326
|
expect(ttl).toBeGreaterThan(
|
|
302
327
|
2 * revalidatedFetchRouteRevalidate -
|
|
@@ -317,7 +342,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
317
342
|
|
|
318
343
|
// The key should also be removed from the hashmap
|
|
319
344
|
const hashmap = await redisClient.hGet(
|
|
320
|
-
process.env.VERCEL_URL +
|
|
345
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
321
346
|
'/api/revalidated-fetch',
|
|
322
347
|
);
|
|
323
348
|
expect(hashmap).toBeNull();
|
|
@@ -339,7 +364,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
339
364
|
expect(keys.length).toBe(0);
|
|
340
365
|
|
|
341
366
|
const hashmap = await redisClient.hGet(
|
|
342
|
-
process.env.VERCEL_URL +
|
|
367
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
343
368
|
cachedStaticPath,
|
|
344
369
|
);
|
|
345
370
|
expect(hashmap).toBeNull();
|
|
@@ -495,7 +520,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
495
520
|
expect(keys.length).toBe(0);
|
|
496
521
|
|
|
497
522
|
const hashmap = await redisClient.hGet(
|
|
498
|
-
process.env.VERCEL_URL +
|
|
523
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
499
524
|
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
500
525
|
);
|
|
501
526
|
expect(hashmap).toBeNull();
|
|
@@ -514,10 +539,9 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
514
539
|
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
515
540
|
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
516
541
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
'094a786b7ad391852168d3a7bcf75736777697d24a856a0089837f4b7de921df';
|
|
542
|
+
const cacheEntryKey = await findFetchCacheEntryKey(
|
|
543
|
+
'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
544
|
+
);
|
|
521
545
|
|
|
522
546
|
// check Redis keys
|
|
523
547
|
const keys = await redisClient.keys(
|
|
@@ -526,7 +550,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
526
550
|
expect(keys.length).toBe(1);
|
|
527
551
|
|
|
528
552
|
const hashmap = (await redisClient.hGet(
|
|
529
|
-
process.env.VERCEL_URL +
|
|
553
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
530
554
|
cacheEntryKey,
|
|
531
555
|
)) as string;
|
|
532
556
|
expect(JSON.parse(hashmap)).toEqual([
|
|
@@ -551,7 +575,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
551
575
|
expect(keys.length).toBe(0);
|
|
552
576
|
|
|
553
577
|
const hashmap = await redisClient.hGet(
|
|
554
|
-
process.env.VERCEL_URL +
|
|
578
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
555
579
|
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
556
580
|
);
|
|
557
581
|
expect(hashmap).toBeNull();
|
|
@@ -569,10 +593,9 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
569
593
|
|
|
570
594
|
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
571
595
|
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
'094a786b7ad391852168d3a7bcf75736777697d24a856a0089837f4b7de921df';
|
|
596
|
+
const cacheEntryKey = await findFetchCacheEntryKey(
|
|
597
|
+
'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
598
|
+
);
|
|
576
599
|
|
|
577
600
|
// check Redis keys
|
|
578
601
|
const keys = await redisClient.keys(
|
|
@@ -581,7 +604,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
581
604
|
expect(keys.length).toBe(1);
|
|
582
605
|
|
|
583
606
|
const hashmap = (await redisClient.hGet(
|
|
584
|
-
process.env.VERCEL_URL +
|
|
607
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
585
608
|
cacheEntryKey,
|
|
586
609
|
)) as string;
|
|
587
610
|
expect(JSON.parse(hashmap)).toEqual([
|
|
@@ -708,7 +731,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
708
731
|
expect(keys.length).toBe(0);
|
|
709
732
|
|
|
710
733
|
const hashmap = await redisClient.hGet(
|
|
711
|
-
process.env.VERCEL_URL +
|
|
734
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
712
735
|
'/pages/no-fetch/default-page',
|
|
713
736
|
);
|
|
714
737
|
expect(hashmap).toBeNull();
|
|
@@ -733,7 +756,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
733
756
|
expect(keys.length).toBe(1);
|
|
734
757
|
|
|
735
758
|
const hashmap = (await redisClient.hGet(
|
|
736
|
-
process.env.VERCEL_URL +
|
|
759
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
737
760
|
'/pages/no-fetch/default-page',
|
|
738
761
|
)) as string;
|
|
739
762
|
expect(JSON.parse(hashmap)).toEqual([
|
|
@@ -771,6 +794,10 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
771
794
|
|
|
772
795
|
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
773
796
|
|
|
797
|
+
const nestedFetchCacheEntryKey = await findFetchCacheEntryKey(
|
|
798
|
+
'revalidated-fetch-revalidate15-default-page',
|
|
799
|
+
);
|
|
800
|
+
|
|
774
801
|
// test cache entry for 3 keys are set
|
|
775
802
|
const keys1 = await redisClient.keys(
|
|
776
803
|
process.env.VERCEL_URL +
|
|
@@ -778,8 +805,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
778
805
|
);
|
|
779
806
|
expect(keys1.length).toBe(1);
|
|
780
807
|
const keys2 = await redisClient.keys(
|
|
781
|
-
process.env.VERCEL_URL +
|
|
782
|
-
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
808
|
+
process.env.VERCEL_URL + nestedFetchCacheEntryKey,
|
|
783
809
|
);
|
|
784
810
|
expect(keys2.length).toBe(1);
|
|
785
811
|
const keys3 = await redisClient.keys(
|
|
@@ -789,7 +815,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
789
815
|
|
|
790
816
|
// test shared tag hashmap to be set for all keys
|
|
791
817
|
const hashmap1 = (await redisClient.hGet(
|
|
792
|
-
process.env.VERCEL_URL +
|
|
818
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
793
819
|
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
794
820
|
)) as string;
|
|
795
821
|
expect(JSON.parse(hashmap1)).toEqual([
|
|
@@ -802,14 +828,14 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
802
828
|
'revalidated-fetch-revalidate15-default-page',
|
|
803
829
|
]);
|
|
804
830
|
const hashmap2 = (await redisClient.hGet(
|
|
805
|
-
process.env.VERCEL_URL +
|
|
806
|
-
|
|
831
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
832
|
+
nestedFetchCacheEntryKey,
|
|
807
833
|
)) as string;
|
|
808
834
|
expect(JSON.parse(hashmap2)).toEqual([
|
|
809
835
|
'revalidated-fetch-revalidate15-default-page',
|
|
810
836
|
]);
|
|
811
837
|
const hashmap3 = (await redisClient.hGet(
|
|
812
|
-
process.env.VERCEL_URL +
|
|
838
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
813
839
|
'/api/revalidated-fetch',
|
|
814
840
|
)) as string;
|
|
815
841
|
expect(JSON.parse(hashmap3)).toEqual([
|
|
@@ -849,20 +875,22 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
849
875
|
expect(keys1.length).toBe(0);
|
|
850
876
|
|
|
851
877
|
const hashmap1 = await redisClient.hGet(
|
|
852
|
-
process.env.VERCEL_URL +
|
|
878
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
853
879
|
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
854
880
|
);
|
|
855
881
|
expect(hashmap1).toBeNull();
|
|
856
882
|
|
|
857
883
|
// sub-fetch-request is not removed directly but will be removed on next get request
|
|
884
|
+
const nestedFetchCacheEntryKey = await findFetchCacheEntryKey(
|
|
885
|
+
'revalidated-fetch-revalidate15-default-page',
|
|
886
|
+
);
|
|
858
887
|
const keys2 = await redisClient.keys(
|
|
859
|
-
process.env.VERCEL_URL +
|
|
860
|
-
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
888
|
+
process.env.VERCEL_URL + nestedFetchCacheEntryKey,
|
|
861
889
|
);
|
|
862
890
|
expect(keys2.length).toBe(1);
|
|
863
891
|
const hashmap2 = await redisClient.hGet(
|
|
864
|
-
process.env.VERCEL_URL +
|
|
865
|
-
|
|
892
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
893
|
+
nestedFetchCacheEntryKey,
|
|
866
894
|
);
|
|
867
895
|
expect(hashmap2).toBeDefined();
|
|
868
896
|
|
|
@@ -881,7 +909,7 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
881
909
|
);
|
|
882
910
|
expect(keys3.length).toBe(1);
|
|
883
911
|
const hashmap3 = (await redisClient.hGet(
|
|
884
|
-
process.env.VERCEL_URL +
|
|
912
|
+
process.env.VERCEL_URL + SHARED_TAGS_KEY,
|
|
885
913
|
'/api/revalidated-fetch',
|
|
886
914
|
)) as string;
|
|
887
915
|
expect(JSON.parse(hashmap3)).toEqual([
|
|
@@ -904,9 +932,14 @@ describe('Next.js Turbo Redis Cache Integration', () => {
|
|
|
904
932
|
expect(timestamp).toBeDefined();
|
|
905
933
|
expect(Number(timestamp)).toBeGreaterThan(Number(firstTimestamp));
|
|
906
934
|
|
|
907
|
-
//
|
|
935
|
+
// Next.js 16.3 revalidates nested fetch entries when the parent page path
|
|
936
|
+
// is revalidated; older versions keep the nested fetch cache intact.
|
|
908
937
|
expect(secondCounter).toBeDefined();
|
|
909
|
-
|
|
938
|
+
if (NEXT_TEST_APP === 'next-app-16-3-0') {
|
|
939
|
+
expect(Number(secondCounter)).toBeGreaterThan(Number(firstCounter));
|
|
940
|
+
} else {
|
|
941
|
+
expect(secondCounter).toBe(firstCounter);
|
|
942
|
+
}
|
|
910
943
|
});
|
|
911
944
|
});
|
|
912
945
|
|
|
@@ -48,6 +48,18 @@ async function delay(ms: number) {
|
|
|
48
48
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function expectIsrRedisTtl(ttl: number) {
|
|
52
|
+
// Next.js 16.3+ passes cacheControl.expire (~1 year) for ISR pages.
|
|
53
|
+
// Redis TTL is keyed on expire, not 2 × revalidate.
|
|
54
|
+
if (NEXT_PAGES_TEST_APP.includes('16-3')) {
|
|
55
|
+
expect(ttl).toBeGreaterThan(2 * ISR_REVALIDATE_SECONDS);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// VERCEL_ENV=production -> expire age is 2 * revalidate
|
|
59
|
+
expect(ttl).toBeGreaterThan(2 * ISR_REVALIDATE_SECONDS - 30);
|
|
60
|
+
expect(ttl).toBeLessThanOrEqual(2 * ISR_REVALIDATE_SECONDS);
|
|
61
|
+
}
|
|
62
|
+
|
|
51
63
|
async function runCommand(cmd: string, args: string[], cwd: string) {
|
|
52
64
|
return new Promise((resolve, reject) => {
|
|
53
65
|
let stderr = '';
|
|
@@ -233,9 +245,7 @@ describe('Pages Router Redis cache integration (two instances)', () => {
|
|
|
233
245
|
const ttl = await redisClient.ttl(
|
|
234
246
|
process.env.VERCEL_URL + '/isr/prebuilt',
|
|
235
247
|
);
|
|
236
|
-
|
|
237
|
-
expect(ttl).toBeGreaterThan(2 * ISR_REVALIDATE_SECONDS - 30);
|
|
238
|
-
expect(ttl).toBeLessThanOrEqual(2 * ISR_REVALIDATE_SECONDS);
|
|
248
|
+
expectIsrRedisTtl(ttl);
|
|
239
249
|
});
|
|
240
250
|
|
|
241
251
|
it('instance B serves the identical cached HTML from the shared Redis', async () => {
|
|
@@ -342,12 +352,12 @@ describe('Pages Router Redis cache integration (two instances)', () => {
|
|
|
342
352
|
tags: ['_N_T_/isr/not-found'],
|
|
343
353
|
});
|
|
344
354
|
|
|
345
|
-
// TTL
|
|
355
|
+
// TTL: Next.js 16.3+ keys on cacheControl.expire (SWR-safe); older
|
|
356
|
+
// versions use estimateExpireAge(revalidate).
|
|
346
357
|
const ttl = await redisClient.ttl(
|
|
347
358
|
process.env.VERCEL_URL + '/isr/not-found',
|
|
348
359
|
);
|
|
349
|
-
|
|
350
|
-
expect(ttl).toBeLessThanOrEqual(2 * ISR_REVALIDATE_SECONDS);
|
|
360
|
+
expectIsrRedisTtl(ttl);
|
|
351
361
|
});
|
|
352
362
|
|
|
353
363
|
it('instance B serves the 404 from the shared cache', async () => {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { SyncedMap } from '../../../src/SyncedMap';
|
|
3
|
+
|
|
4
|
+
class HashMockClient {
|
|
5
|
+
hash = new Map<string, string>();
|
|
6
|
+
stringKeys: string[] = [];
|
|
7
|
+
hDelCalls: string[][] = [];
|
|
8
|
+
isOpen = true;
|
|
9
|
+
isReady = true;
|
|
10
|
+
|
|
11
|
+
on() {
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async connect() {
|
|
16
|
+
this.isOpen = true;
|
|
17
|
+
this.isReady = true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async configGet() {
|
|
21
|
+
return { 'notify-keyspace-events': 'Exe' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async subscribe() {}
|
|
25
|
+
|
|
26
|
+
async quit() {}
|
|
27
|
+
|
|
28
|
+
async disconnect() {
|
|
29
|
+
this.isOpen = false;
|
|
30
|
+
this.isReady = false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
duplicate() {
|
|
34
|
+
return new HashMockClient();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async hScan() {
|
|
38
|
+
return {
|
|
39
|
+
cursor: 0,
|
|
40
|
+
tuples: [...this.hash.entries()].map(([field, value]) => ({
|
|
41
|
+
field,
|
|
42
|
+
value,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async scan() {
|
|
48
|
+
return { cursor: 0, keys: this.stringKeys };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async hSet(_key: string, field: string, value: string) {
|
|
52
|
+
this.hash.set(field, value);
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async hDel(_key: string, fields: string | string[]) {
|
|
57
|
+
const list = Array.isArray(fields) ? fields : [fields];
|
|
58
|
+
this.hDelCalls.push(list);
|
|
59
|
+
for (const field of list) {
|
|
60
|
+
this.hash.delete(field);
|
|
61
|
+
}
|
|
62
|
+
return list.length;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async publish() {
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('SyncedMap orphan cleanup', () => {
|
|
71
|
+
it('deletes hash fields whose names are not Redis string keys', async () => {
|
|
72
|
+
const client = new HashMockClient();
|
|
73
|
+
const map = new SyncedMap<number>({
|
|
74
|
+
client: client as never,
|
|
75
|
+
keyPrefix: 'app:',
|
|
76
|
+
redisKey: '__revalidated_tags__',
|
|
77
|
+
database: 0,
|
|
78
|
+
querySize: 50,
|
|
79
|
+
filterKeys: () => true,
|
|
80
|
+
});
|
|
81
|
+
await map.waitUntilReady();
|
|
82
|
+
await map.set('posts', 1);
|
|
83
|
+
|
|
84
|
+
client.stringKeys = ['app:some-cache-entry'];
|
|
85
|
+
await (map as any).initialSync();
|
|
86
|
+
|
|
87
|
+
expect(client.hDelCalls.flat()).toContain('posts');
|
|
88
|
+
expect(map.get('posts')).toBeUndefined();
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('keeps tag-manifest fields when withoutOrphanCleanup is set', async () => {
|
|
92
|
+
const client = new HashMockClient();
|
|
93
|
+
const map = new SyncedMap<number>({
|
|
94
|
+
client: client as never,
|
|
95
|
+
keyPrefix: 'app:',
|
|
96
|
+
redisKey: '__cacheComponents_revalidated_tags__',
|
|
97
|
+
database: 0,
|
|
98
|
+
querySize: 50,
|
|
99
|
+
filterKeys: () => true,
|
|
100
|
+
customizedSync: { withoutOrphanCleanup: true },
|
|
101
|
+
});
|
|
102
|
+
await map.waitUntilReady();
|
|
103
|
+
await map.set('posts', 1);
|
|
104
|
+
|
|
105
|
+
client.stringKeys = ['app:some-cache-entry'];
|
|
106
|
+
await (map as any).initialSync();
|
|
107
|
+
|
|
108
|
+
expect(client.hDelCalls).toEqual([]);
|
|
109
|
+
expect(map.get('posts')).toBe(1);
|
|
110
|
+
});
|
|
111
|
+
});
|