@trieb.work/nextjs-turbo-redis-cache 1.2.1 → 1.4.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 +31 -6
- package/.github/workflows/release.yml +7 -3
- package/.next/trace +11 -0
- package/.vscode/settings.json +10 -0
- package/CHANGELOG.md +71 -0
- package/README.md +154 -34
- package/dist/index.d.mts +96 -20
- package/dist/index.d.ts +96 -20
- package/dist/index.js +317 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +313 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +14 -7
- package/scripts/vitest-run-staged.cjs +1 -1
- package/src/CachedHandler.ts +23 -9
- package/src/DeduplicatedRequestHandler.ts +50 -1
- package/src/RedisStringsHandler.ts +331 -91
- package/src/SyncedMap.ts +74 -4
- package/src/ZodHandler.ts +45 -0
- package/src/index.ts +4 -2
- package/src/utils/debug.ts +30 -0
- package/src/utils/json.ts +26 -0
- package/test/integration/next-app-15-0-3/README.md +36 -0
- package/test/integration/next-app-15-0-3/eslint.config.mjs +16 -0
- package/test/integration/next-app-15-0-3/next.config.js +6 -0
- package/test/integration/next-app-15-0-3/package-lock.json +5833 -0
- package/test/integration/next-app-15-0-3/package.json +29 -0
- package/test/integration/next-app-15-0-3/pnpm-lock.yaml +3679 -0
- package/test/integration/next-app-15-0-3/postcss.config.mjs +5 -0
- package/test/integration/next-app-15-0-3/public/file.svg +1 -0
- package/test/integration/next-app-15-0-3/public/globe.svg +1 -0
- package/test/integration/next-app-15-0-3/public/next.svg +1 -0
- package/test/integration/next-app-15-0-3/public/vercel.svg +1 -0
- package/test/integration/next-app-15-0-3/public/window.svg +1 -0
- package/test/integration/next-app-15-0-3/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/integration/next-app-15-0-3/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/integration/next-app-15-0-3/src/app/api/revalidatePath/route.ts +15 -0
- package/test/integration/next-app-15-0-3/src/app/api/revalidateTag/route.ts +15 -0
- package/test/integration/next-app-15-0-3/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/integration/next-app-15-0-3/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/integration/next-app-15-0-3/src/app/globals.css +26 -0
- package/test/integration/next-app-15-0-3/src/app/layout.tsx +59 -0
- package/test/integration/next-app-15-0-3/src/app/page.tsx +755 -0
- package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/integration/next-app-15-0-3/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-0-3/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/integration/next-app-15-0-3/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/integration/next-app-15-0-3/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-0-3/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app-15-0-3/tsconfig.json +27 -0
- package/test/integration/next-app-15-3-2/README.md +36 -0
- package/test/integration/next-app-15-3-2/eslint.config.mjs +16 -0
- package/test/integration/next-app-15-3-2/next.config.js +6 -0
- package/test/integration/next-app-15-3-2/package-lock.json +5969 -0
- package/test/integration/next-app-15-3-2/package.json +33 -0
- package/test/integration/next-app-15-3-2/pnpm-lock.yaml +3688 -0
- package/test/integration/next-app-15-3-2/postcss.config.mjs +5 -0
- package/test/integration/next-app-15-3-2/public/file.svg +1 -0
- package/test/integration/next-app-15-3-2/public/globe.svg +1 -0
- package/test/integration/next-app-15-3-2/public/next.svg +1 -0
- package/test/integration/next-app-15-3-2/public/vercel.svg +1 -0
- package/test/integration/next-app-15-3-2/public/window.svg +1 -0
- package/test/integration/next-app-15-3-2/src/app/api/cached-static-fetch/route.ts +18 -0
- package/test/integration/next-app-15-3-2/src/app/api/nested-fetch-in-api-route/revalidated-fetch/route.ts +27 -0
- package/test/integration/next-app-15-3-2/src/app/api/revalidatePath/route.ts +15 -0
- package/test/integration/next-app-15-3-2/src/app/api/revalidateTag/route.ts +15 -0
- package/test/integration/next-app-15-3-2/src/app/api/revalidated-fetch/route.ts +17 -0
- package/test/integration/next-app-15-3-2/src/app/api/uncached-fetch/route.ts +15 -0
- package/test/integration/next-app-15-3-2/src/app/globals.css +26 -0
- package/test/integration/next-app-15-3-2/src/app/layout.tsx +59 -0
- package/test/integration/next-app-15-3-2/src/app/page.tsx +755 -0
- package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/revalidate15--default-page/page.tsx +34 -0
- package/test/integration/next-app-15-3-2/src/app/pages/cached-static-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-3-2/src/app/pages/no-fetch/default-page/page.tsx +55 -0
- package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/revalidate15--default-page/page.tsx +35 -0
- package/test/integration/next-app-15-3-2/src/app/pages/revalidated-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/default--force-dynamic-page/page.tsx +19 -0
- package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/revalidate15--default-page/page.tsx +32 -0
- package/test/integration/next-app-15-3-2/src/app/pages/uncached-fetch/revalidate15--force-dynamic-page/page.tsx +25 -0
- package/test/integration/next-app-15-3-2/src/app/revalidation-interface.tsx +267 -0
- package/test/integration/next-app-15-3-2/tsconfig.json +27 -0
- package/test/integration/next-app-customized/README.md +36 -0
- package/test/integration/next-app-customized/customized-cache-handler.js +34 -0
- package/test/integration/next-app-customized/eslint.config.mjs +16 -0
- package/test/integration/next-app-customized/next.config.js +6 -0
- package/test/integration/nextjs-cache-handler.integration.test.ts +859 -0
- package/vite.config.ts +23 -8
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import { createClient } from 'redis';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { CacheEntry } from '../../src/RedisStringsHandler';
|
|
7
|
+
import { revalidate as next1503_revalidatedFetch_route } from './next-app-15-0-3/src/app/api/revalidated-fetch/route';
|
|
8
|
+
|
|
9
|
+
// const NEXT_APP_DIR = join(__dirname, 'next-app-15-0-3');
|
|
10
|
+
const NEXT_APP_DIR = join(__dirname, 'next-app-15-3-2');
|
|
11
|
+
console.log('NEXT_APP_DIR', NEXT_APP_DIR);
|
|
12
|
+
const NEXT_START_PORT = 3055;
|
|
13
|
+
const NEXT_START_URL = `http://localhost:${NEXT_START_PORT}`;
|
|
14
|
+
|
|
15
|
+
const REDIS_BACKGROUND_SYNC_DELAY = 250; //ms delay to prevent flaky tests in slow CI environments
|
|
16
|
+
|
|
17
|
+
let nextProcess;
|
|
18
|
+
let redisClient;
|
|
19
|
+
|
|
20
|
+
async function delay(ms: number) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function runCommand(cmd: string, args: string[], cwd: string) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
let stderr = '';
|
|
27
|
+
let stdout = '';
|
|
28
|
+
const proc = spawn(cmd, args, { cwd, stdio: 'pipe' });
|
|
29
|
+
|
|
30
|
+
proc.stdout.on('data', (data) => {
|
|
31
|
+
if (process.env.DEBUG_INTEGRATION) {
|
|
32
|
+
console.log(data.toString());
|
|
33
|
+
}
|
|
34
|
+
stdout += data.toString();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
proc.stderr.on('data', (data) => {
|
|
38
|
+
if (process.env.DEBUG_INTEGRATION) {
|
|
39
|
+
console.error(data.toString());
|
|
40
|
+
}
|
|
41
|
+
stderr += data.toString();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
proc.on('exit', (code) => {
|
|
45
|
+
if (code === 0) resolve(undefined);
|
|
46
|
+
else {
|
|
47
|
+
reject(
|
|
48
|
+
new Error(
|
|
49
|
+
`${cmd} ${args.join(' ')} failed with code ${code}\n` +
|
|
50
|
+
`stdout: ${stdout}\n` +
|
|
51
|
+
`stderr: ${stderr}`,
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function waitForServer(url, timeout = 20000) {
|
|
60
|
+
const start = Date.now();
|
|
61
|
+
while (Date.now() - start < timeout) {
|
|
62
|
+
try {
|
|
63
|
+
const res = await fetch(url + '/api/cached-static-fetch');
|
|
64
|
+
if (res.ok) return;
|
|
65
|
+
} catch {}
|
|
66
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
67
|
+
}
|
|
68
|
+
throw new Error('Next.js server did not start in time');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe('Next.js Turbo Redis Cache Integration', () => {
|
|
72
|
+
beforeAll(async () => {
|
|
73
|
+
// If there was detected to run a server before (any old server which was not stopped correctly), kill it
|
|
74
|
+
try {
|
|
75
|
+
const res = await fetch(NEXT_START_URL + '/api/cached-static-fetch');
|
|
76
|
+
if (res.ok) {
|
|
77
|
+
await runCommand('pkill', ['next'], NEXT_APP_DIR);
|
|
78
|
+
}
|
|
79
|
+
} catch {}
|
|
80
|
+
|
|
81
|
+
// Set up environment variables
|
|
82
|
+
process.env.VERCEL_ENV = 'production';
|
|
83
|
+
process.env.VERCEL_URL =
|
|
84
|
+
'integration-test-' + Math.random().toString(36).substring(2, 15);
|
|
85
|
+
console.log('redis key prefix is:', process.env.VERCEL_URL);
|
|
86
|
+
|
|
87
|
+
// Only override if redis env vars if not set. This can be set in the CI env.
|
|
88
|
+
process.env.REDISHOST = process.env.REDISHOST || 'localhost';
|
|
89
|
+
process.env.REDISPORT = process.env.REDISPORT || '6379';
|
|
90
|
+
process.env.NEXT_START_PORT = String(NEXT_START_PORT);
|
|
91
|
+
|
|
92
|
+
if (process.env.SKIP_BUILD === 'true') {
|
|
93
|
+
console.log('skipping build');
|
|
94
|
+
} else {
|
|
95
|
+
// Build Next.js app first
|
|
96
|
+
await runCommand('pnpm', ['i'], NEXT_APP_DIR);
|
|
97
|
+
console.log('pnpm i done');
|
|
98
|
+
await runCommand('pnpm', ['build'], NEXT_APP_DIR);
|
|
99
|
+
console.log('pnpm build done');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Start Next.js app
|
|
103
|
+
nextProcess = spawn(
|
|
104
|
+
'npx',
|
|
105
|
+
['next', 'start', '-p', String(NEXT_START_PORT)],
|
|
106
|
+
{
|
|
107
|
+
cwd: NEXT_APP_DIR,
|
|
108
|
+
env: {
|
|
109
|
+
...process.env,
|
|
110
|
+
},
|
|
111
|
+
stdio: 'pipe',
|
|
112
|
+
},
|
|
113
|
+
);
|
|
114
|
+
if (process.env.DEBUG_INTEGRATION) {
|
|
115
|
+
nextProcess.stdout.on('data', (data) => {
|
|
116
|
+
console.log(`stdout: ${data}`);
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
nextProcess.stderr.on('data', (data) => {
|
|
121
|
+
console.error(`stderr: ${data}`);
|
|
122
|
+
});
|
|
123
|
+
await waitForServer(NEXT_START_URL);
|
|
124
|
+
console.log('next start successful');
|
|
125
|
+
|
|
126
|
+
// Connect to Redis
|
|
127
|
+
redisClient = createClient({
|
|
128
|
+
url: `redis://${process.env.REDISHOST}:${process.env.REDISPORT}`,
|
|
129
|
+
});
|
|
130
|
+
await redisClient.connect();
|
|
131
|
+
|
|
132
|
+
console.log('redis key prefix is:', process.env.VERCEL_URL);
|
|
133
|
+
}, 60_000);
|
|
134
|
+
|
|
135
|
+
afterAll(async () => {
|
|
136
|
+
if (process.env.KEEP_SERVER_RUNNING === 'true') {
|
|
137
|
+
console.log('keeping server running');
|
|
138
|
+
} else {
|
|
139
|
+
if (nextProcess) nextProcess.kill();
|
|
140
|
+
}
|
|
141
|
+
if (redisClient) await redisClient.quit();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('should have the correct caching behavior for API routes', () => {
|
|
145
|
+
describe('should cache static API routes in Redis', () => {
|
|
146
|
+
let counter1: number;
|
|
147
|
+
|
|
148
|
+
it('First request (should increment counter)', async () => {
|
|
149
|
+
const res = await fetch(NEXT_START_URL + '/api/cached-static-fetch');
|
|
150
|
+
const data: any = await res.json();
|
|
151
|
+
expect(data.counter).toBe(1);
|
|
152
|
+
counter1 = data.counter;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('Second request (should hit cache, counter should not increment if cache works)', async () => {
|
|
156
|
+
const res = await fetch(NEXT_START_URL + '/api/cached-static-fetch');
|
|
157
|
+
const data: any = await res.json();
|
|
158
|
+
// If cache is working, counter should stay 1; if not, it will increment
|
|
159
|
+
expect(data.counter).toBe(counter1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('The data in the redis key should match the expected format', async () => {
|
|
163
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
164
|
+
const keys = await redisClient.keys(process.env.VERCEL_URL + '*');
|
|
165
|
+
expect(keys.length).toBeGreaterThan(0);
|
|
166
|
+
|
|
167
|
+
// check the content of redis key
|
|
168
|
+
const value = await redisClient.get(
|
|
169
|
+
process.env.VERCEL_URL + '/api/cached-static-fetch',
|
|
170
|
+
);
|
|
171
|
+
expect(value).toBeDefined();
|
|
172
|
+
const cacheEntry: CacheEntry = JSON.parse(value);
|
|
173
|
+
|
|
174
|
+
// The format should be as expected
|
|
175
|
+
expect(cacheEntry).toEqual({
|
|
176
|
+
value: {
|
|
177
|
+
kind: 'APP_ROUTE',
|
|
178
|
+
status: 200,
|
|
179
|
+
body: { $binary: 'eyJjb3VudGVyIjoxfQ==' },
|
|
180
|
+
headers: {
|
|
181
|
+
'cache-control': 'public, max-age=1',
|
|
182
|
+
'content-type': 'application/json',
|
|
183
|
+
'x-next-cache-tags':
|
|
184
|
+
'_N_T_/layout,_N_T_/api/layout,_N_T_/api/cached-static-fetch/layout,_N_T_/api/cached-static-fetch/route,_N_T_/api/cached-static-fetch',
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
lastModified: expect.any(Number),
|
|
188
|
+
tags: [
|
|
189
|
+
'_N_T_/layout',
|
|
190
|
+
'_N_T_/api/layout',
|
|
191
|
+
'_N_T_/api/cached-static-fetch/layout',
|
|
192
|
+
'_N_T_/api/cached-static-fetch/route',
|
|
193
|
+
'_N_T_/api/cached-static-fetch',
|
|
194
|
+
],
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect((cacheEntry.value as any).kind).toBe('APP_ROUTE');
|
|
198
|
+
const bodyBuffer = Buffer.from(
|
|
199
|
+
(cacheEntry.value as any)?.body?.$binary,
|
|
200
|
+
'base64',
|
|
201
|
+
);
|
|
202
|
+
const bodyJson = JSON.parse(bodyBuffer.toString('utf-8'));
|
|
203
|
+
expect(bodyJson.counter).toBe(counter1);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('A request to revalidatePath API should remove the route from redis (string and hashmap)', async () => {
|
|
207
|
+
const revalidateRes = await fetch(
|
|
208
|
+
NEXT_START_URL + '/api/revalidatePath?path=/api/cached-static-fetch',
|
|
209
|
+
);
|
|
210
|
+
const revalidateResJson: any = await revalidateRes.json();
|
|
211
|
+
expect(revalidateResJson.success).toBe(true);
|
|
212
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
213
|
+
|
|
214
|
+
// check Redis keys
|
|
215
|
+
const keys = await redisClient.keys(
|
|
216
|
+
process.env.VERCEL_URL + '/api/cached-static-fetch',
|
|
217
|
+
);
|
|
218
|
+
expect(keys.length).toBe(0);
|
|
219
|
+
|
|
220
|
+
const hashmap = await redisClient.hGet(
|
|
221
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
222
|
+
'/api/cached-static-fetch',
|
|
223
|
+
);
|
|
224
|
+
expect(hashmap).toBeNull();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('A new request after the revalidation should increment the counter (because the route was re-evaluated)', async () => {
|
|
228
|
+
const res = await fetch(NEXT_START_URL + '/api/cached-static-fetch');
|
|
229
|
+
const data: any = await res.json();
|
|
230
|
+
expect(data.counter).toBe(counter1 + 1);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
234
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
235
|
+
// check Redis keys
|
|
236
|
+
const keys = await redisClient.keys(
|
|
237
|
+
process.env.VERCEL_URL + '/api/cached-static-fetch',
|
|
238
|
+
);
|
|
239
|
+
expect(keys.length).toBe(1);
|
|
240
|
+
|
|
241
|
+
const hashmap = await redisClient.hGet(
|
|
242
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
243
|
+
'/api/cached-static-fetch',
|
|
244
|
+
);
|
|
245
|
+
expect(JSON.parse(hashmap)).toEqual([
|
|
246
|
+
'_N_T_/layout',
|
|
247
|
+
'_N_T_/api/layout',
|
|
248
|
+
'_N_T_/api/cached-static-fetch/layout',
|
|
249
|
+
'_N_T_/api/cached-static-fetch/route',
|
|
250
|
+
'_N_T_/api/cached-static-fetch',
|
|
251
|
+
]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('should cache revalidation API routes in Redis', () => {
|
|
256
|
+
let counter1: number;
|
|
257
|
+
|
|
258
|
+
it('First request (should increment counter)', async () => {
|
|
259
|
+
const res = await fetch(NEXT_START_URL + '/api/revalidated-fetch');
|
|
260
|
+
const data: any = await res.json();
|
|
261
|
+
expect(data.counter).toBe(1);
|
|
262
|
+
counter1 = data.counter;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('Second request which is send in revalidation time should hit cache (counter should not increment)', async () => {
|
|
266
|
+
const res = await fetch(NEXT_START_URL + '/api/revalidated-fetch');
|
|
267
|
+
const data: any = await res.json();
|
|
268
|
+
expect(data.counter).toBe(counter1);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
if (process.env.SKIP_OPTIONAL_LONG_RUNNER_TESTS !== 'true') {
|
|
272
|
+
const FIRST_DELAY = 6000;
|
|
273
|
+
const SECOND_DELAY = 1000;
|
|
274
|
+
it('Third request which is send directly after revalidation time will still serve cache but trigger re-evaluation (stale-while-revalidate)', async () => {
|
|
275
|
+
await delay(FIRST_DELAY);
|
|
276
|
+
const res = await fetch(NEXT_START_URL + '/api/revalidated-fetch');
|
|
277
|
+
const data: any = await res.json();
|
|
278
|
+
expect(data.counter).toBe(counter1);
|
|
279
|
+
}, 10_000);
|
|
280
|
+
|
|
281
|
+
it('Third request which is send directly after revalidation time will serve re-evaluated data (stale-while-revalidate)', async () => {
|
|
282
|
+
await delay(SECOND_DELAY);
|
|
283
|
+
const res = await fetch(NEXT_START_URL + '/api/revalidated-fetch');
|
|
284
|
+
const data: any = await res.json();
|
|
285
|
+
expect(data.counter).toBe(counter1 + 1);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('After expiration, the redis key should be removed from redis and the hashmap', async () => {
|
|
289
|
+
const ttl = await redisClient.ttl(
|
|
290
|
+
process.env.VERCEL_URL + '/api/revalidated-fetch',
|
|
291
|
+
);
|
|
292
|
+
expect(ttl).toBeLessThan(2 * next1503_revalidatedFetch_route);
|
|
293
|
+
expect(ttl).toBeGreaterThan(
|
|
294
|
+
2 * next1503_revalidatedFetch_route -
|
|
295
|
+
FIRST_DELAY -
|
|
296
|
+
SECOND_DELAY -
|
|
297
|
+
REDIS_BACKGROUND_SYNC_DELAY,
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
await delay(ttl * 1000 + 500);
|
|
301
|
+
|
|
302
|
+
// check Redis keys
|
|
303
|
+
const keys = await redisClient.keys(
|
|
304
|
+
process.env.VERCEL_URL + '/api/revalidated-fetch',
|
|
305
|
+
);
|
|
306
|
+
expect(keys.length).toBe(0);
|
|
307
|
+
|
|
308
|
+
await delay(1000);
|
|
309
|
+
|
|
310
|
+
// The key should also be removed from the hashmap
|
|
311
|
+
const hashmap = await redisClient.hGet(
|
|
312
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
313
|
+
'/api/revalidated-fetch',
|
|
314
|
+
);
|
|
315
|
+
expect(hashmap).toBeNull();
|
|
316
|
+
}, 15_000);
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('should not cache uncached API routes in Redis', () => {
|
|
321
|
+
let counter1: number;
|
|
322
|
+
|
|
323
|
+
it('First request should increment counter', async () => {
|
|
324
|
+
const res1 = await fetch(NEXT_START_URL + '/api/uncached-fetch');
|
|
325
|
+
const data1: any = await res1.json();
|
|
326
|
+
expect(data1.counter).toBe(1);
|
|
327
|
+
counter1 = data1.counter;
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('Second request should hit cache (counter should not increment if cache works)', async () => {
|
|
331
|
+
const res2 = await fetch(NEXT_START_URL + '/api/uncached-fetch');
|
|
332
|
+
const data2: any = await res2.json();
|
|
333
|
+
|
|
334
|
+
// If not caching it is working request 2 should be higher as request one
|
|
335
|
+
expect(data2.counter).toBe(counter1 + 1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('The redis key should not be set', async () => {
|
|
339
|
+
// check the content of redis key
|
|
340
|
+
const value = await redisClient.get(
|
|
341
|
+
process.env.VERCEL_URL + '/api/uncached-fetch',
|
|
342
|
+
);
|
|
343
|
+
expect(value).toBeNull();
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('should cache a nested fetch request inside a uncached API route', () => {
|
|
348
|
+
describe('should cache the nested fetch request (but not the API route itself)', () => {
|
|
349
|
+
let counter: number;
|
|
350
|
+
let subCounter: number;
|
|
351
|
+
|
|
352
|
+
it('should deduplicate requests to the sub-fetch-request, but not to the API route itself', async () => {
|
|
353
|
+
// make two requests, both should return the same subFetchData but different counter
|
|
354
|
+
const res1 = await fetch(
|
|
355
|
+
NEXT_START_URL + '/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
356
|
+
);
|
|
357
|
+
const res2 = await fetch(
|
|
358
|
+
NEXT_START_URL + '/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
359
|
+
);
|
|
360
|
+
const [data1, data2]: any[] = await Promise.all([
|
|
361
|
+
res1.json(),
|
|
362
|
+
res2.json(),
|
|
363
|
+
]);
|
|
364
|
+
|
|
365
|
+
// API route counter itself increments for each request
|
|
366
|
+
// But we do not know which request is first and which is second
|
|
367
|
+
if (data1.counter < data2.counter) {
|
|
368
|
+
expect(data2.counter).toBeGreaterThan(data1.counter);
|
|
369
|
+
counter = data2.counter;
|
|
370
|
+
} else {
|
|
371
|
+
expect(data1.counter).toBeGreaterThan(data2.counter);
|
|
372
|
+
counter = data1.counter;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// API route counter of revalidated sub-fetch-request should be the same (request deduplication of fetch requests)
|
|
376
|
+
expect(data1.subFetchData.counter).toBe(data1.subFetchData.counter);
|
|
377
|
+
subCounter = data1.subFetchData.counter;
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
if (process.env.SKIP_OPTIONAL_LONG_RUNNER_TESTS !== 'true') {
|
|
381
|
+
it('should return the same subFetchData after 2 seconds (regular caching within revalidation interval (=3s) works)', async () => {
|
|
382
|
+
// make another request after 2 seconds, it should return the same subFetchData
|
|
383
|
+
await delay(2000); // 2s < 3s (revalidate interval)
|
|
384
|
+
const res = await fetch(
|
|
385
|
+
NEXT_START_URL +
|
|
386
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
387
|
+
);
|
|
388
|
+
const data: any = await res.json();
|
|
389
|
+
|
|
390
|
+
expect(data.counter).toBe(counter + 1);
|
|
391
|
+
expect(data.subFetchData.counter).toBe(subCounter);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should return the same subFetchData after 2 seconds and new data after another 2 seconds (caching while revalidation works)', async () => {
|
|
395
|
+
// make another request after another 2 seconds, it should return the same subFetchData (caching while revalidation works)
|
|
396
|
+
await delay(2000); // 2s+2s < 3s*2 (=TTL = revalidate=3s*2)
|
|
397
|
+
const res1 = await fetch(
|
|
398
|
+
NEXT_START_URL +
|
|
399
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
400
|
+
);
|
|
401
|
+
const data1: any = await res1.json();
|
|
402
|
+
expect(data1.counter).toBe(counter + 2);
|
|
403
|
+
expect(data1.subFetchData.counter).toBe(subCounter);
|
|
404
|
+
|
|
405
|
+
// make another request directly after first request which was still in TTL, it should return new data (caching while revalidation works)
|
|
406
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
407
|
+
const res2 = await fetch(
|
|
408
|
+
NEXT_START_URL +
|
|
409
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
410
|
+
);
|
|
411
|
+
const data2: any = await res2.json();
|
|
412
|
+
expect(data2.counter).toBe(counter + 3);
|
|
413
|
+
expect(data2.subFetchData.counter).toBe(subCounter + 1);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('A request to revalidatePage API should remove the route from redis (string and hashmap)', async () => {
|
|
417
|
+
const revalidateRes = await fetch(
|
|
418
|
+
NEXT_START_URL +
|
|
419
|
+
'/api/revalidatePath?path=/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
420
|
+
);
|
|
421
|
+
const revalidateResJson: any = await revalidateRes.json();
|
|
422
|
+
expect(revalidateResJson.success).toBe(true);
|
|
423
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
424
|
+
|
|
425
|
+
// check Redis keys
|
|
426
|
+
const keys = await redisClient.keys(
|
|
427
|
+
process.env.VERCEL_URL +
|
|
428
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
429
|
+
);
|
|
430
|
+
expect(keys.length).toBe(0);
|
|
431
|
+
|
|
432
|
+
const hashmap = await redisClient.hGet(
|
|
433
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
434
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
435
|
+
);
|
|
436
|
+
expect(hashmap).toBeNull();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('A new request after the revalidation should increment the counter (because the route was re-evaluated)', async () => {
|
|
440
|
+
const res = await fetch(
|
|
441
|
+
NEXT_START_URL +
|
|
442
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
443
|
+
);
|
|
444
|
+
const data: any = await res.json();
|
|
445
|
+
expect(data.counter).toBe(counter + 4);
|
|
446
|
+
expect(data.subFetchData.counter).toBe(subCounter + 2);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
450
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
451
|
+
|
|
452
|
+
// This cache entry key is the key of the sub-fetch-request, it will be generated by nextjs based on the headers/payload etc.
|
|
453
|
+
// So it should stay the same unless nextjs will change something in there implementation
|
|
454
|
+
const cacheEntryKey =
|
|
455
|
+
'094a786b7ad391852168d3a7bcf75736777697d24a856a0089837f4b7de921df';
|
|
456
|
+
|
|
457
|
+
// check Redis keys
|
|
458
|
+
const keys = await redisClient.keys(
|
|
459
|
+
process.env.VERCEL_URL + cacheEntryKey,
|
|
460
|
+
);
|
|
461
|
+
expect(keys.length).toBe(1);
|
|
462
|
+
|
|
463
|
+
const hashmap = await redisClient.hGet(
|
|
464
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
465
|
+
cacheEntryKey,
|
|
466
|
+
);
|
|
467
|
+
expect(JSON.parse(hashmap)).toEqual([
|
|
468
|
+
'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
469
|
+
]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it('A request to revalidateTag API should remove the route from redis (string and hashmap)', async () => {
|
|
473
|
+
const revalidateRes = await fetch(
|
|
474
|
+
NEXT_START_URL +
|
|
475
|
+
'/api/revalidateTag?tag=revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
476
|
+
);
|
|
477
|
+
const revalidateResJson: any = await revalidateRes.json();
|
|
478
|
+
expect(revalidateResJson.success).toBe(true);
|
|
479
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
480
|
+
|
|
481
|
+
// check Redis keys
|
|
482
|
+
const keys = await redisClient.keys(
|
|
483
|
+
process.env.VERCEL_URL +
|
|
484
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
485
|
+
);
|
|
486
|
+
expect(keys.length).toBe(0);
|
|
487
|
+
|
|
488
|
+
const hashmap = await redisClient.hGet(
|
|
489
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
490
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
491
|
+
);
|
|
492
|
+
expect(hashmap).toBeNull();
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('Another new request after the revalidation should increment the counter (because the route was re-evaluated)', async () => {
|
|
496
|
+
const res = await fetch(
|
|
497
|
+
NEXT_START_URL +
|
|
498
|
+
'/api/nested-fetch-in-api-route/revalidated-fetch',
|
|
499
|
+
);
|
|
500
|
+
const data: any = await res.json();
|
|
501
|
+
expect(data.counter).toBe(counter + 5);
|
|
502
|
+
expect(data.subFetchData.counter).toBe(subCounter + 3);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
506
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
507
|
+
// This cache entry key is the key of the sub-fetch-request, it will be generated by nextjs based on the headers/payload etc.
|
|
508
|
+
// So it should stay the same unless nextjs will change something in there implementation
|
|
509
|
+
const cacheEntryKey =
|
|
510
|
+
'094a786b7ad391852168d3a7bcf75736777697d24a856a0089837f4b7de921df';
|
|
511
|
+
|
|
512
|
+
// check Redis keys
|
|
513
|
+
const keys = await redisClient.keys(
|
|
514
|
+
process.env.VERCEL_URL + cacheEntryKey,
|
|
515
|
+
);
|
|
516
|
+
expect(keys.length).toBe(1);
|
|
517
|
+
|
|
518
|
+
const hashmap = await redisClient.hGet(
|
|
519
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
520
|
+
cacheEntryKey,
|
|
521
|
+
);
|
|
522
|
+
expect(JSON.parse(hashmap)).toEqual([
|
|
523
|
+
'revalidated-fetch-revalidate3-nested-fetch-in-api-route',
|
|
524
|
+
]);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
// describe('With a API route that has a unstable_cacheTag', () => {
|
|
531
|
+
// // TODO: implement API route for this test as well as the test itself
|
|
532
|
+
// });
|
|
533
|
+
|
|
534
|
+
// describe('With a API route that has a unstable_cacheLife', () => {
|
|
535
|
+
// // TODO: implement API route for this test as well as the test itself
|
|
536
|
+
// });
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
describe('should have the correct caching behavior for pages', () => {
|
|
540
|
+
describe('Without any fetch requests inside the page', () => {
|
|
541
|
+
describe('With default page configuration for revalidate and dynamic values', () => {
|
|
542
|
+
let timestamp1: string | undefined;
|
|
543
|
+
|
|
544
|
+
it('Two parallel requests should return the same timestamp (because requests are deduplicated)', async () => {
|
|
545
|
+
// First request (should increment counter)
|
|
546
|
+
const [pageRes1, pageRes2] = await Promise.all([
|
|
547
|
+
fetch(NEXT_START_URL + '/pages/no-fetch/default-page'),
|
|
548
|
+
fetch(NEXT_START_URL + '/pages/no-fetch/default-page'),
|
|
549
|
+
]);
|
|
550
|
+
|
|
551
|
+
const pageText1 = await pageRes1.text();
|
|
552
|
+
timestamp1 = pageText1.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
553
|
+
expect(timestamp1).toBeDefined();
|
|
554
|
+
|
|
555
|
+
const pageText2 = await pageRes2.text();
|
|
556
|
+
const timestamp2 = pageText2.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
557
|
+
expect(timestamp2).toBeDefined();
|
|
558
|
+
expect(timestamp1).toBe(timestamp2);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it('Redis should have a key for the page which should have a TTL set to 28 days (2 * 14 days default revalidate time)', async () => {
|
|
562
|
+
// check Redis keys
|
|
563
|
+
const ttl = await redisClient.ttl(
|
|
564
|
+
process.env.VERCEL_URL + '/pages/no-fetch/default-page',
|
|
565
|
+
);
|
|
566
|
+
// 14 days is default revalidate for pages -> expiration time is 2 * revalidate time -> -10 seconds for testing offset stability
|
|
567
|
+
expect(ttl).toBeGreaterThan(2 * 14 * 24 * 60 * 60 - 30);
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
it('The data in the redis key should match the expected format', async () => {
|
|
571
|
+
const data = await redisClient.get(
|
|
572
|
+
process.env.VERCEL_URL + '/pages/no-fetch/default-page',
|
|
573
|
+
);
|
|
574
|
+
expect(data).toBeDefined();
|
|
575
|
+
const cacheEntry: CacheEntry = JSON.parse(data);
|
|
576
|
+
|
|
577
|
+
// The format should be as expected
|
|
578
|
+
expect(cacheEntry).toEqual({
|
|
579
|
+
value: {
|
|
580
|
+
kind: 'APP_PAGE',
|
|
581
|
+
html: expect.any(String),
|
|
582
|
+
rscData: {
|
|
583
|
+
$binary: expect.any(String),
|
|
584
|
+
},
|
|
585
|
+
headers: {
|
|
586
|
+
'x-nextjs-stale-time': expect.any(String),
|
|
587
|
+
'x-next-cache-tags':
|
|
588
|
+
'_N_T_/layout,_N_T_/pages/layout,_N_T_/pages/no-fetch/layout,_N_T_/pages/no-fetch/default-page/layout,_N_T_/pages/no-fetch/default-page/page,_N_T_/pages/no-fetch/default-page',
|
|
589
|
+
},
|
|
590
|
+
status: 200,
|
|
591
|
+
},
|
|
592
|
+
lastModified: expect.any(Number),
|
|
593
|
+
tags: [
|
|
594
|
+
'_N_T_/layout',
|
|
595
|
+
'_N_T_/pages/layout',
|
|
596
|
+
'_N_T_/pages/no-fetch/layout',
|
|
597
|
+
'_N_T_/pages/no-fetch/default-page/layout',
|
|
598
|
+
'_N_T_/pages/no-fetch/default-page/page',
|
|
599
|
+
'_N_T_/pages/no-fetch/default-page',
|
|
600
|
+
],
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
if (process.env.SKIP_OPTIONAL_LONG_RUNNER_TESTS !== 'true') {
|
|
605
|
+
it('A new request after 3 seconds should return the same timestamp (because the page was cached in in-memory cache)', async () => {
|
|
606
|
+
await delay(3_000);
|
|
607
|
+
const pageRes3 = await fetch(
|
|
608
|
+
NEXT_START_URL + '/pages/no-fetch/default-page',
|
|
609
|
+
);
|
|
610
|
+
const pageText3 = await pageRes3.text();
|
|
611
|
+
const timestamp3 = pageText3.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
612
|
+
expect(timestamp3).toBeDefined();
|
|
613
|
+
expect(timestamp1).toBe(timestamp3);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('A new request after 11 seconds should return the same timestamp (because the page was cached in redis cache)', async () => {
|
|
617
|
+
await delay(11_000);
|
|
618
|
+
const pageRes4 = await fetch(
|
|
619
|
+
NEXT_START_URL + '/pages/no-fetch/default-page',
|
|
620
|
+
);
|
|
621
|
+
const pageText4 = await pageRes4.text();
|
|
622
|
+
const timestamp4 = pageText4.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
623
|
+
expect(timestamp4).toBeDefined();
|
|
624
|
+
expect(timestamp1).toBe(timestamp4);
|
|
625
|
+
}, 15_000);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
it('A request to revalidatePage API should remove the page from redis (string and hashmap)', async () => {
|
|
629
|
+
const revalidateRes = await fetch(
|
|
630
|
+
NEXT_START_URL +
|
|
631
|
+
'/api/revalidatePath?path=/pages/no-fetch/default-page',
|
|
632
|
+
);
|
|
633
|
+
const revalidateResJson: any = await revalidateRes.json();
|
|
634
|
+
expect(revalidateResJson.success).toBe(true);
|
|
635
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
636
|
+
|
|
637
|
+
// check Redis keys
|
|
638
|
+
const keys = await redisClient.keys(
|
|
639
|
+
process.env.VERCEL_URL + '/pages/no-fetch/default-page',
|
|
640
|
+
);
|
|
641
|
+
expect(keys.length).toBe(0);
|
|
642
|
+
|
|
643
|
+
const hashmap = await redisClient.hGet(
|
|
644
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
645
|
+
'/pages/no-fetch/default-page',
|
|
646
|
+
);
|
|
647
|
+
expect(hashmap).toBeNull();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('A new request after the revalidation should return a new timestamp (because the page was recreated)', async () => {
|
|
651
|
+
const pageRes4 = await fetch(
|
|
652
|
+
NEXT_START_URL + '/pages/no-fetch/default-page',
|
|
653
|
+
);
|
|
654
|
+
const pageText4 = await pageRes4.text();
|
|
655
|
+
const timestamp4 = pageText4.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
656
|
+
expect(timestamp4).toBeDefined();
|
|
657
|
+
expect(Number(timestamp4)).toBeGreaterThan(Number(timestamp1));
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
it('After the new request was made the redis key and hashmap should be set again', async () => {
|
|
661
|
+
// check Redis keys
|
|
662
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
663
|
+
const keys = await redisClient.keys(
|
|
664
|
+
process.env.VERCEL_URL + '/pages/no-fetch/default-page',
|
|
665
|
+
);
|
|
666
|
+
expect(keys.length).toBe(1);
|
|
667
|
+
|
|
668
|
+
const hashmap = await redisClient.hGet(
|
|
669
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
670
|
+
'/pages/no-fetch/default-page',
|
|
671
|
+
);
|
|
672
|
+
expect(JSON.parse(hashmap)).toEqual([
|
|
673
|
+
'_N_T_/layout',
|
|
674
|
+
'_N_T_/pages/layout',
|
|
675
|
+
'_N_T_/pages/no-fetch/layout',
|
|
676
|
+
'_N_T_/pages/no-fetch/default-page/layout',
|
|
677
|
+
'_N_T_/pages/no-fetch/default-page/page',
|
|
678
|
+
'_N_T_/pages/no-fetch/default-page',
|
|
679
|
+
]);
|
|
680
|
+
});
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
// describe('With a cached static fetch request inside a page', () => {
|
|
685
|
+
// // TODO: implement test for `test/integration/next-app/src/app/pages/cached-static-fetch`
|
|
686
|
+
// });
|
|
687
|
+
|
|
688
|
+
describe('With a cached revalidation fetch request inside a page', () => {
|
|
689
|
+
let firstTimestamp: string;
|
|
690
|
+
let firstCounter: string;
|
|
691
|
+
|
|
692
|
+
it('should set all cache entries for this page after request is finished', async () => {
|
|
693
|
+
const pageRes = await fetch(
|
|
694
|
+
NEXT_START_URL +
|
|
695
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
696
|
+
);
|
|
697
|
+
const pageText = await pageRes.text();
|
|
698
|
+
const timestamp = pageText.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
699
|
+
const counter = pageText.match(/Counter: <!-- -->(\d+)/)?.[1];
|
|
700
|
+
expect(timestamp).toBeDefined();
|
|
701
|
+
expect(counter).toBeDefined();
|
|
702
|
+
firstTimestamp = timestamp!;
|
|
703
|
+
firstCounter = counter!;
|
|
704
|
+
|
|
705
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
706
|
+
|
|
707
|
+
// test cache entry for 3 keys are set
|
|
708
|
+
const keys1 = await redisClient.keys(
|
|
709
|
+
process.env.VERCEL_URL +
|
|
710
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
711
|
+
);
|
|
712
|
+
expect(keys1.length).toBe(1);
|
|
713
|
+
const keys2 = await redisClient.keys(
|
|
714
|
+
process.env.VERCEL_URL +
|
|
715
|
+
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
716
|
+
);
|
|
717
|
+
expect(keys2.length).toBe(1);
|
|
718
|
+
const keys3 = await redisClient.keys(
|
|
719
|
+
process.env.VERCEL_URL + '/api/revalidated-fetch',
|
|
720
|
+
);
|
|
721
|
+
expect(keys3.length).toBe(1);
|
|
722
|
+
|
|
723
|
+
// test shared tag hashmap to be set for all keys
|
|
724
|
+
const hashmap1 = await redisClient.hGet(
|
|
725
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
726
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
727
|
+
);
|
|
728
|
+
expect(JSON.parse(hashmap1)).toEqual([
|
|
729
|
+
'_N_T_/layout',
|
|
730
|
+
'_N_T_/pages/layout',
|
|
731
|
+
'_N_T_/pages/revalidated-fetch/layout',
|
|
732
|
+
'_N_T_/pages/revalidated-fetch/revalidate15--default-page/layout',
|
|
733
|
+
'_N_T_/pages/revalidated-fetch/revalidate15--default-page/page',
|
|
734
|
+
'_N_T_/pages/revalidated-fetch/revalidate15--default-page',
|
|
735
|
+
'revalidated-fetch-revalidate15-default-page',
|
|
736
|
+
]);
|
|
737
|
+
const hashmap2 = await redisClient.hGet(
|
|
738
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
739
|
+
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
740
|
+
);
|
|
741
|
+
expect(JSON.parse(hashmap2)).toEqual([
|
|
742
|
+
'revalidated-fetch-revalidate15-default-page',
|
|
743
|
+
]);
|
|
744
|
+
const hashmap3 = await redisClient.hGet(
|
|
745
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
746
|
+
'/api/revalidated-fetch',
|
|
747
|
+
);
|
|
748
|
+
expect(JSON.parse(hashmap3)).toEqual([
|
|
749
|
+
'_N_T_/layout',
|
|
750
|
+
'_N_T_/api/layout',
|
|
751
|
+
'_N_T_/api/revalidated-fetch/layout',
|
|
752
|
+
'_N_T_/api/revalidated-fetch/route',
|
|
753
|
+
'_N_T_/api/revalidated-fetch',
|
|
754
|
+
]);
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
it('a new request should return the same timestamp as the first request', async () => {
|
|
758
|
+
const pageRes = await fetch(
|
|
759
|
+
NEXT_START_URL +
|
|
760
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
761
|
+
);
|
|
762
|
+
const pageText = await pageRes.text();
|
|
763
|
+
const timestamp = pageText.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
764
|
+
expect(timestamp).toBeDefined();
|
|
765
|
+
expect(timestamp).toBe(firstTimestamp);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
it('A request to revalidatePath API should remove the page from redis (string and hashmap) but not the api route', async () => {
|
|
769
|
+
const revalidateRes = await fetch(
|
|
770
|
+
NEXT_START_URL +
|
|
771
|
+
'/api/revalidatePath?path=/pages/revalidated-fetch/revalidate15--default-page',
|
|
772
|
+
);
|
|
773
|
+
const revalidateResJson: any = await revalidateRes.json();
|
|
774
|
+
expect(revalidateResJson.success).toBe(true);
|
|
775
|
+
await delay(REDIS_BACKGROUND_SYNC_DELAY);
|
|
776
|
+
|
|
777
|
+
// test no cache entry for 2 keys
|
|
778
|
+
const keys1 = await redisClient.keys(
|
|
779
|
+
process.env.VERCEL_URL +
|
|
780
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
781
|
+
);
|
|
782
|
+
expect(keys1.length).toBe(0);
|
|
783
|
+
|
|
784
|
+
const hashmap1 = await redisClient.hGet(
|
|
785
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
786
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
787
|
+
);
|
|
788
|
+
expect(hashmap1).toBeNull();
|
|
789
|
+
|
|
790
|
+
// sub-fetch-request is not removed directly but will be removed on next get request
|
|
791
|
+
const keys2 = await redisClient.keys(
|
|
792
|
+
process.env.VERCEL_URL +
|
|
793
|
+
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
794
|
+
);
|
|
795
|
+
expect(keys2.length).toBe(1);
|
|
796
|
+
const hashmap2 = await redisClient.hGet(
|
|
797
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
798
|
+
'e978cf5ddb8bf799209e828635cfe9ae6862f6735cea97f01ab752ff6fa489b4',
|
|
799
|
+
);
|
|
800
|
+
expect(hashmap2).toBeDefined();
|
|
801
|
+
|
|
802
|
+
// the page should also be in revalidatedTagsMap so that the nested fetch requests knows that the page was invalidated
|
|
803
|
+
const revalidationTimestamp = await redisClient.hGet(
|
|
804
|
+
process.env.VERCEL_URL + '__revalidated_tags__',
|
|
805
|
+
'_N_T_/pages/revalidated-fetch/revalidate15--default-page',
|
|
806
|
+
);
|
|
807
|
+
const ts = Number(revalidationTimestamp);
|
|
808
|
+
expect(ts).toBeGreaterThan(1);
|
|
809
|
+
expect(ts).toBeLessThan(Number(Date.now()));
|
|
810
|
+
|
|
811
|
+
// API route should still be cached
|
|
812
|
+
const keys3 = await redisClient.keys(
|
|
813
|
+
process.env.VERCEL_URL + '/api/revalidated-fetch',
|
|
814
|
+
);
|
|
815
|
+
expect(keys3.length).toBe(1);
|
|
816
|
+
const hashmap3 = await redisClient.hGet(
|
|
817
|
+
process.env.VERCEL_URL + '__sharedTags__',
|
|
818
|
+
'/api/revalidated-fetch',
|
|
819
|
+
);
|
|
820
|
+
expect(JSON.parse(hashmap3)).toEqual([
|
|
821
|
+
'_N_T_/layout',
|
|
822
|
+
'_N_T_/api/layout',
|
|
823
|
+
'_N_T_/api/revalidated-fetch/layout',
|
|
824
|
+
'_N_T_/api/revalidated-fetch/route',
|
|
825
|
+
'_N_T_/api/revalidated-fetch',
|
|
826
|
+
]);
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
it('a new request should return a newer timestamp as the first request (which was invalidated by revalidatePath)', async () => {
|
|
830
|
+
const pageRes = await fetch(
|
|
831
|
+
NEXT_START_URL +
|
|
832
|
+
'/pages/revalidated-fetch/revalidate15--default-page',
|
|
833
|
+
);
|
|
834
|
+
const pageText = await pageRes.text();
|
|
835
|
+
const timestamp = pageText.match(/Timestamp: <!-- -->(\d+)/)?.[1];
|
|
836
|
+
const secondCounter = pageText.match(/Counter: <!-- -->(\d+)/)?.[1];
|
|
837
|
+
expect(timestamp).toBeDefined();
|
|
838
|
+
expect(Number(timestamp)).toBeGreaterThan(Number(firstTimestamp));
|
|
839
|
+
|
|
840
|
+
//but the new request should not have a higher counter than the first request (because the cache of the API route should not be invalidated)
|
|
841
|
+
expect(secondCounter).toBeDefined();
|
|
842
|
+
expect(secondCounter).toBe(firstCounter);
|
|
843
|
+
});
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// describe('With a uncached fetch request inside a page', () => {
|
|
847
|
+
// // TODO: implement test for `test/integration/next-app/src/app/pages/uncached-fetch`
|
|
848
|
+
//
|
|
849
|
+
// });
|
|
850
|
+
|
|
851
|
+
// describe('With a page that has a unstable_cacheTag', () => {
|
|
852
|
+
// // TODO: implement page for this test as well as the test itself
|
|
853
|
+
// });
|
|
854
|
+
|
|
855
|
+
// describe('With a page that has a unstable_cacheLife', () => {
|
|
856
|
+
// // TODO: implement page for this test as well as the test itself
|
|
857
|
+
// });
|
|
858
|
+
});
|
|
859
|
+
});
|