@opennextjs/cloudflare 1.0.0-beta.2 → 1.0.0-beta.4
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 +5 -0
- package/dist/api/overrides/incremental-cache/kv-incremental-cache.js +10 -3
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.d.ts +6 -0
- package/dist/api/overrides/incremental-cache/r2-incremental-cache.js +11 -2
- package/dist/api/overrides/incremental-cache/regional-cache.d.ts +17 -2
- package/dist/api/overrides/incremental-cache/regional-cache.js +52 -16
- package/dist/api/overrides/incremental-cache/static-assets-incremental-cache.js +0 -1
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.d.ts +0 -1
- package/dist/api/overrides/tag-cache/d1-next-tag-cache.js +3 -8
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +1 -1
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +13 -10
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.spec.js +2 -4
- package/dist/api/overrides/tag-cache/tag-cache-filter.d.ts +26 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.js +41 -0
- package/dist/api/overrides/tag-cache/tag-cache-filter.spec.js +96 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.js +2 -1
- package/dist/cli/build/build.js +4 -1
- package/dist/cli/build/bundle-server.js +3 -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/patches/plugins/pages-router-context.d.ts +11 -0
- package/dist/cli/build/patches/plugins/pages-router-context.js +32 -0
- 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 +51 -23
- package/dist/cli/commands/populate-cache.spec.d.ts +1 -0
- 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.d.ts +3 -1
- package/dist/cli/templates/worker.js +5 -53
- package/package.json +3 -3
- package/templates/open-next.config.ts +2 -2
- package/templates/wrangler.jsonc +9 -7
- 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.js +0 -71
- /package/dist/{cli/build/patches/plugins/next-minimal.spec.d.ts → api/overrides/tag-cache/tag-cache-filter.spec.d.ts} +0 -0
package/dist/api/config.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { CacheValue, IncrementalCache, WithLastModified } from "@opennextjs/aws/types/overrides.js";
|
|
2
2
|
export declare const NAME = "cf-kv-incremental-cache";
|
|
3
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;
|
|
4
9
|
/**
|
|
5
10
|
* Open Next cache based on Cloudflare KV.
|
|
6
11
|
*
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
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";
|
|
4
5
|
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
5
6
|
export const NAME = "cf-kv-incremental-cache";
|
|
6
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
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* Open Next cache based on Cloudflare KV.
|
|
9
15
|
*
|
|
@@ -28,7 +34,6 @@ class KVIncrementalCache {
|
|
|
28
34
|
// if there is no lastModified property, the file was stored during build-time cache population.
|
|
29
35
|
return {
|
|
30
36
|
value: entry,
|
|
31
|
-
// __BUILD_TIMESTAMP_MS__ is injected by ESBuild.
|
|
32
37
|
lastModified: globalThis.__BUILD_TIMESTAMP_MS__,
|
|
33
38
|
};
|
|
34
39
|
}
|
|
@@ -70,8 +75,10 @@ class KVIncrementalCache {
|
|
|
70
75
|
}
|
|
71
76
|
}
|
|
72
77
|
getKVKey(key, isFetch) {
|
|
73
|
-
|
|
74
|
-
|
|
78
|
+
return computeCacheKey(key, {
|
|
79
|
+
buildId: process.env.NEXT_BUILD_ID,
|
|
80
|
+
isFetch,
|
|
81
|
+
});
|
|
75
82
|
}
|
|
76
83
|
}
|
|
77
84
|
export default new KVIncrementalCache();
|
|
@@ -3,6 +3,12 @@ export declare const NAME = "cf-r2-incremental-cache";
|
|
|
3
3
|
export declare const BINDING_NAME = "NEXT_INC_CACHE_R2_BUCKET";
|
|
4
4
|
export declare const PREFIX_ENV_NAME = "NEXT_INC_CACHE_R2_PREFIX";
|
|
5
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;
|
|
6
12
|
/**
|
|
7
13
|
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
8
14
|
* underlying data store.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
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";
|
|
@@ -6,6 +7,11 @@ export const NAME = "cf-r2-incremental-cache";
|
|
|
6
7
|
export const BINDING_NAME = "NEXT_INC_CACHE_R2_BUCKET";
|
|
7
8
|
export const PREFIX_ENV_NAME = "NEXT_INC_CACHE_R2_PREFIX";
|
|
8
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
|
+
}
|
|
9
15
|
/**
|
|
10
16
|
* An instance of the Incremental Cache that uses an R2 bucket (`NEXT_INC_CACHE_R2_BUCKET`) as it's
|
|
11
17
|
* underlying data store.
|
|
@@ -59,8 +65,11 @@ class R2IncrementalCache {
|
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
getR2Key(key, isFetch) {
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
return computeCacheKey(key, {
|
|
69
|
+
directory: getCloudflareContext().env[PREFIX_ENV_NAME],
|
|
70
|
+
buildId: process.env.NEXT_BUILD_ID,
|
|
71
|
+
isFetch,
|
|
72
|
+
});
|
|
64
73
|
}
|
|
65
74
|
}
|
|
66
75
|
export default new R2IncrementalCache();
|
|
@@ -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
|
*/
|
|
@@ -24,9 +24,9 @@ class RegionalCache {
|
|
|
24
24
|
async get(key, isFetch) {
|
|
25
25
|
try {
|
|
26
26
|
const cache = await this.getCacheInstance();
|
|
27
|
-
const
|
|
27
|
+
const urlKey = this.getCacheUrlKey(key, isFetch);
|
|
28
28
|
// Check for a cached entry as this will be faster than the store response.
|
|
29
|
-
const cachedResponse = await cache.match(
|
|
29
|
+
const cachedResponse = await cache.match(urlKey);
|
|
30
30
|
if (cachedResponse) {
|
|
31
31
|
debugCache("Get - cached response");
|
|
32
32
|
// Re-fetch from the store and update the regional cache in the background
|
|
@@ -34,7 +34,7 @@ class RegionalCache {
|
|
|
34
34
|
getCloudflareContext().ctx.waitUntil(this.store.get(key, isFetch).then(async (rawEntry) => {
|
|
35
35
|
const { value, lastModified } = rawEntry ?? {};
|
|
36
36
|
if (value && typeof lastModified === "number") {
|
|
37
|
-
await this.putToCache(
|
|
37
|
+
await this.putToCache({ key, isFetch, entry: { value, lastModified } });
|
|
38
38
|
}
|
|
39
39
|
}));
|
|
40
40
|
}
|
|
@@ -45,7 +45,7 @@ class RegionalCache {
|
|
|
45
45
|
if (!value || typeof lastModified !== "number")
|
|
46
46
|
return null;
|
|
47
47
|
// Update the locale cache after retrieving from the store.
|
|
48
|
-
getCloudflareContext().ctx.waitUntil(this.putToCache(
|
|
48
|
+
getCloudflareContext().ctx.waitUntil(this.putToCache({ key, isFetch, entry: { value, lastModified } }));
|
|
49
49
|
return { value, lastModified };
|
|
50
50
|
}
|
|
51
51
|
catch (e) {
|
|
@@ -56,11 +56,15 @@ class RegionalCache {
|
|
|
56
56
|
async set(key, value, isFetch) {
|
|
57
57
|
try {
|
|
58
58
|
await this.store.set(key, value, isFetch);
|
|
59
|
-
await this.putToCache(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
},
|
|
64
68
|
});
|
|
65
69
|
}
|
|
66
70
|
catch (e) {
|
|
@@ -71,7 +75,7 @@ class RegionalCache {
|
|
|
71
75
|
try {
|
|
72
76
|
await this.store.delete(key);
|
|
73
77
|
const cache = await this.getCacheInstance();
|
|
74
|
-
await cache.delete(this.
|
|
78
|
+
await cache.delete(this.getCacheUrlKey(key));
|
|
75
79
|
}
|
|
76
80
|
catch (e) {
|
|
77
81
|
error("Failed to delete from regional cache", e);
|
|
@@ -83,16 +87,28 @@ class RegionalCache {
|
|
|
83
87
|
this.localCache = await caches.open("incremental-cache");
|
|
84
88
|
return this.localCache;
|
|
85
89
|
}
|
|
86
|
-
|
|
87
|
-
|
|
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"}`);
|
|
88
93
|
}
|
|
89
|
-
async putToCache(key, entry) {
|
|
94
|
+
async putToCache({ key, isFetch, entry }) {
|
|
95
|
+
const urlKey = this.getCacheUrlKey(key, isFetch);
|
|
90
96
|
const cache = await this.getCacheInstance();
|
|
91
97
|
const age = this.opts.mode === "short-lived"
|
|
92
98
|
? ONE_MINUTE_IN_SECONDS
|
|
93
|
-
: entry.value.revalidate || THIRTY_MINUTES_IN_SECONDS;
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}),
|
|
96
112
|
}));
|
|
97
113
|
}
|
|
98
114
|
}
|
|
@@ -113,9 +129,29 @@ class RegionalCache {
|
|
|
113
129
|
* or an ISR/SSG entry for up to 30 minutes.
|
|
114
130
|
* @param opts.shouldLazilyUpdateOnCacheHit Whether the regional cache entry should be updated in
|
|
115
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`
|
|
116
135
|
*
|
|
117
136
|
* @default `false` for the `short-lived` mode, and `true` for the `long-lived` mode.
|
|
118
137
|
*/
|
|
119
138
|
export function withRegionalCache(cache, opts) {
|
|
120
139
|
return new RegionalCache(cache, opts);
|
|
121
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
|
+
}
|
|
@@ -7,7 +7,6 @@ export declare class D1NextModeTagCache implements NextModeTagCache {
|
|
|
7
7
|
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
8
8
|
writeTags(tags: string[]): Promise<void>;
|
|
9
9
|
private getConfig;
|
|
10
|
-
protected removeBuildId(key: string): string;
|
|
11
10
|
protected getCacheKey(key: string): string;
|
|
12
11
|
protected getBuildId(): string;
|
|
13
12
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { error } from "@opennextjs/aws/adapters/logger.js";
|
|
2
|
-
import { RecoverableError } from "@opennextjs/aws/utils/error.js";
|
|
3
2
|
import { getCloudflareContext } from "../../cloudflare-context.js";
|
|
4
3
|
import { debugCache, FALLBACK_BUILD_ID } from "../internal.js";
|
|
5
4
|
export const NAME = "d1-next-mode-tag-cache";
|
|
@@ -27,13 +26,12 @@ export class D1NextModeTagCache {
|
|
|
27
26
|
}
|
|
28
27
|
async writeTags(tags) {
|
|
29
28
|
const { isDisabled, db } = this.getConfig();
|
|
30
|
-
|
|
29
|
+
// TODO: Remove `tags.length === 0` when https://github.com/opennextjs/opennextjs-aws/pull/828 is used
|
|
30
|
+
if (isDisabled || tags.length === 0)
|
|
31
31
|
return Promise.resolve();
|
|
32
|
-
|
|
32
|
+
await db.batch(tags.map((tag) => db
|
|
33
33
|
.prepare(`INSERT INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`)
|
|
34
34
|
.bind(this.getCacheKey(tag), Date.now())));
|
|
35
|
-
if (!result)
|
|
36
|
-
throw new RecoverableError(`D1 insert failed for ${tags}`);
|
|
37
35
|
}
|
|
38
36
|
getConfig() {
|
|
39
37
|
const db = getCloudflareContext().env[BINDING_NAME];
|
|
@@ -48,9 +46,6 @@ export class D1NextModeTagCache {
|
|
|
48
46
|
db,
|
|
49
47
|
};
|
|
50
48
|
}
|
|
51
|
-
removeBuildId(key) {
|
|
52
|
-
return key.replace(`${this.getBuildId()}/`, "");
|
|
53
|
-
}
|
|
54
49
|
getCacheKey(key) {
|
|
55
50
|
return `${this.getBuildId()}/${key}`.replaceAll("//", "/");
|
|
56
51
|
}
|
|
@@ -119,7 +119,7 @@ declare class ShardedDOTagCache implements NextModeTagCache {
|
|
|
119
119
|
writeTags(tags: string[]): Promise<void>;
|
|
120
120
|
performWriteTagsWithRetry(doId: DOId, tags: string[], lastModified: number, retryNumber?: number): Promise<void>;
|
|
121
121
|
getCacheInstance(): Promise<Cache | undefined>;
|
|
122
|
-
|
|
122
|
+
getCacheUrlKey(doId: DOId, tags: string[]): string;
|
|
123
123
|
getFromRegionalCache(doId: DOId, tags: string[]): Promise<Response | undefined>;
|
|
124
124
|
putToRegionalCache(doId: DOId, tags: string[], hasBeenRevalidated: boolean): Promise<void>;
|
|
125
125
|
deleteRegionalCache(doId: DOId, tags: string[]): Promise<void>;
|
|
@@ -194,8 +194,8 @@ class ShardedDOTagCache {
|
|
|
194
194
|
}
|
|
195
195
|
return this.localCache;
|
|
196
196
|
}
|
|
197
|
-
|
|
198
|
-
return
|
|
197
|
+
getCacheUrlKey(doId, tags) {
|
|
198
|
+
return `http://local.cache/shard/${doId.shardId}?tags=${encodeURIComponent(tags.join(";"))}`;
|
|
199
199
|
}
|
|
200
200
|
async getFromRegionalCache(doId, tags) {
|
|
201
201
|
try {
|
|
@@ -204,12 +204,10 @@ class ShardedDOTagCache {
|
|
|
204
204
|
const cache = await this.getCacheInstance();
|
|
205
205
|
if (!cache)
|
|
206
206
|
return;
|
|
207
|
-
|
|
208
|
-
return cache.match(key);
|
|
207
|
+
return cache.match(this.getCacheUrlKey(doId, tags));
|
|
209
208
|
}
|
|
210
209
|
catch (e) {
|
|
211
210
|
error("Error while fetching from regional cache", e);
|
|
212
|
-
return;
|
|
213
211
|
}
|
|
214
212
|
}
|
|
215
213
|
async putToRegionalCache(doId, tags, hasBeenRevalidated) {
|
|
@@ -218,9 +216,15 @@ class ShardedDOTagCache {
|
|
|
218
216
|
const cache = await this.getCacheInstance();
|
|
219
217
|
if (!cache)
|
|
220
218
|
return;
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
await cache.put(this.getCacheUrlKey(doId, tags), new Response(`${hasBeenRevalidated}`, {
|
|
220
|
+
headers: {
|
|
221
|
+
"cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
|
|
222
|
+
...(tags.length > 0
|
|
223
|
+
? {
|
|
224
|
+
"cache-tag": tags.join(","),
|
|
225
|
+
}
|
|
226
|
+
: {}),
|
|
227
|
+
},
|
|
224
228
|
}));
|
|
225
229
|
}
|
|
226
230
|
async deleteRegionalCache(doId, tags) {
|
|
@@ -231,8 +235,7 @@ class ShardedDOTagCache {
|
|
|
231
235
|
const cache = await this.getCacheInstance();
|
|
232
236
|
if (!cache)
|
|
233
237
|
return;
|
|
234
|
-
|
|
235
|
-
await cache.delete(key);
|
|
238
|
+
await cache.delete(this.getCacheUrlKey(doId, tags));
|
|
236
239
|
}
|
|
237
240
|
catch (e) {
|
|
238
241
|
debugCache("Error while deleting from regional cache", e);
|
|
@@ -277,15 +277,13 @@ describe("DOShardedTagCache", () => {
|
|
|
277
277
|
it("should return the cache key without the random part", async () => {
|
|
278
278
|
const cache = shardedDOTagCache();
|
|
279
279
|
const doId1 = new DOId({ baseShardId: "shard-0", numberOfReplicas: 1, shardType: "hard" });
|
|
280
|
-
|
|
281
|
-
expect(reqKey.url).toBe("http://local.cache/shard/tag-hard;shard-0?tags=_N_T_%2Ftag1");
|
|
280
|
+
expect(cache.getCacheUrlKey(doId1, ["_N_T_/tag1"])).toBe("http://local.cache/shard/tag-hard;shard-0?tags=_N_T_%2Ftag1");
|
|
282
281
|
const doId2 = new DOId({
|
|
283
282
|
baseShardId: "shard-1",
|
|
284
283
|
numberOfReplicas: 1,
|
|
285
284
|
shardType: "hard",
|
|
286
285
|
});
|
|
287
|
-
|
|
288
|
-
expect(reqKey2.url).toBe("http://local.cache/shard/tag-hard;shard-1?tags=tag1");
|
|
286
|
+
expect(cache.getCacheUrlKey(doId2, ["tag1"])).toBe("http://local.cache/shard/tag-hard;shard-1?tags=tag1");
|
|
289
287
|
});
|
|
290
288
|
});
|
|
291
289
|
describe("performWriteTagsWithRetry", () => {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextModeTagCache } from "@opennextjs/aws/types/overrides";
|
|
2
|
+
interface WithFilterOptions {
|
|
3
|
+
/**
|
|
4
|
+
* The original tag cache.
|
|
5
|
+
* Call to this will receive only the filtered tags.
|
|
6
|
+
*/
|
|
7
|
+
tagCache: NextModeTagCache;
|
|
8
|
+
/**
|
|
9
|
+
* The function to filter tags.
|
|
10
|
+
* @param tag The tag to filter.
|
|
11
|
+
* @returns true if the tag should be forwarded, false otherwise.
|
|
12
|
+
*/
|
|
13
|
+
filterFn: (tag: string) => boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new tag cache that filters tags based on the provided filter function.
|
|
17
|
+
* This is useful to remove tags that are not used by the app, this could reduce the number of requests to the underlying tag cache.
|
|
18
|
+
*/
|
|
19
|
+
export declare function withFilter({ tagCache, filterFn }: WithFilterOptions): NextModeTagCache;
|
|
20
|
+
/**
|
|
21
|
+
* Filter function to exclude tags that start with "_N_T_".
|
|
22
|
+
* This is used to filter out internal soft tags.
|
|
23
|
+
* Can be used if `revalidatePath` is not used.
|
|
24
|
+
*/
|
|
25
|
+
export declare function softTagFilter(tag: string): boolean;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a new tag cache that filters tags based on the provided filter function.
|
|
3
|
+
* This is useful to remove tags that are not used by the app, this could reduce the number of requests to the underlying tag cache.
|
|
4
|
+
*/
|
|
5
|
+
export function withFilter({ tagCache, filterFn }) {
|
|
6
|
+
return {
|
|
7
|
+
name: `filtered-${tagCache.name}`,
|
|
8
|
+
mode: "nextMode",
|
|
9
|
+
getPathsByTags: tagCache.getPathsByTags
|
|
10
|
+
? async (tags) => {
|
|
11
|
+
const filteredTags = tags.filter(filterFn);
|
|
12
|
+
if (filteredTags.length === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return tagCache.getPathsByTags(filteredTags);
|
|
16
|
+
}
|
|
17
|
+
: undefined,
|
|
18
|
+
hasBeenRevalidated: async (tags, lastModified) => {
|
|
19
|
+
const filteredTags = tags.filter(filterFn);
|
|
20
|
+
if (filteredTags.length === 0) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
return tagCache.hasBeenRevalidated(filteredTags, lastModified);
|
|
24
|
+
},
|
|
25
|
+
writeTags: async (tags) => {
|
|
26
|
+
const filteredTags = tags.filter(filterFn);
|
|
27
|
+
if (filteredTags.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
return tagCache.writeTags(filteredTags);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Filter function to exclude tags that start with "_N_T_".
|
|
36
|
+
* This is used to filter out internal soft tags.
|
|
37
|
+
* Can be used if `revalidatePath` is not used.
|
|
38
|
+
*/
|
|
39
|
+
export function softTagFilter(tag) {
|
|
40
|
+
return !tag.startsWith("_N_T_");
|
|
41
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { softTagFilter, withFilter } from "./tag-cache-filter";
|
|
3
|
+
const mockedTagCache = {
|
|
4
|
+
name: "mocked",
|
|
5
|
+
mode: "nextMode",
|
|
6
|
+
getPathsByTags: vi.fn(),
|
|
7
|
+
hasBeenRevalidated: vi.fn(),
|
|
8
|
+
writeTags: vi.fn(),
|
|
9
|
+
};
|
|
10
|
+
const filterFn = (tag) => tag.startsWith("valid_");
|
|
11
|
+
describe("withFilter", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
it("should filter out tags based on writeTags", async () => {
|
|
16
|
+
const tagCache = withFilter({
|
|
17
|
+
tagCache: mockedTagCache,
|
|
18
|
+
filterFn,
|
|
19
|
+
});
|
|
20
|
+
const tags = ["valid_tag", "invalid_tag"];
|
|
21
|
+
await tagCache.writeTags(tags);
|
|
22
|
+
expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
|
|
23
|
+
});
|
|
24
|
+
it("should not call writeTags if no tags are valid", async () => {
|
|
25
|
+
const tagCache = withFilter({
|
|
26
|
+
tagCache: mockedTagCache,
|
|
27
|
+
filterFn,
|
|
28
|
+
});
|
|
29
|
+
const tags = ["invalid_tag"];
|
|
30
|
+
await tagCache.writeTags(tags);
|
|
31
|
+
expect(mockedTagCache.writeTags).not.toHaveBeenCalled();
|
|
32
|
+
});
|
|
33
|
+
it("should filter out tags based on hasBeenRevalidated", async () => {
|
|
34
|
+
const tagCache = withFilter({
|
|
35
|
+
tagCache: mockedTagCache,
|
|
36
|
+
filterFn,
|
|
37
|
+
});
|
|
38
|
+
const tags = ["valid_tag", "invalid_tag"];
|
|
39
|
+
const lastModified = Date.now();
|
|
40
|
+
await tagCache.hasBeenRevalidated(tags, lastModified);
|
|
41
|
+
expect(mockedTagCache.hasBeenRevalidated).toHaveBeenCalledWith(["valid_tag"], lastModified);
|
|
42
|
+
});
|
|
43
|
+
it("should not call hasBeenRevalidated if no tags are valid", async () => {
|
|
44
|
+
const tagCache = withFilter({
|
|
45
|
+
tagCache: mockedTagCache,
|
|
46
|
+
filterFn,
|
|
47
|
+
});
|
|
48
|
+
const tags = ["invalid_tag"];
|
|
49
|
+
const lastModified = Date.now();
|
|
50
|
+
await tagCache.hasBeenRevalidated(tags, lastModified);
|
|
51
|
+
expect(mockedTagCache.hasBeenRevalidated).not.toHaveBeenCalled();
|
|
52
|
+
});
|
|
53
|
+
it("should filter out tags based on getPathsByTags", async () => {
|
|
54
|
+
const tagCache = withFilter({
|
|
55
|
+
tagCache: mockedTagCache,
|
|
56
|
+
filterFn,
|
|
57
|
+
});
|
|
58
|
+
const tags = ["valid_tag", "invalid_tag"];
|
|
59
|
+
await tagCache.getPathsByTags?.(tags);
|
|
60
|
+
expect(mockedTagCache.getPathsByTags).toHaveBeenCalledWith(["valid_tag"]);
|
|
61
|
+
});
|
|
62
|
+
it("should not call getPathsByTags if no tags are valid", async () => {
|
|
63
|
+
const tagCache = withFilter({
|
|
64
|
+
tagCache: mockedTagCache,
|
|
65
|
+
filterFn,
|
|
66
|
+
});
|
|
67
|
+
const tags = ["invalid_tag"];
|
|
68
|
+
await tagCache.getPathsByTags?.(tags);
|
|
69
|
+
expect(mockedTagCache.getPathsByTags).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
it("should return the correct name", () => {
|
|
72
|
+
const tagCache = withFilter({
|
|
73
|
+
tagCache: mockedTagCache,
|
|
74
|
+
filterFn,
|
|
75
|
+
});
|
|
76
|
+
expect(tagCache.name).toBe("filtered-mocked");
|
|
77
|
+
});
|
|
78
|
+
it("should not create a function if getPathsByTags is not defined", async () => {
|
|
79
|
+
const tagCache = withFilter({
|
|
80
|
+
tagCache: {
|
|
81
|
+
...mockedTagCache,
|
|
82
|
+
getPathsByTags: undefined,
|
|
83
|
+
},
|
|
84
|
+
filterFn,
|
|
85
|
+
});
|
|
86
|
+
expect(tagCache.getPathsByTags).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
it("should filter soft tags", () => {
|
|
89
|
+
const tagCache = withFilter({
|
|
90
|
+
tagCache: mockedTagCache,
|
|
91
|
+
filterFn: softTagFilter,
|
|
92
|
+
});
|
|
93
|
+
tagCache.writeTags(["valid_tag", "_N_T_/", "_N_T_/test", "_N_T_/layout"]);
|
|
94
|
+
expect(mockedTagCache.writeTags).toHaveBeenCalledWith(["valid_tag"]);
|
|
95
|
+
});
|
|
96
|
+
});
|
package/dist/cli/args.d.ts
CHANGED
package/dist/cli/args.js
CHANGED
|
@@ -28,6 +28,7 @@ export function getArgs() {
|
|
|
28
28
|
};
|
|
29
29
|
case "preview":
|
|
30
30
|
case "deploy":
|
|
31
|
+
case "upload":
|
|
31
32
|
return {
|
|
32
33
|
command: positionals[0],
|
|
33
34
|
outputDir,
|
|
@@ -44,7 +45,7 @@ export function getArgs() {
|
|
|
44
45
|
environment: getWranglerEnvironmentFlag(passthroughArgs),
|
|
45
46
|
};
|
|
46
47
|
default:
|
|
47
|
-
throw new Error("Error: invalid command, expected 'build' | 'preview' | 'deploy' | 'populateCache'");
|
|
48
|
+
throw new Error("Error: invalid command, expected 'build' | 'preview' | 'deploy' | 'upload' | 'populateCache'");
|
|
48
49
|
}
|
|
49
50
|
}
|
|
50
51
|
function getPassthroughArgs() {
|
package/dist/cli/build/build.js
CHANGED
|
@@ -8,6 +8,7 @@ import logger from "@opennextjs/aws/logger.js";
|
|
|
8
8
|
import { bundleServer } from "./bundle-server.js";
|
|
9
9
|
import { compileCacheAssetsManifestSqlFile } from "./open-next/compile-cache-assets-manifest.js";
|
|
10
10
|
import { compileEnvFiles } from "./open-next/compile-env-files.js";
|
|
11
|
+
import { compileInit } from "./open-next/compile-init.js";
|
|
11
12
|
import { compileDurableObjects } from "./open-next/compileDurableObjects.js";
|
|
12
13
|
import { createServerBundle } from "./open-next/createServerBundle.js";
|
|
13
14
|
import { createWranglerConfigIfNotExistent } from "./utils/index.js";
|
|
@@ -49,9 +50,11 @@ export async function build(options, config, projectOpts) {
|
|
|
49
50
|
compileCache(options);
|
|
50
51
|
// Compile .env files
|
|
51
52
|
compileEnvFiles(options);
|
|
53
|
+
// Compile workerd init
|
|
54
|
+
compileInit(options);
|
|
52
55
|
// Compile middleware
|
|
53
56
|
await createMiddleware(options, { forceOnlyBuildOnce: true });
|
|
54
|
-
createStaticAssets(options);
|
|
57
|
+
createStaticAssets(options, { useBasePath: true });
|
|
55
58
|
if (config.dangerous?.disableIncrementalCache !== true) {
|
|
56
59
|
const { useTagCache, metaFiles } = createCacheAssets(options);
|
|
57
60
|
if (useTagCache) {
|