Effect 事务 API 重构解析:Effect.tx / Effect.txRetry 与原子事务组合(effect-smol)
2026/9/15 3:02:30 网站建设 项目流程

Effect 事务 API 重构解析:Effect.tx / Effect.txRetry 与原子事务组合(effect-smol)

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

本指南以 effect-smol 仓库中remove-effect-transactionwith.md变更说明为主线,深入解析 Effect 事务(Transaction)API 的重新命名与架构调整:Effect.transaction更名为Effect.txEffect.retryTransaction更名为Effect.txRetry,移除Effect.transactionWith/Effect.withTxState,并让嵌套Effect.tx调用自动组合进当前活动事务。读完本文,你将掌握新事务 API 的调用方式、嵌套组合与乐观重试的底层机制,并理解Tx*系列 API 如何在常见场景中无需显式Transaction依赖即可建立原子事务。

变更背景与目标

在 effect-smol(Effect 库的零依赖、精简实现)中,事务(transaction)是协调多个共享可变状态(如TxRef)的核心机制,保证对事务值的修改“要么全部提交、要么全部回滚”(all or nothing)。本次变更(见 changeset 原文)是一次面向可用性的 API 重构,包含四个核心动作:

  1. 重命名Effect.transactionEffect.txEffect.retryTransactionEffect.txRetry
  2. 移除Effect.transactionWithEffect.withTxState两个 API 被彻底删除;
  3. 嵌套组合:嵌套的Effect.tx调用不再创建新的事务边界,而是组合进当前活动事务;
  4. 默认原子性:公共的Tx*API(如TxRefTxQueueTxPubSub等)在常见用法中能够自动建立原子事务,无需显式提供Transaction服务。

