11import type { CacheHandler , CacheHandlerContext , CacheHandlerValue } from './'
2- import type {
3- CachedFetchValue ,
4- IncrementalCacheValue ,
5- } from '../../response-cache'
2+ import type { IncrementalCacheValue } from '../../response-cache'
63
74import 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-
125import {
136 CACHE_ONE_YEAR ,
147 NEXT_CACHE_SOFT_TAGS_HEADER ,
@@ -31,22 +24,40 @@ const CACHE_REVALIDATE_HEADER = 'x-vercel-revalidate' as const
3124const CACHE_FETCH_URL_HEADER = 'x-vercel-cache-item-name' as const
3225const CACHE_CONTROL_VALUE_HEADER = 'x-vercel-cache-control' as const
3326
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+ }
4557
4658export default class FetchCache implements CacheHandler {
4759 private headers : Record < string , string >
4860 private cacheEndpoint ?: string
49- private debug : boolean
5061
5162 private hasMatchingTags ( arr1 : string [ ] , arr2 : string [ ] ) {
5263 if ( arr1 . length !== arr2 . length ) return false
@@ -72,7 +83,6 @@ export default class FetchCache implements CacheHandler {
7283 }
7384
7485 constructor ( ctx : CacheHandlerContext ) {
75- this . debug = ! ! process . env . NEXT_PRIVATE_DEBUG_CACHE
7686 this . headers = { }
7787 this . headers [ 'Content-Type' ] = 'application/json'
7888
@@ -99,17 +109,18 @@ export default class FetchCache implements CacheHandler {
99109 }
100110
101111 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 ) {
104115 console . log ( 'using cache endpoint' , this . cacheEndpoint )
105116 }
106- } else if ( this . debug ) {
117+ } else if ( DEBUG ) {
107118 console . log ( 'no cache endpoint available' )
108119 }
109120
110121 if ( ctx . maxMemoryCacheSize ) {
111122 if ( ! memoryCache ) {
112- if ( this . debug ) {
123+ if ( DEBUG ) {
113124 console . log ( 'using memory store for fetch cache' )
114125 }
115126
@@ -129,13 +140,15 @@ export default class FetchCache implements CacheHandler {
129140 }
130141 // rough estimate of size of cache value
131142 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 )
133146 )
134147 } ,
135148 } )
136149 }
137150 } else {
138- if ( this . debug ) {
151+ if ( DEBUG ) {
139152 console . log ( 'not using memory store for fetch cache' )
140153 }
141154 }
@@ -145,23 +158,29 @@ export default class FetchCache implements CacheHandler {
145158 memoryCache ?. reset ( )
146159 }
147160
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 )
151168 }
152169
170+ if ( ! tags . length ) return
171+
153172 if ( Date . now ( ) < rateLimitedUntil ) {
154- if ( this . debug ) {
173+ if ( DEBUG ) {
155174 console . log ( 'rate limited ' , rateLimitedUntil )
156175 }
157176 return
158177 }
159178
160179 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 ( ',' ) } `,
165184 {
166185 method : 'POST' ,
167186 headers : this . headers ,
@@ -179,7 +198,7 @@ export default class FetchCache implements CacheHandler {
179198 throw new Error ( `Request failed with status ${ res . status } .` )
180199 }
181200 } catch ( err ) {
182- console . warn ( `Failed to revalidate tag ${ tag } ` , err )
201+ console . warn ( `Failed to revalidate tag ${ tags } ` , err )
183202 }
184203 }
185204
@@ -192,7 +211,7 @@ export default class FetchCache implements CacheHandler {
192211 }
193212
194213 if ( Date . now ( ) < rateLimitedUntil ) {
195- if ( this . debug ) {
214+ if ( DEBUG ) {
196215 console . log ( 'rate limited' )
197216 }
198217 return null
@@ -238,7 +257,7 @@ export default class FetchCache implements CacheHandler {
238257 }
239258
240259 if ( res . status === 404 ) {
241- if ( this . debug ) {
260+ if ( DEBUG ) {
242261 console . log (
243262 `no fetch cache entry for ${ key } , duration: ${
244263 Date . now ( ) - start
@@ -253,16 +272,13 @@ export default class FetchCache implements CacheHandler {
253272 throw new Error ( `invalid response from cache ${ res . status } ` )
254273 }
255274
256- const json : IncrementalCacheValue = await res . json ( )
257- const parsed = zCachedFetchValue . safeParse ( json )
275+ const cached : IncrementalCacheValue = await res . json ( )
258276
259- if ( ! parsed . success ) {
260- this . debug && console . log ( { json } )
277+ if ( ! cached || cached . kind !== 'FETCH' ) {
278+ DEBUG && console . log ( { cached } )
261279 throw new Error ( 'invalid cache value' )
262280 }
263281
264- const { data : cached } = parsed
265-
266282 // if new tags were specified, merge those tags to the existing tags
267283 if ( cached . kind === 'FETCH' ) {
268284 cached . tags ??= [ ]
@@ -286,7 +302,7 @@ export default class FetchCache implements CacheHandler {
286302 : Date . now ( ) - parseInt ( age || '0' , 10 ) * 1000 ,
287303 }
288304
289- if ( this . debug ) {
305+ if ( DEBUG ) {
290306 console . log (
291307 `got fetch cache entry for ${ key } , duration: ${
292308 Date . now ( ) - start
@@ -303,7 +319,7 @@ export default class FetchCache implements CacheHandler {
303319 }
304320 } catch ( err ) {
305321 // unable to get data from fetch-cache
306- if ( this . debug ) {
322+ if ( DEBUG ) {
307323 console . error ( `Failed to get from fetch-cache` , err )
308324 }
309325 }
@@ -314,11 +330,31 @@ export default class FetchCache implements CacheHandler {
314330
315331 public async set ( ...args : Parameters < CacheHandler [ 'set' ] > ) {
316332 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+
317353 const { fetchCache, fetchIdx, fetchUrl, tags } = ctx
318354 if ( ! fetchCache ) return
319355
320356 if ( Date . now ( ) < rateLimitedUntil ) {
321- if ( this . debug ) {
357+ if ( DEBUG ) {
322358 console . log ( 'rate limited' )
323359 }
324360 return
@@ -350,7 +386,7 @@ export default class FetchCache implements CacheHandler {
350386 tags : undefined ,
351387 } )
352388
353- if ( this . debug ) {
389+ if ( DEBUG ) {
354390 console . log ( 'set cache' , key )
355391 }
356392 const fetchParams : NextFetchCacheParams = {
@@ -379,11 +415,11 @@ export default class FetchCache implements CacheHandler {
379415 }
380416
381417 if ( ! res . ok ) {
382- this . debug && console . log ( await res . text ( ) )
418+ DEBUG && console . log ( await res . text ( ) )
383419 throw new Error ( `invalid response ${ res . status } ` )
384420 }
385421
386- if ( this . debug ) {
422+ if ( DEBUG ) {
387423 console . log (
388424 `successfully set to fetch-cache for ${ key } , duration: ${
389425 Date . now ( ) - start
@@ -392,7 +428,7 @@ export default class FetchCache implements CacheHandler {
392428 }
393429 } catch ( err ) {
394430 // unable to set to fetch-cache
395- if ( this . debug ) {
431+ if ( DEBUG ) {
396432 console . error ( `Failed to update fetch cache` , err )
397433 }
398434 }
0 commit comments