@@ -2,11 +2,12 @@ import {
2
2
S3Client ,
3
3
GetObjectCommand ,
4
4
PutObjectCommand ,
5
+ DeleteObjectsCommand ,
5
6
PutObjectCommandInput ,
6
7
ListObjectsV2Command ,
7
8
} from "@aws-sdk/client-s3" ;
8
9
import path from "node:path" ;
9
- import { error , awsLogger } from "./logger.js" ;
10
+ import { awsLogger , debug , error } from "./logger.js" ;
10
11
import { loadBuildId } from "./util.js" ;
11
12
12
13
interface CachedFetchValue {
@@ -75,7 +76,14 @@ interface CacheHandlerValue {
75
76
value : IncrementalCacheValue | null ;
76
77
}
77
78
78
- type Extension = "json" | "html" | "rsc" | "body" | "meta" | "fetch" ;
79
+ type Extension =
80
+ | "json"
81
+ | "html"
82
+ | "rsc"
83
+ | "body"
84
+ | "meta"
85
+ | "fetch"
86
+ | "redirect" ;
79
87
80
88
// Expected environment variables
81
89
const { CACHE_BUCKET_NAME , CACHE_BUCKET_KEY_PREFIX , CACHE_BUCKET_REGION } =
@@ -100,6 +108,7 @@ export default class S3Cache {
100
108
}
101
109
102
110
async getFetchCache ( key : string ) {
111
+ debug ( "get fetch cache" , { key } ) ;
103
112
try {
104
113
const { Body, LastModified } = await this . getS3Object ( key , "fetch" ) ;
105
114
return {
@@ -113,10 +122,12 @@ export default class S3Cache {
113
122
}
114
123
115
124
async getIncrementalCache ( key : string ) : Promise < CacheHandlerValue | null > {
116
- const { Contents } = await this . listS3Objects ( key ) ;
117
- const keys = ( Contents ?? [ ] ) . map ( ( { Key } ) => Key ) ;
125
+ const keys = await this . listS3Object ( key ) ;
126
+ if ( keys . length === 0 ) return null ;
127
+ debug ( "keys" , keys ) ;
118
128
119
129
if ( keys . includes ( this . buildS3Key ( key , "body" ) ) ) {
130
+ debug ( "get body cache " , { key } ) ;
120
131
try {
121
132
const [ { Body, LastModified } , { Body : MetaBody } ] = await Promise . all ( [
122
133
this . getS3Object ( key , "body" ) ,
@@ -143,6 +154,7 @@ export default class S3Cache {
143
154
if ( keys . includes ( this . buildS3Key ( key , "html" ) ) ) {
144
155
const isJson = keys . includes ( this . buildS3Key ( key , "json" ) ) ;
145
156
const isRsc = keys . includes ( this . buildS3Key ( key , "rsc" ) ) ;
157
+ debug ( "get html cache " , { key, isJson, isRsc } ) ;
146
158
if ( ! isJson && ! isRsc ) return null ;
147
159
148
160
try {
@@ -166,10 +178,27 @@ export default class S3Cache {
166
178
}
167
179
return null ;
168
180
}
181
+
182
+ // Check for redirect last. This way if a page has been regenerated
183
+ // after having been redirected, we'll get the page data
184
+ if ( keys . includes ( this . buildS3Key ( key , "redirect" ) ) ) {
185
+ debug ( "get redirect cache" , { key } ) ;
186
+ try {
187
+ const { Body, LastModified } = await this . getS3Object ( key , "redirect" ) ;
188
+ return {
189
+ lastModified : LastModified ?. getTime ( ) ,
190
+ value : JSON . parse ( ( await Body ?. transformToString ( ) ) ?? "{}" ) ,
191
+ } ;
192
+ } catch ( e ) {
193
+ error ( "Failed to get redirect cache" , e ) ;
194
+ }
195
+ return null ;
196
+ }
197
+
169
198
return null ;
170
199
}
171
200
172
- async set ( key : string , data ?: IncrementalCacheValue ) : Promise < void > {
201
+ async set ( key : string , data ?: IncrementalCacheValue | null ) : Promise < void > {
173
202
if ( data ?. kind === "ROUTE" ) {
174
203
const { body, status, headers } = data ;
175
204
await Promise . all ( [
@@ -189,6 +218,12 @@ export default class S3Cache {
189
218
] ) ;
190
219
} else if ( data ?. kind === "FETCH" ) {
191
220
await this . putS3Object ( key , "fetch" , JSON . stringify ( data ) ) ;
221
+ } else if ( data ?. kind === "REDIRECT" ) {
222
+ // delete potential page data if we're redirecting
223
+ await this . deleteS3Objects ( key ) ;
224
+ await this . putS3Object ( key , "redirect" , JSON . stringify ( data ) ) ;
225
+ } else if ( data === null || data === undefined ) {
226
+ await this . deleteS3Objects ( key ) ;
192
227
}
193
228
}
194
229
@@ -205,13 +240,16 @@ export default class S3Cache {
205
240
return path . posix . join ( CACHE_BUCKET_KEY_PREFIX ?? "" , this . buildId , key ) ;
206
241
}
207
242
208
- private listS3Objects ( key : string ) {
209
- return this . client . send (
243
+ private async listS3Object ( key : string ) {
244
+ const { Contents } = await this . client . send (
210
245
new ListObjectsV2Command ( {
211
246
Bucket : CACHE_BUCKET_NAME ,
212
- Prefix : this . buildS3KeyPrefix ( key ) ,
247
+ // add a point to the key so that it only matches the key and
248
+ // not other keys starting with the same string
249
+ Prefix : `${ this . buildS3KeyPrefix ( key ) } .` ,
213
250
} )
214
251
) ;
252
+ return ( Contents ?? [ ] ) . map ( ( { Key } ) => Key ) ;
215
253
}
216
254
217
255
private getS3Object ( key : string , extension : Extension ) {
@@ -236,4 +274,24 @@ export default class S3Cache {
236
274
} )
237
275
) ;
238
276
}
277
+
278
+ private async deleteS3Objects ( key : string ) {
279
+ try {
280
+ const regex = new RegExp ( `\.(json|rsc|html|body|meta|fetch|redirect)$` ) ;
281
+ const s3Keys = ( await this . listS3Object ( key ) ) . filter (
282
+ ( key ) => key && regex . test ( key )
283
+ ) ;
284
+
285
+ await this . client . send (
286
+ new DeleteObjectsCommand ( {
287
+ Bucket : CACHE_BUCKET_NAME ,
288
+ Delete : {
289
+ Objects : s3Keys . map ( ( Key ) => ( { Key } ) ) ,
290
+ } ,
291
+ } )
292
+ ) ;
293
+ } catch ( e ) {
294
+ error ( "Failed to delete cache" , e ) ;
295
+ }
296
+ }
239
297
}
0 commit comments