ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Alchemy 2.0.0-beta.50 深度解读:`alchemy dev` 的确定性端口分配与 Worker `env` 的 Output 绑定修复

Alchemy 2.0.0-beta.50 深度解读:`alchemy dev` 的确定性端口分配与 Worker `env` 的 Output 绑定修复 Alchemy 2.0.0-beta.50 深度解读alchemy dev的确定性端口分配与 Workerenv的 Output 绑定修复【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3codev2.0.0-beta.50 是 Alchemy 针对 Cloudflare 本地开发体验的一次双修复补丁发布其一让alchemy dev下的 Worker/Vite 端口分配变得确定可靠、冲突可被真实检测其二修复了将Output值如另一个 Worker 的 URL绑定进 Workerenv时在部署期触发Service not found: RuntimeContext崩溃的问题。本文以官方发布说明为骨架结合仓库内cloudflare-runtime与alchemy包的源码实现还原两个 Bug 的成因、修复路径与底层原理。该版本对应发布说明原文位于 2026-06-01-beta-50.md两项修复均由John Royal贡献。版本概览一次聚焦 Cloudflare 本地开发的两项修复v2.0.0-beta.50是一个小型补丁发布改动集中在两个 Cloudflare 相关问题上alchemy dev中 Worker/Vite 端口分配的可靠性cloudflare-runtime的端口冲突检测此前无法可靠触发且在 Vite 场景下问题最严重PR [#513]。把Output绑定到 Workerenv的部署期崩溃向 Worker 的env传入Output值例如另一个 Worker 的 URL时部署阶段会以Service not found: RuntimeContext崩溃PR [#514]。两个问题都发生在本地开发与部署的边界上修复后端口分配从撞运气变成确定性资源引用则从部署期崩溃变成线程穿透到运行时。对应变更记录可在 CHANGELOG.md 中按版本查阅。修复一alchemy dev中可靠的 Worker/Vite 端口分配问题背景端口冲突检测为何失灵且 Vite 场景最严重根据发布说明cloudflare-runtime里的端口冲突检测没有可靠地触发wasnt firing reliably其中Vite 是最严重的场景。要理解这一点需要先知道 Vite 在端口分配上的两个历史行为Vite 默认从5173开始向上寻找可用端口只有较新版本Vite 8.2.1才把server.port: 0当作真正的由操作系统分配随机端口。在cloudflare-runtime的端口实现中这一点被直接写成了运行时版本探测。见 core/internal/Port.ts/** * Whether the given Vite version treats server.port: 0 as a true * OS-assigned random port (vitejs/vite#23158, shipped in Vite 8.2.1). * Older Vite treats 0 as no port given and hunts upward from its 5173 * default — colliding with (or IPv6-shadowing) user-facing dev ports. */ export const viteSupportsPortZero ( version: string | null | undefined, ): boolean { if (typeof version ! string) return false; const match /^(\d)\.(\d)\.(\d)/.exec(version); if (!match) return false; const major Number(match[1]); const minor Number(match[2]); const patch Number(match[3]); return ( major 8 || (major 8 (minor 2 || (minor 2 patch 1))) ); };这段注释信息量很大旧版 Vite 会把port: 0解释为未指定端口转而从默认的 5173 向上打猎结果会与用户面向的 dev 端口冲突colliding或者出现 IPv6 影子端口IPv6-shadowing——即进程占用了127.0.0.1但::1上同样端口被别的进程抢走浏览器解析localhost优先走 IPv6流量被悄悄劫持。这正是冲突检测没触发的一种典型成因端口并非被检测为占用而是被另一个进程以另一种地址族静默接管。解决方案Vite 监听随机端口 WorkerProxy 前置代理beta.50 的修复思路很直接发布说明原文为beta.50 has Vite listen on a random port and places aWorkerProxyin front of it, so ports are assigned deterministically and conflicts are actually detected.即Vite 监听一个随机端口随机端口天然不会被固定端口占用在 Vite 前面放一个WorkerProxyWorker 代理对外暴露确定性的端口因此对外端口分配确定、可预测而冲突检测也真正发生作用——因为现在需要稳定绑定的只是代理本身这一个端口。源码级实现进程级端口协调器PortsWorkerProxy要可靠工作首先需要一个可靠的端口分配器。仓库中这部分实现在 core/internal/Port.ts它对外暴露四个核心操作方法语义用途find(port)从指定端口开始寻找可用端口随机/顺序分配find(0)直接交给操作系统分配临时端口check(port)检查端口是否可用可用则保留否则返回AddressInUse错误strictPort场景下的严格校验waitFor(port)在优雅窗口内轮询等待端口释放成功则保留用户配置端口的分配器处理 dev 会话重启竞态reserve(port)标记端口在缓存生命周期内被占用防止并发分配拿到同一个端口围绕这套接口实现上有几个值得注意的工程细节有界扫描避免 socket 风暴。搜索不是无限向上递增的而是被MAX_PORT_SEARCH_ATTEMPTS 128限制Port.ts。注释解释了原因在 Windows CI 上环境性故障如ENOBUFS临时端口/缓冲区耗尽会让每一次探测都失败若无界扫描会瞬间产生数十万次 socket bind放大本已存在的资源耗尽。进程级全局搜索锁与保留表。每个 runtime 实例都构造自己的Ports但它们都从同一个操作系统端口空间中分配。两个实例可能在同一 tick 内探测到同一个空闲端口并交给两个 workerd 进程——在 macOS 上由于SO_REUSEADDR的 bind/listen 窗口两个近乎同时的bind()可能都成功导致两个监听器静默瓜分该端口的流量。因此实现引入进程级globalSearchLock信号量串行化搜索循环进程级保留表globalReservations: Mapport, expiryTTL 为RESERVATION_TTL_MS 30_00030 秒保证探测后的中标者有足够时间建立监听见 Port.ts。错误码细分。绑定失败被区分为两类Port.tsEADDRINUSE/EACCES地址确实被占用可以顺延到下一个端口EADDRNOTAVAIL/EAFNOSUPPORT/EINVAL/ENOTFOUND主机本身不可用如本机无 IPv6 栈时绑定::此时不应否决端口——否则每个端口看起来都被占用搜索会扫遍整个端口空间。waitFor的优雅窗口。用户配置的固定端口非strictPort在 dev 会话重启时会与上一会话的 teardown 竞争如果立即回退会静默地把配置栈中的每个端口都不确定地顺延一位导致用户在熟悉的端口上访问到错误的服务。waitFor以 250ms 间隔轮询最多 12 次约 3 秒等待旧进程释放监听后再分配Port.ts。WorkerProxy一个 workerd 进程内托管的转发代理代理本体是一个运行在 workerd 进程里的内部 Worker实现在 core/proxy/WorkerProxy.ts对外暴露serve方法并返回一个WorkerProxyInstance含url、set(upstream)、unset()。其ServeOptions完整定义如下WorkerProxy.tsexport interface ServeOptions { /** * The port to serve the proxy on. If not provided, a random port will be chosen. * default 0 */ readonly port?: number; /** * Whether to throw an error if the port is not available. * default false */ readonly strictPort?: boolean; /** * The host to serve the proxy on. * default localhost */ readonly host?: string; }端口解析逻辑WorkerProxy.ts依次处理三种情况port提供且strictPort: true调用ports.check()端口被占用就直接失败——这是确定性 严格模式port提供但非 strict调用ports.waitFor()先等待 teardown若在窗口内仍未释放再顺延查找调用方会收到端口漂移警告未提供端口默认0调用ports.find(0)由操作系统分配唯一临时端口——这是随机 前置代理模式。此外还有两个细节IPv6 loopback 双绑定localhost会同时解析到127.0.0.1和::1浏览器优先 IPv6。如果代理只绑定127.0.0.1[::1]:port就可能被框架 dev server 抢占之后http://localhost:port静默服务的是别的进程。因此代理在 loopback 默认 host 上额外绑定一个[::1]套接字无 IPv6 栈的机器会探测一次并跳过。有界重试MAX_SERVE_ATTEMPTS 8每次重试都会新起一个 workerd 进程因此必须设上限。在 Windows 上isAddressInUseError会匹配任何std::terminate启动崩溃若无界重试会把环境性故障放大成无休止的 workerd 生成风暴WorkerProxy.ts。代理的上游切换通过控制器端点完成set()向url /cdn-cgi/proxy/controller发送带Bearer token的PUT请求写入 upstreamunset()发送DELETE。Vite 监听随机端口后代理将稳定的对外端口与随机内网端口解耦上游地址随时可换而对外 URL 不变。对开发者的实际意义端口可预测用户配置的端口被稳定保留不再因会话重启被不确定地顺延冲突可诊断strictPort: true下冲突直接失败并给出明确错误非 strict 模式下冲突会触发带端口漂移说明的警告日志Port X is in use by another process; serving on Y instead...见 WorkerProxy.ts无影子端口IPv6 双绑定杜绝了localhost被其他进程劫持的静默故障。修复二将Output绑定到 Worker 的env复现场景把另一个 Worker 的 URL 注入 Vite 站点发布说明给出了一个非常典型的复现代码声明一个后端 Worker再声明一个 Vite 站点并把后端 Worker 的 URL一个Output作为环境变量注入前端构建const backend yield* Cloudflare.Worker(/* ... */); const website yield* Cloudflare.Website.Vite(Website, { rootDir: path.resolve(import.meta.dirname, frontend), env: { VITE_API_URL: backend.url.asstring(), // crashed during deploy }, });注释标注了症状crashed during deploy。即这段代码在部署时而非运行时就会崩溃。崩溃根因部署期对Output提前调用.bind()Output是 Alchemy 的延迟值抽象——它描述一个将来才会解析出来的值其.bind()方法需要RuntimeContext才能解析。而问题出在 Worker 异步绑定的装配函数bindWorkerAsyncBindings上bindWorkerAsyncBindingswasyield*ing the value, which called.bind()on theOutputprematurely — andbind()needsRuntimeContext, which doesnt exist at deploy time.也就是说原来的实现把env中的每个值当作 Effect 直接yield*而yield*一个Output会触发它的.bind()——但.bind()需要RuntimeContext部署期根本不存在这个上下文于是抛出Service not found: RuntimeContext。修复方式yield 之前先判定Outputbeta.50 的修复是在 yield 之前先检查值是否为Output若是则不 yield把它原样交给引擎由引擎在运行时解析。对应实现在 packages/alchemy/src/Cloudflare/Workers/WorkerAsyncBindings.tsif (props.env) { for (const bindingName in props.env) { // ts-expect-error const bindingEff props.env?.[bindingName] as | WorkerBindingResource | Effect.EffectWorkerBindingResource; // ... // Bindings can be passed as a plain resource value, an Effect that // yields a resource, or an effect-class (e.g. a Cloudflare.Worker // class). Resolve the yieldable forms before deriving binding metadata. // Avoid yielding outputs as this requires RuntimeContext; // allow the engine to resolve them instead. const binding ( isYieldableEffectLike(bindingEff) !Output.isOutput(bindingEff) ? yield* bindingEff as Effect.Effectunknown : bindingEff ) as WorkerBindingResource; // ... } }源码注释明确写道Avoid yielding outputs as this requiresRuntimeContext; allow the engine to resolve them instead.避免 yield Output因为这会需要RuntimeContext改为让引擎去解析它们。修复后Output引用不再在部署期被提前解析而是作为绑定元数据穿透到运行时由引擎在正确的上下文里求值。配套机制延迟分类与部署期守卫围绕这个修复WorkerAsyncBindings.ts里还有两层配套逻辑值得说明toBinding对 Output 的延迟分类deferred classification。一个Output在分类阶段被包装成Output.map(...)其真实线格式plain_text/secret_text/json被推迟到解析时决定WorkerAsyncBindings.ts。注释解释原因如果急切分类一个解析后是Redacted密钥的 Output 会落入json兜底分支把本该加密的 secret 部署成未加密的 json 绑定。延迟分类保证了解析后的真实类型决定线格式。整资源 Output 的部署期拒绝。如果用户把整个资源如整个 Queue当作 env 值传入Output解析结果会是该资源的原始属性把它们作为明文 json 上传绝不是期望行为。由于类型层面InputT会接纳任何结构上是 Json 的 Output无法在编译期拒绝bindWorkerAsyncBindings就成了强制拦截点WorkerAsyncBindings.tsif (Output.isResourceExpr(binding) || Output.isRefExpr(binding)) { return yield* Effect.die( Cannot bind whole-resource Output ${bindingName}: pass the resource (or a typed ref) directly, or bind one of its attribute Outputs, ); }类型级保障。仓库中的类型探针 packages/alchemy/test/types/WorkerEnvOutput.ts 固定了这一行为一个Outputenv 值不得把兄弟键的类型推断折叠成绑定联合类型。探针同时验证了OutputRedactedstringAlchemy.makeRandom应推断为解密后的string、OutputstringOutput.literal推断为string——这保证了类型层与运行时层的行为一致。修复后的正确写法修复后文档中的示例无需任何改动即可正常工作backend.url.asstring()作为一个Outputstring被安全地放入env部署期不再崩溃运行时由引擎解析为后端的真实 URL。如果确实需要绑定某个资源的整体应直接传资源本身或使用其属性 Output而不是传整资源 Output。如何验证与升级验证这两个修复最直接的方式在项目里使用alchemy dev同时声明多个 Worker 与 Vite 站点重复启动/停止多次确认对外端口保持稳定、不再出现端口漂移或localhost被劫持构造一个Worker A 的 URL 注入 Worker B / Vite 站点env的声明如本文复现代码执行部署确认不再出现Service not found: RuntimeContext。升级渠道与版本对照可参考仓库根目录的 CHANGELOG.md其中按版本列出了全部变更发布说明原文还提供了与上一版本beta.49的完整差异视角可在对应 release 页面查看。需要注意的是本文描述的是2.0.0-beta.50这一时间点的行为若你使用的是更新版本端口与绑定机制可能在此基础上持续演进请以你实际安装版本的源码与文档为准。总结v2.0.0-beta.50虽然只是一次双修复补丁但它同时触及了本地开发体验与资源引用语义两个关键面端口侧cloudflare-runtime通过Vite 随机端口 WorkerProxy 前置代理的组合配合进程级端口协调器全局搜索锁、30 秒保留表、128 次有界扫描、IPv6 双绑定把端口分配从不可靠的探测升级为确定性分配绑定侧bindWorkerAsyncBindings在 yield 前增加Output.isOutput判定让Output引用穿透部署期、由引擎在运行时解析同时以延迟分类和整资源守卫补齐了线格式与错误路径。对 Alchemy 使用者而言这个版本标志着本地端口不再看运气、资源间引用不再在部署期爆炸——两个长期困扰 Cloudflare 本地开发的问题都在这一版里得到了源码级的根治。【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表