From 5f3cd47218aa0b3c9a022dc0edca32cdec11f78c Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 21:02:44 +0900 Subject: [PATCH 1/9] Require Node.js 20 --- README.md | 2 +- package-lock.json | 40 +++++++++++++++++++++++++++--------- package.json | 4 ++-- src/cli/commands/init-app.ts | 2 +- 4 files changed, 34 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e09ebf7..b83e7f2 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Using a static site generator to build your website? Do you simply need to serve ## Prerequisites -Node 18 or newer is required during the build step, as we now rely on its `experimental-fetch` feature. +Although your published application runs on a Fastly Compute service, the publishing process offered by this package requires Node.js 20 or newer. ## How it works diff --git a/package-lock.json b/package-lock.json index 69f8b62..0d34eb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,12 @@ "@fastly/js-compute": "^3.0.0", "@types/command-line-args": "^5.2.0", "@types/glob-to-regexp": "^0.4.1", - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "rimraf": "^4.3.0", "typescript": "^5.0.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "peerDependencies": { "@fastly/js-compute": "^2.0.0 || ^3.0.0" @@ -1117,10 +1117,14 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", - "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", - "dev": true + "version": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } }, "node_modules/acorn": { "version": "8.14.1", @@ -2368,6 +2372,13 @@ "through": "^2.3.8" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", @@ -3004,10 +3015,13 @@ "dev": true }, "@types/node": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.0.0.tgz", - "integrity": "sha512-cHlGmko4gWLVI27cGJntjs/Sj8th9aYwplmZFwmmgYQQvL5NUsgVJG7OddLvNfLqYS31KFN0s3qlaD9qCaxACA==", - "dev": true + "version": "20.17.27", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", + "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", + "dev": true, + "requires": { + "undici-types": "~6.19.2" + } }, "acorn": { "version": "8.14.1", @@ -3848,6 +3862,12 @@ "through": "^2.3.8" } }, + "undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", diff --git a/package.json b/package.json index 8b8e5b8..7be042c 100644 --- a/package.json +++ b/package.json @@ -41,12 +41,12 @@ "@fastly/js-compute": "^3.0.0", "@types/command-line-args": "^5.2.0", "@types/glob-to-regexp": "^0.4.1", - "@types/node": "^18.0.0", + "@types/node": "^20.0.0", "rimraf": "^4.3.0", "typescript": "^5.0.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "files": [ "build", diff --git a/src/cli/commands/init-app.ts b/src/cli/commands/init-app.ts index a03e4c3..35a0025 100644 --- a/src/cli/commands/init-app.ts +++ b/src/cli/commands/init-app.ts @@ -543,7 +543,7 @@ ${staticFiles} '@fastly/js-compute': '^3.0.0', }, engines: { - node: '>=18.0.0', + node: '>=20.0.0', }, license: 'UNLICENSED', private: true, From 27173e58fbb05a300ae9d8967aaf7c284109ee14 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 13:58:33 +0900 Subject: [PATCH 2/9] Code cleanup: remove unneeded explicit function return types --- src/cli/util/kv-store.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/cli/util/kv-store.ts b/src/cli/util/kv-store.ts index ddaa5b3..582141b 100644 --- a/src/cli/util/kv-store.ts +++ b/src/cli/util/kv-store.ts @@ -22,7 +22,7 @@ type CachedValues = { const cache = new WeakMap(); -export async function getKVStoreIdForNameMap(fastlyApiContext: FastlyApiContext): Promise> { +export async function getKVStoreIdForNameMap(fastlyApiContext: FastlyApiContext) { const cacheEntry = cache.get(fastlyApiContext); let kvStoreNameMap = cacheEntry?.kvStoreNameMap; @@ -42,15 +42,16 @@ export async function getKVStoreIdForNameMap(fastlyApiContext: FastlyApiContext) return kvStoreNameMap; } -export async function getKVStoreIdForName(fastlyApiContext: FastlyApiContext, kvStoreName: string): Promise { +export async function getKVStoreIdForName(fastlyApiContext: FastlyApiContext, kvStoreName: string) { const kvStoreNameMap = await getKVStoreIdForNameMap(fastlyApiContext); return kvStoreNameMap[kvStoreName] ?? null; + } function createArrayGetter() { return function string>(fn: TFn) { - return async function(fastlyApiContext: FastlyApiContext, ...args: Parameters): Promise { + return async function(fastlyApiContext: FastlyApiContext, ...args: Parameters) { const results: TEntry[] = []; let cursor: string | null = null; @@ -86,7 +87,7 @@ function createArrayGetter() { const _getKVStoreInfos = createArrayGetter()(() => `/resources/stores/kv`); -export async function getKVStoreInfos(fastlyApiContext: FastlyApiContext): Promise { +export async function getKVStoreInfos(fastlyApiContext: FastlyApiContext) { const cacheEntry = cache.get(fastlyApiContext); @@ -104,7 +105,7 @@ export async function getKVStoreInfos(fastlyApiContext: FastlyApiContext): Promi export const _getKVStoreKeys = createArrayGetter()((kvStoreId: string) => `/resources/stores/kv/${encodeURIComponent(kvStoreId)}/keys`); -export async function getKVStoreKeys(fastlyApiContext: FastlyApiContext, kvStoreName: string): Promise { +export async function getKVStoreKeys(fastlyApiContext: FastlyApiContext, kvStoreName: string) { const kvStoreId = await getKVStoreIdForName(fastlyApiContext, kvStoreName); if (kvStoreId == null) { @@ -114,7 +115,7 @@ export async function getKVStoreKeys(fastlyApiContext: FastlyApiContext, kvStore return await _getKVStoreKeys(fastlyApiContext, kvStoreId); } -export async function kvStoreEntryExists(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string): Promise { +export async function kvStoreEntryExists(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string) { const kvStoreId = await getKVStoreIdForName(fastlyApiContext, kvStoreName); if (kvStoreId == null) { @@ -130,7 +131,7 @@ export async function kvStoreEntryExists(fastlyApiContext: FastlyApiContext, kvS } const encoder = new TextEncoder(); -export async function kvStoreSubmitFile(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string, data: Uint8Array | string): Promise { +export async function kvStoreSubmitFile(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string, data: Uint8Array | string) { const kvStoreId = await getKVStoreIdForName(fastlyApiContext, kvStoreName); if (kvStoreId == null) { @@ -154,7 +155,7 @@ export async function kvStoreSubmitFile(fastlyApiContext: FastlyApiContext, kvSt } -export async function kvStoreDeleteFile(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string): Promise { +export async function kvStoreDeleteFile(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string) { const kvStoreId = await getKVStoreIdForName(fastlyApiContext, kvStoreName); if (kvStoreId == null) { From d807efd223c8080251f20609aecb565129141ab1 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 14:03:54 +0900 Subject: [PATCH 3/9] Code cleanup: remove unnecessary nesting --- src/cli/commands/build-static.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/cli/commands/build-static.ts b/src/cli/commands/build-static.ts index 76fee97..24fc7dd 100644 --- a/src/cli/commands/build-static.ts +++ b/src/cli/commands/build-static.ts @@ -125,14 +125,12 @@ type KVStoreItemDesc = { async function uploadFilesToKVStore(fastlyApiContext: FastlyApiContext, kvStoreName: string, kvStoreItems: KVStoreItemDesc[]) { for (const { kvStoreKey, staticFilePath, text } of kvStoreItems) { if (await kvStoreEntryExists(fastlyApiContext, kvStoreName, kvStoreKey)) { - // Already exists in KV Store - console.log(`✔️ Asset already exists in KV Store with key "${kvStoreKey}".`) - } else { - // Upload to KV Store - const fileData = fs.readFileSync(staticFilePath); - await kvStoreSubmitFile(fastlyApiContext!, kvStoreName!, kvStoreKey, fileData); - console.log(`✔️ Submitted ${text ? 'text' : 'binary'} asset "${staticFilePath}" to KV Store with key "${kvStoreKey}".`) + console.log(`✔️ Asset already exists in KV Store with key "${kvStoreKey}".`); + continue; } + const fileData = fs.readFileSync(staticFilePath); + await kvStoreSubmitFile(fastlyApiContext!, kvStoreName!, kvStoreKey, fileData); + console.log(`✔️ Submitted ${text ? 'text' : 'binary'} asset "${staticFilePath}" to KV Store with key "${kvStoreKey}".`) } } From 560870283a6e4e069d269cc95a2434abbfc58ff6 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 14:25:30 +0900 Subject: [PATCH 4/9] callFastlyApi throws new FetchError objects --- src/cli/util/fastly-api.ts | 35 ++++++++++++++++++++++++++++++++++- src/cli/util/kv-store.ts | 38 ++++++++++++++++++-------------------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/cli/util/fastly-api.ts b/src/cli/util/fastly-api.ts index 1f46367..b10151c 100644 --- a/src/cli/util/fastly-api.ts +++ b/src/cli/util/fastly-api.ts @@ -42,7 +42,33 @@ export function loadApiKey(): LoadApiKeyResult | null { } -export async function callFastlyApi(fastlyApiContext: FastlyApiContext, endpoint: string, queryParams?: URLSearchParams | null, requestInit?: RequestInit): Promise { +const RETRYABLE_STATUS_CODES = [ + 408, // Request Timeout + 409, // Conflict (depends) + 423, // Locked + 429, // Too Many Requests + 500, // Internal Server Error + 502, // Bad Gateway + 503, // Service Unavailable + 504, // Gateway Timeout +]; + +export class FetchError extends Error { + constructor(message: string, status: number) { + super(message); + this.name = 'FetchError'; + this.status = status; + } + status: number; +} + +export async function callFastlyApi( + fastlyApiContext: FastlyApiContext, + endpoint: string, + operationName: string, + queryParams?: URLSearchParams | null, + requestInit?: RequestInit, +): Promise { let finalEndpoint = endpoint; if (queryParams != null) { @@ -60,8 +86,15 @@ export async function callFastlyApi(fastlyApiContext: FastlyApiContext, endpoint const request = new Request(url, { ...requestInit, headers, + redirect: 'error', }); const response = await fetch(request); + if (!response.ok) { + if (!RETRYABLE_STATUS_CODES.includes(response.status)) { + throw new FetchError(`${operationName} failed: ${response.status}`, response.status); + } + throw new FetchError(`Retryable ${operationName} error: ${response.status}`, response.status); + } return response; } diff --git a/src/cli/util/kv-store.ts b/src/cli/util/kv-store.ts index 582141b..3eb2733 100644 --- a/src/cli/util/kv-store.ts +++ b/src/cli/util/kv-store.ts @@ -1,4 +1,4 @@ -import { callFastlyApi, FastlyApiContext } from "./fastly-api.js"; +import { callFastlyApi, FastlyApiContext, FetchError } from "./fastly-api.js"; type KVStoreInfo = { id: string, @@ -51,7 +51,7 @@ export async function getKVStoreIdForName(fastlyApiContext: FastlyApiContext, kv function createArrayGetter() { return function string>(fn: TFn) { - return async function(fastlyApiContext: FastlyApiContext, ...args: Parameters) { + return async function(fastlyApiContext: FastlyApiContext, operationName: string, ...args: Parameters) { const results: TEntry[] = []; let cursor: string | null = null; @@ -63,10 +63,7 @@ function createArrayGetter() { const endpoint = fn(...args); - const response = await callFastlyApi(fastlyApiContext, endpoint, queryParams); - if (response.status !== 200) { - throw new Error('Unexpected data format'); - } + const response = await callFastlyApi(fastlyApiContext, endpoint, operationName, queryParams); const infos = await response.json() as DataAndMeta; @@ -96,7 +93,7 @@ export async function getKVStoreInfos(fastlyApiContext: FastlyApiContext) { return kvStoreInfos; } - kvStoreInfos = await _getKVStoreInfos(fastlyApiContext); + kvStoreInfos = await _getKVStoreInfos(fastlyApiContext, 'Listing KV Stores'); cache.set(fastlyApiContext, { ...cacheEntry, kvStoreInfos }); @@ -112,7 +109,7 @@ export async function getKVStoreKeys(fastlyApiContext: FastlyApiContext, kvStore return null; } - return await _getKVStoreKeys(fastlyApiContext, kvStoreId); + return await _getKVStoreKeys(fastlyApiContext, `Listing Keys for KV Store [${kvStoreId}] ${kvStoreName}`, kvStoreId); } export async function kvStoreEntryExists(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string) { @@ -124,9 +121,18 @@ export async function kvStoreEntryExists(fastlyApiContext: FastlyApiContext, kvS const endpoint = `/resources/stores/kv/${encodeURIComponent(kvStoreId)}/keys/${encodeURIComponent(key)}`; - const response = await callFastlyApi(fastlyApiContext, endpoint, null, { method: 'HEAD' }); + try { + + await callFastlyApi(fastlyApiContext, endpoint, `Checking existence of [${key}]`, null, { method: 'HEAD' }); + + } catch(err) { + if (err instanceof FetchError && err.status === 404) { + return false; + } + throw err; + } - return response.status === 200; + return true; } @@ -141,7 +147,7 @@ export async function kvStoreSubmitFile(fastlyApiContext: FastlyApiContext, kvSt const endpoint = `/resources/stores/kv/${encodeURIComponent(kvStoreId)}/keys/${encodeURIComponent(key)}`; const body = typeof data === 'string' ? encoder.encode(data) : data; - const response = await callFastlyApi(fastlyApiContext, endpoint, null, { + await callFastlyApi(fastlyApiContext, endpoint, `Submitting item [${key}]`, null, { method: 'PUT', headers: { 'content-type': 'application/octet-stream', @@ -149,10 +155,6 @@ export async function kvStoreSubmitFile(fastlyApiContext: FastlyApiContext, kvSt body, }); - if (response.status !== 200) { - throw new Error(`Submitting item ${key} gave error: ${response.status}`); - } - } export async function kvStoreDeleteFile(fastlyApiContext: FastlyApiContext, kvStoreName: string, key: string) { @@ -164,12 +166,8 @@ export async function kvStoreDeleteFile(fastlyApiContext: FastlyApiContext, kvSt const endpoint = `/resources/stores/kv/${encodeURIComponent(kvStoreId)}/keys/${encodeURIComponent(key)}`; - const response = await callFastlyApi(fastlyApiContext, endpoint, null, { + await callFastlyApi(fastlyApiContext, endpoint, `Deleting item [${key}]`, null, { method: 'DELETE', }); - if (response.status < 200 || response.status >= 300) { - throw new Error(`Deleting item '${key}' gave error: ${response.status}`); - } - } From cd2c925c0e1003b675b21865749cd9d4821cd346 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 21:37:41 +0900 Subject: [PATCH 5/9] Retryable utility --- src/cli/util/fastly-api.ts | 14 +++++++-- src/cli/util/retryable.ts | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 src/cli/util/retryable.ts diff --git a/src/cli/util/fastly-api.ts b/src/cli/util/fastly-api.ts index b10151c..d52719c 100644 --- a/src/cli/util/fastly-api.ts +++ b/src/cli/util/fastly-api.ts @@ -1,4 +1,5 @@ import { execSync } from 'child_process'; +import { makeRetryable } from './retryable.js'; export interface FastlyApiContext { apiToken: string, @@ -88,12 +89,21 @@ export async function callFastlyApi( headers, redirect: 'error', }); - const response = await fetch(request); + let response; + try { + response = await fetch(request); + } catch(err) { + if (err instanceof TypeError) { + throw makeRetryable(err); + } else { + throw err; + } + } if (!response.ok) { if (!RETRYABLE_STATUS_CODES.includes(response.status)) { throw new FetchError(`${operationName} failed: ${response.status}`, response.status); } - throw new FetchError(`Retryable ${operationName} error: ${response.status}`, response.status); + throw makeRetryable(new FetchError(`Retryable ${operationName} error: ${response.status}`, response.status)); } return response; diff --git a/src/cli/util/retryable.ts b/src/cli/util/retryable.ts new file mode 100644 index 0000000..fe4e543 --- /dev/null +++ b/src/cli/util/retryable.ts @@ -0,0 +1,62 @@ +let _globalBackoffUntil = 0; + +const retryableSymbol = Symbol(); +export function makeRetryable(error: Error) { + (error as any)[retryableSymbol] = true; + return error; +} +export function isRetryableError(error: unknown) { + if (!(error instanceof Error)) { + return false; + } + return (error as any)[retryableSymbol] ?? false; +} + +type RetryWithBackoffOptions = { + maxRetries?: number, + initialDelay?: number, + onAttempt?: (attempt: number) => void, + onSuccess?: (attempt: number) => void, + onRetry?: (attempt: number, err: unknown, delay: number) => void, +}; +export async function attemptWithRetries( + fn: () => Promise, + options: RetryWithBackoffOptions = {}, +) { + const { + maxRetries = 5, + initialDelay = 60000, + onAttempt, + onSuccess, + onRetry, + } = options; + + let attempt = 0; + let result: TResult; + + while (true) { + const waitTime = _globalBackoffUntil - Date.now(); + if (waitTime > 0) { + await new Promise(res => setTimeout(res, waitTime)); + } + + try { + onAttempt?.(attempt); + result = await fn(); + onSuccess?.(attempt); + break; + } catch (err) { + if (!isRetryableError(err) || attempt >= maxRetries) { + throw err; + } + + const delay = initialDelay * (attempt + 1); + _globalBackoffUntil = Date.now() + delay; + onRetry?.(attempt, err, delay); + } + + attempt++; + } + + return result; +} From 067bfcc0c7682b1dd30827d83855c4d1b3cd1e3e Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 15:24:00 +0900 Subject: [PATCH 6/9] Use retryable utility to upload to kvstore --- src/cli/commands/build-static.ts | 58 +++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/src/cli/commands/build-static.ts b/src/cli/commands/build-static.ts index 24fc7dd..d6dec5e 100644 --- a/src/cli/commands/build-static.ts +++ b/src/cli/commands/build-static.ts @@ -68,7 +68,7 @@ import { applyDefaults } from "../util/data.js"; import { calculateFileSizeAndHash } from "../util/hash.js"; import { getFiles } from "../util/files.js"; import { generateOrLoadPublishId } from "../util/publish-id.js"; -import { FastlyApiContext, loadApiKey } from "../util/fastly-api.js"; +import { FastlyApiContext, FetchError, loadApiKey } from "../util/fastly-api.js"; import { kvStoreEntryExists, kvStoreSubmitFile } from "../util/kv-store.js"; import { mergeContentTypes, testFileContentType } from "../../util/content-types.js"; import { algs } from "../compression/index.js"; @@ -94,6 +94,9 @@ import type { ContentFileInfoForWasmInline, ContentFileInfoForKVStore, } from "../../types/content-assets.js"; +import { + attemptWithRetries, +} from "../util/retryable.js"; type AssetInfo = ContentTypeTestResult & @@ -123,15 +126,54 @@ type KVStoreItemDesc = { }; async function uploadFilesToKVStore(fastlyApiContext: FastlyApiContext, kvStoreName: string, kvStoreItems: KVStoreItemDesc[]) { - for (const { kvStoreKey, staticFilePath, text } of kvStoreItems) { - if (await kvStoreEntryExists(fastlyApiContext, kvStoreName, kvStoreKey)) { - console.log(`✔️ Asset already exists in KV Store with key "${kvStoreKey}".`); - continue; + + const maxConcurrent = 12; + let index = 0; + + async function worker() { + while (index < kvStoreItems.length) { + const currentIndex = index; + index = index + 1; + const { kvStoreKey, staticFilePath, text } = kvStoreItems[currentIndex]; + + try { + await attemptWithRetries( + async() => { + if (await kvStoreEntryExists(fastlyApiContext, kvStoreName, kvStoreKey)) { + console.log(`✔️ Asset already exists in KV Store with key "${kvStoreKey}".`); + return; + } + const fileData = fs.readFileSync(staticFilePath); + await kvStoreSubmitFile(fastlyApiContext, kvStoreName, kvStoreKey, fileData); + console.log(`✔️ Submitted ${text ? 'text' : 'binary'} asset "${staticFilePath}" to KV Store with key "${kvStoreKey}".`) + }, + { + onAttempt(attempt) { + if (attempt > 0) { + console.log(`Attempt ${attempt + 1} for: ${kvStoreKey}`); + } + }, + onRetry(attempt, err, delay) { + let statusMessage = 'unknown'; + if (err instanceof FetchError) { + statusMessage = `HTTP ${err.status}`; + } else if (err instanceof TypeError) { + statusMessage = 'transport'; + } + console.log(`Attempt ${attempt + 1} for ${kvStoreKey} gave retryable error (${statusMessage}), delaying ${delay} ms`); + }, + } + ); + } catch (err) { + const e = err instanceof Error ? err : new Error(String(err)); + console.error(`❌ Failed: ${kvStoreKey} → ${e.message}`); + console.error(e.stack); + } } - const fileData = fs.readFileSync(staticFilePath); - await kvStoreSubmitFile(fastlyApiContext!, kvStoreName!, kvStoreKey, fileData); - console.log(`✔️ Submitted ${text ? 'text' : 'binary'} asset "${staticFilePath}" to KV Store with key "${kvStoreKey}".`) } + + const workers = Array.from({ length: maxConcurrent }, () => worker()); + await Promise.all(workers); } function writeKVStoreEntriesToFastlyToml(kvStoreName: string, kvStoreItems: KVStoreItemDesc[]) { From 0f0361f16caa3018fec17468672841d193a2ef96 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Mon, 24 Mar 2025 17:28:39 +0900 Subject: [PATCH 7/9] Fix error message template --- src/cli/commands/build-static.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/commands/build-static.ts b/src/cli/commands/build-static.ts index d6dec5e..797abc5 100644 --- a/src/cli/commands/build-static.ts +++ b/src/cli/commands/build-static.ts @@ -194,7 +194,7 @@ function writeKVStoreEntriesToFastlyToml(kvStoreName: string, kvStoreItems: KVSt if (fastlyToml.indexOf(kvStoreName) !== -1) { // don't do this! - console.error("improperly configured entry for '${kvStoreName}' in fastly.toml"); + console.error(`improperly configured entry for '${kvStoreName}' in fastly.toml`); // TODO: handle thrown exception from callers throw "No"! } From b628611ffef79560d3df6a4eea0a5c200229630e Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Tue, 25 Mar 2025 09:15:08 +0900 Subject: [PATCH 8/9] Add comment to clarify loop --- src/cli/commands/build-static.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cli/commands/build-static.ts b/src/cli/commands/build-static.ts index 797abc5..2005a9b 100644 --- a/src/cli/commands/build-static.ts +++ b/src/cli/commands/build-static.ts @@ -131,6 +131,8 @@ async function uploadFilesToKVStore(fastlyApiContext: FastlyApiContext, kvStoreN let index = 0; async function worker() { + // This loop is safe because JavaScript is single-threaded. + // Workers pull work one item at a time using a shared index. while (index < kvStoreItems.length) { const currentIndex = index; index = index + 1; From 8139c76a3b273a93ec346483e9123c4d384d2fa8 Mon Sep 17 00:00:00 2001 From: Katsuyuki Omuro Date: Wed, 26 Mar 2025 13:01:01 +0900 Subject: [PATCH 9/9] Improve comment --- src/cli/commands/build-static.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cli/commands/build-static.ts b/src/cli/commands/build-static.ts index 2005a9b..f5a79f3 100644 --- a/src/cli/commands/build-static.ts +++ b/src/cli/commands/build-static.ts @@ -128,11 +128,9 @@ type KVStoreItemDesc = { async function uploadFilesToKVStore(fastlyApiContext: FastlyApiContext, kvStoreName: string, kvStoreItems: KVStoreItemDesc[]) { const maxConcurrent = 12; - let index = 0; + let index = 0; // Shared among workers async function worker() { - // This loop is safe because JavaScript is single-threaded. - // Workers pull work one item at a time using a shared index. while (index < kvStoreItems.length) { const currentIndex = index; index = index + 1;