The publish bundler artifact, worker-bundler source and esbuild wasm, is fetched from static assets at first use instead of being inlined into the worker script, returning the script to its previous size. Cold-start latency regressed while the 19MB artifact chunk was part of the script blob.
Unified · Split
| @@ -75,6 +75,9 @@ interface CloudPluginDeps { · 74 unmodified lines | ||
| 75 | 75 | readonly globalOutbound?: null; |
| 76 | 76 | }) => { readonly getEntrypoint: () => unknown }; |
| 77 | 77 | }; |
| 78 | + readonly workerAssets?: { | |
| 79 | + readonly fetch: (request: Request) => Promise<Response>; | |
| 80 | + }; | |
| 78 | 81 | } |
| 79 | 82 | |
| 80 | 83 | const base64ToArrayBuffer = (value: string): ArrayBuffer => { |
| @@ -82,6 +85,20 @@ const base64ToArrayBuffer = (value: string): ArrayBuffer => { · 1 unmodified line | ||
| 82 | 85 | return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); |
| 83 | 86 | }; |
| 84 | 87 | |
| 88 | +const assetRequest = (path: string): Request => new Request(new URL(path, "https://assets.local")); | |
| 89 | + | |
| 90 | +const fetchAsset = async ( | |
| 91 | + assets: { readonly fetch: (request: Request) => Promise<Response> }, | |
| 92 | + path: string, | |
| 93 | +): Promise<Response> => { | |
| 94 | + const response = await assets.fetch(assetRequest(path)); | |
| 95 | + if (!response.ok) { | |
| 96 | + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: worker artifact asset fetch must reject the dynamic bundler setup | |
| 97 | + throw new Error(`failed to fetch worker-bundler asset ${path}: ${response.status}`); | |
| 98 | + } | |
| 99 | + return response; | |
| 100 | +}; | |
| 101 | + | |
| 85 | 102 | export default defineExecutorConfig({ |
| 86 | 103 | plugins: ({ |
| 87 | 104 | workosCredentials, |
| @@ -89,6 +106,7 @@ export default defineExecutorConfig({ · 1 unmodified line | ||
| 89 | 106 | activeToolkitSlug, |
| 90 | 107 | allowLocalNetwork, |
| 91 | 108 | workerLoader, |
| 109 | + workerAssets, | |
| 92 | 110 | }: CloudPluginDeps = {}) => |
| 93 | 111 | [ |
| 94 | 112 | openApiHttpPlugin({ |
| @@ -107,9 +125,27 @@ export default defineExecutorConfig({ · 12 unmodified lines | ||
| 107 | 125 | loader: workerLoader, |
| 108 | 126 | artifact: async () => { |
| 109 | 127 | const artifact = await import("virtual:executor/worker-bundler-artifact"); |
| 128 | + if (artifact.source !== undefined && artifact.wasmBase64 !== undefined) { | |
| 129 | + return { | |
| 130 | + source: artifact.source, | |
| 131 | + wasm: base64ToArrayBuffer(artifact.wasmBase64), | |
| 132 | + }; | |
| 133 | + } | |
| 134 | + if (workerAssets === undefined) { | |
| 135 | + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: worker artifact asset binding is required for production bundler setup | |
| 136 | + throw new Error("worker-bundler artifact assets binding is unavailable"); | |
| 137 | + } | |
| 138 | + const [source, wasm] = await Promise.all([ | |
| 139 | + fetchAsset(workerAssets, artifact.sourcePath).then((response) => | |
| 140 | + response.text(), | |
| 141 | + ), | |
| 142 | + fetchAsset(workerAssets, artifact.wasmPath).then((response) => | |
| 143 | + response.arrayBuffer(), | |
| 144 | + ), | |
| 145 | + ]); | |
| 110 | 146 | return { |
| 111 | − source: artifact.source, | |
| 112 | − wasm: base64ToArrayBuffer(artifact.wasmBase64), | |
| 147 | + source, | |
| 148 | + wasm, | |
| 113 | 149 | }; |
| 114 | 150 | }, |
| 115 | 151 | }), |
| @@ -0,0 +1,54 @@ | ||
| 1 | +import { readdirSync, readFileSync, statSync } from "node:fs"; | |
| 2 | +import { join } from "node:path"; | |
| 3 | + | |
| 4 | +const appRoot = new URL("..", import.meta.url); | |
| 5 | +const serverAssetsDir = new URL("dist/server/assets/", appRoot); | |
| 6 | +const clientAssetsDir = new URL("dist/client/assets/", appRoot); | |
| 7 | +const largeFileLimit = 3 * 1024 * 1024; | |
| 8 | +const sourceAssetMinimum = 512 * 1024; | |
| 9 | + | |
| 10 | +const fail = (message) => { | |
| 11 | + // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: build assertion script reports a failed artifact contract | |
| 12 | + throw new Error(message); | |
| 13 | +}; | |
| 14 | + | |
| 15 | +const fileSize = (dir, name) => statSync(join(dir.pathname, name)).size; | |
| 16 | + | |
| 17 | +const serverAssets = readdirSync(serverAssetsDir); | |
| 18 | +for (const name of serverAssets) { | |
| 19 | + const path = join(serverAssetsDir.pathname, name); | |
| 20 | + const size = statSync(path).size; | |
| 21 | + if (size <= largeFileLimit) continue; | |
| 22 | + if (name.includes("worker-bundler")) { | |
| 23 | + fail(`server assets include oversized worker-bundler chunk ${name}`); | |
| 24 | + } | |
| 25 | + const sample = readFileSync(path, "utf8"); | |
| 26 | + if (sample.includes("worker-bundler")) { | |
| 27 | + fail(`server assets include worker-bundler content in oversized chunk ${name}`); | |
| 28 | + } | |
| 29 | +} | |
| 30 | + | |
| 31 | +const clientAssets = readdirSync(clientAssetsDir); | |
| 32 | +const source = clientAssets.find((name) => /^worker-bundler-source-[a-f0-9]{12}\.js$/.test(name)); | |
| 33 | +const wasm = clientAssets.find((name) => | |
| 34 | + /^worker-bundler-esbuild-wasm-[a-f0-9]{12}\.wasm$/.test(name), | |
| 35 | +); | |
| 36 | + | |
| 37 | +if (source === undefined) fail("missing worker-bundler source asset"); | |
| 38 | +if (wasm === undefined) fail("missing worker-bundler esbuild wasm asset"); | |
| 39 | + | |
| 40 | +const sourceSize = fileSize(clientAssetsDir, source); | |
| 41 | +const wasmSize = fileSize(clientAssetsDir, wasm); | |
| 42 | +if (sourceSize <= sourceAssetMinimum) { | |
| 43 | + fail(`worker-bundler source asset is unexpectedly small: ${source}`); | |
| 44 | +} | |
| 45 | +if (wasmSize <= largeFileLimit) fail(`worker-bundler wasm asset is unexpectedly small: ${wasm}`); | |
| 46 | + | |
| 47 | +const wasmMagic = readFileSync(join(clientAssetsDir.pathname, wasm)).subarray(0, 4); | |
| 48 | +if (!wasmMagic.equals(Buffer.from([0x00, 0x61, 0x73, 0x6d]))) { | |
| 49 | + fail(`worker-bundler wasm asset does not start with wasm magic bytes: ${wasm}`); | |
| 50 | +} | |
| 51 | + | |
| 52 | +console.log( | |
| 53 | + `[assert-worker-bundler-assets] ${source} (${sourceSize} bytes), ${wasm} (${wasmSize} bytes)`, | |
| 54 | +); | |
| @@ -24,6 +24,7 @@ const steps = [ · 23 unmodified lines | ||
| 24 | 24 | // imports, so they must be built before vite starts. |
| 25 | 25 | "turbo run build --filter @executor-js/vite-plugin --filter @executor-js/react --filter @executor-js/plugin-apps", |
| 26 | 26 | "vite build", |
| 27 | + "node scripts/assert-worker-bundler-assets.mjs", | |
| 27 | 28 | ]; |
| 28 | 29 | |
| 29 | 30 | for (const step of steps) { |
| @@ -64,6 +64,7 @@ const cloudPluginFactory = executorConfig.plugins as (deps: { · 63 unmodified lines | ||
| 64 | 64 | readonly activeToolkitSlug?: string; |
| 65 | 65 | readonly allowLocalNetwork?: boolean; |
| 66 | 66 | readonly workerLoader?: WorkerLoader; |
| 67 | + readonly workerAssets?: { readonly fetch: (request: Request) => Promise<Response> }; | |
| 67 | 68 | }) => readonly AnyPlugin[]; |
| 68 | 69 | |
| 69 | 70 | // Fresh plugin instances per request, carrying the Worker env's WorkOS Vault |
| @@ -80,6 +81,7 @@ export const CloudPluginsProvider: Layer.Layer<PluginsProvider> = Layer.succeed( · 10 unmodified lines | ||
| 80 | 81 | context?.mcpResource?.kind === "toolkit" ? context.mcpResource.slug : undefined, |
| 81 | 82 | allowLocalNetwork: env.ALLOW_LOCAL_NETWORK === "true", |
| 82 | 83 | workerLoader: env.LOADER, |
| 84 | + workerAssets: env.ASSETS, | |
| 83 | 85 | }), |
| 84 | 86 | }); |
| 85 | 87 | |
| @@ -22,6 +22,10 @@ declare global { · 21 unmodified lines | ||
| 22 | 22 | DATABASE_URL?: string; |
| 23 | 23 | EXECUTOR_DIRECT_DATABASE_URL?: string; |
| 24 | 24 | |
| 25 | + // Static asset binding emitted by @cloudflare/vite-plugin in | |
| 26 | + // dist/server/wrangler.json as assets.directory = "../client". | |
| 27 | + ASSETS: { readonly fetch: (request: Request) => Promise<Response> }; | |
| 28 | + | |
| 25 | 29 | // Plugin blob seam backend (wrangler.jsonc `r2_buckets`). Declared here |
| 26 | 30 | // (optional) rather than regenerating worker-configuration.d.ts: test |
| 27 | 31 | // workers and older local setups run without the binding, and the db |
| @@ -1,4 +1,6 @@ | ||
| 1 | 1 | declare module "virtual:executor/worker-bundler-artifact" { |
| 2 | − export const source: string; | |
| 3 | − export const wasmBase64: string; | |
| 2 | + export const sourcePath: string; | |
| 3 | + export const wasmPath: string; | |
| 4 | + export const source: string | undefined; | |
| 5 | + export const wasmBase64: string | undefined; | |
| 4 | 6 | } |
| @@ -4,6 +4,12 @@ · 3 unmodified lines | ||
| 4 | 4 | "compatibility_date": "2025-04-01", |
| 5 | 5 | "compatibility_flags": ["nodejs_compat"], |
| 6 | 6 | "main": "src/server.ts", |
| 7 | + // The vite plugin fills in assets.directory (dist/client); the binding must | |
| 8 | + // be declared here or env.ASSETS does not exist at runtime. The apps plugin | |
| 9 | + // fetches the worker-bundler artifact through it on first sync. | |
| 10 | + "assets": { | |
| 11 | + "binding": "ASSETS", | |
| 12 | + }, | |
| 7 | 13 | "routes": [ |
| 8 | 14 | { |
| 9 | 15 | "pattern": "executor.sh", |
| @@ -1,22 +1,74 @@ | ||
| 1 | +import { createHash } from "node:crypto"; | |
| 2 | +import { mkdir, writeFile } from "node:fs/promises"; | |
| 3 | +import { join } from "node:path"; | |
| 1 | 4 | import type { Plugin } from "vite"; |
| 2 | 5 | |
| 3 | 6 | import { bundledWorkerBundler } from "./pipeline/worker-bundler-artifact"; |
| 4 | 7 | |
| 5 | 8 | const virtualId = "virtual:executor/worker-bundler-artifact"; |
| 6 | 9 | const resolvedVirtualId = `\0${virtualId}`; |
| 10 | +const clientAssetsDir = "dist/client/assets"; | |
| 7 | 11 | |
| 8 | −export const workerBundlerArtifact = (): Plugin => ({ | |
| 9 | − name: "executor-worker-bundler-artifact", | |
| 10 | − resolveId(id) { | |
| 11 | − return id === virtualId ? resolvedVirtualId : null; | |
| 12 | − }, | |
| 13 | − async load(id) { | |
| 14 | − if (id !== resolvedVirtualId) return null; | |
| 15 | − const artifact = await bundledWorkerBundler(); | |
| 16 | − return [ | |
| 17 | − `export const source = ${JSON.stringify(artifact.source)};`, | |
| 18 | − `export const wasmBase64 = ${JSON.stringify(Buffer.from(artifact.wasm).toString("base64"))};`, | |
| 19 | − "export default { source, wasmBase64 };", | |
| 20 | − ].join("\n"); | |
| 21 | − }, | |
| 12 | +const digest = (value: string | Uint8Array): string => | |
| 13 | + createHash("sha256").update(value).digest("hex").slice(0, 12); | |
| 14 | + | |
| 15 | +const artifactPaths = (artifact: { | |
| 16 | + readonly source: string; | |
| 17 | + readonly wasm: Uint8Array; | |
| 18 | +}): { | |
| 19 | + readonly sourcePath: string; | |
| 20 | + readonly wasmPath: string; | |
| 21 | +} => ({ | |
| 22 | + sourcePath: `/assets/worker-bundler-source-${digest(artifact.source)}.js`, | |
| 23 | + wasmPath: `/assets/worker-bundler-esbuild-wasm-${digest(artifact.wasm)}.wasm`, | |
| 22 | 24 | }); |
| 25 | + | |
| 26 | +export const workerBundlerArtifact = (): Plugin => { | |
| 27 | + let command: "build" | "serve" = "build"; | |
| 28 | + | |
| 29 | + return { | |
| 30 | + name: "executor-worker-bundler-artifact", | |
| 31 | + configResolved(config) { | |
| 32 | + command = config.command; | |
| 33 | + }, | |
| 34 | + resolveId(id) { | |
| 35 | + return id === virtualId ? resolvedVirtualId : null; | |
| 36 | + }, | |
| 37 | + async load(id) { | |
| 38 | + if (id !== resolvedVirtualId) return null; | |
| 39 | + const artifact = await bundledWorkerBundler(); | |
| 40 | + const paths = artifactPaths(artifact); | |
| 41 | + const output = [ | |
| 42 | + `export const sourcePath = ${JSON.stringify(paths.sourcePath)};`, | |
| 43 | + `export const wasmPath = ${JSON.stringify(paths.wasmPath)};`, | |
| 44 | + ]; | |
| 45 | + | |
| 46 | + if (command === "serve") { | |
| 47 | + output.push( | |
| 48 | + `export const source = ${JSON.stringify(artifact.source)};`, | |
| 49 | + `export const wasmBase64 = ${JSON.stringify(Buffer.from(artifact.wasm).toString("base64"))};`, | |
| 50 | + "export default { sourcePath, wasmPath, source, wasmBase64 };", | |
| 51 | + ); | |
| 52 | + } else { | |
| 53 | + output.push( | |
| 54 | + "export const source = undefined;", | |
| 55 | + "export const wasmBase64 = undefined;", | |
| 56 | + "export default { sourcePath, wasmPath, source, wasmBase64 };", | |
| 57 | + ); | |
| 58 | + } | |
| 59 | + | |
| 60 | + return output.join("\n"); | |
| 61 | + }, | |
| 62 | + async closeBundle() { | |
| 63 | + if (command !== "build") return; | |
| 64 | + const artifact = await bundledWorkerBundler(); | |
| 65 | + const paths = artifactPaths(artifact); | |
| 66 | + const outputDir = join(process.cwd(), clientAssetsDir); | |
| 67 | + await mkdir(outputDir, { recursive: true }); | |
| 68 | + await Promise.all([ | |
| 69 | + writeFile(join(process.cwd(), "dist/client", paths.sourcePath), artifact.source), | |
| 70 | + writeFile(join(process.cwd(), "dist/client", paths.wasmPath), artifact.wasm), | |
| 71 | + ]); | |
| 72 | + }, | |
| 73 | + }; | |
| 74 | +}; | |