为什么 Effect 应用只该 provide 一次 Layer?

在 Effect.all 里并列写两个 Effect.provide(useDb, DbLive),同一个 Layer 被构建了两次,得到的是两个不同的实例。

应该:先把依赖图组合成一个 Layer,应用入口只出现一次 Effect.provide(AppLive)。 嵌套 provide 的自动复用是安全网,不是设计手段,它只覆盖一种形态,稍微换一下代码结构,行为就变了。

背景是这样的:effect v4 里,嵌套的 Effect.provide 会复用外层已构建的 Layer,兄弟 provide 则各自构建,细节和计数器输出见 Effect v4 里同一个 Layer 会被构建几次。下面讲三个理由,以及怎么把「构建次数」变成可断言的测试。

自动复用为什么不能当设计手段?

三个理由,都能从计数器的实测推出来。

  • 兄弟不共享。 Effect.all 里并列两个 provide 就是两份构建。自动复用只覆盖「内层在外层作用域里」这一种形态。
  • 隐式泄漏。 外层藏在内部的 Layer 也会被内层复用,依赖图在类型上看不出来,只有读源码或加计数器才知道。
  • 生命周期。 构建两次意味着资源的 acquire 与 release 也各发生两次。对连接池、文件句柄这类资源型 Layer,这就是多出一份资源。这一条是我从构建次数推出来的,没有拿真实连接池测过。

组合一次则没有这些问题:依赖图在一处显式写出,构建次数等于图里的节点数。下面这段是基线,两次使用共用一次构建:

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

class Db extends Context.Service<Db, { readonly id: number }>()("Db") {}

let builds = 0
const DbLive = Layer.effect(Db, Effect.sync(() => ({ id: ++builds })))
const useDb = Effect.gen(function*() {
  const db = yield* Db
  return db.id
})

const ids = await Effect.runPromise(Effect.provide(Effect.all([useDb, useDb]), DbLive))
console.log("provide once", ids, "builds", builds)

输出是 provide once [ 1, 1 ] builds 1。多个 Layer 怎么组合成一个,见 Effect 的 provide 和 provideMerge 有什么区别。

这条规则怎么落成 review 习惯?

迁移文档的说法是,自动记忆化是安全网,不是组合的替代品。我同意,并把它当成 review 规则:一个应用入口只出现一次 Effect.provide(AppLive)。运行时入口该怎么组织,见 Effect 运行时边界与测试。

怎么用计数器 Layer 做回归测试?

构建次数是可以断言的行为,不必靠肉眼。做法就是上面的计数器模式:给关键资源 Layer 加一个构建计数,在测试里组装应用 Layer,断言 builds === 1。以后有人在中间加了一层 Effect.provide,或者把 Layer 包进了 Layer.fresh,测试会第一时间失败。

版本是 effect 4.0.5。构建次数的输出是我用计数器 Layer 在 Effect.runPromise 的单次运行里跑出来的;「安全网,不是组合的替代品」来自我读到的迁移文档,只读过。「回归测试断言 builds === 1」是我从计数器模式推出来的做法,没有搭一个完整的测试工程验证。资源 acquire 与 release 的次数、HttpRouter、LayerMap 等模块的复用边界都没有实测。