1
1
import type { CacheHandler , CacheHandlerContext , CacheHandlerValue } from './'
2
- import type {
3
- CachedFetchValue ,
4
- IncrementalCacheValue ,
5
- } from '../../response-cache'
2
+ import type { IncrementalCacheValue } from '../../response-cache'
6
3
7
4
import LRUCache from 'next/dist/compiled/lru-cache'
8
-
9
- import { z } from 'next/dist/compiled/zod'
10
- import type zod from 'next/dist/compiled/zod'
11
-
12
5
import {
13
6
CACHE_ONE_YEAR ,
14
7
NEXT_CACHE_SOFT_TAGS_HEADER ,
@@ -31,22 +24,40 @@ const CACHE_REVALIDATE_HEADER = 'x-vercel-revalidate' as const
31
24
const CACHE_FETCH_URL_HEADER = 'x-vercel-cache-item-name' as const
32
25
const CACHE_CONTROL_VALUE_HEADER = 'x-vercel-cache-control' as const
33
26
34
- const zCachedFetchValue : zod . ZodType < CachedFetchValue > = z . object ( {
35
- kind : z . literal ( 'FETCH' ) ,
36
- data : z . object ( {
37
- headers : z . record ( z . string ( ) ) ,
38
- body : z . string ( ) ,
39
- url : z . string ( ) ,
40
- status : z . number ( ) . optional ( ) ,
41
- } ) ,
42
- tags : z . array ( z . string ( ) ) . optional ( ) ,
43
- revalidate : z . number ( ) ,
44
- } )
27
+ const DEBUG = Boolean ( process . env . NEXT_PRIVATE_DEBUG_CACHE )
28
+
29
+ async function fetchRetryWithTimeout (
30
+ url : Parameters < typeof fetch > [ 0 ] ,
31
+ init : Parameters < typeof fetch > [ 1 ] ,
32
+ retryIndex = 0
33
+ ) : Promise < Response > {
34
+ const controller = new AbortController ( )
35
+ const timeout = setTimeout ( ( ) => {
36
+ controller . abort ( )
37
+ } , 500 )
38
+
39
+ return fetch ( url , {
40
+ ...( init || { } ) ,
41
+ signal : controller . signal ,
42
+ } )
43
+ . catch ( ( err ) => {
44
+ if ( retryIndex === 3 ) {
45
+ throw err
46
+ } else {
47
+ if ( DEBUG ) {
48
+ console . log ( `Fetch failed for ${ url } retry ${ retryIndex } ` )
49
+ }
50
+ return fetchRetryWithTimeout ( url , init , retryIndex + 1 )
51
+ }
52
+ } )
53
+ . finally ( ( ) => {
54
+ clearTimeout ( timeout )
55
+ } )
56
+ }
45
57
46
58
export default class FetchCache implements CacheHandler {
47
59
private headers : Record < string , string >
48
60
private cacheEndpoint ?: string
49
- private debug : boolean
50
61
51
62
private hasMatchingTags ( arr1 : string [ ] , arr2 : string [ ] ) {
52
63
if ( arr1 . length !== arr2 . length ) return false
@@ -72,7 +83,6 @@ export default class FetchCache implements CacheHandler {
72
83
}
73
84
74
85
constructor ( ctx : CacheHandlerContext ) {
75
- this . debug = ! ! process . env . NEXT_PRIVATE_DEBUG_CACHE
76
86
this . headers = { }
77
87
this . headers [ 'Content-Type' ] = 'application/json'
78
88
@@ -99,17 +109,18 @@ export default class FetchCache implements CacheHandler {
99
109
}
100
110
101
111
if ( scHost ) {
102
- this . cacheEndpoint = `https://${ scHost } ${ scBasePath || '' } `
103
- if ( this . debug ) {
112
+ const scProto = process . env . SUSPENSE_CACHE_PROTO || 'https'
113
+ this . cacheEndpoint = `${ scProto } ://${ scHost } ${ scBasePath || '' } `
114
+ if ( DEBUG ) {
104
115
console . log ( 'using cache endpoint' , this . cacheEndpoint )
105
116
}
106
- } else if ( this . debug ) {
117
+ } else if ( DEBUG ) {
107
118
console . log ( 'no cache endpoint available' )
108
119
}
109
120
110
121
if ( ctx . maxMemoryCacheSize ) {
111
122
if ( ! memoryCache ) {
112
- if ( this . debug ) {
123
+ if ( DEBUG ) {
113
124
console . log ( 'using memory store for fetch cache' )
114
125
}
115
126
@@ -129,13 +140,15 @@ export default class FetchCache implements CacheHandler {
129
140
}
130
141
// rough estimate of size of cache value
131
142
return (
132
- value . html . length + ( JSON . stringify ( value . pageData ) ?. length || 0 )
143
+ value . html . length +
144
+ ( JSON . stringify ( value . kind === 'PAGE' && value . pageData )
145
+ ?. length || 0 )
133
146
)
134
147
} ,
135
148
} )
136
149
}
137
150
} else {
138
- if ( this . debug ) {
151
+ if ( DEBUG ) {
139
152
console . log ( 'not using memory store for fetch cache' )
140
153
}
141
154
}
@@ -145,23 +158,29 @@ export default class FetchCache implements CacheHandler {
145
158
memoryCache ?. reset ( )
146
159
}
147
160
148
- public async revalidateTag ( tag : string ) {
149
- if ( this . debug ) {
150
- console . log ( 'revalidateTag' , tag )
161
+ public async revalidateTag (
162
+ ...args : Parameters < CacheHandler [ 'revalidateTag' ] >
163
+ ) {
164
+ let [ tags ] = args
165
+ tags = typeof tags === 'string' ? [ tags ] : tags
166
+ if ( DEBUG ) {
167
+ console . log ( 'revalidateTag' , tags )
151
168
}
152
169
170
+ if ( ! tags . length ) return
171
+
153
172
if ( Date . now ( ) < rateLimitedUntil ) {
154
- if ( this . debug ) {
173
+ if ( DEBUG ) {
155
174
console . log ( 'rate limited ' , rateLimitedUntil )
156
175
}
157
176
return
158
177
}
159
178
160
179
try {
161
- const res = await fetch (
162
- `${
163
- this . cacheEndpoint
164
- } /v1/suspense-cache/revalidate?tags= ${ encodeURIComponent ( tag ) } `,
180
+ const res = await fetchRetryWithTimeout (
181
+ `${ this . cacheEndpoint } /v1/suspense-cache/revalidate?tags= ${ tags
182
+ . map ( ( tag ) => encodeURIComponent ( tag ) )
183
+ . join ( ',' ) } `,
165
184
{
166
185
method : 'POST' ,
167
186
headers : this . headers ,
@@ -179,7 +198,7 @@ export default class FetchCache implements CacheHandler {
179
198
throw new Error ( `Request failed with status ${ res . status } .` )
180
199
}
181
200
} catch ( err ) {
182
- console . warn ( `Failed to revalidate tag ${ tag } ` , err )
201
+ console . warn ( `Failed to revalidate tag ${ tags } ` , err )
183
202
}
184
203
}
185
204
@@ -192,7 +211,7 @@ export default class FetchCache implements CacheHandler {
192
211
}
193
212
194
213
if ( Date . now ( ) < rateLimitedUntil ) {
195
- if ( this . debug ) {
214
+ if ( DEBUG ) {
196
215
console . log ( 'rate limited' )
197
216
}
198
217
return null
@@ -238,7 +257,7 @@ export default class FetchCache implements CacheHandler {
238
257
}
239
258
240
259
if ( res . status === 404 ) {
241
- if ( this . debug ) {
260
+ if ( DEBUG ) {
242
261
console . log (
243
262
`no fetch cache entry for ${ key } , duration: ${
244
263
Date . now ( ) - start
@@ -253,16 +272,13 @@ export default class FetchCache implements CacheHandler {
253
272
throw new Error ( `invalid response from cache ${ res . status } ` )
254
273
}
255
274
256
- const json : IncrementalCacheValue = await res . json ( )
257
- const parsed = zCachedFetchValue . safeParse ( json )
275
+ const cached : IncrementalCacheValue = await res . json ( )
258
276
259
- if ( ! parsed . success ) {
260
- this . debug && console . log ( { json } )
277
+ if ( ! cached || cached . kind !== 'FETCH' ) {
278
+ DEBUG && console . log ( { cached } )
261
279
throw new Error ( 'invalid cache value' )
262
280
}
263
281
264
- const { data : cached } = parsed
265
-
266
282
// if new tags were specified, merge those tags to the existing tags
267
283
if ( cached . kind === 'FETCH' ) {
268
284
cached . tags ??= [ ]
@@ -286,7 +302,7 @@ export default class FetchCache implements CacheHandler {
286
302
: Date . now ( ) - parseInt ( age || '0' , 10 ) * 1000 ,
287
303
}
288
304
289
- if ( this . debug ) {
305
+ if ( DEBUG ) {
290
306
console . log (
291
307
`got fetch cache entry for ${ key } , duration: ${
292
308
Date . now ( ) - start
@@ -303,7 +319,7 @@ export default class FetchCache implements CacheHandler {
303
319
}
304
320
} catch ( err ) {
305
321
// unable to get data from fetch-cache
306
- if ( this . debug ) {
322
+ if ( DEBUG ) {
307
323
console . error ( `Failed to get from fetch-cache` , err )
308
324
}
309
325
}
@@ -314,11 +330,31 @@ export default class FetchCache implements CacheHandler {
314
330
315
331
public async set ( ...args : Parameters < CacheHandler [ 'set' ] > ) {
316
332
const [ key , data , ctx ] = args
333
+
334
+ const newValue = data ?. kind === 'FETCH' ? data . data : undefined
335
+ const existingCache = memoryCache ?. get ( key )
336
+ const existingValue = existingCache ?. value
337
+ if (
338
+ existingValue ?. kind === 'FETCH' &&
339
+ Object . keys ( existingValue . data ) . every (
340
+ ( field ) =>
341
+ JSON . stringify (
342
+ ( existingValue . data as Record < string , string | Object > ) [ field ]
343
+ ) ===
344
+ JSON . stringify ( ( newValue as Record < string , string | Object > ) [ field ] )
345
+ )
346
+ ) {
347
+ if ( DEBUG ) {
348
+ console . log ( `skipping cache set for ${ key } as not modified` )
349
+ }
350
+ return
351
+ }
352
+
317
353
const { fetchCache, fetchIdx, fetchUrl, tags } = ctx
318
354
if ( ! fetchCache ) return
319
355
320
356
if ( Date . now ( ) < rateLimitedUntil ) {
321
- if ( this . debug ) {
357
+ if ( DEBUG ) {
322
358
console . log ( 'rate limited' )
323
359
}
324
360
return
@@ -350,7 +386,7 @@ export default class FetchCache implements CacheHandler {
350
386
tags : undefined ,
351
387
} )
352
388
353
- if ( this . debug ) {
389
+ if ( DEBUG ) {
354
390
console . log ( 'set cache' , key )
355
391
}
356
392
const fetchParams : NextFetchCacheParams = {
@@ -379,11 +415,11 @@ export default class FetchCache implements CacheHandler {
379
415
}
380
416
381
417
if ( ! res . ok ) {
382
- this . debug && console . log ( await res . text ( ) )
418
+ DEBUG && console . log ( await res . text ( ) )
383
419
throw new Error ( `invalid response ${ res . status } ` )
384
420
}
385
421
386
- if ( this . debug ) {
422
+ if ( DEBUG ) {
387
423
console . log (
388
424
`successfully set to fetch-cache for ${ key } , duration: ${
389
425
Date . now ( ) - start
@@ -392,7 +428,7 @@ export default class FetchCache implements CacheHandler {
392
428
}
393
429
} catch ( err ) {
394
430
// unable to set to fetch-cache
395
- if ( this . debug ) {
431
+ if ( DEBUG ) {
396
432
console . error ( `Failed to update fetch cache` , err )
397
433
}
398
434
}
0 commit comments