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.tx、Effect.retryTransaction更名为Effect.txRetry,移除Effect.transactionWith/Effect.withTxState,并让嵌套Effect.tx调用自动组合进当前活动事务。读完本文,你将掌握新事务 API 的调用方式、嵌套组合与乐观重试的底层机制,并理解Tx*系列 API 如何在常见场景中无需显式Transaction依赖即可建立原子事务。
变更背景与目标
在 effect-smol(Effect 库的零依赖、精简实现)中,事务(transaction)是协调多个共享可变状态(如TxRef)的核心机制,保证对事务值的修改“要么全部提交、要么全部回滚”(all or nothing)。本次变更(见 changeset 原文)是一次面向可用性的 API 重构,包含四个核心动作:
- 重命名:
Effect.transaction→Effect.tx,Effect.retryTransaction→Effect.txRetry; - 移除:
Effect.transactionWith、Effect.withTxState两个 API 被彻底删除; - 嵌套组合:嵌套的
Effect.tx调用不再创建新的事务边界,而是组合进当前活动事务; - 默认原子性:公共的
Tx*API(如TxRef、TxQueue、TxPubSub等)在常见用法中能够自动建立原子事务,无需显式提供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 的事务采用乐观并发 + 重试模型。事务在以下两种情况下会重试:
- 事务体显式调用
Effect.txRetry,且任何被访问的事务值发生了变化; - 事务体执行期间,某个被访问的事务值因其他事务先提交而发生变化(版本不一致)。
txRetry的实现非常简洁(Effect.ts):
export const txRetry: Effect<never, never, Transaction> = flatMap( Transaction, (state) => { state.retry = true return interrupt } )它读取Transaction服务,将state.retry置为true,然后通过interrupt中断当前事务体执行。最外层tx的whileLoop检测到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,因此ref1、ref2的修改会在最外层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尚未记录,则记录当前的version与value快照; - 在 journal 中执行
f,实现读-改-写; - 最后通过
Effect.tx包裹——当调用方未处于任何事务中时,tx自动创建一个原子事务边界并提交;当调用方已在事务中时,tx复用现有事务。
因此,在常见用法中直接调用TxRef.get/TxRef.set/TxRef.modify等 API 即可获得原子性,而无需像旧 API 那样手动搭建事务状态。TxChunk、TxDeferred、TxPriorityQueue、TxPubSub、TxQueue、TxReentrantLock、TxRef、TxSemaphore等事务集合(见 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.retryTransaction | Effect.txRetry |
Effect.transactionWith | 已移除,改用Effect.tx(嵌套组合自动生效) |
Effect.withTxState | 已移除,改用Transaction服务直接访问 |
注意事项:
transactionWith/withTxState在源码中已完全不存在,直接搜索 packages/effect/src 无任何匹配,说明旧 API 已彻底删除而非弃用(deprecated);- 嵌套事务语义发生变化:旧 API 下嵌套可能产生独立事务,新 API 下嵌套
tx总是组合进当前活动事务,迁移后需确认原代码是否依赖旧的嵌套提交行为; - 如果代码显式传递
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),仅供参考