中的 Alchemy 2.0.0-beta.37:跨棧引用、類型化綁定與 R2 自動清空的 IaC 實踐指南)
t3code 生態(tài)中的 Alchemy 2.0.0-beta.37跨棧引用、類型化綁定與 R2 自動清空的 IaC 實踐指南【免費下載鏈接】t3code項目地址: https://gitcode.com/GitHub_Trending/t3/t3code本篇文章基于 Alchemy 項目官方發(fā)布說明2026-05-12-beta-37.md展開全面解析v2.0.0-beta.37中最核心的七大能力跨棧/跨階段資源引用、Alchemy.Secret/Alchemy.Variable一行式聲明、完全類型化的 Worker-to-Worker 綁定、帶類型化輸入輸出的 Workflows、Cron 觸發(fā)器、Analytics Engine 綁定以及 R2 桶銷毀時自動清空。讀完本文你將能夠利用這些能力在 PR 預覽階段復用staging數(shù)據(jù)庫、在 monorepo 中跨棧讀取部署輸出并寫出端到端類型安全的多 Worker 云應用。說明本倉庫通過reference-repos機制同步了 Alchemy 的源碼與示例見 scripts/lib/reference-repos.ts文中所引源碼均位于.repos/alchemy-effect/下供讀者對照驗證。一、版本背景beta.37 解決了什么v2.0.0-beta.37被官方稱為一段時間以來最大的 beta。其頭號特性是cross-stack跨棧與 cross-stage跨階段引用——這是讓 PR 預覽階段共享staging數(shù)據(jù)庫、而不必在每次有人打開草稿 PR 時重新供應一整套 Postgres 集群的缺失拼圖。在此基礎上該版本還帶來了完全類型化的 Worker-to-Worker 綁定無需 codegen、無需手寫接口、無需as強轉(zhuǎn)帶類型化輸入/輸出的 Workflow I/O一行式Alchemy.Secret/Alchemy.VariableCron 觸發(fā)器支持Analytics Engine 綁定R2 桶銷毀時自動清空emptyOnDestroy。其中多項功能來自核心團隊之外的貢獻者Dawson、Michael K、Baptiste Arnaud、Zé Yuri、齊天大圣等完整致謝見文末 Contributors 一節(jié)。對應源碼位于 .repos/alchemy-effect/packages/alchemy/src/Cloudflare/R2、Workers、Workflows、AnalyticsEngine、KV 等模塊與 .repos/alchemy-effect/packages/alchemy/src/Neon/Project、Branch 等模塊。二、跨棧與跨階段引用Cross-stack and cross-stage references2.1 核心概念跨棧/跨階段引用是指惰性lazy、類型化地引用由另一個 stack 或 stage 部署的資源。它的典型設計用例是臨時性的 PR 預覽階段需要一個 Neon 項目但不應自掏腰包創(chuàng)建一個新的而應共享一個由staging階段擁有的項目。從實現(xiàn)上看兩種引用形態(tài)最終都落到 Output.ts 與 Resource.ts 中對state store狀態(tài)存儲的讀取——Output.stackRef與Resource.ref是底層原語Neon.Project.ref、yield* Backend等新 API 只是讓它們更符合人體工程學。2.2 把 PR 階段指向staging的數(shù)據(jù)庫在同一個alchemy.run.ts中根據(jù) stage 條件分支若 stage 形如 PR 預覽pr-147、pr-148……則用Neon.Project.ref深入staging階段的 state file取回已部署的 Neon 項目否則該階段創(chuàng)建自己的項目。// src/Db.ts import * as Alchemy from alchemy; import * as Drizzle from alchemy/Drizzle; import * as Neon from alchemy/Neon; import * as Effect from effect/Effect; export const NeonDb Effect.gen(function* () { const { stage } yield* Alchemy.Stack; const schema yield* Drizzle.Schema(app-schema, { schema: ./src/schema.ts, out: ./migrations, }); // PR previews share the long-lived staging project. // Every other stage gets its own. const project stage.startsWith(pr-) ? yield* Neon.Project.ref(app-db, { stage: staging }) : yield* Neon.Project(app-db, { region: aws-us-east-1 }); // Branches are cheap and per-stage either way. const branch yield* Neon.Branch(app-branch, { project, migrationsDir: schema.out, }); return { project, branch, schema }; });上述代碼是發(fā)布說明中的簡化版。倉庫中的完整示例位于 .repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Db.ts注意真實示例將引用階段寫為stage: \staging-${stage}并把Neon.Branch的遷移源直接接到 schema 資源上migrations: schema注釋中還點明了 provider 的執(zhí)行順序Drizzle.Schema重新生成待執(zhí)行的遷移 SQL 文件Neon.Branch掃描該目錄并以事務方式應用新遷移。兩者結(jié)合可以看作發(fā)布說明示例的生產(chǎn)版。這段代碼有三個要點相同的邏輯 id、相同的類型app-db與staging創(chuàng)建項目時使用的 id 一致無論走哪條分支project都是Neon.Project類型因此下游Neon.Branch({ project })根本不知道也不關心它是真實創(chuàng)建還是引用而來。在 plan 階段解析Alchemy 從staging持久化的 state store對應倉庫源碼目錄中讀取項目的屬性id、host 等。若staging尚未部署plan 會以InvalidReferenceError大聲失敗——這是一個刻意的快速失敗設計避免把引用了一個不存在的資源悄悄帶到云端。PR 銷毀范圍被約束alchemy destroy --stage pr-147只刪除該 PR 的Neon.Branch不會觸碰共享項目——因為這個 stage 并不擁有它。部署時只需先部署一次staging之后所有 PR 階段都可以指向它alchemy deploy --stage staging # creates the project once alchemy deploy --stage pr-147 # references it, creates only the branch2.3 引用整個棧的輸出Monorepo 場景上面是跨階段引用單個資源的形態(tài)另一種形態(tài)是拉取整個棧的輸出——在 monorepo 中當 frontend 包想讀取 backend 棧部署出來的 URL 時就用這種形態(tài)。首先聲明一次類型化的棧句柄// backend/src/Stack.ts import * as Alchemy from alchemy; export class Backend extends Alchemy.Stack Backend, { url: string } ()(Backend) {}用Backend.make(...)部署 backend即Alchemy.Stack的類型化簡寫然后在 frontend 的棧里yield* Backend即可拿回它的輸出全程類型檢查// frontend/alchemy.run.ts import * as Alchemy from alchemy; import * as Cloudflare from alchemy/Cloudflare; import { Backend } from backend; import * as Effect from effect/Effect; export default Alchemy.Stack( Frontend, { providers: Cloudflare.providers(), state: Cloudflare.state() }, Effect.gen(function* () { // Resolves Backends outputs from the same stage of the same // stack name. pr-42 frontend reads pr-42 backend. const backend yield* Backend; // ^? { url: string } return yield* Cloudflare.Website.Vite(Website, { env: { VITE_API_URL: backend.url }, }); }), );yield* Backend默認解析為與消費方相同的 stage。當你需要固定指向某個具體階段時——例如生產(chǎn) frontend 無論由哪個分支部署都始終讀取生產(chǎn) backend——使用Backend.stage.nameconst backend yield* Backend.stage.prod; // always pin to prod const backend yield* Backend.stage[pr-42]; // arbitrary stage nameyield* Backend的默認行為同 stage 同棧名讓pr-42的 frontend 自然讀到pr-42的 backend這正是 PR 預覽環(huán)境的理想語義。三、Alchemy.Secret與Alchemy.Variable一行式環(huán)境變量接線把環(huán)境變量接進部署目標是棧里最無聊的部分過去要散落在三個文件里。現(xiàn)在一次yield就把它壓縮成一行而且這行同時還是一個類型化的運行時訪問器。// alchemy.run.ts — declare once on the Worker export default Cloudflare.Worker(Api, { main: import.meta.filename }, Effect.gen(function* () { const apiKey yield* Alchemy.Secret(OPENAI_API_KEY); // ^? OutputRedactedstring return { fetch: Effect.gen(function* () { // …and read the bound value inside the handler. const key yield* apiKey; // Redactedstring return HttpServerResponse.text( key has ${Redacted.value(key).length} chars, ); }), }; }), );Alchemy.Variable與Secret形狀相同只是沒有Redacted包裹const port yield* Alchemy.Variable(PORT, 3000); const flags yield* Alchemy.Variable(FLAGS, { beta: true }); // inside fetch const p yield* port; // number — 3000 const f yield* flags; // { beta: true }兩者的參數(shù)形態(tài)都很靈活可以接受一個字面量、一個Effect、一個Config或者默認從活躍ConfigProvider中按同名讀取值。同一個調(diào)用會路由到平臺原生的 secret/variable 綁定——Cloudflare 上是secret_textAWS 上是 Lambda 加密環(huán)境變量——而運行時訪問器會把值解碼回原始類型。也就是說聲明與讀取共用同一個句柄聲明處決定了平臺的綁定形態(tài)讀取處決定了類型。四、Worker-to-Worker 綁定全類型化的三種調(diào)用形態(tài)現(xiàn)在一個 Worker 可以綁定到另一個 Worker 上作為 binding且調(diào)用方在另一端獲得完整的 RPC 類型——不需要 codegen、不需要手寫接口、不需要對env做as強轉(zhuǎn)。三種調(diào)用形態(tài)都在同一次部署上可用。以發(fā)布說明中的示例為例一個BackendWorker 同時暴露一個 RPC 方法與一個 HTTP 路由另有一個 TanStack Start 前端用三種不同方式調(diào)用它對應倉庫中的 .repos/alchemy-effect/examples/cloudflare-website-tanstack-start/ 示例項目文中代碼做了裁剪。后端 Worker// src/backend.ts import * as Cloudflare from alchemy/Cloudflare; import * as Effect from effect/Effect; import { HttpServerRequest } from effect/unstable/http/HttpServerRequest; import * as HttpServerResponse from effect/unstable/http/HttpServerResponse; export const Bucket Cloudflare.R2.Bucket(Bucket); export default class Backend extends Cloudflare.WorkerBackend()( Backend, { main: import.meta.filename }, Effect.gen(function* () { const bucket yield* Cloudflare.R2.ReadWriteBucket(Bucket); return { // RPC method — callable via backend.hello(key) on the other side. hello: Effect.fn(Backend.hello)(function* (key: string) { const object yield* bucket.get(key); return object null ? null : yield* object.text(); }), // HTTP handler — callable via env.BACKEND.fetch(...). fetch: Effect.gen(function* () { const request yield* HttpServerRequest; const key new URL(request.url, http://backend).searchParams.get(key); if (!key) return HttpServerResponse.text(missing key, { status: 400 }); if (request.method GET) { const object yield* bucket.get(key); return object null ? HttpServerResponse.text(not found, { status: 404 }) : HttpServerResponse.stream(object.body); } return HttpServerResponse.text(method not allowed, { status: 405 }); }), }; }).pipe(Effect.provide(Cloudflare.R2.ReadWriteBucketBinding)), ) {}把它作為 binding 接到另一個 Worker 上// alchemy.run.ts import Backend, { Bucket } from ./src/backend.ts; export const Website Cloudflare.Website.Vite(Website, { bindings: { BUCKET: Bucket, // R2 binding BACKEND: Backend, // Worker-to-Worker binding }, });現(xiàn)在調(diào)用方有三種與后端對話的方式三者都真實可用、都帶類型、都能在同一個 handler 里混用——按調(diào)用點需求挑選即可// frontend route handler import * as Cloudflare from alchemy/Cloudflare; import type Backend from ../backend.ts; import { env } from ../env.ts; // Option 1 — async binding (just call the platform API directly). const object await env.BUCKET.get(key); // Option 2 — Worker-to-Worker fetch over the service binding. const res await env.BACKEND.fetch(https://backend/?key${encodeURIComponent(key)}); // Option 3 — typed RPC. toPromiseApiBackend wraps the wire-shape // binding into a PromiseT view that throws on Effect.fail and // unwraps stream envelopes — full method signatures from Backend. const backend Cloudflare.toPromiseApiBackend(env.BACKEND); const value await backend.hello(key); // ^? string | null (typed end-to-end)Effect 原生的調(diào)用方還有第四條路——yield* Backend.bind(env.BACKEND)返回同樣的 RPC 表面但沒有 Promise 信封。各形態(tài)的取舍很清晰Option 1異步綁定直接調(diào)用平臺 API適合對象存儲這類原生接口Option 2service binding fetch需要請求/響應語義、特別是流式 body時用這個Option 3類型化 RPC想要類型化方法調(diào)用時用這個toPromiseApiBackend會把線上形態(tài)包裝成PromiseT視圖Effect.fail會拋異常流信封會被解包。五、Workflows類型化輸入與輸出Cloudflare.Workflow現(xiàn)在對輸入和輸出類型都是泛型。函數(shù)體是一個直接接收類型化輸入的Effect.fnworkflow.create(input)端到端類型檢查返回值一路流到instance.status().output。發(fā)布說明給了一個非常貼近實戰(zhàn)的例子——一個通知型 workflow觸碰 KV、讀取Alchemy.Secret、通過 Durable Object 廣播、sleep、最后收尾。每個副作用都用task包裹這樣崩潰 重放時會返回持久化結(jié)果而不是重新執(zhí)行// src/NotifyWorkflow.ts import * as Alchemy from alchemy; import * as Cloudflare from alchemy/Cloudflare; import * as Effect from effect/Effect; import * as Redacted from effect/Redacted; import { KV } from ./KV.ts; import Room from ./Room.ts; export default class NotifyWorkflow extends Cloudflare.WorkflowNotifyWorkflow()( Notifier, Effect.gen(function* () { // Outer init phase: resolve shared dependencies once. const rooms yield* Room; const kv yield* Cloudflare.KV.ReadWriteNamespace(KV); const secret yield* Alchemy.Secret(WORKFLOW_SECRET); return Effect.fn(function* (input: { roomId: string; message: string }) { const { roomId, message } input; // Each task is a checkpoint — replay-safe. const stored yield* Cloudflare.Workflows.task(kv-roundtrip, Effect.gen(function* () { const key notify:${roomId}; yield* kv.put(key, message); return (yield* kv.get(key)) ?? message; }).pipe(Effect.orDie), ); const value Redacted.value(yield* secret); const processed yield* Cloudflare.Workflows.task(process, Effect.succeed({ text: Processed: ${stored}, secret: value }), ); yield* Cloudflare.Workflows.task(broadcast, rooms.getByName(roomId).broadcast([workflow] ${processed.text}), ); yield* Cloudflare.Workflows.sleep(cooldown, 2 seconds); yield* Cloudflare.Workflows.task(finalize, rooms.getByName(roomId).broadcast([workflow] complete for ${roomId}), ); return processed; }); }), ) {}這個例子值得細讀它展示了 workflow 的完整編程模型外層 init 階段只解析一次共享依賴rooms、KV 命名空間、Alchemy.Secret隨后返回一個Effect.fn作為 workflow 主體每個Cloudflare.Workflows.task(名稱, ...)都是一個檢查點checkpoint——崩潰重放時直接返回持久化結(jié)果Redacted.value負責把 secret 解包為可用的字符串值Cloudflare.Workflows.sleep(cooldown, 2 seconds)在 workflow 內(nèi)部休眠這是被持久化的虛擬時間不是阻塞真實線程。從 Worker 中啟動它——create針對輸入形狀做類型檢查instance.status()報告類型化輸出// inside a Workers fetch handler const notifier yield* NotifyWorkflow; const instance yield* notifier.create({ roomId: room-42, message: hello, }); // instance.id: string const status yield* (yield* notifier.get(instance.id)).status(); // status.output: { text: string; secret: string } | undefined六、Cron 觸發(fā)器Cloudflare.Workers.cron(...)訂閱一個 Cloudflare Cron Trigger并掛上 Effect handler。部署時的一半負責把 cron 表達式掛到宿主 Worker 上運行時的一半負責注冊scheduled監(jiān)聽器。它可以與任何已接好的 binding 并用——handler 運行在同一個 Worker 上下文中。export default Cloudflare.Worker(Reporter, { main: import.meta.filename }, Effect.gen(function* () { const kv yield* Cloudflare.KV.ReadWriteNamespace(Counters); // Fires once at the top of every hour. yield* Cloudflare.Workers.cron(0 * * * *).subscribe((controller) Effect.gen(function* () { yield* kv.put(tick:${controller.scheduledTime}, ok); yield* Effect.log(tick at ${new Date(controller.scheduledTime).toISOString()}); }), ); return { fetch: Effect.succeed(HttpServerResponse.text(ok)) }; }), );幾點注意多次調(diào)用cron(…)會在同一個 Worker 上注冊多個 scheduleCloudflare 提供的最細 cron 粒度是一分鐘* * * * *controller.scheduledTime是觸發(fā)時間戳可直接用于寫 KV key 或日志訂閱句柄.subscribe(handler)的 handler 是 Effect 程序天然融入現(xiàn)有錯誤處理鏈路。七、Analytics Engine 綁定Cloudflare Workers Analytics Engine 現(xiàn)在以**零配置zero-provisioning**的 Worker 綁定形式暴露聲明一個 dataset 資源、在 Worker 上綁定、從 handler 里調(diào)用writeDataPoint——與 Alchemy 其它綁定一樣走同一個 Effect 錯誤通道。// alchemy.run.ts export const Events Cloudflare.AnalyticsEngine.Dataset(Events, { dataset: app-events, }); // inside the Worker export default Cloudflare.Worker(Api, { main: import.meta.filename }, Effect.gen(function* () { const analytics yield* Cloudflare.AnalyticsEngineDataset.bind(Events); return { fetch: Effect.gen(function* () { yield* analytics.writeDataPoint({ indexes: [account-1], // queryable, low-cardinality blobs: [signup], // arbitrary string columns doubles: [1], // numeric metrics }); return HttpServerResponse.text(recorded); }), }; }).pipe(Effect.provide(Cloudflare.AnalyticsEngineDatasetBindingLive)), );writeDataPoint的三個字段語義與平臺保持一致indexes可查詢、低基數(shù)的索引字段如賬號 idblobs任意字符串列如事件名doubles數(shù)值型指標。由于是零供應綁定Analytics Engine 不需要單獨創(chuàng)建基礎設施聲明 dataset 資源即完成了接線。對應實現(xiàn)位于 .repos/alchemy-effect/packages/alchemy/src/Cloudflare/AnalyticsEngine/。八、R2 桶銷毀時自動清空destroy一個Bucket時現(xiàn)在會先排空桶內(nèi)內(nèi)容再刪除桶。這徹底消除了 teardown 期間的BucketNotEmpty失敗——臨時 PR 預覽和集成測試現(xiàn)在只需一次alchemy destroy就能干凈收場const Photos Cloudflare.R2.Bucket(Photos); // alchemy destroy empties Photos and deletes it in one go如果你出于生產(chǎn)環(huán)境的保護意圖想要舊行為桶非空則失敗可以按桶選擇退出const Photos Cloudflare.R2.Bucket(Photos, { emptyOnDestroy: false });這行配置與 R2 模塊源碼.repos/alchemy-effect/packages/alchemy/src/Cloudflare/R2/中的 Bucket 資源選項對應emptyOnDestroy默認值為true。九、值得關注的修復項D1prepare()/bind()現(xiàn)在同步了與上游 Cloudflare Workers API 對齊——瑣碎的語句構(gòu)造不再需要yield*。本地 sidecar 中的 WASM 模塊bun alchemy dev現(xiàn)在能正確地把.wasm模塊打進本地 sidecar修復了一類依賴 WASM 的 Worker 的 module not found 錯誤。未解析的Output做 JS 強制轉(zhuǎn)換時直接拋錯在 Effect 之外誤把未解析的Outputstring用在模板字符串里例如${bucket.bucketName}過去會被靜默強轉(zhuǎn)為[object Output]并帶著垃圾值部署上云現(xiàn)在會直接拋錯。移除廢棄的 libsodium 包裝類型無公開 API 影響但如果你 import 過內(nèi)部類型它們已經(jīng)不存在了。十、如何在當前倉庫中進一步探索如果你想把本文講到的能力落實到自己的基礎設施代碼中可以從以下幾個入口繼續(xù)深入示例工程完整的 Neon Drizzle 示例見 .repos/alchemy-effect/examples/cloudflare-neon-drizzle/src/Db.tsTanStack Start 前后端橋接示例見 .repos/alchemy-effect/examples/cloudflare-website-tanstack-start/。源碼模塊跨棧引用的底層原語在 Output.ts 與 Resource.tsNeon 相關資源在 .repos/alchemy-effect/packages/alchemy/src/Neon/Project.ts、Branch.tsCloudflare 全家桶在 .repos/alchemy-effect/packages/alchemy/src/Cloudflare/其中Workers/、Workflows/、R2/、AnalyticsEngine/、KV/與本版本特性一一對應。變更記錄本版本的完整 CHANGELOG 與上一版本beta.36的對比可在 .repos/alchemy-effect/CHANGELOG.md 中找到。一個實用的落地建議如果你正在為 PR 預覽環(huán)境維護共享數(shù)據(jù)庫先alchemy deploy --stage staging部署一次長生命周期資源再在Db.ts中以stage.startsWith(pr-)為條件切換ref/新建兩條路徑最后用alchemy destroy --stage pr-147驗證銷毀范圍確實被約束在 per-PR 分支上——這正好是本文第二節(jié)所講能力的最小閉環(huán)演練。【免費下載鏈接】t3code項目地址: https://gitcode.com/GitHub_Trending/t3/t3code創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考