Effect 的 Layer.fresh 和 local 选项有什么区别?
给一个带子依赖的 Layer 套上 Layer.fresh,Repo 和它下面的 Db 都被重新构建了;只给 Repo 套,Db 却仍然共享。同样是「强制重新构建」,范围差了一整层。
区别在决定权放在哪:Layer.fresh(layer) 把「不共享」写在 Layer 值上,任何使用它的地方都拿到独立实例;Effect.provide(layer, { local: true }) 把它写在调用点上,同一个 Layer 值在别处依然共享。 两者都能隔离整棵子依赖树,具体隔离到哪一层,取决于你把它套在哪一层。
先交代背景:在 effect v4 里,嵌套的 Effect.provide 默认会复用外层已经构建好的 Layer,所以才需要这两个开关来要一份独立的实例。复用的规则本身见 Effect v4 里同一个 Layer 会被构建几次。下面先看 local: true,再看 fresh 的粒度。
local: true 做了什么?
它让这一次 provide 使用一张全新的空记忆表,于是内层不再复用外层。同样的嵌套形态,加上 local: true:
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 program = Effect.provide(
Effect.gen(function*() {
const outer = yield* useDb
const inner = yield* Effect.provide(useDb, DbLive, { local: true })
return [outer, inner]
}),
DbLive
)
console.log("nested + local", await Effect.runPromise(program), "builds", builds)输出是 nested + local [ 1, 2 ] builds 2。不加 local: true 的同一段嵌套代码,输出是 [ 1, 1 ]、构建一次。
Layer.fresh 隔离的范围有多大?
我用一个带子依赖的 Layer 数了一遍。RepoOnDb 内含 Db,外层先 provide 一次,内层再 provide 第二次,内层分别用普通的、整体套 fresh 的、只给 Repo 套 fresh 的三种 Layer:
import { Context, Effect, Layer } from "effect"
class Db extends Context.Service<Db, { readonly id: number }>()("Db") {}
class Repo extends Context.Service<Repo, { readonly dbId: number; readonly repoId: number }>()("Repo") {}
let dbBuilds = 0
let repoBuilds = 0
const DbLive = Layer.effect(Db, Effect.sync(() => ({ id: ++dbBuilds })))
const RepoLive = Layer.effect(
Repo,
Effect.gen(function*() {
const db = yield* Db
return { dbId: db.id, repoId: ++repoBuilds }
})
)
const RepoOnDb = RepoLive.pipe(Layer.provide(DbLive))
const describe = Effect.gen(function*() {
const r = yield* Repo
return `repo${r.repoId}/db${r.dbId}`
})
const run = async (label: string, inner: Layer.Layer<Repo>) => {
dbBuilds = 0
repoBuilds = 0
const out = await Effect.runPromise(
Effect.provide(
Effect.gen(function*() {
const a = yield* describe
const b = yield* Effect.provide(describe, inner)
return [a, b]
}),
RepoOnDb
)
)
console.log(label.padEnd(20), out, "db", dbBuilds, "repo", repoBuilds)
}
await run("plain", RepoOnDb)
await run("fresh(whole)", Layer.fresh(RepoOnDb))
await run("fresh(Repo only)", Layer.fresh(RepoLive).pipe(Layer.provide(DbLive)))输出:
plain [ 'repo1/db1', 'repo1/db1' ] db 1 repo 1
fresh(whole) [ 'repo1/db1', 'repo2/db2' ] db 2 repo 2
fresh(Repo only) [ 'repo1/db1', 'repo2/db1' ] db 1 repo 2Layer.fresh(RepoOnDb) 把 Repo 和它下面的 Db 都重新构建了。只对 RepoLive 套 fresh、Db 另行 provide,则只有 Repo 是新的,Db 仍然共享。fresh 的粒度由你把它套在哪一层决定,套在包含子依赖的 Layer 上,子依赖一起被隔离。
什么时候用哪一个?
我自己的做法是这样分的:
- 资源语义上必须独立(两个会话、两个连接池):用
Layer.fresh,让意图写在 Layer 定义旁边,不依赖调用方记得加参数。 - 测试里想让某一次 provide 与外层完全隔离:用
{ local: true },不需要改 Layer 定义。 - 不要为了绕开「看不懂的组合」而用
Layer.fresh,默认共享通常才是想要的。
如果你发现自己到处都要加这两个开关,多半是 provide 的结构有问题,把依赖图先组合好更省事,见 为什么 Effect 应用只该 provide 一次 Layer。
版本是 effect 4.0.5。两段代码和输出都是我用计数器 Layer 在 Effect.runPromise 的单次运行里跑出来的。「不要为了绕开看不懂的组合而用 fresh」这条来自我读到的官方 JSDoc 提示,只读过,没有另外实验。上面「fresh 隔离整棵子依赖树」只验证了 Repo 与 Db 这两层的例子,更深的依赖树没有测。