9
9
ListObjectsV2Command ,
10
10
PutObjectCommand ,
11
11
PutObjectCommandInput ,
12
- PutObjectCommandOutput ,
13
12
S3Client ,
14
13
} from "@aws-sdk/client-s3" ;
15
14
import path from "path" ;
@@ -88,17 +87,38 @@ interface CacheHandlerValue {
88
87
value : IncrementalCacheValue | null ;
89
88
}
90
89
91
- type CacheExtension =
92
- | "json"
93
- | "html"
94
- | "rsc"
95
- | "body"
96
- | "meta"
97
- | "fetch"
98
- | "redirect" ;
90
+ type Extension = "cache" | "fetch" ;
91
+
92
+ interface Meta {
93
+ status ?: number ;
94
+ headers ?: Record < string , undefined | string | string [ ] > ;
95
+ }
96
+ type S3CachedFile =
97
+ | {
98
+ type : "redirect" ;
99
+ props ?: Object ;
100
+ meta ?: Meta ;
101
+ }
102
+ | {
103
+ type : "page" ;
104
+ html : string ;
105
+ json : Object ;
106
+ meta ?: Meta ;
107
+ }
108
+ | {
109
+ type : "app" ;
110
+ html : string ;
111
+ rsc : string ;
112
+ meta ?: Meta ;
113
+ }
114
+ | {
115
+ type : "route" ;
116
+ body : string ;
117
+ meta ?: Meta ;
118
+ } ;
99
119
100
120
/** Beginning single backslash is intentional, to look for the dot + the extension. Do not escape it again. */
101
- const CACHE_EXTENSION_REGEX = / \. ( j s o n | h t m l | r s c | b o d y | m e t a | f e t c h | r e d i r e c t ) $ / ;
121
+ const CACHE_EXTENSION_REGEX = / \. ( c a c h e | f e t c h ) $ / ;
102
122
103
123
export function hasCacheExtension ( key : string ) {
104
124
return CACHE_EXTENSION_REGEX . test ( key ) ;
@@ -136,18 +156,15 @@ export default class S3Cache {
136
156
}
137
157
const isFetchCache =
138
158
typeof options === "object" ? options . fetchCache : options ;
139
- const keys = await this . listS3Object ( key ) ;
140
- if ( keys . length === 0 ) return null ;
141
- debug ( "keys" , keys ) ;
142
159
return isFetchCache
143
- ? this . getFetchCache ( key , keys )
144
- : this . getIncrementalCache ( key , keys ) ;
160
+ ? this . getFetchCache ( key )
161
+ : this . getIncrementalCache ( key ) ;
145
162
}
146
163
147
- async getFetchCache ( key : string , keys : string [ ] ) {
164
+ async getFetchCache ( key : string ) {
148
165
debug ( "get fetch cache" , { key } ) ;
149
166
try {
150
- const { Body, LastModified } = await this . getS3Object ( key , "fetch" , keys ) ;
167
+ const { Body, LastModified } = await this . getS3Object ( key , "fetch" ) ;
151
168
const lastModified = await this . getHasRevalidatedTags (
152
169
key ,
153
170
LastModified ?. getTime ( ) ,
@@ -169,96 +186,59 @@ export default class S3Cache {
169
186
}
170
187
}
171
188
172
- async getIncrementalCache (
173
- key : string ,
174
- keys : string [ ] ,
175
- ) : Promise < CacheHandlerValue | null > {
176
- if ( keys . includes ( this . buildS3Key ( key , "body" ) ) ) {
177
- debug ( "get body cache " , { key } ) ;
178
- try {
179
- const [ { Body, LastModified } , { Body : MetaBody } ] = await Promise . all ( [
180
- this . getS3Object ( key , "body" , keys ) ,
181
- this . getS3Object ( key , "meta" , keys ) ,
182
- ] ) ;
183
- const body = await Body ?. transformToByteArray ( ) ;
184
- const meta = JSON . parse ( ( await MetaBody ?. transformToString ( ) ) ?? "{}" ) ;
185
-
189
+ async getIncrementalCache ( key : string ) : Promise < CacheHandlerValue | null > {
190
+ try {
191
+ const { Body, LastModified } = await this . getS3Object ( key , "cache" ) ;
192
+ const cacheData = JSON . parse (
193
+ ( await Body ?. transformToString ( ) ) ?? "{}" ,
194
+ ) as S3CachedFile ;
195
+ const meta = cacheData . meta ;
196
+ const lastModified = await this . getHasRevalidatedTags (
197
+ key ,
198
+ LastModified ?. getTime ( ) ,
199
+ ) ;
200
+ if ( lastModified === - 1 ) {
201
+ // If some tags are stale we need to force revalidation
202
+ return null ;
203
+ }
204
+ if ( cacheData . type === "route" ) {
186
205
return {
187
206
lastModified : LastModified ?. getTime ( ) ,
188
207
value : {
189
208
kind : "ROUTE" ,
190
- body : Buffer . from ( body ?? Buffer . alloc ( 0 ) ) ,
191
- status : meta . status ,
192
- headers : meta . headers ,
209
+ body : Buffer . from ( cacheData . body ?? Buffer . alloc ( 0 ) ) ,
210
+ status : meta ? .status ,
211
+ headers : meta ? .headers ,
193
212
} ,
194
213
} as CacheHandlerValue ;
195
- } catch ( e ) {
196
- error ( "Failed to get body cache" , e ) ;
197
- }
198
- return null ;
199
- }
200
-
201
- if ( keys . includes ( this . buildS3Key ( key , "html" ) ) ) {
202
- const isJson = keys . includes ( this . buildS3Key ( key , "json" ) ) ;
203
- const isRsc = keys . includes ( this . buildS3Key ( key , "rsc" ) ) ;
204
- debug ( "get html cache " , { key, isJson, isRsc } ) ;
205
- if ( ! isJson && ! isRsc ) return null ;
206
-
207
- try {
208
- const [ { Body, LastModified } , { Body : PageBody } , { Body : MetaBody } ] =
209
- await Promise . all ( [
210
- this . getS3Object ( key , "html" , keys ) ,
211
- this . getS3Object ( key , isJson ? "json" : "rsc" , keys ) ,
212
- this . getS3Object ( key , "meta" , keys ) ,
213
- ] ) ;
214
- const lastModified = await this . getHasRevalidatedTags (
215
- key ,
216
- LastModified ?. getTime ( ) ,
217
- ) ;
218
- if ( lastModified === - 1 ) {
219
- // If some tags are stale we need to force revalidation
220
- return null ;
221
- }
222
- const meta = JSON . parse ( ( await MetaBody ?. transformToString ( ) ) ?? "{}" ) ;
214
+ } else if ( cacheData . type === "page" || cacheData . type === "app" ) {
223
215
return {
224
- lastModified,
216
+ lastModified : LastModified ?. getTime ( ) ,
225
217
value : {
226
218
kind : "PAGE" ,
227
- html : ( await Body ?. transformToString ( ) ) ?? "" ,
228
- pageData : isJson
229
- ? JSON . parse ( ( await PageBody ?. transformToString ( ) ) ?? "{}" )
230
- : await PageBody ?. transformToString ( ) ,
231
- status : meta . status ,
232
- headers : meta . headers ,
219
+ html : cacheData . html ,
220
+ pageData :
221
+ cacheData . type === "page" ? cacheData . json : cacheData . rsc ,
222
+ status : meta ?. status ,
223
+ headers : meta ?. headers ,
233
224
} ,
234
225
} as CacheHandlerValue ;
235
- } catch ( e ) {
236
- error ( "Failed to get html cache" , e ) ;
237
- }
238
- return null ;
239
- }
240
-
241
- // Check for redirect last. This way if a page has been regenerated
242
- // after having been redirected, we'll get the page data
243
- if ( keys . includes ( this . buildS3Key ( key , "redirect" ) ) ) {
244
- debug ( "get redirect cache" , { key } ) ;
245
- try {
246
- const { Body, LastModified } = await this . getS3Object (
247
- key ,
248
- "redirect" ,
249
- keys ,
250
- ) ;
226
+ } else if ( cacheData . type === "redirect" ) {
251
227
return {
252
228
lastModified : LastModified ?. getTime ( ) ,
253
- value : JSON . parse ( ( await Body ?. transformToString ( ) ) ?? "{}" ) ,
254
- } ;
255
- } catch ( e ) {
256
- error ( "Failed to get redirect cache" , e ) ;
229
+ value : {
230
+ kind : "REDIRECT" ,
231
+ props : cacheData . props ,
232
+ } ,
233
+ } as CacheHandlerValue ;
234
+ } else {
235
+ error ( "Unknown cache type" , cacheData ) ;
236
+ return null ;
257
237
}
238
+ } catch ( e ) {
239
+ error ( "Failed to get body cache" , e ) ;
258
240
return null ;
259
241
}
260
-
261
- return null ;
262
242
}
263
243
264
244
async set ( key : string , data ?: IncrementalCacheValue ) : Promise < void > {
@@ -267,37 +247,44 @@ export default class S3Cache {
267
247
}
268
248
if ( data ?. kind === "ROUTE" ) {
269
249
const { body, status, headers } = data ;
270
- await Promise . all ( [
271
- this . putS3Object ( key , "body" , body ) ,
272
- this . putS3Object ( key , "meta" , JSON . stringify ( { status, headers } ) ) ,
273
- ] ) ;
250
+ this . putS3Object (
251
+ key ,
252
+ "cache" ,
253
+ JSON . stringify ( {
254
+ type : "route" ,
255
+ body : body . toString ( "utf8" ) ,
256
+ meta : {
257
+ status,
258
+ headers,
259
+ } ,
260
+ } as S3CachedFile ) ,
261
+ ) ;
274
262
} else if ( data ?. kind === "PAGE" ) {
275
263
const { html, pageData } = data ;
276
264
const isAppPath = typeof pageData === "string" ;
277
- let metaPromise : Promise < PutObjectCommandOutput | void > =
278
- Promise . resolve ( ) ;
279
- if ( data . status || data . headers ) {
280
- metaPromise = this . putS3Object (
281
- key ,
282
- "meta" ,
283
- JSON . stringify ( { status : data . status , headers : data . headers } ) ,
284
- ) ;
285
- }
286
- await Promise . all ( [
287
- this . putS3Object ( key , "html" , html ) ,
288
- this . putS3Object (
289
- key ,
290
- isAppPath ? "rsc" : "json" ,
291
- isAppPath ? pageData : JSON . stringify ( pageData ) ,
292
- ) ,
293
- metaPromise ,
294
- ] ) ;
265
+ this . putS3Object (
266
+ key ,
267
+ "cache" ,
268
+ JSON . stringify ( {
269
+ type : isAppPath ? "app" : "page" ,
270
+ html,
271
+ rsc : isAppPath ? pageData : undefined ,
272
+ json : isAppPath ? undefined : pageData ,
273
+ meta : { status : data . status , headers : data . headers } ,
274
+ } as S3CachedFile ) ,
275
+ ) ;
295
276
} else if ( data ?. kind === "FETCH" ) {
296
277
await this . putS3Object ( key , "fetch" , JSON . stringify ( data ) ) ;
297
278
} else if ( data ?. kind === "REDIRECT" ) {
298
- // delete potential page data if we're redirecting
299
- await this . deleteS3Objects ( key ) ;
300
- await this . putS3Object ( key , "redirect" , JSON . stringify ( data ) ) ;
279
+ // // delete potential page data if we're redirecting
280
+ await this . putS3Object (
281
+ key ,
282
+ "cache" ,
283
+ JSON . stringify ( {
284
+ type : "redirect" ,
285
+ props : data . props ,
286
+ } as S3CachedFile ) ,
287
+ ) ;
301
288
} else if ( data === null || data === undefined ) {
302
289
await this . deleteS3Objects ( key ) ;
303
290
}
@@ -466,7 +453,7 @@ export default class S3Cache {
466
453
467
454
// S3 handling
468
455
469
- private buildS3Key ( key : string , extension : CacheExtension ) {
456
+ private buildS3Key ( key : string , extension : Extension ) {
470
457
return path . posix . join (
471
458
CACHE_BUCKET_KEY_PREFIX ?? "" ,
472
459
extension === "fetch" ? "__fetch" : "" ,
@@ -491,14 +478,8 @@ export default class S3Cache {
491
478
return ( Contents ?? [ ] ) . map ( ( { Key } ) => Key ) as string [ ] ;
492
479
}
493
480
494
- private async getS3Object (
495
- key : string ,
496
- extension : CacheExtension ,
497
- keys : string [ ] ,
498
- ) {
481
+ private async getS3Object ( key : string , extension : Extension ) {
499
482
try {
500
- if ( ! keys . includes ( this . buildS3Key ( key , extension ) ) )
501
- return { Body : null , LastModified : null } ;
502
483
const result = await this . client . send (
503
484
new GetObjectCommand ( {
504
485
Bucket : CACHE_BUCKET_NAME ,
@@ -514,7 +495,7 @@ export default class S3Cache {
514
495
515
496
private putS3Object (
516
497
key : string ,
517
- extension : CacheExtension ,
498
+ extension : Extension ,
518
499
value : PutObjectCommandInput [ "Body" ] ,
519
500
) {
520
501
return this . client . send (
0 commit comments