该变更被标记为patch级别(见 changeset/config.json),并已记录在 packages/effect/CHANGELOG.md 中,由 mikearnaldi 提交(PR #1898)。

核心 API:Effect.tx 定义事务边界

Effect.tx是重构后定义事务边界的主要入口,签名如下(源码见 packages/effect/src/Effect.ts):

export const tx = <A, E, R>( effect: Effect<A, E, R> ): Effect<A, E, Exclude<R, Transaction>> =>

它接收一个Effect作为事务体,返回的 Effect 在类型层面将Transaction服务需求从环境R中剔除(Exclude<R, Transaction>),即调用方无需再关心事务状态服务是否可用。

嵌套组合语义

tx在内部通过withFiber检查当前 Fiber 上下文中是否已存在Transaction状态:

let state = Context.getOrUndefined(fiber.context, Transaction) if (state) { return effect as Effect<A, E, Exclude<R, Transaction>> } // 仅在最外层边界创建事务状态 state = { journal: new Map(), retry: false }
  • 如果已经处于活动事务中:直接返回原始 effect,复用现有事务的 journal(日志)与 retry 状态,不创建新的嵌套边界;
  • 如果是最外层:创建全新的{ journal: new Map(), retry: false }状态,并把整个循环包裹在uninterruptibleMask中,确保提交/回滚过程不可中断。

这一实现正是 changeset 中所说的“make nestedEffect.txcalls compose into the active transaction”的源码证据:嵌套调用是组合(compose)而非嵌套提交,最外层的tx调用负责统一提交或回滚整个组合事务。

提交与回滚流程

最外层tx通过whileLoop循环执行事务体,直到得到确定结果:

step(exit: Exit.Exit<A, E>) { if (state.retry || !isTransactionConsistent(state)) { return clearTransaction(state) // 需要重试或冲突:清空日志 } if (Exit.isSuccess(exit)) { commitTransaction(fiber, state) // 成功:统一提交 } else { clearTransaction(state) // 失败:回滚(丢弃日志) } result = exit }

关键辅助函数:

  • isTransactionConsistent(Effect.ts):遍历 journal 中记录的每个TxRef,比对ref.version !== version。任一事务值在事务期间被其他事务修改过,即判定为冲突;
  • commitTransaction(Effect.ts):对 journal 中值发生变化的TxRef递增version并写回新值,随后调度唤醒所有pending等待者;
  • clearTransaction(Effect.ts):重置retry标志并清空 journal,相当于回滚。

乐观并发与重试:Effect.txRetry

Effect 的事务采用乐观并发 + 重试模型。事务在以下两种情况下会重试:

  1. 事务体显式调用Effect.txRetry,且任何被访问的事务值发生了变化;
  2. 事务体执行期间,某个被访问的事务值因其他事务先提交而发生变化(版本不一致)。

txRetry的实现非常简洁(Effect.ts):

export const txRetry: Effect<never, never, Transaction> = flatMap( Transaction, (state) => { state.retry = true return interrupt } )

它读取Transaction服务,将state.retry置为true,然后通过interrupt中断当前事务体执行。最外层txwhileLoop检测到state.retry后:

  • 若当前被访问的事务值已变化,则通过awaitPendingTransaction(Effect.ts)挂起等待——在 journal 中所有被访问的TxRef上注册pending回调,任一 ref 被其他事务提交时唤醒,重新执行整个事务体;
  • 若事务值未变化,则直接重新执行事务体。

典型使用示例

从源码 docstring 中可以看到标准用法(Effect.ts):当读取到旧值时,先触发外部更新,再请求重试,最终返回最新值:

import { Deferred, Effect, TxRef } from "effect" const program = Effect.gen(function*() { const ref = yield* TxRef.make(0) const update = yield* Deferred.make<void>() yield* Effect.forkChild( Deferred.await(update).pipe(Effect.andThen(Effect.tx(TxRef.set(ref, 1)))) ) return yield* Effect.tx(Effect.gen(function*() { const value = yield* TxRef.get(ref) if (value === 0) { yield* Deferred.succeed(update, undefined) return yield* Effect.txRetry } return value })) }) await Effect.runPromise(program) // => 1

嵌套事务组合的实践

源码 docstring 给出了嵌套组合的直接演示(Effect.ts):

import { Effect, TxRef } from "effect" const output: Array<unknown> = [] const program = Effect.gen(function*() { const ref1 = yield* TxRef.make(0) const ref2 = yield* TxRef.make(0) // 嵌套 tx 调用组合进同一个事务 yield* Effect.tx(Effect.gen(function*() { yield* TxRef.set(ref1, 10) yield* Effect.tx(TxRef.set(ref2, 20)) // 内层:复用当前事务,不嵌套提交 const sum = (yield* TxRef.get(ref1)) + (yield* TxRef.get(ref2)) void output.push(`Transaction sum: ${sum}`) })) void output.push(`Final ref1: ${yield* TxRef.get(ref1)}`) void output.push(`Final ref2: ${yield* TxRef.get(ref2)}`) }) Effect.runSync(program) // output => ["Transaction sum: 30", "Final ref1: 10", "Final ref2: 20"]

要点:内层Effect.tx(TxRef.set(ref2, 20))并没有创建独立事务,而是直接复用外层事务的 journal,因此ref1ref2的修改会在最外层tx结束时一次性原子提交。如果内层嵌套创建独立边界,将无法保证这两个修改的原子性。

Tx* API 的自动原子事务

changeset 的最后一点是“make the publicTx*APIs establish atomic transactions without requiringTransactionin common usage”。以TxRef为例,其modify实现(packages/effect/src/TxRef.ts)充分体现了这一设计:

export const modify = dual(2, <A, R>( self: TxRef<A>, f: (current: A) => [returnValue: R, newValue: A] ): Effect.Effect<R> => Effect.Transaction.pipe( Effect.flatMap((state) => Effect.sync(() => { if (!state.journal.has(self)) { state.journal.set(self, { version: self.version, value: self.value }) } const current = state.journal.get(self)! const [returnValue, next] = f(current.value) current.value = next return returnValue }) ), Effect.tx // 自动建立事务边界 ))

分析:

  • 读取Transaction服务状态(journal),如果该TxRef尚未记录,则记录当前的versionvalue快照;
  • 在 journal 中执行f,实现读-改-写;
  • 最后通过Effect.tx包裹——当调用方未处于任何事务中时,tx自动创建一个原子事务边界并提交;当调用方已在事务中时,tx复用现有事务。

因此,在常见用法中直接调用TxRef.get/TxRef.set/TxRef.modify等 API 即可获得原子性,而无需像旧 API 那样手动搭建事务状态。TxChunkTxDeferredTxPriorityQueueTxPubSubTxQueueTxReentrantLockTxRefTxSemaphore等事务集合(见 packages/effect/src 目录)均遵循这一模式。

Transaction 服务模型

事务状态的载体是Transaction服务类(Effect.ts),其结构如下:

export class Transaction extends Context.Service< Transaction, { retry: boolean readonly journal: Map< TxRef<any>, { readonly version: number value: any } > } >()("effect/Effect/Transaction") {}
  • journal:记录事务访问过的每个TxRef及其进入事务时的version快照和事务内修改的value,是冲突检测与提交的根基;
  • retry:布尔标志,由txRetry置位,驱动事务体的重试循环。

Transaction仍可被显式获取(yield* Effect.Transaction),用于需要直接操纵事务状态的高级场景,例如在测试中手动提供事务服务:

const runnable = Effect.provideService(txEffect, Effect.Transaction, { retry: false, journal: new Map() }) Effect.runSync(runnable) // => "Transaction complete"

迁移指南:从旧 API 到新 API

根据 changeset 与 CHANGELOG 的说明,迁移时只需进行机械替换:

旧 API(已移除/重命名)新 API
Effect.transaction(effect)Effect.tx(effect)
Effect.retryTransactionEffect.txRetry
Effect.transactionWith已移除,改用Effect.tx(嵌套组合自动生效)
Effect.withTxState已移除,改用Transaction服务直接访问

注意事项:

  1. transactionWith/withTxState在源码中已完全不存在,直接搜索 packages/effect/src 无任何匹配,说明旧 API 已彻底删除而非弃用(deprecated);
  2. 嵌套事务语义发生变化:旧 API 下嵌套可能产生独立事务,新 API 下嵌套tx总是组合进当前活动事务,迁移后需确认原代码是否依赖旧的嵌套提交行为;
  3. 如果代码显式传递Transaction服务,类型层面tx的返回值已剔除Transaction环境需求(Exclude<R, Transaction>),通常可以简化调用链。

小结

本次重构把 Effect 事务 API 收敛为三个清晰的概念:

  • Effect.tx:唯一的事务边界入口,最外层创建并提交事务,嵌套调用自动组合;
  • Effect.txRetry:显式请求重试,配合乐观并发模型与版本冲突检测实现一致性;
  • Tx*API 自动原子性TxRef等事务集合在常见用法下自动建立原子事务,降低使用门槛。

对于正在使用 effect-smol 或 Effect 生态的开发者,本文对应的源码证据(Effect.ts、TxRef.ts、CHANGELOG.md)可作为深入阅读的起点,理解乐观事务、journal 快照与版本冲突检测的完整实现。

【免费下载链接】t3code项目地址: https://gitcode.com/GitHub_Trending/t3/t3code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询