diff --git a/bun.lock b/bun.lock index 63d6613853..3112facc40 100644 --- a/bun.lock +++ b/bun.lock @@ -824,6 +824,7 @@ }, "devDependencies": { "@clack/prompts": "^0.11.0", + "@cloudflare/containers": "catalog:", "@cloudflare/puppeteer": "^1.1.0", "@cloudflare/workers-types": "catalog:", "@effect/platform-bun": "catalog:", @@ -854,6 +855,7 @@ "typescript": "catalog:", "uuid": "catalog:", "vitest": "catalog:", + "wrangler": "catalog:", "ws": "catalog:", }, "peerDependencies": { @@ -938,7 +940,7 @@ "@aws-crypto/util": "^5.2.0", "@aws-sdk/credential-providers": "^3.0.0", "@aws-sdk/types": "^3.973.1", - "@cloudflare/containers": "^0.1.1", + "@cloudflare/containers": "^0.3.7", "@cloudflare/vite-plugin": "^1.13.12", "@cloudflare/workers-types": "^4.20250805.0", "@distilled.cloud/aws": "workspace:*", @@ -987,6 +989,7 @@ "vite": "^8.0.7", "vitest": "^4.1.8", "web-tree-sitter": "0.25.10", + "wrangler": "^4.103.0", "ws": "^8.20.0", "yaml": "^2.0.0", }, @@ -1203,6 +1206,8 @@ "@clack/prompts": ["@clack/prompts@0.11.0", "", { "dependencies": { "@clack/core": "0.5.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw=="], + "@cloudflare/containers": ["@cloudflare/containers@0.3.7", "", {}, "sha512-DM9dm3FnIBSyiSJ1FLavKwl/lk3oAmTaynCzZQ9pZR0ncRPquSxkxd8Nu2MFILxmDDsPkxKsSNEh9mHHMty4Fw=="], + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/puppeteer": ["@cloudflare/puppeteer@1.1.0", "", { "dependencies": { "@puppeteer/browsers": "2.2.4", "debug": "^4.3.5", "devtools-protocol": "0.0.1299070", "ws": "^8.18.0" } }, "sha512-lN10En49avRDQvz8Gpv/WiIoGvjjDiP6P+v2y9bA6rfPT5WHfnlg2O6MwjHc1POm8A9LXf1sMdgrsdW8xWapOw=="], @@ -1211,15 +1216,15 @@ "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.40.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "4.20260611.0", "unenv": "2.0.0-rc.24", "wrangler": "4.100.0", "ws": "8.20.1" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-OBo1uYM/Y26WpFhUhaHU+/QxJqFTB8uFhVPBypuf8Dz51CMTNs3Y1JWazaujD/DrS5pt6Q6e6BHhxREXLpzsrA=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260617.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260625.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-naCfBv0WnnTQIQPTniqMoUlklOIFjrAcSn1X+IAOhY8aFLF/xGYtFjs1eEE8sFib3ZuChGGpU23FFORVczqr0A=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260617.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260625.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-jmH6zjp6Wrux46+qtFwDwrj+vd7s5bdwEqeGvdnwE0a4IEeAhKs0L42HQOyID+g5lkrHq9m55+AbhtmRAm63Pw=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260617.1", "", { "os": "linux", "cpu": "x64" }, "sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260625.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MiQkpA/dX8d83Zp64pzHUKfd6ca4cvwxnNobSP6CnXvfESvnNI9pfa+nfwnParla36sPmnYntNkjR7NjRuDeKQ=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260617.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260625.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-LxxW7Qv60Xvv37+w6gUSDpYZziyqMy+cZWd9IvSA5ehVgKAxmzEaYPMiSZlxk32nbIWL9u/tfjXYCOKJ4Lo+XQ=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260617.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260625.1", "", { "os": "win32", "cpu": "x64" }, "sha512-LH6iIX1HHaTwVKV5VokDxxUErXJzQoNZFRwVm7Vx/3fB/ApcTcRCUaMqcxI4as94jEUqg+pmX5czOndiveohow=="], "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260616.1", "", {}, "sha512-AHyA0NC2/gNOHiMN6vzS25xenMaQ0XTzfw+MUQSQHXQDjLldxCBwjjtbNfKhBg540qGXIEx31kM1+L84GyECJw=="], @@ -3459,7 +3464,7 @@ "mini-svg-data-uri": ["mini-svg-data-uri@1.4.4", "", { "bin": { "mini-svg-data-uri": "cli.js" } }, "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg=="], - "miniflare": ["miniflare@4.20260611.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.24.8", "workerd": "1.20260611.1", "ws": "8.20.1", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg=="], + "miniflare": ["miniflare@4.20260625.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.28.0", "workerd": "1.20260625.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-3kKXwRUObJsnBYPBgR0NiNZYKF/yv8GFyha1cx2EeAEraxNODgRVcyeRo+F1ok1tg5Mg7iUpOWSkknQTHuFhwA=="], "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -3611,7 +3616,7 @@ "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], @@ -4243,9 +4248,9 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "workerd": ["workerd@1.20260617.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260617.1", "@cloudflare/workerd-darwin-arm64": "1.20260617.1", "@cloudflare/workerd-linux-64": "1.20260617.1", "@cloudflare/workerd-linux-arm64": "1.20260617.1", "@cloudflare/workerd-windows-64": "1.20260617.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew=="], + "workerd": ["workerd@1.20260625.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260625.1", "@cloudflare/workerd-darwin-arm64": "1.20260625.1", "@cloudflare/workerd-linux-64": "1.20260625.1", "@cloudflare/workerd-linux-arm64": "1.20260625.1", "@cloudflare/workerd-windows-64": "1.20260625.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-GApQvFX52SDM6L4u0+RRnUDB1wJOnEwoXjinkmOPtIyofWBxrlZckdegJSYc1leg++lLZ3+DQ4zMVmBqYVtzfA=="], - "wrangler": ["wrangler@4.100.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260611.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260611.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260611.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg=="], + "wrangler": ["wrangler@4.105.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "4.20260625.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260625.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260625.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-7dXFH6OLj1Fv0y6ZeRPUxFTkp+duWD7/xxVi/1c0vfOeEYwIFKWB7cdqnY05DvY1Ta3BnqAwRkXfLs8PDj538g=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -4351,89 +4356,95 @@ "@better-auth/core/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "@cloudflare/vite-plugin/miniflare": ["miniflare@4.20260611.0", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.34.5", "undici": "7.24.8", "workerd": "1.20260611.1", "ws": "8.20.1", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-i+JwEo8vN96naz1WL3ntFgFyRluBDYL408zwhHKvR2jefJ464KsZ/gCmJAQ5k+oaWeb5Ug+s7yne5AyiAEswjg=="], + + "@cloudflare/vite-plugin/wrangler": ["wrangler@4.100.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260611.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260611.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260611.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "cf-wrangler": "bin/cf-wrangler.js", "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-dSQO7DO+mD6XDzkVWIWBoGLO3yw+lacWSc/KhFvd7pgfpth+kX98qb5SGRHZN8ACCDhhfwzDLXwB6qHsIHhfBg=="], + "@cloudflare/vite-plugin/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], "@cspotcode/source-map-support/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], - "@distilled.cloud/aws/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/aws/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/aws/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/axiom/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/axiom/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/axiom/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/azure/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/azure/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/azure/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/cloudflare/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/cloudflare/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/cloudflare/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], "@distilled.cloud/cloudflare/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "@distilled.cloud/coinbase/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/cloudflare-runtime/workerd": ["workerd@1.20260617.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260617.1", "@cloudflare/workerd-darwin-arm64": "1.20260617.1", "@cloudflare/workerd-linux-64": "1.20260617.1", "@cloudflare/workerd-linux-arm64": "1.20260617.1", "@cloudflare/workerd-windows-64": "1.20260617.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-Re5pl6pdowt3ZmWUzGlOuB7jbRIIPetgKalmo4cYmucQnVhpo7/3e4MfpekbhLi2EhZZz5EY9NWRu8zFzuEZew=="], + + "@distilled.cloud/coinbase/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/coinbase/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/core/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/core/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/core/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/expo-eas/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/expo-eas/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/expo-eas/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/fly-io/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/fly-io/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/fly-io/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/gcp/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/gcp/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/gcp/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/kubernetes/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/kubernetes/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/kubernetes/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/mongodb-atlas/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/mongodb-atlas/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/mongodb-atlas/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/neon/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/neon/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/neon/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/planetscale/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/planetscale/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/planetscale/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/posthog/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/posthog/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/posthog/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/prisma-postgres/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/prisma-postgres/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/prisma-postgres/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/stripe/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/stripe/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/stripe/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/supabase/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/supabase/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/supabase/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/turso/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/turso/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/turso/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/typesense/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/typesense/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/typesense/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], - "@distilled.cloud/workos/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "@distilled.cloud/workos/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "@distilled.cloud/workos/dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], @@ -4497,6 +4508,8 @@ "@solidjs/start/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + "@solidjs/start/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + "@solidjs/start/srvx": ["srvx@0.9.8", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ=="], "@solidjs/start/vite": ["vite@7.3.5", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww=="], @@ -4559,7 +4572,7 @@ "@vue/language-core/@vue/shared": ["@vue/shared@3.5.38", "", {}, "sha512-FTW0AFZNaK5/mOqvGBwVfUlNLU38TiQn4+DQgIFUnrBBJQ1crMJ82yeGQLV5jyKFsO8yRukpbuP7x+nRbH6aug=="], - "alchemy/@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], + "alchemy/@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], "ansi-align/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -4687,12 +4700,6 @@ "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - "miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], - - "miniflare/workerd": ["workerd@1.20260611.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260611.1", "@cloudflare/workerd-darwin-arm64": "1.20260611.1", "@cloudflare/workerd-linux-64": "1.20260611.1", "@cloudflare/workerd-linux-arm64": "1.20260611.1", "@cloudflare/workerd-windows-64": "1.20260611.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w=="], - - "miniflare/ws": ["ws@8.20.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w=="], - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], "minizlib/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -4817,12 +4824,6 @@ "website/typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], - - "wrangler/path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], - - "wrangler/workerd": ["workerd@1.20260611.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260611.1", "@cloudflare/workerd-darwin-arm64": "1.20260611.1", "@cloudflare/workerd-linux-64": "1.20260611.1", "@cloudflare/workerd-linux-arm64": "1.20260611.1", "@cloudflare/workerd-windows-64": "1.20260611.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w=="], - "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -4887,12 +4888,30 @@ "@babel/traverse/@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "@cloudflare/vite-plugin/miniflare/undici": ["undici@7.24.8", "", {}, "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ=="], + + "@cloudflare/vite-plugin/miniflare/workerd": ["workerd@1.20260611.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260611.1", "@cloudflare/workerd-darwin-arm64": "1.20260611.1", "@cloudflare/workerd-linux-64": "1.20260611.1", "@cloudflare/workerd-linux-arm64": "1.20260611.1", "@cloudflare/workerd-windows-64": "1.20260611.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w=="], + + "@cloudflare/vite-plugin/wrangler/esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "@cloudflare/vite-plugin/wrangler/workerd": ["workerd@1.20260611.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260611.1", "@cloudflare/workerd-darwin-arm64": "1.20260611.1", "@cloudflare/workerd-linux-64": "1.20260611.1", "@cloudflare/workerd-linux-arm64": "1.20260611.1", "@cloudflare/workerd-windows-64": "1.20260611.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-CS/640T7pIJ2HYX6x2DwKFGbcSckAWN3tgcdq+ptB6SaqjWUhlzIgA/YhPuwIU+/NnMnGpqOFX/hC18Oyge63w=="], + "@distilled.cloud/aws/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "@distilled.cloud/axiom/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "@distilled.cloud/azure/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260617.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jWwmgEVVWbsHNrLSNXzwjJaH90VzRxq1cWkQFUidxyeUPnMxemeNE8I9qFAfrpzGgE11e9sKDcE3ettJW08swQ=="], + + "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260617.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LHH7b565g9znfCUOkwbec6FG2rmRbsgCy6aJiU9KN662mNheWl5sw/iKleiFSiljPKQQP3HkjnC/NSkdgi/aSA=="], + + "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260617.1", "", { "os": "linux", "cpu": "x64" }, "sha512-FMnaAKXe4Cfd8TQurCVd9fs2XQVBFRCsP+Id/SRdUv89MlwYu9zXfoyx6BxM+brPTIUK38SHbo8iaxiwzLi9JQ=="], + + "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260617.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MRoifFYcqbxxIIQy7PqO5tFY/qPFSnjXzakWl0sO93l+HLyG35jRAgOi6jfqa4kBxc7gKKtH861DcewjxUfkjA=="], + + "@distilled.cloud/cloudflare-runtime/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260617.1", "", { "os": "win32", "cpu": "x64" }, "sha512-rgBV9wQrv0OSKgCTTbhFUFY3sLGNANZ88aqaLvtmEn2gmbFVb1J4PDGochVUdB7NSEp4D/ghHva6/8SZmbONpw=="], + "@distilled.cloud/cloudflare/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], "@distilled.cloud/coinbase/@types/node/undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], @@ -5299,16 +5318,6 @@ "listhen/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], - "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260611.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA=="], - - "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260611.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ=="], - - "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260611.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg=="], - - "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260611.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ=="], - - "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260611.1", "", { "os": "win32", "cpu": "x64" }, "sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw=="], - "nitropack/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "nitropack/h3/cookie-es": ["cookie-es@1.2.3", "", {}, "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw=="], @@ -5419,68 +5428,6 @@ "vue-router/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], - - "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], - - "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], - - "wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], - - "wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], - - "wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], - - "wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], - - "wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], - - "wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], - - "wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], - - "wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], - - "wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], - - "wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], - - "wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], - - "wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], - - "wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], - - "wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], - - "wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], - - "wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], - - "wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], - - "wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], - - "wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], - - "wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], - - "wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], - - "wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], - - "wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], - - "wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260611.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA=="], - - "wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260611.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ=="], - - "wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260611.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg=="], - - "wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260611.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ=="], - - "wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260611.1", "", { "os": "win32", "cpu": "x64" }, "sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw=="], - "wrap-ansi-cjs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "wrap-ansi-cjs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -5563,6 +5510,78 @@ "@astrojs/starlight/@astrojs/markdown-remark/shiki/@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], + "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260611.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA=="], + + "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260611.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ=="], + + "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260611.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg=="], + + "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260611.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ=="], + + "@cloudflare/vite-plugin/miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260611.1", "", { "os": "win32", "cpu": "x64" }, "sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@cloudflare/vite-plugin/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260611.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-iJICldmi4sBGgi7IrQles8cStOGXM/Tmv95C4OODVs6VIbMsJPqThUM5h3uYVQNULuJ8I/aVvnJ3Eh/wZCKwuA=="], + + "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260611.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-yBbVXvbZyltR3I7NJdC4C4ItkItjZSiabcA/3HzEWOUQjLVKFqRh4so6ToHr70VCYh8VGeR8EDZL23igLhXqFQ=="], + + "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260611.1", "", { "os": "linux", "cpu": "x64" }, "sha512-PfNjpxOlaIgZFYuhD7+neEEewCN2Ud993wEEN0fmbtSOax1AK53LGqmXUDvFhnbkHxJLFAxYCSNISW8QbzaAIg=="], + + "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260611.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-GEp4XbuIKjlF8pakqXcUDJfKiJosD/Q7S83J0d+r+z9XIlYGfF3ntm08e2aiF5TFTwp3fnG4yMoPUAKNhNJpvQ=="], + + "@cloudflare/vite-plugin/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260611.1", "", { "os": "win32", "cpu": "x64" }, "sha512-S6JkS0kEbcCKs19RGqEPhjCRbP8GBkQwqYLp2fhBJtD/KTlwqLzOJ9E6PQ7gQKgWHtxy1NBG3oXarlNFRNU/dw=="], + "@expressive-code/plugin-shiki/shiki/@shikijs/engine-javascript/oniguruma-to-es": ["oniguruma-to-es@4.3.6", "", { "dependencies": { "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA=="], "@octokit/action/@octokit/core/@octokit/request/@octokit/endpoint": ["@octokit/endpoint@9.0.6", "", { "dependencies": { "@octokit/types": "^13.1.0", "universal-user-agent": "^6.0.0" } }, "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw=="], diff --git a/package.json b/package.json index 858d3d1286..72dfd73377 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "catalog": { "@alchemy.run/node-utils": "0.0.5", "@aws-sdk/credential-providers": "^3.0.0", - "@cloudflare/containers": "^0.1.1", + "@cloudflare/containers": "^0.3.7", "@cloudflare/vite-plugin": "^1.13.12", "@cloudflare/workers-types": "^4.20250805.0", "@distilled.cloud/aws": "workspace:*", @@ -106,6 +106,7 @@ "vitest": "^4.1.8", "uuid": "^14.0.0", "web-tree-sitter": "0.25.10", + "wrangler": "^4.103.0", "ws": "^8.20.0", "yaml": "^2.0.0", "@aws-crypto/crc32": "^5.2.0", diff --git a/packages/alchemy/package.json b/packages/alchemy/package.json index a36785ce31..56bb1e2301 100644 --- a/packages/alchemy/package.json +++ b/packages/alchemy/package.json @@ -348,6 +348,7 @@ }, "devDependencies": { "@clack/prompts": "^0.11.0", + "@cloudflare/containers": "catalog:", "@cloudflare/puppeteer": "^1.1.0", "@cloudflare/workers-types": "catalog:", "@effect/platform-bun": "catalog:", @@ -378,6 +379,7 @@ "typescript": "catalog:", "uuid": "catalog:", "vitest": "catalog:", + "wrangler": "catalog:", "ws": "catalog:" }, "peerDependencies": { diff --git a/packages/alchemy/src/Cloudflare/Containers/Container.ts b/packages/alchemy/src/Cloudflare/Containers/Container.ts index 75c663623f..ff1d329655 100644 --- a/packages/alchemy/src/Cloudflare/Containers/Container.ts +++ b/packages/alchemy/src/Cloudflare/Containers/Container.ts @@ -43,6 +43,45 @@ export class ContainerError extends Data.TaggedError("ContainerError")<{ readonly cause?: unknown; }> {} +/** + * No container instance could be allocated within the start budget — the + * account is at its concurrent-instance cap (`maxInstances`) or the platform + * is still provisioning. Mirrors `@cloudflare/containers`' + * `NO_CONTAINER_INSTANCE_ERROR` (surfaced as HTTP 503 by native). + */ +export class NoContainerInstanceError extends Data.TaggedError( + "NoContainerInstanceError", +)<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +/** + * Cloudflare is rate limiting container starts ("too many containers per + * second"). Mirrors `@cloudflare/containers`' `RATE_LIMITED_ERROR` (HTTP 429). + * Hammering `start()` while rate limited only prolongs it, so callers should + * back off rather than retry tightly. + */ +export class ContainerRateLimitedError extends Data.TaggedError( + "ContainerRateLimitedError", +)<{ + readonly message: string; + readonly cause?: unknown; +}> {} + +/** + * The container instance exited/crashed while we were waiting for its port — + * the entrypoint failed to bind or died. Mirrors native's "container exited" + * detection (`!this.container.running` mid-wait); not curable by continuing to + * poll the same instance. + */ +export class ContainerCrashedError extends Data.TaggedError( + "ContainerCrashedError", +)<{ + readonly message: string; + readonly cause?: unknown; +}> {} + export interface ContainerStartupOptions extends cf.ContainerStartupOptions {} /** diff --git a/packages/alchemy/src/Cloudflare/Containers/ContainerApplication.ts b/packages/alchemy/src/Cloudflare/Containers/ContainerApplication.ts index 87eb937fe4..f127b43982 100644 --- a/packages/alchemy/src/Cloudflare/Containers/ContainerApplication.ts +++ b/packages/alchemy/src/Cloudflare/Containers/ContainerApplication.ts @@ -149,13 +149,17 @@ export interface ContainerApplicationProps extends PlatformProps { */ dockerfile?: string; /** - * Initial number of instances to maintain. - * @default 1 + * Initial number of instances to maintain. Matches wrangler, which forces + * this to 0 whenever {@link maxInstances} is set (pure scale-from-zero). + * @default 0 */ instances?: number; /** - * Maximum number of instances the application may scale to. - * @default 1 + * Maximum number of instances the application may scale to. Matches + * wrangler's default of 20. A value of 1 serializes every Durable Object + * instance through a single container slot, so the default lets containers + * scale concurrently out of the box. + * @default 20 */ maxInstances?: number; /** @@ -164,8 +168,10 @@ export interface ContainerApplicationProps extends PlatformProps { */ schedulingPolicy?: ContainerApplication.SchedulingPolicy; /** - * Instance type for each deployment. - * @default "dev" + * Instance type for each deployment. Defaults to wrangler's `"lite"` tier + * (1/16 vCPU, 256 MiB, 2 GB disk) when no explicit {@link vcpu}/{@link memory}/ + * {@link disk} is set. (`"dev"` is wrangler's deprecated alias for `"lite"`.) + * @default "lite" */ instanceType?: ContainerApplication.InstanceType; /** diff --git a/packages/alchemy/src/Cloudflare/Containers/ContainerProvider.ts b/packages/alchemy/src/Cloudflare/Containers/ContainerProvider.ts index 7f1ec2b8ba..fc2119b614 100644 --- a/packages/alchemy/src/Cloudflare/Containers/ContainerProvider.ts +++ b/packages/alchemy/src/Cloudflare/Containers/ContainerProvider.ts @@ -110,7 +110,17 @@ export const LiveContainerProvider = () => ) => normalizeNulls({ image: imageRef, - instanceType: props.instanceType, + // Default to wrangler's instance type ("lite") so containers schedule + // the same way out of the box. `instance_type` is mutually exclusive + // with explicit vcpu/memory/disk, so only default it when none are + // set. ("dev" is wrangler's deprecated alias for "lite".) + instanceType: + props.instanceType ?? + (props.vcpu === undefined && + props.memory === undefined && + props.disk === undefined + ? "lite" + : undefined), observability: props.observability, sshPublicKeyIds: props.sshPublicKeyIds, secrets: props.secrets, @@ -130,6 +140,28 @@ export const LiveContainerProvider = () => checks: props.checks, }) as ContainerApplication.Configuration; + // Scaling/placement defaults mirror wrangler's container defaults + // (`wrangler-dist/cli.js`) so an Alchemy container behaves like a + // `wrangler deploy`d one without extra config: + // - max_instances: 20 (`container.max_instances ?? 20`) + // - instances: 0 (wrangler forces 0 whenever + // max_instances is set, which we always + // do — pure scale-from-zero) + // - scheduling_policy: "default" + // (wrangler also defaults `constraints.tiers` to `[1, 2]`, but the + // distilled SDK models constraints as singular `tier`, not the `tiers` + // array, so we leave constraints untouched — it's a minor placement hint + // next to the scaling defaults.) + // A maxInstances default of 1 (the previous value) silently serialised + // every Durable Object instance through a single container slot, which is + // the dominant cause of "containers are slow under load". + const scalingDefaults = (props: ContainerApplicationProps) => ({ + instances: props.instances ?? 0, + maxInstances: props.maxInstances ?? 20, + schedulingPolicy: props.schedulingPolicy ?? "default", + constraints: props.constraints ?? {}, + }); + const computeImage = Effect.fn(function* ( id: string, props: ContainerApplicationProps, @@ -420,10 +452,7 @@ export const LiveContainerProvider = () => const application = yield* Containers.createContainerApplication({ accountId, name, - instances: news.instances ?? 1, - maxInstances: news.maxInstances ?? 1, - schedulingPolicy: news.schedulingPolicy ?? "default", - constraints: news.constraints ?? {}, + ...scalingDefaults(news), affinities: news.affinities, configuration, durableObjects, @@ -518,10 +547,7 @@ export const LiveContainerProvider = () => Containers.updateContainerApplication({ accountId, applicationId: existing.applicationId, - instances: news.instances ?? 1, - maxInstances: news.maxInstances ?? 1, - schedulingPolicy: news.schedulingPolicy ?? "default", - constraints: news.constraints ?? {}, + ...scalingDefaults(news), affinities: news.affinities, configuration, }), @@ -544,10 +570,7 @@ export const LiveContainerProvider = () => return yield* Containers.createContainerApplication({ accountId, name: existing.applicationName, - instances: news.instances ?? 1, - maxInstances: news.maxInstances ?? 1, - schedulingPolicy: news.schedulingPolicy ?? "default", - constraints: news.constraints ?? {}, + ...scalingDefaults(news), affinities: news.affinities, configuration, durableObjects, diff --git a/packages/alchemy/src/Cloudflare/Containers/StartContainer.ts b/packages/alchemy/src/Cloudflare/Containers/StartContainer.ts index 590744f47e..70feab8276 100644 --- a/packages/alchemy/src/Cloudflare/Containers/StartContainer.ts +++ b/packages/alchemy/src/Cloudflare/Containers/StartContainer.ts @@ -2,6 +2,7 @@ import * as Duration from "effect/Duration"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Schedule from "effect/Schedule"; +import * as Semaphore from "effect/Semaphore"; import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"; import * as HttpServerRequest from "effect/unstable/http/HttpServerRequest"; @@ -9,13 +10,62 @@ import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; import { ALCHEMY_PHASE } from "../../Phase.ts"; import { makeFetchRpcStub } from "../../Rpc.ts"; import { type Fetcher } from "../Fetcher.ts"; +import { DurableObjectState } from "../Workers/DurableObjectState.ts"; import { + ContainerCrashedError, ContainerError, + ContainerRateLimitedError, ContainerTag, + NoContainerInstanceError, type Container, type ContainerStartupOptions, } from "./Container.ts"; +/** + * Per-container-instance start coordination (start mutex + confirmed-ready + * port cache). + * + * `startContainer` can be invoked more than once for the same physical + * container: the {@link layer} helper calls it once per Durable Object, but a + * caller may also use `startContainer` directly from several places. If the + * mutex/ready-cache lived in each `startContainer` closure, those calls would + * not coordinate — two of them could both observe `running === false` and both + * call `container.start()`, with the second throwing "already running" (see + * cloudflare/containers#173). + * + * Keying off the Durable Object's `DurableObjectState` (stable for the life of + * a DO instance, and a distinct object per instance) makes the coordination + * per-container-instance regardless of how many `startContainer` calls share + * it. The map is module-scoped, but Workers isolates are per-isolate and each + * DO instance has its own `DurableObjectState`, so instances never collide; the + * `WeakMap` lets entries be collected with the instance. We deliberately do NOT + * key on the container's logical id — that is shared across every + * `getByName(...)` instance of the same class, which must each start + * independently. + */ +const startCoordination = new WeakMap< + object, + { + readonly startMutex: ReturnType; + readonly readyPorts: Set; + } +>(); + +const getStartCoordination = (key: object) => { + let coordination = startCoordination.get(key); + if (!coordination) { + // `makeUnsafe` (rather than `Effect.makeSemaphore`) because this runs in a + // synchronous cache-miss branch; the semaphore is a plain mutable primitive + // shared across effects, created once per DO instance. + coordination = { + startMutex: Semaphore.makeUnsafe(1), + readyPorts: new Set(), + }; + startCoordination.set(key, coordination); + } + return coordination; +}; + export const layer = ( container: Image, options?: ContainerStartupOptions, @@ -45,39 +95,223 @@ export const startContainer = Effect.fn(function* < ? yield* bound as Effect.Effect : (bound as Container); - const ensureRunning = Effect.gen(function* () { - if (yield* container.running) return; - yield* Effect.logInfo("Container not running, starting..."); - yield* container.start(options); - yield* Effect.logInfo("Container started, launching monitor"); - yield* Effect.forkDetach( - container.monitor().pipe( - Effect.flatMap(() => Effect.logInfo("Container monitor exited")), - Effect.catchTag("ContainerError", (error) => - Effect.logError(`Container monitor error: ${error.message}`), - ), - ), + // Every constant below is taken directly from Cloudflare's own + // `@cloudflare/containers` runtime (`dist/lib/container.js`) rather than + // guessed, so our readiness behaviour matches `startAndWaitForPorts`: + // + // INSTANCE_POLL_INTERVAL_MS = 300 → fixed poll interval + // PING_TIMEOUT_MS = 5_000 → per-probe cap (addTimeoutSignal) + // TIMEOUT_TO_GET_CONTAINER_MS = 8_000 → phase 1: find+start an instance + // TIMEOUT_TO_GET_PORTS_MS = 20_000 → phase 2: wait for the port to listen + // + // Native derives its retry COUNTS as ceil(timeout / pollInterval); we keep + // the two phases separate (as native does) on a 300ms cadence. + const READINESS_POLL_INTERVAL = Duration.millis(300); // INSTANCE_POLL_INTERVAL_MS + const READINESS_PROBE_TIMEOUT = Duration.seconds(5); // PING_TIMEOUT_MS + const TIMEOUT_TO_GET_CONTAINER_MS = 8_000; + const TIMEOUT_TO_GET_PORTS_MS = 20_000; + const GET_CONTAINER_RETRIES = Math.ceil(TIMEOUT_TO_GET_CONTAINER_MS / 300); // 27 + const PORT_READY_RETRIES = Math.ceil(TIMEOUT_TO_GET_PORTS_MS / 300); // 67 + // When rate limited, back off hard before retrying — hammering `start()` + // only prolongs the limit (native returns 429 immediately and does not loop). + const RATE_LIMIT_BACKOFF = Duration.seconds(2); + const RATE_LIMIT_RETRIES = 5; + // Light retry for the real request only — covers the transient + // "Network connection lost" window when a container instance is recycled. + const REQUEST_RETRIES = 3; + + // Error classification, mirroring native's string matchers in + // `@cloudflare/containers` (`isErrorOfType` against these exact phrases). + const errString = (e: unknown) => + (e instanceof Error ? e.message : String(e)).toLowerCase(); + const isNoInstanceError = (e: unknown) => + errString(e).includes( + "there is no container instance that can be provided to this durable object", ); - }); + const isRateLimitedError = (e: unknown) => + errString(e).includes("you are requesting too many containers per second"); - // Poll the container roughly every 2–3s while it cold-starts, but bound the - // total wait (~3 min) so an unreachable container surfaces a `ContainerError` - // instead of hanging the Durable Object request forever. Without this cap a - // container that never accepts connections on the requested port (e.g. it - // crash-loops, or the process never binds the port) would retry indefinitely - // and the worker request would never return. - const startupBackoff = Schedule.exponential(100, 1.5).pipe( - Schedule.modifyDelay((_, delay) => - Effect.succeed( - Duration.min( - Duration.max(delay, Duration.seconds(1)), - Duration.seconds(3), - ), + type ReadinessError = + | ContainerError + | NoContainerInstanceError + | ContainerRateLimitedError + | ContainerCrashedError; + + // Map a raw defect to a typed readiness error (native classifies the same + // message phrases via `isErrorOfType`). + const classifyDefect = ( + defect: unknown, + portNumber: number, + ): ContainerError | NoContainerInstanceError | ContainerRateLimitedError => + isRateLimitedError(defect) + ? new ContainerRateLimitedError({ + message: "Rate limited starting container", + cause: defect, + }) + : isNoInstanceError(defect) + ? new NoContainerInstanceError({ + message: "No container instance available", + cause: defect, + }) + : new ContainerError({ + message: `Container not ready on port ${portNumber}: ${defect}`, + cause: defect, + }); + + // Coalesce concurrent starts and share the confirmed-ready port cache across + // every caller for THIS Durable Object instance (see `startCoordination`). + // Native uses a `startInFlight` promise so two racing callers never both + // invoke `container.start()` ("already running" — cloudflare/containers#173); + // the 1-permit mutex serialises the start path so the second entrant sees + // `running === true` and skips. `readyPorts` lets steady-state requests skip + // the readiness probe entirely (like native's `healthy` state). + // + // Key on `DurableObjectState` (stable + unique per DO instance). Requiring it + // is correct: a container only ever runs inside a Durable Object, so this is + // an honest dependency rather than a silent fallback. + const doState = yield* DurableObjectState; + const coordinationKey: object = + (doState.container as object | undefined) ?? doState; + const { startMutex, readyPorts } = getStartCoordination(coordinationKey); + + const launchMonitor = Effect.forkDetach( + container.monitor().pipe( + Effect.flatMap(() => Effect.logInfo("Container monitor exited")), + Effect.catchTag("ContainerError", (error) => + Effect.logError(`Container monitor error: ${error.message}`), ), ), - Schedule.both(Schedule.recurs(75)), ); + // Start the container if it isn't running, serialised + classified. Backs + // off on rate limiting instead of hammering the allocator. + const ensureRunning = Semaphore.withPermits( + startMutex, + 1, + )( + Effect.gen(function* () { + if (yield* container.running) return; + yield* Effect.logDebug("Container not running, starting..."); + yield* container.start(options).pipe( + Effect.catchDefect((defect) => Effect.fail(classifyDefect(defect, -1))), + // A rate limit is transient — back off well clear of the per-second + // window and try again a few times before surfacing it. + Effect.retry({ + while: ( + e: + | ContainerError + | NoContainerInstanceError + | ContainerRateLimitedError, + ) => e._tag === "ContainerRateLimitedError", + schedule: Schedule.spaced(RATE_LIMIT_BACKOFF), + times: RATE_LIMIT_RETRIES, + }), + ); + yield* launchMonitor; + }), + ); + + // A single readiness probe: any response at all (even a non-2xx) proves the + // port is accepting connections. Each probe is hard-capped so a hung connect + // to a not-yet-bound port cannot stall the request. If the instance has gone + // away (`running === false`) after we started it, surface a crash rather than + // polling out the whole budget (native's `if (!this.container.running)`). + const probePort = (portNumber: number) => + container.getTcpPort(portNumber).pipe( + Effect.andThen((port: Fetcher) => + port.fetch( + HttpClientRequest.get("http://containerstarthealthcheck") as any, + ), + ), + Effect.timeout(READINESS_PROBE_TIMEOUT), + Effect.catchDefect((defect: unknown) => + container.running.pipe( + Effect.andThen((running) => + Effect.fail( + !running && + !isRateLimitedError(defect) && + !isNoInstanceError(defect) + ? new ContainerCrashedError({ + message: `Container exited while waiting for port ${portNumber}`, + cause: defect, + }) + : classifyDefect(defect, portNumber), + ), + ), + ), + ), + // A probe that hangs past the per-attempt cap, or fails the HTTP fetch + // before the port is up, is just "not ready yet" — retry it like any + // other transient readiness failure. + Effect.catchTag(["TimeoutError", "HttpClientError"], (cause: unknown) => + container.running.pipe( + Effect.andThen((running) => + Effect.fail( + !running + ? new ContainerCrashedError({ + message: `Container exited while waiting for port ${portNumber}`, + cause, + }) + : new ContainerError({ + message: `Container probe not ready on port ${portNumber}`, + cause, + }), + ), + ), + ), + ), + ); + + // Retry every readiness failure that could plausibly clear by waiting on the + // same instance — a not-yet-listening port, a still-allocating instance, a + // hung/timed-out probe, even a transient `running === false` read during cold + // start (which we can't reliably distinguish from a real crash without + // native's monitor exit code, so we keep polling within the bounded budget + // and only surface `ContainerCrashedError` if it never recovers). The one + // failure NOT retried here is a rate limit, which `ensureRunning` already + // backs off on — spinning the port loop would only make it worse. + const isTransientReadiness = (e: ReadinessError) => + e._tag !== "ContainerRateLimitedError"; + + // Phase 2 (native `waitForPort`): poll the port until it accepts connections. + const waitForPort = (portNumber: number) => + readyPorts.has(portNumber) + ? Effect.void + : probePort(portNumber).pipe( + Effect.tapError((err) => + Effect.logDebug(`Container not ready (will retry): ${err}`), + ), + Effect.retry({ + while: isTransientReadiness, + schedule: Schedule.spaced(READINESS_POLL_INTERVAL), + times: PORT_READY_RETRIES, + }), + Effect.andThen(() => + Effect.sync(() => { + readyPorts.add(portNumber); + }), + ), + ); + + // Phase 1 (native `doStartContainer`) + phase 2: ensure the instance is + // started (bounded, rate-limit aware), then wait for the port. + const ensureReady = (portNumber: number) => + readyPorts.has(portNumber) + ? Effect.void + : ensureRunning.pipe( + Effect.retry({ + while: ( + e: + | ContainerError + | NoContainerInstanceError + | ContainerRateLimitedError, + ) => e._tag === "NoContainerInstanceError", + schedule: Schedule.spaced(READINESS_POLL_INTERVAL), + times: GET_CONTAINER_RETRIES, + }), + Effect.andThen(() => waitForPort(portNumber)), + ); + const getTcpPort = (portNumber: number) => Effect.succeed({ fetch: (( @@ -85,20 +319,24 @@ export const startContainer = Effect.fn(function* < | HttpClientRequest.HttpClientRequest | HttpServerRequest.HttpServerRequest, ) => - ensureRunning.pipe( + // Block until the port is ready (bounded), THEN forward the real + // request exactly once. Readiness is a cheap healthcheck, so the user + // request is never replayed against a cold container. + ensureReady(portNumber).pipe( Effect.andThen(() => container.getTcpPort(portNumber)), Effect.andThen((port: Fetcher) => port.fetch(request as any)), Effect.catchDefect((defect: unknown) => Effect.fail( new ContainerError({ - message: `Container not ready on port ${portNumber}: ${defect}`, + message: `Container fetch failed on port ${portNumber}: ${defect}`, + cause: defect, }), ), ), - Effect.tapError((err) => - Effect.logDebug(`Container fetch error (will retry): ${err}`), - ), - Effect.retry({ schedule: startupBackoff }), + Effect.retry({ + schedule: Schedule.spaced(READINESS_POLL_INTERVAL), + times: REQUEST_RETRIES, + }), )) as { ( request: HttpClientRequest.HttpClientRequest, diff --git a/packages/alchemy/test/Cloudflare/Container/Container.benchmark.test.ts b/packages/alchemy/test/Cloudflare/Container/Container.benchmark.test.ts new file mode 100644 index 0000000000..0167d82360 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/Container.benchmark.test.ts @@ -0,0 +1,247 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Test from "@/Test/Vitest"; +import { describe, expect } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import { MinimumLogLevel } from "effect/References"; +import * as Schedule from "effect/Schedule"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import BenchmarkStack from "./fixtures/benchmark/stack.ts"; + +const { test, beforeAll, afterAll, deploy, destroy } = Test.make({ + providers: Cloudflare.providers(), + state: Cloudflare.state(), +}); + +const logLevel = Effect.provideService( + MinimumLogLevel, + process.env.DEBUG ? "Debug" : "Info", +); + +// Building + pushing the effectful image, pulling the remote image, and +// deploying the worker/DOs comfortably exceeds the default hook budget. +const HOOK_TIMEOUT = 600_000; +// The benchmark spins up N containers per variant across three variants run +// serially; each cold start can take well over a minute, so give the whole +// run a very wide ceiling. +const TEST_TIMEOUT = 2_400_000; + +// Number of container instances to spin up per variant, and how many to start +// concurrently. Override with BENCH_N / BENCH_CONCURRENCY when probing limits. +const N = Number(process.env.BENCH_N ?? 100); +const CONCURRENCY = Number(process.env.BENCH_CONCURRENCY ?? N); +// Each named DO instance boots its own container; the route blocks until the +// container is reachable (up to the start layer's ~3min cap), so allow a long +// per-request ceiling. +const REQUEST_TIMEOUT = "240 seconds"; + +const DEPLOY_PLACEHOLDER = "Alchemy worker is being deployed..."; + +// Force `Connection: close` so each request opens a fresh connection rather +// than pinning to one edge metal over a pooled keep-alive socket. +const freshConn = HttpClient.mapRequest( + HttpClientRequest.setHeader("connection", "close"), +); + +// Wait for the freshly-deployed worker's base route to answer 200 with "ok" +// (i.e. not the pre-create deploy stub) before starting the benchmark. +const waitForWorker = (url: string) => + Effect.gen(function* () { + const client = freshConn(yield* HttpClient.HttpClient); + yield* client.get(url).pipe( + Effect.flatMap((r) => + r.status !== 200 + ? Effect.fail(new Error(`worker not ready: ${r.status}`)) + : Effect.flatMap(r.text, (body) => + body.includes(DEPLOY_PLACEHOLDER) || !body.includes("ok") + ? Effect.fail(new Error(`not ready: ${body}`)) + : Effect.succeed(body), + ), + ), + Effect.timeout("30 seconds"), + Effect.retry({ + schedule: Schedule.exponential("500 millis").pipe( + Schedule.either(Schedule.spaced("3 seconds")), + ), + times: 30, + }), + ); + }); + +interface Sample { + /** Wall-clock latency of the whole request, measured outside (the client). */ + readonly outside: number; + /** Latency reported from inside the DO (container start → reachable). */ + readonly inside: number | undefined; +} + +interface VariantResult { + readonly label: string; + readonly samples: ReadonlyArray; + readonly failures: ReadonlyArray; +} + +// Fire one boot request and time the full outside round-trip. A 200 carries +// `{ ms }` (the inside-DO measurement); anything else is recorded as a failure +// rather than throwing, so one bad instance doesn't sink the whole run. +const boot = (baseUrl: string, path: string, name: string) => + Effect.gen(function* () { + const client = freshConn(yield* HttpClient.HttpClient); + const start = yield* Effect.sync(() => Date.now()); + const result = yield* client + .get(`${baseUrl}${path}?name=${encodeURIComponent(name)}`) + .pipe( + Effect.flatMap((r) => + Effect.map(r.text, (body) => ({ status: r.status, body })), + ), + Effect.timeout(REQUEST_TIMEOUT), + Effect.map((res) => ({ ok: true as const, ...res })), + Effect.catch((err) => + Effect.succeed({ ok: false as const, error: String(err) }), + ), + ); + const outside = (yield* Effect.sync(() => Date.now())) - start; + + if (!result.ok) { + return { sample: undefined, failure: `${name}: ${result.error}` }; + } + if (result.status !== 200) { + return { + sample: undefined, + failure: `${name}: HTTP ${result.status} ${result.body.slice(0, 200)}`, + }; + } + const inside = (() => { + try { + return (JSON.parse(result.body) as { ms?: number }).ms; + } catch { + return undefined; + } + })(); + return { sample: { outside, inside }, failure: undefined }; + }); + +const runVariant = ( + baseUrl: string, + path: string, + label: string, + nonce: string, +) => + Effect.gen(function* () { + const outcomes = yield* Effect.forEach( + Array.from({ length: N }, (_, i) => `${nonce}-${i}`), + (name) => boot(baseUrl, path, name), + { concurrency: CONCURRENCY }, + ); + const samples = outcomes + .map((o) => o.sample) + .filter((s): s is Sample => s !== undefined); + const failures = outcomes + .map((o) => o.failure) + .filter((f): f is string => f !== undefined); + return { label, samples, failures } satisfies VariantResult; + }); + +const stats = (xs: ReadonlyArray) => { + if (xs.length === 0) { + return { min: 0, max: 0, mean: 0, p50: 0, p90: 0, p95: 0, p99: 0 }; + } + const sorted = [...xs].sort((a, b) => a - b); + const pct = (p: number) => + sorted[Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length))]; + return { + min: sorted[0], + max: sorted[sorted.length - 1], + mean: Math.round(sorted.reduce((a, b) => a + b, 0) / sorted.length), + p50: pct(50), + p90: pct(90), + p95: pct(95), + p99: pct(99), + }; +}; + +const formatVariant = (r: VariantResult) => { + const outside = stats(r.samples.map((s) => s.outside)); + const inside = stats( + r.samples + .map((s) => s.inside) + .filter((m): m is number => typeof m === "number"), + ); + const ms = (n: number) => `${(n / 1000).toFixed(1)}s`; + return [ + `── ${r.label} ──`, + ` ok: ${r.samples.length}/${N} failed: ${r.failures.length}`, + ` outside (client round-trip):`, + ` min ${ms(outside.min)} p50 ${ms(outside.p50)} p90 ${ms(outside.p90)} p95 ${ms(outside.p95)} p99 ${ms(outside.p99)} max ${ms(outside.max)} mean ${ms(outside.mean)}`, + ` inside (DO start→reachable):`, + ` min ${ms(inside.min)} p50 ${ms(inside.p50)} p90 ${ms(inside.p90)} p95 ${ms(inside.p95)} p99 ${ms(inside.p99)} max ${ms(inside.max)} mean ${ms(inside.mean)}`, + ...(r.failures.length > 0 + ? [` failures:`, ...r.failures.slice(0, 5).map((f) => ` - ${f}`)] + : []), + ].join("\n"); +}; + +/** + * Cold-start benchmark: spin up N container instances per variant (each a + * distinct DO → distinct container) and time how long each takes to start and + * become reachable, comparing an Effect-native (bundled) container against a + * non-Effect (remote pre-built image) container. + * + * Set NO_DESTROY=1 to keep the deploy between runs while iterating. + */ +describe("container cold-start benchmark", () => { + const stack = beforeAll(deploy(BenchmarkStack), { timeout: HOOK_TIMEOUT }); + afterAll.skipIf(!!process.env.NO_DESTROY)(destroy(BenchmarkStack), { + timeout: HOOK_TIMEOUT, + }); + + test( + `spins up ${N} instances per variant and compares startup latency`, + Effect.gen(function* () { + const { url } = yield* stack; + yield* waitForWorker(url); + + const nonce = yield* Effect.sync(() => crypto.randomUUID().slice(0, 8)); + + // Run the two variants sequentially so they don't contend for the same + // pool of concurrent container starts (which would muddy the comparison). + const effectful = yield* runVariant( + url, + "/effectful", + "effectful (bundled Effect image)", + `eff-${nonce}`, + ); + const remote = yield* runVariant( + url, + "/remote", + "non-effectful (remote echo image)", + `rem-${nonce}`, + ); + const bun = yield* runVariant( + url, + "/bun", + "non-effectful (oven/bun:latest, no Effect bundle)", + `bun-${nonce}`, + ); + + const report = [ + "", + `Container cold-start benchmark (N=${N}, concurrency=${CONCURRENCY})`, + formatVariant(effectful), + formatVariant(bun), + formatVariant(remote), + "", + ].join("\n"); + // `console.log` (not `Effect.logInfo`) so the report always reaches the + // terminal — vitest buffers the structured logger for passing tests. + yield* Effect.sync(() => console.log(report)); + + // The benchmark is informational, but a run where nothing started at all + // indicates a broken deploy rather than slow containers. + expect(effectful.samples.length).toBeGreaterThan(0); + expect(remote.samples.length).toBeGreaterThan(0); + expect(bun.samples.length).toBeGreaterThan(0); + }).pipe(logLevel), + { timeout: TEST_TIMEOUT }, + ); +}); diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-context/Dockerfile b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-context/Dockerfile new file mode 100644 index 0000000000..b28db1e03f --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-context/Dockerfile @@ -0,0 +1,11 @@ +# Same base image as the effectful container (`oven/bun:latest`), but with NO +# Effect program bundled in — just a one-line Bun HTTP server. This isolates +# the Effect-runtime/bundle cost from the base-image pull/boot cost: any +# cold-start gap between this and the effectful variant is attributable to the +# bundled Effect runtime, not the image. +FROM oven/bun:latest + +RUN printf 'Bun.serve({ port: 8080, hostname: "0.0.0.0", fetch: () => new Response("ok") });\n' > /server.ts + +EXPOSE 8080 +CMD ["bun", "/server.ts"] diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-object.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-object.ts new file mode 100644 index 0000000000..7201403217 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/bun-object.ts @@ -0,0 +1,60 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; + +/** + * Non-Effect container built from a Dockerfile whose base image is the *same* + * `oven/bun:latest` the effectful variant uses — but with no Effect program + * bundled, just a raw `Bun.serve`. Comparing this against the effectful + * variant rules out base-image pull/boot time as the cause of the gap: the + * remaining difference is the bundled Effect runtime. + */ +export class BenchBunContainer extends Cloudflare.Container()( + "BenchBunContainer", + { + context: `${import.meta.dirname}/bun-context`, + // Match wrangler's config exactly: max_instances 100 (Alchemy defaults to + // 1) and instance_type "lite" (Alchemy/API default differs), so the + // comparison isolates the framework rather than the instance tier. + maxInstances: 100, + instanceType: "lite", + // wrangler deploys containers with `instances: 0` (pure scale-from-zero); + // Alchemy defaults to 1 (an always-on rollout) which can contend with + // on-demand starts. Match wrangler. + instances: 0, + }, +) {} + +/** + * Durable Object backing one bun-baseline container instance. `boot()` times + * cold-start until the `Bun.serve` HTTP server answers on its TCP port. + */ +export class BenchBunObject extends Cloudflare.DurableObject()( + "BenchBunObject", + Effect.gen(function* () { + const container = yield* BenchBunContainer; + + return Effect.gen(function* () { + const { fetch } = yield* container.getTcpPort(8080); + + return { + boot: () => + Effect.gen(function* () { + const start = yield* Effect.sync(() => Date.now()); + const response = yield* fetch( + HttpClientRequest.get("http://container/"), + ); + yield* response.text; + const end = yield* Effect.sync(() => Date.now()); + return { ms: end - start }; + }), + }; + }); + }).pipe( + Effect.provide( + Cloudflare.Containers.layer(BenchBunContainer, { + enableInternet: true, + }), + ), + ), +) {} diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-container.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-container.ts new file mode 100644 index 0000000000..e6d50539e2 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-container.ts @@ -0,0 +1,36 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; + +/** + * Minimal Effect-native container used purely for the cold-start benchmark. + * Alchemy bundles this file's Effect program into a generated image; the DO + * proxies a trivial `ping()` RPC into it, which is enough to prove the + * container has started and is accepting connections on its RPC port. + */ +export class BenchEffectfulContainer extends Cloudflare.Container< + BenchEffectfulContainer, + { + ping: () => Effect.Effect; + } +>()("BenchEffectfulContainer") {} + +export default BenchEffectfulContainer.make( + { + main: import.meta.filename, + dockerfile: "FROM oven/bun:latest", + // Alchemy defaults maxInstances to 1, which serializes every named DO + // instance through a single container slot. Match wrangler's + // `max_instances: 100` so the benchmark actually runs concurrently, and + // `instance_type: "lite"` so we compare the same tier. + maxInstances: 100, + instanceType: "lite", + instances: 0, + }, + Effect.gen(function* () { + return { + ping: () => Effect.succeed("pong"), + fetch: Effect.succeed(HttpServerResponse.text("ok")), + }; + }), +); diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-object.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-object.ts new file mode 100644 index 0000000000..981091d493 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/effectful-object.ts @@ -0,0 +1,38 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import { BenchEffectfulContainer } from "./effectful-container.ts"; + +/** + * Durable Object backing one effectful container instance. `boot()` measures + * the time, from inside the DO, until the container is accepting RPC — the + * `ping()` call blocks through the container's cold start (the start layer + * polls the container until its port answers), so the elapsed delta is the + * "started and reachable" latency. + * + * Each distinct `getByName(name)` is a distinct DO instance and therefore a + * distinct container instance, which is how the benchmark spins up N of them. + */ +export class BenchEffectfulObject extends Cloudflare.DurableObject()( + "BenchEffectfulObject", + Effect.gen(function* () { + const container = yield* BenchEffectfulContainer; + + return Effect.gen(function* () { + return { + boot: () => + Effect.gen(function* () { + const start = yield* Effect.sync(() => Date.now()); + yield* container.ping(); + const end = yield* Effect.sync(() => Date.now()); + return { ms: end - start }; + }), + }; + }); + }).pipe( + Effect.provide( + Cloudflare.Containers.layer(BenchEffectfulContainer, { + enableInternet: true, + }), + ), + ), +) {} diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/remote-object.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/remote-object.ts new file mode 100644 index 0000000000..c34a43697e --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/remote-object.ts @@ -0,0 +1,57 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; + +/** + * Non-Effect ("remote image") container for the benchmark: Alchemy pulls a + * pre-built public image and re-pushes it — no Effect program is bundled and + * no runtime is injected. `mendhak/http-https-echo` serves on port 8080 and + * writes to its inherited stdout fd directly, so it boots cleanly inside + * Cloudflare's container sandbox. + */ +export class BenchRemoteContainer extends Cloudflare.Container()( + "BenchRemoteContainer", + { + image: "mendhak/http-https-echo:latest", + // Match wrangler's config exactly (max_instances 100, instance_type lite). + maxInstances: 100, + instanceType: "lite", + instances: 0, + }, +) {} + +/** + * Durable Object backing one remote (non-effectful) container instance. + * `boot()` measures the time, from inside the DO, until the container's HTTP + * server answers on its TCP port — the first `fetch` blocks through cold start + * (the start layer polls the port until it accepts connections). + */ +export class BenchRemoteObject extends Cloudflare.DurableObject()( + "BenchRemoteObject", + Effect.gen(function* () { + const container = yield* BenchRemoteContainer; + + return Effect.gen(function* () { + const { fetch } = yield* container.getTcpPort(8080); + + return { + boot: () => + Effect.gen(function* () { + const start = yield* Effect.sync(() => Date.now()); + const response = yield* fetch( + HttpClientRequest.get("http://container/"), + ); + yield* response.text; + const end = yield* Effect.sync(() => Date.now()); + return { ms: end - start }; + }), + }; + }); + }).pipe( + Effect.provide( + Cloudflare.Containers.layer(BenchRemoteContainer, { + enableInternet: true, + }), + ), + ), +) {} diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/stack.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/stack.ts new file mode 100644 index 0000000000..d984224cfc --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/stack.ts @@ -0,0 +1,22 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Alchemy from "@/index.ts"; +import * as Effect from "effect/Effect"; +import BenchEffectfulContainerLive from "./effectful-container.ts"; +import BenchWorker from "./worker.ts"; + +export default Alchemy.Stack( + "ContainerBenchmarkStack", + { + providers: Cloudflare.providers(), + state: Cloudflare.state(), + }, + Effect.gen(function* () { + const worker = yield* BenchWorker; + return { + url: worker.url.as(), + }; + // The effectful container's `.make()` runtime must be reachable so the + // bundler emits its entrypoint; the remote container is image-only and + // needs no runtime layer. + }).pipe(Effect.provide(BenchEffectfulContainerLive)), +); diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/worker.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/worker.ts new file mode 100644 index 0000000000..548f6c9163 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/worker.ts @@ -0,0 +1,60 @@ +import * as Cloudflare from "@/Cloudflare"; +import * as Effect from "effect/Effect"; +import { HttpServerRequest } from "effect/unstable/http/HttpServerRequest"; +import * as HttpServerResponse from "effect/unstable/http/HttpServerResponse"; +import { BenchBunObject } from "./bun-object.ts"; +import { BenchEffectfulObject } from "./effectful-object.ts"; +import { BenchRemoteObject } from "./remote-object.ts"; + +/** + * Benchmark entrypoint. Each request names a fresh DO instance (`?name=`), + * which boots its own container and reports the cold-start-to-reachable + * latency measured inside the DO: + * + * - `GET /effectful?name=X` → effectful (bundled Effect program) container + * - `GET /remote?name=X` → remote (pre-built echo image) container + * - `GET /bun?name=X` → bun-baseline (same base image, no Effect bundle) + * + * The test fires N distinct names per route concurrently to spin up N + * containers and compares the two variants. + */ +export default class BenchWorker extends Cloudflare.Worker()( + "BenchWorker", + { + main: import.meta.filename, + }, + Effect.gen(function* () { + const effectful = yield* BenchEffectfulObject; + const remote = yield* BenchRemoteObject; + const bun = yield* BenchBunObject; + + return { + fetch: Effect.gen(function* () { + const request = yield* HttpServerRequest; + const url = new URL(request.url, "http://x"); + const name = url.searchParams.get("name") ?? "default"; + + if (url.pathname === "/effectful") { + const result = yield* effectful.getByName(name).boot(); + return yield* HttpServerResponse.json(result); + } + + if (url.pathname === "/remote") { + const result = yield* remote.getByName(name).boot(); + return yield* HttpServerResponse.json(result); + } + + if (url.pathname === "/bun") { + const result = yield* bun.getByName(name).boot(); + return yield* HttpServerResponse.json(result); + } + + return HttpServerResponse.text("ok"); + }).pipe( + Effect.catch((err) => + Effect.succeed(HttpServerResponse.text(String(err), { status: 500 })), + ), + ), + }; + }), +) {} diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/Dockerfile b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/Dockerfile new file mode 100644 index 0000000000..0b4fdbaef4 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/Dockerfile @@ -0,0 +1,11 @@ +# Same base image as the Alchemy "bun-baseline" benchmark variant +# (`oven/bun:latest`) running a one-line Bun HTTP server on port 8080. Keeping +# the image identical makes the wrangler numbers directly comparable to the +# Alchemy ones — any difference is attributable to the framework, not the +# image. +FROM oven/bun:latest + +RUN printf 'Bun.serve({ port: 8080, hostname: "0.0.0.0", fetch: () => new Response("ok") });\n' > /server.ts + +EXPOSE 8080 +CMD ["bun", "/server.ts"] diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/bench.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/bench.ts new file mode 100644 index 0000000000..7fd1c47130 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/bench.ts @@ -0,0 +1,164 @@ +/** + * Standalone cold-start benchmark for the plain wrangler + `@cloudflare/containers` + * worker. Mirrors the methodology of the Alchemy benchmark + * (`packages/alchemy/test/Cloudflare/Container/Container.benchmark.test.ts`) so + * the two are directly comparable, but uses zero Alchemy code — just `fetch` + * against the deployed worker. + * + * Usage (from this directory): + * bun x wrangler deploy # build + push the image, deploy the worker + * WORKER_URL=https://.workers.dev bun bench.ts + * BENCH_N=2 BENCH_CONCURRENCY=2 WORKER_URL=... bun bench.ts + * bun x wrangler delete # tear down + * + * `wrangler` and `@cloudflare/containers` resolve from the `alchemy` package's + * devDependencies (catalog), so no separate install is needed. + */ + +const WORKER_URL = (process.env.WORKER_URL ?? process.argv[2] ?? "").replace( + /\/$/, + "", +); +const N = Number(process.env.BENCH_N ?? 2); +const CONCURRENCY = Number(process.env.BENCH_CONCURRENCY ?? N); +const REQUEST_TIMEOUT_MS = Number( + process.env.BENCH_REQUEST_TIMEOUT_MS ?? 240_000, +); + +if (!WORKER_URL) { + console.error( + "Set WORKER_URL to the deployed worker, e.g. WORKER_URL=https://x.workers.dev bun bench.ts", + ); + process.exit(1); +} + +interface Sample { + outside: number; + inside: number | undefined; +} + +const wait = async (url: string) => { + for (let i = 0; i < 30; i++) { + try { + const res = await fetch(url, { headers: { connection: "close" } }); + const body = await res.text(); + if (res.status === 200 && body.includes("ok")) return; + } catch { + // retry + } + await new Promise((r) => setTimeout(r, Math.min(3000, 500 * 2 ** i))); + } + throw new Error("worker never became ready"); +}; + +const boot = async ( + name: string, +): Promise<{ sample?: Sample; failure?: string }> => { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); + const start = Date.now(); + try { + const res = await fetch( + `${WORKER_URL}/start?name=${encodeURIComponent(name)}`, + { headers: { connection: "close" }, signal: controller.signal }, + ); + const body = await res.text(); + const outside = Date.now() - start; + if (res.status !== 200) { + return { failure: `${name}: HTTP ${res.status} ${body.slice(0, 200)}` }; + } + let inside: number | undefined; + try { + inside = (JSON.parse(body) as { ms?: number }).ms; + } catch { + inside = undefined; + } + return { sample: { outside, inside } }; + } catch (err) { + return { failure: `${name}: ${String(err)}` }; + } finally { + clearTimeout(timer); + } +}; + +// Simple bounded-concurrency pool. +const runPool = async ( + names: string[], + concurrency: number, +): Promise> => { + const results: Array<{ sample?: Sample; failure?: string }> = []; + let next = 0; + const workers = Array.from( + { length: Math.min(concurrency, names.length) }, + async () => { + while (next < names.length) { + const i = next++; + results[i] = await boot(names[i]); + } + }, + ); + await Promise.all(workers); + return results; +}; + +const stats = (xs: number[]) => { + if (xs.length === 0) + return { min: 0, max: 0, mean: 0, p50: 0, p90: 0, p95: 0, p99: 0 }; + const s = [...xs].sort((a, b) => a - b); + const pct = (p: number) => + s[Math.min(s.length - 1, Math.floor((p / 100) * s.length))]; + return { + min: s[0], + max: s[s.length - 1], + mean: Math.round(s.reduce((a, b) => a + b, 0) / s.length), + p50: pct(50), + p90: pct(90), + p95: pct(95), + p99: pct(99), + }; +}; + +const main = async () => { + await wait(WORKER_URL); + + const nonce = crypto.randomUUID().slice(0, 8); + const names = Array.from({ length: N }, (_, i) => `${nonce}-${i}`); + const outcomes = await runPool(names, CONCURRENCY); + + const samples = outcomes + .map((o) => o.sample) + .filter((s): s is Sample => s !== undefined); + const failures = outcomes + .map((o) => o.failure) + .filter((f): f is string => f !== undefined); + + const outside = stats(samples.map((s) => s.outside)); + const inside = stats( + samples + .map((s) => s.inside) + .filter((m): m is number => typeof m === "number"), + ); + const sec = (n: number) => `${(n / 1000).toFixed(1)}s`; + + console.log( + [ + "", + `wrangler + @cloudflare/containers cold-start benchmark (N=${N}, concurrency=${CONCURRENCY})`, + `── non-effectful (oven/bun:latest, plain wrangler) ──`, + ` ok: ${samples.length}/${N} failed: ${failures.length}`, + ` outside (client round-trip):`, + ` min ${sec(outside.min)} p50 ${sec(outside.p50)} p90 ${sec(outside.p90)} p95 ${sec(outside.p95)} p99 ${sec(outside.p99)} max ${sec(outside.max)} mean ${sec(outside.mean)}`, + ` inside (worker start→reachable):`, + ` min ${sec(inside.min)} p50 ${sec(inside.p50)} p90 ${sec(inside.p90)} p95 ${sec(inside.p95)} p99 ${sec(inside.p99)} max ${sec(inside.max)} mean ${sec(inside.mean)}`, + ...(failures.length > 0 + ? [" failures:", ...failures.slice(0, 5).map((f) => ` - ${f}`)] + : []), + "", + ].join("\n"), + ); +}; + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/worker.ts b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/worker.ts new file mode 100644 index 0000000000..70eb357c40 --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/worker.ts @@ -0,0 +1,42 @@ +import { Container, getContainer } from "@cloudflare/containers"; + +interface Env { + BENCH_CONTAINER: DurableObjectNamespace; +} + +/** + * Plain `@cloudflare/containers` container — no Alchemy involved. The DO proxies + * `fetch` to the Bun HTTP server on port 8080; the first `fetch` blocks through + * the container's cold start (the helper calls `startAndWaitForPorts` + * internally), so timing a single `fetch` measures start → reachable. + */ +export class BenchContainer extends Container { + defaultPort = 8080; + sleepAfter = "2m"; +} + +export default { + async fetch(request, env): Promise { + const url = new URL(request.url); + + // `/start?name=X` boots the container instance named `X` and reports the + // cold-start-to-reachable latency measured inside the Worker. + if (url.pathname === "/start") { + const name = url.searchParams.get("name") ?? "default"; + const container = getContainer(env.BENCH_CONTAINER, name); + const start = Date.now(); + try { + const res = await container.fetch(new Request("http://container/")); + await res.text(); + return Response.json({ ms: Date.now() - start, status: res.status }); + } catch (err) { + return Response.json( + { error: String(err), ms: Date.now() - start }, + { status: 500 }, + ); + } + } + + return new Response("ok"); + }, +} satisfies ExportedHandler; diff --git a/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/wrangler.jsonc b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/wrangler.jsonc new file mode 100644 index 0000000000..61b23ad18c --- /dev/null +++ b/packages/alchemy/test/Cloudflare/Container/fixtures/benchmark/wrangler/wrangler.jsonc @@ -0,0 +1,30 @@ +{ + "name": "alchemy-bench-containers-wrangler", + "main": "worker.ts", + "compatibility_date": "2026-06-29", + // A single container class, backed by a Durable Object. Each + // `getContainer(binding, name)` is a distinct DO instance and therefore a + // distinct container instance — the benchmark drives N distinct names to + // spin up N containers. `max_instances` must be >= the largest N we test. + "containers": [ + { + "class_name": "BenchContainer", + "image": "./Dockerfile", + "max_instances": 100 + } + ], + "durable_objects": { + "bindings": [ + { + "class_name": "BenchContainer", + "name": "BENCH_CONTAINER" + } + ] + }, + "migrations": [ + { + "new_sqlite_classes": ["BenchContainer"], + "tag": "v1" + } + ] +} diff --git a/packages/alchemy/tsconfig.test.json b/packages/alchemy/tsconfig.test.json index cbd323f107..b2336771ee 100644 --- a/packages/alchemy/tsconfig.test.json +++ b/packages/alchemy/tsconfig.test.json @@ -4,7 +4,13 @@ "exclude": [ "test/Local/fixtures/rpc-server-entry.ts", "test/Local/fixtures/rpc-spawner-parent.ts", - "test/Local/fixtures/rpc-spawner-devserver-parent.ts" + "test/Local/fixtures/rpc-spawner-devserver-parent.ts", + // Standalone wrangler + @cloudflare/containers control harness — it imports + // `cloudflare:workers` (via @cloudflare/containers) and uses Workers runtime + // globals that only resolve under wrangler's bundler, not plain tsc. It is + // deployed/run on its own (`wrangler deploy` + `bun bench.ts`), never + // imported by the vitest suite. + "test/Cloudflare/Container/fixtures/benchmark/wrangler/**" ], "compilerOptions": { "composite": true,