怎么把回调式 API 包装成 Effect?

fs.readFile(path, cb) 没有返回 Promise,所以 Effect.promise 和 Effect.tryPromise 都无从下手。而且回调里的 err 类型是 Error | null,失败信息本来就不精确。

答案是:用 Effect.callback。 它给你一个 resume,你在回调触发时调用它,传入「最终应该变成什么 Effect」,成功就传 Effect.succeed,失败就传 Effect.fail。

这个写法还带来一个好处:注册函数可以返回一个清理 Effect,在被中断时运行,这就是回调世界里的资源安全。如果你想了解整组构造函数怎么选,见 怎么把已有的同步、Promise 代码包装成 Effect?

Effect.callback 的基本写法是什么?

const readText = (path: string) =>
  Effect.callback<string, FileError>((resume) => {
    readFile(path, "utf8", (err, data) => {
      if (err) resume(Effect.fail(new FileError({ cause: err })))
      else resume(Effect.succeed(data))
    })
  })

注册函数收到一个 resume,在回调触发时调用它。有三个要点:

  • resume 只有第一次调用有效,后续调用会被忽略
  • 注册函数还会收到 AbortSignal 作为第二个参数
  • 注册函数可以返回一个清理 Effect,当这个 Effect 被中断时运行

被中断时怎么清理定时器?

const sleep1s = Effect.callback<void>((resume) => {
  const t = setTimeout(() => resume(Effect.void), 1000)
  return Effect.sync(() => clearTimeout(t)) // 被中断时清掉定时器
})

返回的清理 Effect 在被中断时运行,定时器被清除,不会在后台泄漏。

v3 里的 Effect.async 去哪了?

v3 里这个构造函数叫 Effect.async,在 4.0.5 里已经不存在,对应的名字是 Effect.callback。从 v3 升级时要改名,其他 v3 到 v4 的变化见 Effect v3 到 v4 迁移。

本文依据 effect 4.0.5 的行为整理,Effect.async 在该版本不存在、对应名字为 Effect.callback 也特指这个版本。示例代码没有在本地逐条运行验证。