Effect 怎么接入 Cloudflare Workers?

Cloudflare Workers 里没有进程级的 env:KV、D1、secrets 这些绑定,都是 fetch(request, env) 的参数,每次请求才拿得到。照搬 Express 那套模块级装配,第一步就走不通。

做法是把依赖按生命周期分成两类:与请求无关的静态依赖放进模块级的 ManagedRuntime,与请求绑定的依赖在 fetch 内部做成 Layer 提供给本次请求的程序。 边缘就是每一次 fetch,run 只发生在那里。

之所以要分,是因为同一个 isolate 会被多个请求复用,但 I/O 对象又绑定在发起它的请求上。ManagedRuntime 是什么、为什么要复用,见 Effect 的 ManagedRuntime 是什么?;run 为什么只能放在边缘,见 Effect 程序应该在哪里 run?。下面从分类开始。

哪些依赖放模块级,哪些放请求内?

  • 与请求无关的静态依赖:整个 isolate 构建一次就够,适合放进模块级的 ManagedRuntime。
  • 与请求绑定的依赖:env、请求 id、租户。它们只能在 fetch 里拿到,生命周期就是这一次请求,要在 fetch 内部做成 Layer 或 service,提供给本次请求的程序。

不管哪一类,边界规则不变:模块导入期不读 env、不运行 Effect,只放定义。

只有请求级依赖时怎么写?

最简单的形态,是直接在 fetch 里 provide 再 runPromise,并把 request.signal 传进去,让客户端断开时 fiber 被中断:

import { Context, Effect, Layer } from "effect"

class Greeting extends Context.Service<Greeting, { readonly text: string }>()("app/Greeting") {}

interface Env {
  readonly GREETING: string
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const EnvLive = Layer.succeed(Greeting, Greeting.of({ text: env.GREETING }))
    const program = Greeting.use((g) => Effect.succeed(new Response(g.text)))
    return Effect.runPromise(Effect.provide(program, EnvLive), { signal: request.signal })
  }
}

这个 Layer 只是把已有的值包起来,每个请求构建一次没有代价。

静态依赖很昂贵时怎么办?

一旦静态依赖里有昂贵的东西,比如客户端或连接配置,在 fetch 里 provide 它们就意味着每个请求都重新构建一遍。

那时的做法是模块级 runtime 加每请求取 context 的组合。细节,以及 Layer 构建次数的实测,放在 Effect 跑在 Cloudflare Workers 上 和 Effect Layer 记忆化 里。

Effect.succeed 为什么会在错误的时间求值?

边缘代码里常见一种错误:Effect.succeed(new SomeRuntimeObject())。succeed 的参数是普通值,在构造 Effect 的那一刻就已经求值了,不是运行时才求值。如果这个对象的构造有运行时副作用,或者依赖请求作用域的东西,它就在错误的时间被创建了。

需要「运行时才创建」时,用 Effect.sync(() => new SomeRuntimeObject())。道理和 Effect 是一个值 里讲的惰性一样:描述里放函数,不要放已经算好的值。

本文整理自本站 Effect 教程中关于运行时边界的一章,没有标注对应的 effect 版本号。代码示例沿用该章原样,本次改写没有重新运行,也没有在真实的 Cloudflare 环境里部署验证;Layer 构建次数的实测结论见所链接的文章,这里没有复述。