@opennextjs/cloudflare 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/config.js +2 -0
- package/dist/api/overrides/incremental-cache/kv-incremental-cache.d.ts +8 -7
- package/dist/api/overrides/incremental-cache/kv-incremental-cache.js +50 -90
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +9 -0
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +22 -9
- package/dist/api/overrides/incremental-cache/regional-cache.d.ts +18 -3
- package/dist/api/overrides/incremental-cache/regional-cache.js +55 -18
- package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.d.ts +17 -0
- package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.js +46 -0
- package/dist/api/overrides/{incremental-cache/internal.d.ts → internal.d.ts} +2 -0
- package/dist/api/overrides/internal.js +6 -0
- package/dist/api/overrides/queue/memory-queue.js +3 -2
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +1 -1
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +9 -13
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +4 -3
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/build/build.js +4 -3
- package/dist/cli/build/bundle-server.js +1 -42
- package/dist/cli/build/open-next/compile-env-files.js +1 -1
- package/dist/cli/build/open-next/compile-init.d.ts +5 -0
- package/dist/cli/build/open-next/compile-init.js +27 -0
- package/dist/cli/build/open-next/createServerBundle.js +7 -2
- package/dist/cli/build/utils/ensure-cf-config.js +2 -0
- package/dist/cli/commands/populate-cache.d.ts +7 -0
- package/dist/cli/commands/populate-cache.js +114 -46
- package/dist/cli/commands/populate-cache.spec.js +61 -0
- package/dist/cli/commands/upload.d.ts +5 -0
- package/dist/cli/commands/upload.js +9 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/templates/init.d.ts +13 -0
- package/dist/cli/templates/init.js +105 -0
- package/dist/cli/templates/worker.js +5 -53
- package/package.json +2 -2
- package/templates/open-next.config.ts +2 -2
- package/templates/wrangler.jsonc +9 -7
- package/dist/cli/build/open-next/copyCacheAssets.d.ts +0 -2
- package/dist/cli/build/open-next/copyCacheAssets.js +0 -10
- package/dist/cli/build/patches/plugins/next-minimal.d.ts +0 -4
- package/dist/cli/build/patches/plugins/next-minimal.js +0 -86
- package/dist/cli/build/patches/plugins/next-minimal.spec.d.ts +0 -1
- package/dist/cli/build/patches/plugins/next-minimal.spec.js +0 -71
- /package/dist/{api/overrides/incremental-cache/internal.js → cli/commands/populate-cache.spec.d.ts} +0 -0
package/dist/api/config.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides";
|
|
2
|
-
export declare const CACHE_ASSET_DIR = "cdn-cgi/_next_cache";
|
|
3
|
-
export declare const STATUS_DELETED = 1;
|
|
1
|
+
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
4
2
|
export declare const NAME = "cf-kv-incremental-cache";
|
|
3
|
+
export declare const BINDING_NAME = "NEXT_INC_CACHE_KV";
|
|
4
|
+
export type KeyOptions = {
|
|
5
|
+
isFetch?: boolean;
|
|
6
|
+
buildId?: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function computeCacheKey(key: string, options: KeyOptions): string;
|
|
5
9
|
/**
|
|
6
|
-
* Open Next cache based on
|
|
10
|
+
* Open Next cache based on Cloudflare KV.
|
|
7
11
|
*
|
|
8
12
|
* Note: The class is instantiated outside of the request context.
|
|
9
13
|
* The cloudflare context and process.env are not initialized yet
|
|
@@ -15,9 +19,6 @@ declare class KVIncrementalCache implements IncrementalCache {
|
|
|
15
19
|
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
16
20
|
delete(key: string): Promise<void>;
|
|
17
21
|
protected getKVKey(key: string, isFetch?: boolean): string;
|
|
18
|
-
protected getAssetUrl(key: string, isFetch?: boolean): string;
|
|
19
|
-
protected debug(...args: unknown[]): void;
|
|
20
|
-
protected getBuildId(): string;
|
|
21
22
|
}
|
|
22
23
|
declare const _default: KVIncrementalCache;
|
|
23
24
|
export default _default;
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
3
|
+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
2
4
|
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
3
|
-
|
|
4
|
-
export const STATUS_DELETED = 1;
|
|
5
|
+
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
5
6
|
export const NAME = "cf-kv-incremental-cache";
|
|
7
|
+
export const BINDING_NAME = "NEXT_INC_CACHE_KV";
|
|
8
|
+
export function computeCacheKey(key, options) {
|
|
9
|
+
const { isFetch = false, buildId = FALLBACK_BUILD_ID } = options;
|
|
10
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
11
|
+
return `${buildId}/${hash}.${isFetch ? "fetch" : "cache"}`.replace(/\/+/g, "/");
|
|
12
|
+
}
|
|
6
13
|
/**
|
|
7
|
-
* Open Next cache based on
|
|
14
|
+
* Open Next cache based on Cloudflare KV.
|
|
8
15
|
*
|
|
9
16
|
* Note: The class is instantiated outside of the request context.
|
|
10
17
|
* The cloudflare context and process.env are not initialized yet
|
|
@@ -13,112 +20,65 @@ export const NAME = "cf-kv-incremental-cache";
|
|
|
13
20
|
class KVIncrementalCache {
|
|
14
21
|
name = NAME;
|
|
15
22
|
async get(key, isFetch) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
throw new IgnorableError(`No KVNamespace nor Fetcher`);
|
|
21
|
-
}
|
|
22
|
-
this.debug(`Get ${key}`);
|
|
23
|
+
const kv = getCloudflareContext().env[BINDING_NAME];
|
|
24
|
+
if (!kv)
|
|
25
|
+
throw new IgnorableError("No KV Namespace");
|
|
26
|
+
debugCache(`Get ${key}`);
|
|
23
27
|
try {
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
entry
|
|
29
|
-
if (entry?.status === STATUS_DELETED) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
if (!entry && assets) {
|
|
34
|
-
this.debug(`- From Assets`);
|
|
35
|
-
const url = this.getAssetUrl(key, isFetch);
|
|
36
|
-
const response = await assets.fetch(url);
|
|
37
|
-
if (response.ok) {
|
|
38
|
-
// TODO: consider populating KV with the asset value if faster.
|
|
39
|
-
// This could be optional as KV writes are $$.
|
|
40
|
-
// See https://github.com/opennextjs/opennextjs-cloudflare/pull/194#discussion_r1893166026
|
|
41
|
-
entry = {
|
|
42
|
-
value: await response.json(),
|
|
43
|
-
// __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
|
|
44
|
-
lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
if (!kv) {
|
|
48
|
-
// The cache can not be updated when there is no KV
|
|
49
|
-
// As we don't want to keep serving stale data for ever,
|
|
50
|
-
// we pretend the entry is not in cache
|
|
51
|
-
if (entry?.value &&
|
|
52
|
-
"kind" in entry.value &&
|
|
53
|
-
entry.value.kind === "FETCH" &&
|
|
54
|
-
entry.value.data?.headers?.expires) {
|
|
55
|
-
const expiresTime = new Date(entry.value.data.headers.expires).getTime();
|
|
56
|
-
if (!isNaN(expiresTime) && expiresTime <= Date.now()) {
|
|
57
|
-
this.debug(`found expired entry (expire time: ${entry.value.data.headers.expires})`);
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
28
|
+
const entry = await kv.get(this.getKVKey(key, isFetch), "json");
|
|
29
|
+
if (!entry)
|
|
30
|
+
return null;
|
|
31
|
+
if ("lastModified" in entry) {
|
|
32
|
+
return entry;
|
|
62
33
|
}
|
|
63
|
-
|
|
64
|
-
return {
|
|
34
|
+
// if there is no lastModified property, the file was stored during build-time cache population.
|
|
35
|
+
return {
|
|
36
|
+
value: entry,
|
|
37
|
+
lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
|
|
38
|
+
};
|
|
65
39
|
}
|
|
66
|
-
catch {
|
|
67
|
-
|
|
40
|
+
catch (e) {
|
|
41
|
+
error("Failed to get from cache", e);
|
|
42
|
+
return null;
|
|
68
43
|
}
|
|
69
44
|
}
|
|
70
45
|
async set(key, value, isFetch) {
|
|
71
|
-
const kv = getCloudflareContext().env
|
|
72
|
-
if (!kv)
|
|
73
|
-
throw new IgnorableError(
|
|
74
|
-
}
|
|
75
|
-
this.debug(`Set ${key}`);
|
|
46
|
+
const kv = getCloudflareContext().env[BINDING_NAME];
|
|
47
|
+
if (!kv)
|
|
48
|
+
throw new IgnorableError("No KV Namespace");
|
|
49
|
+
debugCache(`Set ${key}`);
|
|
76
50
|
try {
|
|
77
|
-
|
|
78
|
-
// Note: We can not set a TTL as we might fallback to assets,
|
|
79
|
-
// still removing old data (old BUILD_ID) could help avoiding
|
|
80
|
-
// the cache growing too big.
|
|
81
|
-
await kv.put(kvKey, JSON.stringify({
|
|
51
|
+
await kv.put(this.getKVKey(key, isFetch), JSON.stringify({
|
|
82
52
|
value,
|
|
83
53
|
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
|
84
54
|
// See https://developers.cloudflare.com/workers/reference/security-model/
|
|
85
55
|
lastModified: Date.now(),
|
|
86
|
-
})
|
|
56
|
+
})
|
|
57
|
+
// TODO: Figure out how to best leverage KV's TTL.
|
|
58
|
+
// NOTE: Ideally, the cache should operate in an SWR-like manner.
|
|
59
|
+
);
|
|
87
60
|
}
|
|
88
|
-
catch {
|
|
89
|
-
|
|
61
|
+
catch (e) {
|
|
62
|
+
error("Failed to set to cache", e);
|
|
90
63
|
}
|
|
91
64
|
}
|
|
92
65
|
async delete(key) {
|
|
93
|
-
const kv = getCloudflareContext().env
|
|
94
|
-
if (!kv)
|
|
95
|
-
throw new IgnorableError(
|
|
96
|
-
}
|
|
97
|
-
this.debug(`Delete ${key}`);
|
|
66
|
+
const kv = getCloudflareContext().env[BINDING_NAME];
|
|
67
|
+
if (!kv)
|
|
68
|
+
throw new IgnorableError("No KV Namespace");
|
|
69
|
+
debugCache(`Delete ${key}`);
|
|
98
70
|
try {
|
|
99
|
-
|
|
100
|
-
// Do not delete the key as we would then fallback to the assets.
|
|
101
|
-
await kv.put(kvKey, JSON.stringify({ status: STATUS_DELETED }));
|
|
71
|
+
await kv.delete(this.getKVKey(key, /* isFetch= */ false));
|
|
102
72
|
}
|
|
103
|
-
catch {
|
|
104
|
-
|
|
73
|
+
catch (e) {
|
|
74
|
+
error("Failed to delete from cache", e);
|
|
105
75
|
}
|
|
106
76
|
}
|
|
107
77
|
getKVKey(key, isFetch) {
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
? `http://assets.local/${CACHE_ASSET_DIR}/__fetch/${this.getBuildId()}/${key}`
|
|
113
|
-
: `http://assets.local/${CACHE_ASSET_DIR}/${this.getBuildId()}/${key}.cache`;
|
|
114
|
-
}
|
|
115
|
-
debug(...args) {
|
|
116
|
-
if (process.env.NEXT_PRIVATE_DEBUG_CACHE) {
|
|
117
|
-
console.log(`[Cache ${this.name}] `, ...args);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
getBuildId() {
|
|
121
|
-
return process.env.NEXT_BUILD_ID ?? "no-build-id";
|
|
78
|
+
return computeCacheKey(key, {
|
|
79
|
+
buildId: process.env.NEXT_BUILD_ID,
|
|
80
|
+
isFetch,
|
|
81
|
+
});
|
|
122
82
|
}
|
|
123
83
|
}
|
|
124
84
|
export default new KVIncrementalCache();
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
2
|
export declare const NAME = "cf-r2-incremental-cache";
|
|
3
|
+
export declare const BINDING_NAME = "NEXT_INC_CACHE_R2_BUCKET";
|
|
4
|
+
export declare const PREFIX_ENV_NAME = "NEXT_INC_CACHE_R2_PREFIX";
|
|
5
|
+
export declare const DEFAULT_PREFIX = "incremental-cache";
|
|
6
|
+
export type KeyOptions = {
|
|
7
|
+
isFetch?: boolean;
|
|
8
|
+
directory?: string;
|
|
9
|
+
buildId?: string;
|
|
10
|
+
};
|
|
11
|
+
export declare function computeCacheKey(key: string, options: KeyOptions): string;
|
|
3
12
|
/**
|
|
4
13
|
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
5
14
|
* underlying data store.
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
3
|
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
3
4
|
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
5
|
+
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
4
6
|
export const NAME = "cf-r2-incremental-cache";
|
|
7
|
+
export const BINDING_NAME = "NEXT_INC_CACHE_R2_BUCKET";
|
|
8
|
+
export const PREFIX_ENV_NAME = "NEXT_INC_CACHE_R2_PREFIX";
|
|
9
|
+
export const DEFAULT_PREFIX = "incremental-cache";
|
|
10
|
+
export function computeCacheKey(key, options) {
|
|
11
|
+
const { isFetch = false, directory = DEFAULT_PREFIX, buildId = FALLBACK_BUILD_ID } = options;
|
|
12
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
13
|
+
return `${directory}/${buildId}/${hash}.${isFetch ? "fetch" : "cache"}`.replace(/\/+/g, "/");
|
|
14
|
+
}
|
|
5
15
|
/**
|
|
6
16
|
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
7
17
|
* underlying data store.
|
|
@@ -12,10 +22,10 @@ export const NAME = "cf-r2-incremental-cache";
|
|
|
12
22
|
class R2IncrementalCache {
|
|
13
23
|
name = NAME;
|
|
14
24
|
async get(key, isFetch) {
|
|
15
|
-
const r2 = getCloudflareContext().env
|
|
25
|
+
const r2 = getCloudflareContext().env[BINDING_NAME];
|
|
16
26
|
if (!r2)
|
|
17
27
|
throw new IgnorableError("No R2 bucket");
|
|
18
|
-
|
|
28
|
+
debugCache(`Get ${key}`);
|
|
19
29
|
try {
|
|
20
30
|
const r2Object = await r2.get(this.getR2Key(key, isFetch));
|
|
21
31
|
if (!r2Object)
|
|
@@ -31,10 +41,10 @@ class R2IncrementalCache {
|
|
|
31
41
|
}
|
|
32
42
|
}
|
|
33
43
|
async set(key, value, isFetch) {
|
|
34
|
-
const r2 = getCloudflareContext().env
|
|
44
|
+
const r2 = getCloudflareContext().env[BINDING_NAME];
|
|
35
45
|
if (!r2)
|
|
36
46
|
throw new IgnorableError("No R2 bucket");
|
|
37
|
-
|
|
47
|
+
debugCache(`Set ${key}`);
|
|
38
48
|
try {
|
|
39
49
|
await r2.put(this.getR2Key(key, isFetch), JSON.stringify(value));
|
|
40
50
|
}
|
|
@@ -43,10 +53,10 @@ class R2IncrementalCache {
|
|
|
43
53
|
}
|
|
44
54
|
}
|
|
45
55
|
async delete(key) {
|
|
46
|
-
const r2 = getCloudflareContext().env
|
|
56
|
+
const r2 = getCloudflareContext().env[BINDING_NAME];
|
|
47
57
|
if (!r2)
|
|
48
58
|
throw new IgnorableError("No R2 bucket");
|
|
49
|
-
|
|
59
|
+
debugCache(`Delete ${key}`);
|
|
50
60
|
try {
|
|
51
61
|
await r2.delete(this.getR2Key(key));
|
|
52
62
|
}
|
|
@@ -55,8 +65,11 @@ class R2IncrementalCache {
|
|
|
55
65
|
}
|
|
56
66
|
}
|
|
57
67
|
getR2Key(key, isFetch) {
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
return computeCacheKey(key, {
|
|
69
|
+
directory: getCloudflareContext().env[PREFIX_ENV_NAME],
|
|
70
|
+
buildId: process.env.NEXT_BUILD_ID,
|
|
71
|
+
isFetch,
|
|
72
|
+
});
|
|
60
73
|
}
|
|
61
74
|
}
|
|
62
75
|
export default new R2IncrementalCache();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
-
import { IncrementalCacheEntry } from "
|
|
2
|
+
import { IncrementalCacheEntry } from "../internal.js";
|
|
3
3
|
type Options = {
|
|
4
4
|
/**
|
|
5
5
|
* The mode to use for the regional cache.
|
|
@@ -9,6 +9,13 @@ type Options = {
|
|
|
9
9
|
* or an ISR/SSG entry for up to 30 minutes.
|
|
10
10
|
*/
|
|
11
11
|
mode: "short-lived" | "long-lived";
|
|
12
|
+
/**
|
|
13
|
+
* The default TTL of long-lived cache entries.
|
|
14
|
+
* When no revalidate is provided, the default age will be used.
|
|
15
|
+
*
|
|
16
|
+
* @default `THIRTY_MINUTES_IN_SECONDS`
|
|
17
|
+
*/
|
|
18
|
+
defaultLongLivedTtlSec?: number;
|
|
12
19
|
/**
|
|
13
20
|
* Whether the regional cache entry should be updated in the background or not when it experiences
|
|
14
21
|
* a cache hit.
|
|
@@ -17,6 +24,11 @@ type Options = {
|
|
|
17
24
|
*/
|
|
18
25
|
shouldLazilyUpdateOnCacheHit?: boolean;
|
|
19
26
|
};
|
|
27
|
+
interface PutToCacheInput {
|
|
28
|
+
key: string;
|
|
29
|
+
isFetch: boolean | undefined;
|
|
30
|
+
entry: IncrementalCacheEntry<boolean>;
|
|
31
|
+
}
|
|
20
32
|
/**
|
|
21
33
|
* Wrapper adding a regional cache on an `IncrementalCache` implementation
|
|
22
34
|
*/
|
|
@@ -30,8 +42,8 @@ declare class RegionalCache implements IncrementalCache {
|
|
|
30
42
|
set<IsFetch extends boolean = false>(key: string, value: CacheValue<IsFetch>, isFetch?: IsFetch): Promise<void>;
|
|
31
43
|
delete(key: string): Promise<void>;
|
|
32
44
|
protected getCacheInstance(): Promise<Cache>;
|
|
33
|
-
protected
|
|
34
|
-
protected putToCache(key
|
|
45
|
+
protected getCacheUrlKey(key: string, isFetch?: boolean): string;
|
|
46
|
+
protected putToCache({ key, isFetch, entry }: PutToCacheInput): Promise<void>;
|
|
35
47
|
}
|
|
36
48
|
/**
|
|
37
49
|
* A regional cache will wrap an incremental cache and provide faster cache lookups for an entry
|
|
@@ -50,6 +62,9 @@ declare class RegionalCache implements IncrementalCache {
|
|
|
50
62
|
* or an ISR/SSG entry for up to 30 minutes.
|
|
51
63
|
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
|
|
52
64
|
* the background or not when it experiences a cache hit.
|
|
65
|
+
* @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
|
|
66
|
+
* When no revalidate is provided, the default age will be used.
|
|
67
|
+
* @default `THIRTY_MINUTES_IN_SECONDS`
|
|
53
68
|
*
|
|
54
69
|
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
55
70
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
2
|
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
3
|
+
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
3
4
|
import { NAME as KV_CACHE_NAME } from "./kv-incremental-cache.js";
|
|
4
5
|
const ONE_MINUTE_IN_SECONDS = 60;
|
|
5
6
|
const THIRTY_MINUTES_IN_SECONDS = ONE_MINUTE_IN_SECONDS * 30;
|
|
@@ -23,17 +24,17 @@ class RegionalCache {
|
|
|
23
24
|
async get(key, isFetch) {
|
|
24
25
|
try {
|
|
25
26
|
const cache = await this.getCacheInstance();
|
|
26
|
-
const
|
|
27
|
+
const urlKey = this.getCacheUrlKey(key, isFetch);
|
|
27
28
|
// Check for a cached entry as this will be faster than the store response.
|
|
28
|
-
const cachedResponse = await cache.match(
|
|
29
|
+
const cachedResponse = await cache.match(urlKey);
|
|
29
30
|
if (cachedResponse) {
|
|
30
|
-
|
|
31
|
+
debugCache("Get - cached response");
|
|
31
32
|
// Re-fetch from the store and update the regional cache in the background
|
|
32
33
|
if (this.opts.shouldLazilyUpdateOnCacheHit) {
|
|
33
34
|
getCloudflareContext().ctx.waitUntil(this.store.get(key, isFetch).then(async (rawEntry) => {
|
|
34
35
|
const { value, lastModified } = rawEntry ?? {};
|
|
35
36
|
if (value && typeof lastModified === "number") {
|
|
36
|
-
await this.putToCache(
|
|
37
|
+
await this.putToCache({ key, isFetch, entry: { value, lastModified } });
|
|
37
38
|
}
|
|
38
39
|
}));
|
|
39
40
|
}
|
|
@@ -44,7 +45,7 @@ class RegionalCache {
|
|
|
44
45
|
if (!value || typeof lastModified !== "number")
|
|
45
46
|
return null;
|
|
46
47
|
// Update the locale cache after retrieving from the store.
|
|
47
|
-
getCloudflareContext().ctx.waitUntil(this.putToCache(
|
|
48
|
+
getCloudflareContext().ctx.waitUntil(this.putToCache({ key, isFetch, entry: { value, lastModified } }));
|
|
48
49
|
return { value, lastModified };
|
|
49
50
|
}
|
|
50
51
|
catch (e) {
|
|
@@ -55,11 +56,15 @@ class RegionalCache {
|
|
|
55
56
|
async set(key, value, isFetch) {
|
|
56
57
|
try {
|
|
57
58
|
await this.store.set(key, value, isFetch);
|
|
58
|
-
await this.putToCache(
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
await this.putToCache({
|
|
60
|
+
key,
|
|
61
|
+
isFetch,
|
|
62
|
+
entry: {
|
|
63
|
+
value,
|
|
64
|
+
// Note: `Date.now()` returns the time of the last IO rather than the actual time.
|
|
65
|
+
// See https://developers.cloudflare.com/workers/reference/security-model/
|
|
66
|
+
lastModified: Date.now(),
|
|
67
|
+
},
|
|
63
68
|
});
|
|
64
69
|
}
|
|
65
70
|
catch (e) {
|
|
@@ -70,7 +75,7 @@ class RegionalCache {
|
|
|
70
75
|
try {
|
|
71
76
|
await this.store.delete(key);
|
|
72
77
|
const cache = await this.getCacheInstance();
|
|
73
|
-
await cache.delete(this.
|
|
78
|
+
await cache.delete(this.getCacheUrlKey(key));
|
|
74
79
|
}
|
|
75
80
|
catch (e) {
|
|
76
81
|
error("Failed to delete from regional cache", e);
|
|
@@ -82,16 +87,28 @@ class RegionalCache {
|
|
|
82
87
|
this.localCache = await caches.open("incremental-cache");
|
|
83
88
|
return this.localCache;
|
|
84
89
|
}
|
|
85
|
-
|
|
86
|
-
|
|
90
|
+
getCacheUrlKey(key, isFetch) {
|
|
91
|
+
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
|
|
92
|
+
return ("http://cache.local" + `/${buildId}/${key}`.replace(/\/+/g, "/") + `.${isFetch ? "fetch" : "cache"}`);
|
|
87
93
|
}
|
|
88
|
-
async putToCache(key, entry) {
|
|
94
|
+
async putToCache({ key, isFetch, entry }) {
|
|
95
|
+
const urlKey = this.getCacheUrlKey(key, isFetch);
|
|
89
96
|
const cache = await this.getCacheInstance();
|
|
90
97
|
const age = this.opts.mode === "short-lived"
|
|
91
98
|
? ONE_MINUTE_IN_SECONDS
|
|
92
|
-
: entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
: entry.value.revalidate || this.opts.defaultLongLivedTtlSec || THIRTY_MINUTES_IN_SECONDS;
|
|
100
|
+
// We default to the entry key if no tags are found.
|
|
101
|
+
// so that we can also revalidate page router based entry this way.
|
|
102
|
+
const tags = getTagsFromCacheEntry(entry) ?? [key];
|
|
103
|
+
await cache.put(urlKey, new Response(JSON.stringify(entry), {
|
|
104
|
+
headers: new Headers({
|
|
105
|
+
"cache-control": `max-age=${age}`,
|
|
106
|
+
...(tags.length > 0
|
|
107
|
+
? {
|
|
108
|
+
"cache-tag": tags.join(","),
|
|
109
|
+
}
|
|
110
|
+
: {}),
|
|
111
|
+
}),
|
|
95
112
|
}));
|
|
96
113
|
}
|
|
97
114
|
}
|
|
@@ -112,9 +129,29 @@ class RegionalCache {
|
|
|
112
129
|
* or an ISR/SSG entry for up to 30 minutes.
|
|
113
130
|
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
|
|
114
131
|
* the background or not when it experiences a cache hit.
|
|
132
|
+
* @param opts.defaultLongLivedTtlSec The default age to use for long-lived cache entries.
|
|
133
|
+
* When no revalidate is provided, the default age will be used.
|
|
134
|
+
* @default `THIRTY_MINUTES_IN_SECONDS`
|
|
115
135
|
*
|
|
116
136
|
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
117
137
|
*/
|
|
118
138
|
export function withRegionalCache(cache, opts) {
|
|
119
139
|
return new RegionalCache(cache, opts);
|
|
120
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Extract the list of tags from a cache entry.
|
|
143
|
+
*/
|
|
144
|
+
function getTagsFromCacheEntry(entry) {
|
|
145
|
+
if ("tags" in entry.value && entry.value.tags) {
|
|
146
|
+
return entry.value.tags;
|
|
147
|
+
}
|
|
148
|
+
if ("meta" in entry.value &&
|
|
149
|
+
entry.value.meta &&
|
|
150
|
+
"headers" in entry.value.meta &&
|
|
151
|
+
entry.value.meta.headers) {
|
|
152
|
+
const rawTags = entry.value.meta.headers["x-next-cache-tags"];
|
|
153
|
+
if (typeof rawTags === "string") {
|
|
154
|
+
return rawTags.split(",");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
export declare const CACHE_DIR = "cdn-cgi/_next_cache";
|
|
3
|
+
export declare const NAME = "cf-static-assets-incremental-cache";
|
|
4
|
+
/**
|
|
5
|
+
* This cache uses Workers static assets.
|
|
6
|
+
*
|
|
7
|
+
* It should only be used for applications that do NOT want revalidation and ONLY want to serve prerendered data.
|
|
8
|
+
*/
|
|
9
|
+
declare class StaticAssetsIncrementalCache implements IncrementalCache {
|
|
10
|
+
readonly name = "cf-static-assets-incremental-cache";
|
|
11
|
+
get<IsFetch extends boolean = false>(key: string, isFetch?: IsFetch): Promise<WithLastModified<CacheValue<IsFetch>> | null>;
|
|
12
|
+
set(): Promise<void>;
|
|
13
|
+
delete(): Promise<void>;
|
|
14
|
+
protected getAssetUrl(key: string, isFetch?: boolean): string;
|
|
15
|
+
}
|
|
16
|
+
declare const _default: StaticAssetsIncrementalCache;
|
|
17
|
+
export default _default;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
+
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
3
|
+
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
4
|
+
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
5
|
+
// Assets inside `cdn-cgi/...` are only accessible by the worker.
|
|
6
|
+
export const CACHE_DIR = "cdn-cgi/_next_cache";
|
|
7
|
+
export const NAME = "cf-static-assets-incremental-cache";
|
|
8
|
+
/**
|
|
9
|
+
* This cache uses Workers static assets.
|
|
10
|
+
*
|
|
11
|
+
* It should only be used for applications that do NOT want revalidation and ONLY want to serve prerendered data.
|
|
12
|
+
*/
|
|
13
|
+
class StaticAssetsIncrementalCache {
|
|
14
|
+
name = NAME;
|
|
15
|
+
async get(key, isFetch) {
|
|
16
|
+
const assets = getCloudflareContext().env.ASSETS;
|
|
17
|
+
if (!assets)
|
|
18
|
+
throw new IgnorableError("No Static Assets");
|
|
19
|
+
debugCache(`Get ${key}`);
|
|
20
|
+
try {
|
|
21
|
+
const response = await assets.fetch(this.getAssetUrl(key, isFetch));
|
|
22
|
+
if (!response.ok)
|
|
23
|
+
return null;
|
|
24
|
+
return {
|
|
25
|
+
value: await response.json(),
|
|
26
|
+
lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
catch (e) {
|
|
30
|
+
error("Failed to get from cache", e);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async set() {
|
|
35
|
+
error("Failed to set to read-only cache");
|
|
36
|
+
}
|
|
37
|
+
async delete() {
|
|
38
|
+
error("Failed to delete from read-only cache");
|
|
39
|
+
}
|
|
40
|
+
getAssetUrl(key, isFetch) {
|
|
41
|
+
const buildId = process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
|
|
42
|
+
const name = (isFetch ? `${CACHE_DIR}/__fetch/${buildId}/${key}` : `${CACHE_DIR}/${buildId}/${key}.cache`).replace(/\/+/g, "/");
|
|
43
|
+
return `http://assets.local/${name}`;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export default new StaticAssetsIncrementalCache();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
2
|
import { IgnorableError } from "@opennextjs/aws/utils/error.js";
|
|
3
3
|
import { getCloudflareContext } from "../../cloudflare-context";
|
|
4
|
+
import { debugCache } from "../internal";
|
|
4
5
|
export const DEFAULT_REVALIDATION_TIMEOUT_MS = 10_000;
|
|
5
6
|
/**
|
|
6
7
|
* The Memory Queue offers basic ISR revalidation by directly requesting a revalidation of a route.
|
|
@@ -41,7 +42,7 @@ export class MemoryQueue {
|
|
|
41
42
|
if (response.status !== 200 || response.headers.get("x-nextjs-cache") !== "REVALIDATED") {
|
|
42
43
|
error(`Revalidation failed for ${url} with status ${response.status}`);
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
debugCache(`Revalidation successful for ${url}`);
|
|
45
46
|
}
|
|
46
47
|
catch (e) {
|
|
47
48
|
error(e);
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
|
|
2
2
|
export declare const NAME = "d1-next-mode-tag-cache";
|
|
3
|
+
export declare const BINDING_NAME = "NEXT_TAG_CACHE_D1";
|
|
3
4
|
export declare class D1NextModeTagCache implements NextModeTagCache {
|
|
4
5
|
readonly mode: "nextMode";
|
|
5
6
|
readonly name = "d1-next-mode-tag-cache";
|
|
6
7
|
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
7
8
|
writeTags(tags: string[]): Promise<void>;
|
|
8
9
|
private getConfig;
|
|
9
|
-
protected removeBuildId(key: string): string;
|
|
10
10
|
protected getCacheKey(key: string): string;
|
|
11
11
|
protected getBuildId(): string;
|
|
12
12
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
1
|
+
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
3
2
|
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
3
|
+
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
4
4
|
export const NAME = "d1-next-mode-tag-cache";
|
|
5
|
+
export const BINDING_NAME = "NEXT_TAG_CACHE_D1";
|
|
5
6
|
export class D1NextModeTagCache {
|
|
6
7
|
mode = "nextMode";
|
|
7
8
|
name = NAME;
|
|
@@ -25,19 +26,17 @@ export class D1NextModeTagCache {
|
|
|
25
26
|
}
|
|
26
27
|
async writeTags(tags) {
|
|
27
28
|
const { isDisabled, db } = this.getConfig();
|
|
28
|
-
|
|
29
|
+
// TODO: Remove `tags.length === 0` when https://github.com/opennextjs/opennextjs-aws/pull/828 is used
|
|
30
|
+
if (isDisabled || tags.length === 0)
|
|
29
31
|
return Promise.resolve();
|
|
30
|
-
|
|
32
|
+
await db.batch(tags.map((tag) => db
|
|
31
33
|
.prepare(`INSERT INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`)
|
|
32
34
|
.bind(this.getCacheKey(tag), Date.now())));
|
|
33
|
-
if (!result)
|
|
34
|
-
throw new RecoverableError(`D1 insert failed for ${tags}`);
|
|
35
35
|
}
|
|
36
36
|
getConfig() {
|
|
37
|
-
const
|
|
38
|
-
const db = cfEnv.NEXT_TAG_CACHE_D1;
|
|
37
|
+
const db = getCloudflareContext().env[BINDING_NAME];
|
|
39
38
|
if (!db)
|
|
40
|
-
|
|
39
|
+
debugCache("No D1 database found");
|
|
41
40
|
const isDisabled = !!globalThis.openNextConfig
|
|
42
41
|
.dangerous?.disableTagCache;
|
|
43
42
|
return !db || isDisabled
|
|
@@ -47,14 +46,11 @@ export class D1NextModeTagCache {
|
|
|
47
46
|
db,
|
|
48
47
|
};
|
|
49
48
|
}
|
|
50
|
-
removeBuildId(key) {
|
|
51
|
-
return key.replace(`${this.getBuildId()}/`, "");
|
|
52
|
-
}
|
|
53
49
|
getCacheKey(key) {
|
|
54
50
|
return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
|
|
55
51
|
}
|
|
56
52
|
getBuildId() {
|
|
57
|
-
return process.env.NEXT_BUILD_ID ??
|
|
53
|
+
return process.env.NEXT_BUILD_ID ?? FALLBACK_BUILD_ID;
|
|
58
54
|
}
|
|
59
55
|
}
|
|
60
56
|
export default new D1NextModeTagCache();
|