@opennextjs/cloudflare 1.19.6 → 1.19.8
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.
|
@@ -4,6 +4,10 @@ import type { Unstable_Config as WranglerConfig } from "wrangler";
|
|
|
4
4
|
import type yargs from "yargs";
|
|
5
5
|
import { type WorkerEnvVar } from "./utils/helpers.js";
|
|
6
6
|
import type { WranglerTarget } from "./utils/run-wrangler.js";
|
|
7
|
+
export declare const MAX_REQUEST_RETRIES = 15;
|
|
8
|
+
export declare const BASE_RETRY_DELAY_MS = 250;
|
|
9
|
+
export declare const MAX_RETRY_DELAY_MS = 10000;
|
|
10
|
+
export declare const BACKOFF_FACTOR: number;
|
|
7
11
|
export declare function populateCache(buildOpts: BuildOptions, config: OpenNextConfig, wranglerConfig: WranglerConfig, populateCacheOptions: PopulateCacheOptions, envVars: WorkerEnvVar): Promise<void>;
|
|
8
12
|
export type CacheAsset = {
|
|
9
13
|
isFetch: boolean;
|
|
@@ -19,6 +19,14 @@ import { normalizePath } from "../utils/normalize-path.js";
|
|
|
19
19
|
import { getEnvFromPlatformProxy, quoteShellMeta } from "./utils/helpers.js";
|
|
20
20
|
import { runWrangler } from "./utils/run-wrangler.js";
|
|
21
21
|
import { getNormalizedOptions, printHeaders, readWranglerConfig, retrieveCompiledConfig, withWranglerOptions, withWranglerPassthroughArgs, } from "./utils/utils.js";
|
|
22
|
+
// Maximum number of attempts to send the request
|
|
23
|
+
export const MAX_REQUEST_RETRIES = 15;
|
|
24
|
+
// Base delay for retries
|
|
25
|
+
export const BASE_RETRY_DELAY_MS = 250;
|
|
26
|
+
// Maximum delay for retries, used to calculate the backoff factor
|
|
27
|
+
export const MAX_RETRY_DELAY_MS = 10_000;
|
|
28
|
+
// Backoff factor for retries, calculated to ensure that the delay grows exponentially up to the maximum delay
|
|
29
|
+
export const BACKOFF_FACTOR = (MAX_RETRY_DELAY_MS / BASE_RETRY_DELAY_MS) ** (1 / (MAX_REQUEST_RETRIES - 1));
|
|
22
30
|
/**
|
|
23
31
|
* Implementation of the `opennextjs-cloudflare populateCache` command.
|
|
24
32
|
*
|
|
@@ -199,49 +207,32 @@ async function populateR2IncrementalCache(buildOpts, config, populateCacheOption
|
|
|
199
207
|
/**
|
|
200
208
|
* Sends cache entries to the R2 worker, one entry per request.
|
|
201
209
|
*
|
|
202
|
-
* Up to `concurrency` requests are in-flight at any given time.
|
|
203
|
-
* Retry logic for transient R2 write failures is handled by the worker.
|
|
204
|
-
*
|
|
205
210
|
* @param options
|
|
206
211
|
* @param options.workerUrl - The URL of the local R2 worker's `/populate` endpoint.
|
|
207
212
|
* @param options.assets - The cache assets to write, as collected by {@link getCacheAssets}.
|
|
208
213
|
* @param options.prefix - Optional prefix prepended to each R2 key.
|
|
209
|
-
* @param options.
|
|
214
|
+
* @param options.maxConcurrency - Maximum number of concurrent in-flight requests.
|
|
210
215
|
* @returns Resolves when all entries have been written successfully.
|
|
211
216
|
* @throws {Error} If any entry fails after all retries or encounters a non-retryable error.
|
|
212
217
|
*/
|
|
213
218
|
async function sendEntriesToR2Worker(options) {
|
|
214
219
|
const { workerUrl, assets, prefix, maxConcurrency } = options;
|
|
215
|
-
// Build the list of entries to send (key + filename).
|
|
216
|
-
// File contents are read lazily in sendEntryToR2Worker to avoid
|
|
217
|
-
// loading all cache values into memory at once.
|
|
218
|
-
const entries = assets.map(({ fullPath, key, buildId, isFetch }) => ({
|
|
219
|
-
key: computeCacheKey(key, {
|
|
220
|
-
prefix,
|
|
221
|
-
buildId,
|
|
222
|
-
cacheType: isFetch ? "fetch" : "cache",
|
|
223
|
-
}),
|
|
224
|
-
filename: fullPath,
|
|
225
|
-
}));
|
|
226
|
-
// Use a concurrency-limited loop with a progress bar.
|
|
227
|
-
// `pending` tracks in-flight promises so we can cap concurrency.
|
|
228
220
|
const pending = new Set();
|
|
229
|
-
|
|
230
|
-
|
|
221
|
+
for (const asset of tqdm(assets)) {
|
|
222
|
+
const { fullPath, key, buildId, isFetch } = asset;
|
|
231
223
|
const task = sendEntryToR2Worker({
|
|
232
224
|
workerUrl,
|
|
233
|
-
key:
|
|
234
|
-
|
|
225
|
+
key: computeCacheKey(key, {
|
|
226
|
+
prefix,
|
|
227
|
+
buildId,
|
|
228
|
+
cacheType: isFetch ? "fetch" : "cache",
|
|
229
|
+
}),
|
|
230
|
+
filename: fullPath,
|
|
235
231
|
}).finally(() => pending.delete(task));
|
|
236
232
|
pending.add(task);
|
|
237
233
|
// If we've reached the concurrency limit, wait for one to finish.
|
|
238
|
-
if (pending.size >=
|
|
234
|
+
if (pending.size >= maxConcurrency) {
|
|
239
235
|
await Promise.race(pending);
|
|
240
|
-
// Increase concurrency gradually to avoid overwhelming the worker
|
|
241
|
-
// with too many requests at once.
|
|
242
|
-
if (concurrency < maxConcurrency) {
|
|
243
|
-
concurrency++;
|
|
244
|
-
}
|
|
245
236
|
}
|
|
246
237
|
}
|
|
247
238
|
await Promise.all(pending);
|
|
@@ -261,9 +252,7 @@ class RetryableWorkerError extends Error {
|
|
|
261
252
|
*/
|
|
262
253
|
async function sendEntryToR2Worker(options) {
|
|
263
254
|
const { workerUrl, key, filename } = options;
|
|
264
|
-
|
|
265
|
-
const CLIENT_RETRY_BASE_DELAY_MS = 250;
|
|
266
|
-
for (let attempt = 0; attempt < CLIENT_RETRY_ATTEMPTS; attempt++) {
|
|
255
|
+
for (let attempt = 0; attempt < MAX_REQUEST_RETRIES; attempt++) {
|
|
267
256
|
try {
|
|
268
257
|
let response;
|
|
269
258
|
try {
|
|
@@ -280,9 +269,7 @@ async function sendEntryToR2Worker(options) {
|
|
|
280
269
|
});
|
|
281
270
|
}
|
|
282
271
|
catch (e) {
|
|
283
|
-
throw new RetryableWorkerError(`Failed to send request to R2 worker: ${e instanceof Error ? e.message : String(e)}`, {
|
|
284
|
-
cause: e,
|
|
285
|
-
});
|
|
272
|
+
throw new RetryableWorkerError(`Failed to send request to R2 worker: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
|
|
286
273
|
}
|
|
287
274
|
const body = await response.text();
|
|
288
275
|
let result;
|
|
@@ -290,6 +277,7 @@ async function sendEntryToR2Worker(options) {
|
|
|
290
277
|
result = JSON.parse(body);
|
|
291
278
|
}
|
|
292
279
|
catch (e) {
|
|
280
|
+
// https://developers.cloudflare.com/support/troubleshooting/http-status-codes/cloudflare-1xxx-errors/error-1102
|
|
293
281
|
if (body.includes("Worker exceeded resource limits")) {
|
|
294
282
|
throw new RetryableWorkerError("Worker exceeded resource limits", { cause: e });
|
|
295
283
|
}
|
|
@@ -300,21 +288,20 @@ async function sendEntryToR2Worker(options) {
|
|
|
300
288
|
cause: e,
|
|
301
289
|
});
|
|
302
290
|
}
|
|
303
|
-
if (!result.success && response.status >= 500) {
|
|
304
|
-
throw new RetryableWorkerError(result.error);
|
|
305
|
-
}
|
|
306
291
|
if (!result.success) {
|
|
307
|
-
throw
|
|
292
|
+
throw response.status >= 500
|
|
293
|
+
? new RetryableWorkerError(result.error)
|
|
294
|
+
: new Error(`Failed to write "${key}" to R2: ${result.error}`);
|
|
308
295
|
}
|
|
309
296
|
return;
|
|
310
297
|
}
|
|
311
298
|
catch (e) {
|
|
312
|
-
if (e instanceof RetryableWorkerError && attempt <
|
|
299
|
+
if (e instanceof RetryableWorkerError && attempt < MAX_REQUEST_RETRIES - 1) {
|
|
313
300
|
logger.error(`Attempt ${attempt + 1} to write "${key}" failed with a retryable error: ${e.message}. Retrying...`);
|
|
314
|
-
await setTimeout(
|
|
301
|
+
await setTimeout(BASE_RETRY_DELAY_MS * Math.pow(BACKOFF_FACTOR, attempt));
|
|
315
302
|
continue;
|
|
316
303
|
}
|
|
317
|
-
throw new Error(`Failed to write "${key}" to R2 after ${
|
|
304
|
+
throw new Error(`Failed to write "${key}" to R2 after ${MAX_REQUEST_RETRIES} attempts`, {
|
|
318
305
|
cause: e,
|
|
319
306
|
});
|
|
320
307
|
}
|
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.19.
|
|
4
|
+
"version": "1.19.8",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"opennextjs-cloudflare": "dist/cli/index.js"
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@ast-grep/napi": "^0.40.5",
|
|
46
46
|
"@dotenvx/dotenvx": "1.31.0",
|
|
47
|
-
"@opennextjs/aws": "
|
|
47
|
+
"@opennextjs/aws": "4.0.1",
|
|
48
48
|
"ci-info": "^4.2.0",
|
|
49
49
|
"cloudflare": "^4.4.1",
|
|
50
50
|
"comment-json": "^4.5.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"yargs": "^18.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
|
-
"@cloudflare/workers-types": "^4.
|
|
57
|
+
"@cloudflare/workers-types": "^4.20260426.1",
|
|
58
58
|
"@eslint/js": "^9.11.1",
|
|
59
59
|
"@tsconfig/strictest": "^2.0.5",
|
|
60
60
|
"@types/mock-fs": "^4.13.4",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"eslint-plugin-unicorn": "^55.0.0",
|
|
70
70
|
"globals": "^15.9.0",
|
|
71
71
|
"mock-fs": "^5.4.1",
|
|
72
|
-
"next": "
|
|
72
|
+
"next": "^15.5.16",
|
|
73
73
|
"picomatch": "^4.0.2",
|
|
74
74
|
"rimraf": "^6.0.1",
|
|
75
75
|
"typescript": "^5.9.3",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"vitest": "^4.1.4"
|
|
78
78
|
},
|
|
79
79
|
"peerDependencies": {
|
|
80
|
-
"next": ">=15.5.
|
|
80
|
+
"next": ">=15.5.16 <16 || >=16.2.5",
|
|
81
81
|
"wrangler": "^4.86.0"
|
|
82
82
|
},
|
|
83
83
|
"scripts": {
|