1
- import type { CacheValue , IncrementalCache , WithLastModified } from "@opennextjs/aws/types/overrides" ;
2
- import { IgnorableError , RecoverableError } from "@opennextjs/aws/utils/error.js" ;
1
+ import { error } from "@opennextjs/aws/adapters/logger.js" ;
2
+ import type { CacheValue , IncrementalCache , WithLastModified } from "@opennextjs/aws/types/overrides.js" ;
3
+ import { IgnorableError } from "@opennextjs/aws/utils/error.js" ;
3
4
4
5
import { getCloudflareContext } from "../../cloudflare-context.js" ;
5
-
6
- export const CACHE_ASSET_DIR = "cdn-cgi/_next_cache" ;
7
-
8
- export const STATUS_DELETED = 1 ;
6
+ import { debugCache , FALLBACK_BUILD_ID , IncrementalCacheEntry } from "../internal.js" ;
9
7
10
8
export const NAME = "cf-kv-incremental-cache" ;
11
9
10
+ export const BINDING_NAME = "NEXT_INC_CACHE_KV" ;
11
+
12
12
/**
13
- * Open Next cache based on cloudflare KV and Assets .
13
+ * Open Next cache based on Cloudflare KV.
14
14
*
15
15
* Note: The class is instantiated outside of the request context.
16
16
* The cloudflare context and process.env are not initialized yet
@@ -23,69 +23,32 @@ class KVIncrementalCache implements IncrementalCache {
23
23
key : string ,
24
24
isFetch ?: IsFetch
25
25
) : Promise < WithLastModified < CacheValue < IsFetch > > | null > {
26
- const cfEnv = getCloudflareContext ( ) . env ;
27
- const kv = cfEnv . NEXT_INC_CACHE_KV ;
28
- const assets = cfEnv . ASSETS ;
29
-
30
- if ( ! ( kv || assets ) ) {
31
- throw new IgnorableError ( `No KVNamespace nor Fetcher` ) ;
32
- }
26
+ const kv = getCloudflareContext ( ) . env [ BINDING_NAME ] ;
27
+ if ( ! kv ) throw new IgnorableError ( "No KV Namespace" ) ;
33
28
34
- this . debug ( `Get ${ key } ` ) ;
29
+ debugCache ( `Get ${ key } ` ) ;
35
30
36
31
try {
37
- let entry : {
38
- value ?: CacheValue < IsFetch > ;
39
- lastModified ?: number ;
40
- status ?: number ;
41
- } | null = null ;
42
-
43
- if ( kv ) {
44
- this . debug ( `- From KV` ) ;
45
- const kvKey = this . getKVKey ( key , isFetch ) ;
46
- entry = await kv . get ( kvKey , "json" ) ;
47
- if ( entry ?. status === STATUS_DELETED ) {
48
- return null ;
49
- }
50
- }
32
+ const entry = await kv . get < IncrementalCacheEntry < IsFetch > | CacheValue < IsFetch > > (
33
+ this . getKVKey ( key , isFetch ) ,
34
+ "json"
35
+ ) ;
36
+
37
+ if ( ! entry ) return null ;
51
38
52
- if ( ! entry && assets ) {
53
- this . debug ( `- From Assets` ) ;
54
- const url = this . getAssetUrl ( key , isFetch ) ;
55
- const response = await assets . fetch ( url ) ;
56
- if ( response . ok ) {
57
- // TODO: consider populating KV with the asset value if faster.
58
- // This could be optional as KV writes are $$.
59
- // See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
60
- entry = {
61
- value : await response . json ( ) ,
62
- // __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
63
- lastModified : ( globalThis as { __BUILD_TIMESTAMP_MS__ ?: number } ) . __BUILD_TIMESTAMP_MS__ ,
64
- } ;
65
- }
66
- if ( ! kv ) {
67
- // The cache can not be updated when there is no KV
68
- // As we don't want to keep serving stale data for ever,
69
- // we pretend the entry is not in cache
70
- if (
71
- entry ?. value &&
72
- "kind" in entry . value &&
73
- entry . value . kind === "FETCH" &&
74
- entry . value . data ?. headers ?. expires
75
- ) {
76
- const expiresTime = new Date ( entry . value . data . headers . expires ) . getTime ( ) ;
77
- if ( ! isNaN ( expiresTime ) && expiresTime <= Date . now ( ) ) {
78
- this . debug ( `found expired entry (expire time: ${ entry . value . data . headers . expires } )` ) ;
79
- return null ;
80
- }
81
- }
82
- }
39
+ if ( "lastModified" in entry ) {
40
+ return entry ;
83
41
}
84
42
85
- this . debug ( entry ? `-> hit` : `-> miss` ) ;
86
- return { value : entry ?. value , lastModified : entry ?. lastModified } ;
87
- } catch {
88
- throw new RecoverableError ( `Failed to get cache [${ key } ]` ) ;
43
+ // if there is no lastModified property, the file was stored during build-time cache population.
44
+ return {
45
+ value : entry ,
46
+ // __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
47
+ lastModified : ( globalThis as { __BUILD_TIMESTAMP_MS__ ?: number } ) . __BUILD_TIMESTAMP_MS__ ,
48
+ } ;
49
+ } catch ( e ) {
50
+ error ( "Failed to get from cache" , e ) ;
51
+ return null ;
89
52
}
90
53
}
91
54
@@ -94,69 +57,44 @@ class KVIncrementalCache implements IncrementalCache {
94
57
value : CacheValue < IsFetch > ,
95
58
isFetch ?: IsFetch
96
59
) : Promise < void > {
97
- const kv = getCloudflareContext ( ) . env . NEXT_INC_CACHE_KV ;
98
-
99
- if ( ! kv ) {
100
- throw new IgnorableError ( `No KVNamespace` ) ;
101
- }
60
+ const kv = getCloudflareContext ( ) . env [ BINDING_NAME ] ;
61
+ if ( ! kv ) throw new IgnorableError ( "No KV Namespace" ) ;
102
62
103
- this . debug ( `Set ${ key } ` ) ;
63
+ debugCache ( `Set ${ key } ` ) ;
104
64
105
65
try {
106
- const kvKey = this . getKVKey ( key , isFetch ) ;
107
- // Note: We can not set a TTL as we might fallback to assets,
108
- // still removing old data (old BUILD_ID) could help avoiding
109
- // the cache growing too big.
110
66
await kv . put (
111
- kvKey ,
67
+ this . getKVKey ( key , isFetch ) ,
112
68
JSON . stringify ( {
113
69
value,
114
70
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
115
71
// See https://developers.cloudflare.com/workers/reference/security-model/
116
72
lastModified : Date . now ( ) ,
117
73
} )
74
+ // TODO: Figure out how to best leverage KV's TTL.
75
+ // NOTE: Ideally, the cache should operate in an SWR-like manner.
118
76
) ;
119
- } catch {
120
- throw new RecoverableError ( ` Failed to set cache [ ${ key } ]` ) ;
77
+ } catch ( e ) {
78
+ error ( " Failed to set to cache" , e ) ;
121
79
}
122
80
}
123
81
124
82
async delete ( key : string ) : Promise < void > {
125
- const kv = getCloudflareContext ( ) . env . NEXT_INC_CACHE_KV ;
83
+ const kv = getCloudflareContext ( ) . env [ BINDING_NAME ] ;
84
+ if ( ! kv ) throw new IgnorableError ( "No KV Namespace" ) ;
126
85
127
- if ( ! kv ) {
128
- throw new IgnorableError ( `No KVNamespace` ) ;
129
- }
130
-
131
- this . debug ( `Delete ${ key } ` ) ;
86
+ debugCache ( `Delete ${ key } ` ) ;
132
87
133
88
try {
134
- const kvKey = this . getKVKey ( key , /* isFetch= */ false ) ;
135
- // Do not delete the key as we would then fallback to the assets.
136
- await kv . put ( kvKey , JSON . stringify ( { status : STATUS_DELETED } ) ) ;
137
- } catch {
138
- throw new RecoverableError ( `Failed to delete cache [${ key } ]` ) ;
89
+ await kv . delete ( this . getKVKey ( key , /* isFetch= */ false ) ) ;
90
+ } catch ( e ) {
91
+ error ( "Failed to delete from cache" , e ) ;
139
92
}
140
93
}
141
94
142
95
protected getKVKey ( key : string , isFetch ?: boolean ) : string {
143
- return `${ this . getBuildId ( ) } /${ key } .${ isFetch ? "fetch" : "cache" } ` . replace ( / \/ + / g, "/" ) ;
144
- }
145
-
146
- protected getAssetUrl ( key : string , isFetch ?: boolean ) : string {
147
- return isFetch
148
- ? `http://assets.local/${ CACHE_ASSET_DIR } /__fetch/${ this . getBuildId ( ) } /${ key } `
149
- : `http://assets.local/${ CACHE_ASSET_DIR } /${ this . getBuildId ( ) } /${ key } .cache` ;
150
- }
151
-
152
- protected debug ( ...args : unknown [ ] ) {
153
- if ( process . env . NEXT_PRIVATE_DEBUG_CACHE ) {
154
- console . log ( `[Cache ${ this . name } ] ` , ...args ) ;
155
- }
156
- }
157
-
158
- protected getBuildId ( ) {
159
- return process . env . NEXT_BUILD_ID ?? "no-build-id" ;
96
+ const buildId = process . env . NEXT_BUILD_ID ?? FALLBACK_BUILD_ID ;
97
+ return `${ buildId } /${ key } .${ isFetch ? "fetch" : "cache" } ` . replace ( / \/ + / g, "/" ) ;
160
98
}
161
99
}
162
100
0 commit comments