@opennextjs/cloudflare 1.6.1 → 1.6.2
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/durable-objects/sharded-tag-cache.d.ts +1 -0
- package/dist/api/durable-objects/sharded-tag-cache.js +6 -0
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.d.ts +23 -4
- package/dist/api/overrides/tag-cache/do-sharded-tag-cache.js +86 -45
- package/dist/cli/build/open-next/createServerBundle.js +2 -0
- package/dist/cli/build/utils/ensure-cf-config.js +6 -4
- package/dist/cli/commands/skew-protection.d.ts +2 -2
- package/dist/cli/commands/skew-protection.js +6 -2
- package/package.json +2 -2
|
@@ -5,4 +5,5 @@ export declare class DOShardedTagCache extends DurableObject<CloudflareEnv> {
|
|
|
5
5
|
getLastRevalidated(tags: string[]): Promise<number>;
|
|
6
6
|
hasBeenRevalidated(tags: string[], lastModified?: number): Promise<boolean>;
|
|
7
7
|
writeTags(tags: string[], lastModified: number): Promise<void>;
|
|
8
|
+
getRevalidationTimes(tags: string[]): Promise<Record<string, number>>;
|
|
8
9
|
}
|
|
@@ -34,4 +34,10 @@ export class DOShardedTagCache extends DurableObject {
|
|
|
34
34
|
this.sql.exec(`INSERT OR REPLACE INTO revalidations (tag, revalidatedAt) VALUES (?, ?)`, tag, lastModified);
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
+
async getRevalidationTimes(tags) {
|
|
38
|
+
const result = this.sql
|
|
39
|
+
.exec(`SELECT tag, revalidatedAt FROM revalidations WHERE tag IN (${tags.map(() => "?").join(", ")})`, ...tags)
|
|
40
|
+
.toArray();
|
|
41
|
+
return Object.fromEntries(result.map((row) => [row.tag, row.revalidatedAt]));
|
|
42
|
+
}
|
|
37
43
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NextModeTagCache } from "@opennextjs/aws/types/overrides.js";
|
|
2
|
+
import { DOShardedTagCache } from "../../durable-objects/sharded-tag-cache.js";
|
|
2
3
|
export declare const DEFAULT_WRITE_RETRIES = 3;
|
|
3
4
|
export declare const DEFAULT_NUM_SHARDS = 4;
|
|
4
5
|
export declare const NAME = "do-sharded-tag-cache";
|
|
@@ -34,6 +35,13 @@ interface ShardedDOTagCacheOptions {
|
|
|
34
35
|
* @default 5
|
|
35
36
|
*/
|
|
36
37
|
regionalCacheTtlSec?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Whether to persist missing tags in the regional cache.
|
|
40
|
+
* This is dangerous if you don't invalidate the Cache API when you revalidate tags as you could end up storing stale data in the data cache.
|
|
41
|
+
*
|
|
42
|
+
* @default false
|
|
43
|
+
*/
|
|
44
|
+
regionalCacheDangerouslyPersistMissingTags?: boolean;
|
|
37
45
|
/**
|
|
38
46
|
* Enable shard replication to handle higher load.
|
|
39
47
|
*
|
|
@@ -91,7 +99,6 @@ export declare class DOId {
|
|
|
91
99
|
interface CacheTagKeyOptions {
|
|
92
100
|
doId: DOId;
|
|
93
101
|
tags: string[];
|
|
94
|
-
type: "boolean" | "number";
|
|
95
102
|
}
|
|
96
103
|
declare class ShardedDOTagCache implements NextModeTagCache {
|
|
97
104
|
private opts;
|
|
@@ -145,9 +152,21 @@ declare class ShardedDOTagCache implements NextModeTagCache {
|
|
|
145
152
|
writeTags(tags: string[]): Promise<void>;
|
|
146
153
|
performWriteTagsWithRetry(doId: DOId, tags: string[], lastModified: number, retryNumber?: number): Promise<void>;
|
|
147
154
|
getCacheInstance(): Promise<Cache | undefined>;
|
|
148
|
-
getCacheUrlKey(
|
|
149
|
-
|
|
150
|
-
|
|
155
|
+
getCacheUrlKey(doId: DOId, tag: string): string;
|
|
156
|
+
/**
|
|
157
|
+
* Get the last revalidation time for the tags from the regional cache
|
|
158
|
+
* If the cache is not enabled, it will return an empty array
|
|
159
|
+
* @returns An array of objects with the tag and the last revalidation time
|
|
160
|
+
*/
|
|
161
|
+
getFromRegionalCache(opts: CacheTagKeyOptions): Promise<{
|
|
162
|
+
tag: string;
|
|
163
|
+
time: number;
|
|
164
|
+
}[]>;
|
|
165
|
+
putToRegionalCache(optsKey: CacheTagKeyOptions, stub: DurableObjectStub<DOShardedTagCache>): Promise<void>;
|
|
166
|
+
/**
|
|
167
|
+
* Deletes the regional cache for the given tags
|
|
168
|
+
* This is used to ensure that the cache is cleared when the tags are revalidated
|
|
169
|
+
*/
|
|
151
170
|
deleteRegionalCache(optsKey: CacheTagKeyOptions): Promise<void>;
|
|
152
171
|
}
|
|
153
172
|
declare const _default: (opts?: ShardedDOTagCacheOptions) => ShardedDOTagCache;
|
|
@@ -179,26 +179,25 @@ class ShardedDOTagCache {
|
|
|
179
179
|
const { isDisabled } = await this.getConfig();
|
|
180
180
|
if (isDisabled)
|
|
181
181
|
return 0;
|
|
182
|
+
if (tags.length === 0)
|
|
183
|
+
return 0; // No tags to check
|
|
184
|
+
const deduplicatedTags = Array.from(new Set(tags)); // We deduplicate the tags to avoid unnecessary requests
|
|
182
185
|
try {
|
|
183
|
-
const shardedTagGroups = this.groupTagsByDO({ tags });
|
|
186
|
+
const shardedTagGroups = this.groupTagsByDO({ tags: deduplicatedTags });
|
|
184
187
|
const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
|
|
185
|
-
const cachedValue = await this.getFromRegionalCache({ doId, tags
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
return parseInt(cached, 10);
|
|
190
|
-
}
|
|
191
|
-
catch (e) {
|
|
192
|
-
debug("Error while parsing cached value", e);
|
|
193
|
-
// If we can't parse the cached value, we should just ignore it and go to the durable object
|
|
194
|
-
}
|
|
188
|
+
const cachedValue = await this.getFromRegionalCache({ doId, tags });
|
|
189
|
+
// If all the value were found in the regional cache, we can just return the max value
|
|
190
|
+
if (cachedValue.length === tags.length) {
|
|
191
|
+
return Math.max(...cachedValue.map((item) => item.time));
|
|
195
192
|
}
|
|
193
|
+
// Otherwise we need to check the durable object on the ones that were not found in the cache
|
|
194
|
+
const filteredTags = deduplicatedTags.filter((tag) => !cachedValue.some((item) => item.tag === tag));
|
|
196
195
|
const stub = this.getDurableObjectStub(doId);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
return
|
|
196
|
+
const lastRevalidated = await stub.getLastRevalidated(filteredTags);
|
|
197
|
+
const result = Math.max(...cachedValue.map((item) => item.time), lastRevalidated);
|
|
198
|
+
// We then need to populate the regional cache with the missing tags
|
|
199
|
+
getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags }, stub));
|
|
200
|
+
return result;
|
|
202
201
|
}));
|
|
203
202
|
return Math.max(...shardedTagRevalidationOutcomes);
|
|
204
203
|
}
|
|
@@ -221,17 +220,20 @@ class ShardedDOTagCache {
|
|
|
221
220
|
try {
|
|
222
221
|
const shardedTagGroups = this.groupTagsByDO({ tags });
|
|
223
222
|
const shardedTagRevalidationOutcomes = await Promise.all(shardedTagGroups.map(async ({ doId, tags }) => {
|
|
224
|
-
const cachedValue = await this.getFromRegionalCache({ doId, tags
|
|
225
|
-
|
|
226
|
-
|
|
223
|
+
const cachedValue = await this.getFromRegionalCache({ doId, tags });
|
|
224
|
+
// If one of the cached values is newer than the lastModified, we can return true
|
|
225
|
+
const cacheHasBeenRevalidated = cachedValue.some((cachedValue) => {
|
|
226
|
+
return (cachedValue.time ?? 0) > (lastModified ?? Date.now());
|
|
227
|
+
});
|
|
228
|
+
if (cacheHasBeenRevalidated) {
|
|
229
|
+
return true;
|
|
227
230
|
}
|
|
228
231
|
const stub = this.getDurableObjectStub(doId);
|
|
229
232
|
const _hasBeenRevalidated = await stub.hasBeenRevalidated(tags, lastModified);
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags, type: "boolean" }, _hasBeenRevalidated));
|
|
233
|
+
const remainingTags = tags.filter((tag) => !cachedValue.some((item) => item.tag === tag));
|
|
234
|
+
if (remainingTags.length > 0) {
|
|
235
|
+
// We need to put the missing tags in the regional cache
|
|
236
|
+
getCloudflareContext().ctx.waitUntil(this.putToRegionalCache({ doId, tags: remainingTags }, stub));
|
|
235
237
|
}
|
|
236
238
|
return _hasBeenRevalidated;
|
|
237
239
|
}));
|
|
@@ -266,10 +268,7 @@ class ShardedDOTagCache {
|
|
|
266
268
|
await stub.writeTags(tags, lastModified);
|
|
267
269
|
// Depending on the shards and the tags, deleting from the regional cache will not work for every tag
|
|
268
270
|
// We also need to delete both cache
|
|
269
|
-
await Promise.all([
|
|
270
|
-
this.deleteRegionalCache({ doId, tags, type: "boolean" }),
|
|
271
|
-
this.deleteRegionalCache({ doId, tags, type: "number" }),
|
|
272
|
-
]);
|
|
271
|
+
await Promise.all([this.deleteRegionalCache({ doId, tags })]);
|
|
273
272
|
}
|
|
274
273
|
catch (e) {
|
|
275
274
|
error("Error while writing tags", e);
|
|
@@ -293,41 +292,79 @@ class ShardedDOTagCache {
|
|
|
293
292
|
}
|
|
294
293
|
return this.localCache;
|
|
295
294
|
}
|
|
296
|
-
getCacheUrlKey(
|
|
297
|
-
|
|
298
|
-
return `http://local.cache/shard/${doId.shardId}?type=${type}&tags=${encodeURIComponent(tags.join(";"))}`;
|
|
295
|
+
getCacheUrlKey(doId, tag) {
|
|
296
|
+
return `http://local.cache/shard/${doId.shardId}?tag=${encodeURIComponent(tag)}`;
|
|
299
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Get the last revalidation time for the tags from the regional cache
|
|
300
|
+
* If the cache is not enabled, it will return an empty array
|
|
301
|
+
* @returns An array of objects with the tag and the last revalidation time
|
|
302
|
+
*/
|
|
300
303
|
async getFromRegionalCache(opts) {
|
|
301
304
|
try {
|
|
302
305
|
if (!this.opts.regionalCache)
|
|
303
|
-
return;
|
|
306
|
+
return [];
|
|
304
307
|
const cache = await this.getCacheInstance();
|
|
305
308
|
if (!cache)
|
|
306
|
-
return;
|
|
307
|
-
|
|
309
|
+
return [];
|
|
310
|
+
const result = await Promise.all(opts.tags.map(async (tag) => {
|
|
311
|
+
const cachedResponse = await cache.match(this.getCacheUrlKey(opts.doId, tag));
|
|
312
|
+
if (!cachedResponse)
|
|
313
|
+
return null;
|
|
314
|
+
const cachedText = await cachedResponse.text();
|
|
315
|
+
try {
|
|
316
|
+
return { tag, time: parseInt(cachedText, 10) };
|
|
317
|
+
}
|
|
318
|
+
catch (e) {
|
|
319
|
+
debugCache("Error while parsing cached value", e);
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}));
|
|
323
|
+
return result.filter((item) => item !== null);
|
|
308
324
|
}
|
|
309
325
|
catch (e) {
|
|
310
326
|
error("Error while fetching from regional cache", e);
|
|
327
|
+
return [];
|
|
311
328
|
}
|
|
312
329
|
}
|
|
313
|
-
async putToRegionalCache(optsKey,
|
|
330
|
+
async putToRegionalCache(optsKey, stub) {
|
|
314
331
|
if (!this.opts.regionalCache)
|
|
315
332
|
return;
|
|
316
333
|
const cache = await this.getCacheInstance();
|
|
317
334
|
if (!cache)
|
|
318
335
|
return;
|
|
319
336
|
const tags = optsKey.tags;
|
|
320
|
-
await
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
337
|
+
const tagsLastRevalidated = await stub.getRevalidationTimes(tags);
|
|
338
|
+
await Promise.all(tags.map(async (tag) => {
|
|
339
|
+
let lastRevalidated = tagsLastRevalidated[tag];
|
|
340
|
+
if (lastRevalidated === undefined) {
|
|
341
|
+
if (this.opts.regionalCacheDangerouslyPersistMissingTags) {
|
|
342
|
+
lastRevalidated = 0; // If the tag is not found, we set it to 0 as it means it has never been revalidated before.
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
debugCache("Tag not found in revalidation times", { tag, optsKey });
|
|
346
|
+
return; // If the tag is not found, we skip it
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
|
|
350
|
+
debugCache("Putting to regional cache", { cacheKey, lastRevalidated });
|
|
351
|
+
await cache.put(cacheKey, new Response(lastRevalidated.toString(), {
|
|
352
|
+
status: 200,
|
|
353
|
+
headers: {
|
|
354
|
+
"cache-control": `max-age=${this.opts.regionalCacheTtlSec ?? 5}`,
|
|
355
|
+
...(tags.length > 0
|
|
356
|
+
? {
|
|
357
|
+
"cache-tag": tags.join(","),
|
|
358
|
+
}
|
|
359
|
+
: {}),
|
|
360
|
+
},
|
|
361
|
+
}));
|
|
329
362
|
}));
|
|
330
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* Deletes the regional cache for the given tags
|
|
366
|
+
* This is used to ensure that the cache is cleared when the tags are revalidated
|
|
367
|
+
*/
|
|
331
368
|
async deleteRegionalCache(optsKey) {
|
|
332
369
|
// We never want to crash because of the cache
|
|
333
370
|
try {
|
|
@@ -336,7 +373,11 @@ class ShardedDOTagCache {
|
|
|
336
373
|
const cache = await this.getCacheInstance();
|
|
337
374
|
if (!cache)
|
|
338
375
|
return;
|
|
339
|
-
await
|
|
376
|
+
await Promise.all(optsKey.tags.map(async (tag) => {
|
|
377
|
+
const cacheKey = this.getCacheUrlKey(optsKey.doId, tag);
|
|
378
|
+
debugCache("Deleting from regional cache", { cacheKey });
|
|
379
|
+
await cache.delete(cacheKey);
|
|
380
|
+
}));
|
|
340
381
|
}
|
|
341
382
|
catch (e) {
|
|
342
383
|
debugCache("Error while deleting from regional cache", e);
|
|
@@ -154,6 +154,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
|
|
|
154
154
|
const isAfter141 = buildHelper.compareSemver(options.nextVersion, ">=", "14.1");
|
|
155
155
|
const isAfter142 = buildHelper.compareSemver(options.nextVersion, ">=", "14.2");
|
|
156
156
|
const isAfter152 = buildHelper.compareSemver(options.nextVersion, ">=", "15.2.0");
|
|
157
|
+
const isAfter154 = buildHelper.compareSemver(options.nextVersion, ">=", "15.4.0");
|
|
157
158
|
const disableRouting = isBefore13413 || config.middleware?.external;
|
|
158
159
|
const plugins = [
|
|
159
160
|
openNextReplacementPlugin({
|
|
@@ -164,6 +165,7 @@ async function generateBundle(name, options, fnOptions, codeCustomization) {
|
|
|
164
165
|
...(disableRouting ? ["withRouting"] : []),
|
|
165
166
|
...(isAfter142 ? ["patchAsyncStorage"] : []),
|
|
166
167
|
...(isAfter141 ? ["appendPrefetch"] : []),
|
|
168
|
+
...(isAfter154 ? [] : ["setInitialURL"]),
|
|
167
169
|
],
|
|
168
170
|
}),
|
|
169
171
|
openNextReplacementPlugin({
|
|
@@ -5,6 +5,8 @@ import logger from "@opennextjs/aws/logger.js";
|
|
|
5
5
|
* @param config OpenNext configuration.
|
|
6
6
|
*/
|
|
7
7
|
export function ensureCloudflareConfig(config) {
|
|
8
|
+
const mwIsMiddlewareExternal = config.middleware?.external === true;
|
|
9
|
+
const mwConfig = mwIsMiddlewareExternal ? config.middleware : undefined;
|
|
8
10
|
const requirements = {
|
|
9
11
|
// Check for the default function
|
|
10
12
|
dftUseCloudflareWrapper: config.default?.override?.wrapper === "cloudflare-node",
|
|
@@ -18,10 +20,10 @@ export function ensureCloudflareConfig(config) {
|
|
|
18
20
|
config.default?.override?.queue === "direct" ||
|
|
19
21
|
typeof config.default?.override?.queue === "function",
|
|
20
22
|
// Check for the middleware function
|
|
21
|
-
mwIsMiddlewareExternal
|
|
22
|
-
mwUseCloudflareWrapper:
|
|
23
|
-
mwUseEdgeConverter:
|
|
24
|
-
mwUseFetchProxy:
|
|
23
|
+
mwIsMiddlewareExternal,
|
|
24
|
+
mwUseCloudflareWrapper: mwConfig?.override?.wrapper === "cloudflare-edge",
|
|
25
|
+
mwUseEdgeConverter: mwConfig?.override?.converter === "edge",
|
|
26
|
+
mwUseFetchProxy: mwConfig?.override?.proxyExternalRequest === "fetch",
|
|
25
27
|
hasCryptoExternal: config.edgeExternals?.includes("node:crypto"),
|
|
26
28
|
};
|
|
27
29
|
if (config.default?.override?.queue === "direct") {
|
|
@@ -28,10 +28,10 @@ import type { WorkerEnvVar } from "./helpers.js";
|
|
|
28
28
|
*
|
|
29
29
|
* @param options Build options
|
|
30
30
|
* @param config OpenNext config
|
|
31
|
-
* @param
|
|
31
|
+
* @param workerEnvVars Worker Environment variables (taken from the wrangler config files)
|
|
32
32
|
* @returns Deployment mapping or undefined
|
|
33
33
|
*/
|
|
34
|
-
export declare function getDeploymentMapping(options: BuildOptions, config: OpenNextConfig,
|
|
34
|
+
export declare function getDeploymentMapping(options: BuildOptions, config: OpenNextConfig, workerEnvVars: WorkerEnvVar): Promise<Record<string, string> | undefined>;
|
|
35
35
|
/**
|
|
36
36
|
* Update an existing deployment mapping:
|
|
37
37
|
* - Replace the "current" version with the latest deployed version
|
|
@@ -36,13 +36,17 @@ const MS_PER_DAY = 24 * 3600 * 1000;
|
|
|
36
36
|
*
|
|
37
37
|
* @param options Build options
|
|
38
38
|
* @param config OpenNext config
|
|
39
|
-
* @param
|
|
39
|
+
* @param workerEnvVars Worker Environment variables (taken from the wrangler config files)
|
|
40
40
|
* @returns Deployment mapping or undefined
|
|
41
41
|
*/
|
|
42
|
-
export async function getDeploymentMapping(options, config,
|
|
42
|
+
export async function getDeploymentMapping(options, config, workerEnvVars) {
|
|
43
43
|
if (config.cloudflare?.skewProtection?.enabled !== true) {
|
|
44
44
|
return undefined;
|
|
45
45
|
}
|
|
46
|
+
// Note that `process.env` is spread after `workerEnvVars` since we do want
|
|
47
|
+
// system environment variables to take precedence over the variables defined
|
|
48
|
+
// in the wrangler config files
|
|
49
|
+
const envVars = { ...workerEnvVars, ...process.env };
|
|
46
50
|
const nextConfig = loadConfig(path.join(options.appBuildOutputPath, ".next"));
|
|
47
51
|
const deploymentId = nextConfig.deploymentId;
|
|
48
52
|
if (!deploymentId) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opennextjs/cloudflare",
|
|
3
3
|
"description": "Cloudflare builder for next apps",
|
|
4
|
-
"version": "1.6.
|
|
4
|
+
"version": "1.6.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"homepage": "https://github.com/opennextjs/opennextjs-cloudflare",
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@dotenvx/dotenvx": "1.31.0",
|
|
46
|
-
"@opennextjs/aws": "3.7.
|
|
46
|
+
"@opennextjs/aws": "3.7.1",
|
|
47
47
|
"cloudflare": "^4.4.1",
|
|
48
48
|
"enquirer": "^2.4.1",
|
|
49
49
|
"glob": "^11.0.0",
|