尧图精选

effect 中 Stream.scoped 与 Channel.scoped 的 Pull Effect 作用域语义解析

🕒 发布时间:2026/9/15 12:52:43 📁 来源:尧图网络
effect 中 Stream.scoped 与 Channel.scoped 的 Pull Effect 作用域语义解析【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect本篇技术指南围绕 effect 仓库.changeset/pre/fix-stream-scoped-scope.md中记录的补丁patch变更展开深入解析Stream.scoped与Channel.scoped这两个资源管理构造器的实现原理为什么经过它们包装后的 pull effects 必须运行在 scoped resource scope 中底层通过fromTransformBracket与Scope.provide(forkedScope)如何保证 finalizer 一定会被执行。读完本文你将掌握 effect 作用域Scope在流式Stream/Channel场景下的传递机制能够正确理解并排查类似资源未释放或作用域未生效的问题。变更记录解读一次针对作用域语义的 patchfix-stream-scoped-scope.md是 effect 仓库采用 changeset 规范生成的预发布变更记录其正文只有一句话FixStream.scopedandChannel.scopedso pull effects run with the scoped resource scope.frontmatter 声明effect: patch表示该变更属于 effect 包即仓库根目录下 packages/effect的补丁级patch修复——不破坏现有 API 签名只修正行为语义。拆解这句话可以得到两个关键信息修复对象Stream.scoped与Channel.scoped两个 API修复内容让pull effects流的拉取效果即驱动流产生元素的实际Effect运行在scoped resource scope被作用域化管理的资源作用域中。也就是说在修复之前这两个 API 包装的流/通道在拉取元素时其内部的 effect 可能运行在错误的作用域中导致对Scope有依赖的效果例如Effect.acquireRelease创建的资源无法正确注册 finalizer。修复后的语义是pull effects 与整个作用域化资源的生命周期绑定。前置知识Scope 与 Pull Effect要理解这次修复先要明确两个基础概念。Scope作用域effect 中管理资源生命周期的核心抽象。它通过 Scope.ts 提供可以注册 finalizer并在作用域关闭时统一执行清理逻辑。Scope.provideScope.ts#L310-L313将一个具体的Scope实例注入到 effect 的R环境需求中从而消解该 effect 对Scope.Scope的类型依赖export const provide: { (value: Scope): A, E, R(self: EffectA, E, R) EffectA, E, ExcludeR, Scope A, E, R(self: EffectA, E, R, value: Scope): EffectA, E, ExcludeR, Scope }Pull Effect拉取效果Channel/Stream 的底层执行模型。一个通道Channel本质上是一系列Pull的序列每次拉取pull都是一个返回下一个输出块的Effect。Channel.fromTransformChannel.ts#L284-L302允许把通道表示为一个函数(upstream, scope) EffectPull其中scope就是驱动该通道执行的作用域。关键点在于pull effects 是否能访问到正确的 scope直接决定了通道执行过程中创建的资源能否被正确回收。Stream.scoped把流的 Scope 需求本地化Stream.scoped定义在 Stream.ts#L1675-L1677export const scoped A, E, R( self: StreamA, E, R ): StreamA, E, ExcludeR, Scope.Scope fromChannel(Channel.scoped(self.channel))它的语义结合 Stream.ts#L1646-L1674 的文档注释输入一个运行在Scope环境中的流StreamA, E, R其环境需求R包含Scope.Scope输出一个不再需要外部Scope的流StreamA, E, ExcludeR, Scope.Scope行为在流执行期间自己创建一个受管作用域流内的acquireRelease等资源在该作用域内注册 finalizer流完成或失败时作用域自动关闭finalizer 全部执行。仓库文档注释中的示例Stream.ts#L1650-L1670直观展示了这一行为import { Effect, Stream } from effect const events: Arraystring [] const stream Stream.scoped( Stream.fromEffect( Effect.acquireRelease( Effect.sync(() { events.push(acquire) return resource }), () Effect.sync(() events.push(release)) ) ) ) await Effect.runPromise(Stream.runCollect(stream)) // [resource] events // [acquire, release]Stream.fromEffect构造的流需要Scope环境因为它内部会创建acquireRelease资源经Stream.scoped包装后即可直接runCollect且 collect 结束时release必然执行。实现上Stream.scoped并不是自己处理作用域而是把任务委托给Channel.scopedfromChannel(Channel.scoped(self.channel))。流的底层就是通道因此真正的作用域管理逻辑全部集中在 Channel 层——这也正是本次修复同时涉及两个 API 的原因。Channel.scoped底层作用域修复的核心Channel.scoped定义在 Channel.ts#L6910-L6918export const scoped OutElem, OutErr, OutDone, InElem, InErr, InDone, Env( self: ChannelOutElem, OutErr, OutDone, InElem, InErr, InDone, Env ): ChannelOutElem, OutErr, OutDone, InElem, InErr, InDone, ExcludeEnv, Scope.Scope fromTransformBracket((upstream, scope, forkedScope) Effect.map( Scope.provide(toTransform(self)(upstream, scope), forkedScope), Scope.provide(forkedScope) ) )这里有两层Scope.provide是理解本次修复的关键Scope.provide(toTransform(self)(upstream, scope), forkedScope)把原通道的 transform 函数即其全部 pull effects的Scope需求用forkedScope填充。这正是 changeset 中所说的pull effects run with the scoped resource scope——修复的核心就是确保这一步将正确的作用域提供给 pull effectsScope.provide(forkedScope)把返回的 pull 本身也置于 forkedScope 中使每次拉取都在作用域保护下执行。fromTransformBracketChannel.ts#L400-L418是这一机制的承载者其实现要点如下完整实现见 Channel.ts#L407-L418fromTransform( Effect.fnUntraced(function*(upstream, scope) { const closableScope Scope.forkUnsafe(scope) const onCause (cause) Scope.close(closableScope, Pull.doneExitFromCause(cause)) const pull yield* Effect.onError( f(upstream, scope, closableScope), onCause ) return Effect.onError(pull, onCause) }) )其工作流程可以归纳为Scope.forkUnsafe(scope)从通道执行作用域派生出一个可单独关闭的子作用域closableScope即Channel.scoped中的forkedScope注册错误回调onCause无论是构造 pull 的阶段出错还是后续每次拉取pull出错都会关闭closableScope并把错误转换为Cause.done传播给下游——保证任何失败路径下资源都会被释放返回受保护的 pull通过Effect.onError(pull, onCause)把错误处理绑定到每次拉取上确保拉取过程中抛出的错误同样触发作用域关闭。由此可以推断本次修复的实质在修复之前Channel.scoped包装后的通道在拉取元素时其 pull effects 缺少Scope.provide(..., forkedScope)这一层注入导致 pull effects 中的Scope需求得不到满足或退回到错误的默认作用域acquireRelease创建的资源无法把 finalizer 注册到受管作用域中出现资源泄漏或过早释放。修复后pull effects 与 scoped resource scope 严格绑定。测试验证作用域确实被提供给 pull effects仓库测试 Stream.test.ts#L521-L536 直接验证了本次修复对应的行为——Stream.scoped必须为fromEffect的 pull effect 提供作用域it.effect(scoped - provides scope to fromEffect pull effects, () Effect.gen(function*() { const releases yield* Ref.make(0) const result yield* Stream.fromEffect( Effect.acquireRelease( Effect.succeed(resource), () Ref.update(releases, (n) n 1) ) ).pipe( Stream.scoped, Stream.runCollect ) assert.deepStrictEqual(result, [resource]) assert.strictEqual(yield* Ref.get(releases), 1) }))断言的关键点runCollect完成后releases计数器为1——即acquireRelease的 finalizer 恰好执行了一次。如果 pull effects 没有运行在 scoped resource scope 中finalizer 要么不会执行计数为 0要么执行时机错误。第二组测试 Stream.test.ts#L538-L554 覆盖了更复杂的顺序场景mapEffect为每个元素创建资源经Stream.scoped包装后两个元素的 finalizer 都必须执行计数为2且元素值正确映射it.effect(scoped - provides scope to sequential mapEffect pull effects, () Effect.gen(function*() { const releases yield* Ref.make(0) const result yield* Stream.fromIterable([1, 2]).pipe( Stream.mapEffect((n) Effect.acquireRelease( Effect.succeed(n * 2), () Ref.update(releases, (count) count 1) ) ), Stream.scoped, Stream.runCollect ) assert.deepStrictEqual(result, [2, 4]) assert.strictEqual(yield* Ref.get(releases), 2) }))这两个用例从侧面印证了修复目标每个元素级别的 pull effect 都被纳入作用域管理无论资源是在流的源头fromEffect创建还是在流的变换阶段mapEffect创建。修复带来的行为影响与实践建议综合 changeset 记录、源码实现与测试用例本次 patch 修复的最终行为可以总结为Stream.scoped(stream)返回的流不再要求外部提供Scope其内部所有 pull effects包括fromEffect、mapEffect、acquireRelease等涉及资源创建的路径都在 scoped resource scope 中运行流执行结束无论成功、失败还是被中断时 finalizer 都会执行Channel.scoped(channel)作为底层实现通过fromTransformBracketScope.forkUnsafeScope.provide(forkedScope)建立拉取即受管的资源生命周期错误路径通过Effect.onError与Scope.close兜底类型层面两者的返回类型都从环境需求中Exclude掉Scope.Scope即调用方无需再关心作用域资源安全由 API 内部保证。对于使用 effect 的开发者以下实践建议值得注意资源型流应优先使用Stream.scoped包装凡流内部出现Effect.acquireRelease、Scope.addFinalizer等资源逻辑应在流定义处或使用处用Stream.scoped封闭作用域避免把Scope需求泄漏到整个程序入口区分Stream.scoped与Stream.unwrap两者都处理Scope环境但语义不同——Stream.scoped包装的是一个已经构建好的流Stream.ts#L1675-L1677而Stream.unwrapStream.ts#L1642-L1644将产生流的 effect展开为流unwrap同样通过Channel.unwrapChannel.ts#L6888-L6901内部的Scope.provide(scope)为通道 effect 提供作用域理解二者的底层委托关系有助于定位作用域类问题排查资源泄漏时关注 pull 层如果在Stream.scoped包装后仍观察到 finalizer 未执行可以从 pull effects 是否真正获得forkedScope入手检查——这正是本次修复针对的薄弱环节。该变更位于.changeset/pre/目录属于 effect 包的预发布变更集结合 pre.json 的 pre 模式管理意味着该修复会在 effect 包的下一个补丁版本中正式生效。升级到包含该修复的版本后建议运行上述两个测试场景fromEffect与mapEffect的资源释放做一次回归验证确认作用域语义符合预期。【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
上一篇/下一篇内容由系统自动关联 返回资讯列表 →