Show HN: Pure Effect – 无需数据库,在笔记本上复现生产环境bug

Hacker News Top 工具

摘要

Pure Effect 是一个零依赖的 JavaScript/TypeScript 效应库,通过将副作用表示为纯数据来分离业务逻辑与 I/O,无需数据库即可复现生产环境 bug。

Hi HN,<p>我想可以说大多数开发者在编写代码时不会多想I/O与业务逻辑交织的问题。太常见了,比如看到这样的代码:const user = findUser(email); if (!user) await saveUser(user);<p>现在你可能会问:这有什么大不了的?当我们这样写代码时,会发生两件事:<p>1. 调试生产bug变得更困难。除非你有完全相同的数据库和远程API服务,否则可能无法复现bug。<p>2. 你必须在测试中使用模拟和假冒,或者使用测试容器,这些只能部分解决问题,而且速度慢!<p>为了解决这些问题,我构建了Pure Effect,一个微型的TypeScript/JavaScript效应库。核心思想很简单:如果一个函数执行I/O,它就不是纯函数。但如果它返回一个对想要执行的I/O的描述,那它就是纯的。因此,不要使用 await findUser(email),而是返回一个Command对象,它说:“我想调用这个函数,当它完成后,接下来要做这些。”你的业务逻辑变成了纯函数。相同的输入,相同的输出,每次如此。在解释器(runEffect)运行之前,数据库永远不会被触及。<p>当我开始这个库时,我没想到这个简单的想法能延伸多远。一旦你的管道只是数据,许多美好的事情就变得可能:<p>- 不再需要模拟库。你在测试中遍历树并断言其结构:assert.equal(flow.cmd.name, 'cmdFindUser')。没有任何东西被执行。<p>- 用 Retry(effect, { attempts: 3, delay: 200, backoff: 2 }) 包装任何效应。配置是纯数据,所以你可以在测试中断言它。<p>- 每个命令的输入和输出都流经解释器,因此你免费获得完整的执行跟踪。你可以编写一个简单的 timeTravel() 函数,在本地重放它,而不触及任何I/O。非常适合调试复杂生产bug。<p>- onBeforeCommand 钩子位于业务逻辑和解释器之间。由于它在每个预期的副作用触发之前都能看到它,因此可用于强制执行运行时防护。例如,你可以在破坏性调用发生之前隔离它们。<p>- 你可以在AI生成的代码运行之前审查它。因为 Pure Effect 管道是纯数据,你可以在它接触任何东西之前检查生成的代码打算做什么。<p>只有六个原语:Success, Failure, Command, Ask, Retry, Parallel,加上 effectPipe 和 runEffect。零依赖。压缩后不足1 KB。<p>与 Effect-TS 的对比<p>Effect-TS 是这个领域中功能齐全的选项,拥有庞大的生态系统。Pure Effect 提供了不同的权衡。它涵盖了80%的用例:可测试的管道、依赖注入、重试和 OpenTelemetry 钩子,全部在1 KB之内,零依赖,无需学习新的词汇。Effect-TS 是一个你需要围绕它构建的框架。而 Pure Effect 是一个你可以嵌入现有代码的模式。<p>我从12月起就在生产环境中使用 Pure Effect。目前是 v0.8.0,还不是1.0,但足够稳定,我想把它推出来听听大家的意见。<p>GitHub: <a href="https://github.com/aycangulez/pure-effect" rel="nofollow">https://github.com/aycangulez/pure-effect</a><p>我写了五篇文章记录了 Pure Effect 的演变过程。它们标签在 <a href="https://lackofimagination.org/tags/effect/" rel="nofollow">https://lackofimagination.org/tags/effect/</a>,如果你想看更长的故事。
查看原文
查看缓存全文

缓存时间: 2026/06/24 16:53

# 业务逻辑即普通数据 来源:https://pure-effect.org/ ## 在*你的笔记本*上复现生产环境bug,无需数据库。 Pure Effect 是一个零依赖的 JavaScript 和 TypeScript 效果库。你的业务逻辑返回普通对象来描述它将执行的 I/O,而不是直接执行 I/O。你可以在测试中读取这些对象,或者从失败的生产运行中重放它们,在解释器运行它们之前,永远不会触及数据库。 $npm install pure-effect ## 它在你的机器上能跑。它在生产环境中崩了。你无法复现。 原因通常都一样:业务逻辑和 I/O 纠缠在一起。当你写 `await db.findUser(email)` 时,调用会立即在逻辑中间触发。因此,测试只能通过让 I/O 也发生(针对 mock、fake 或容器)来检查发生了什么。而当生产环境失败时,你只有一个堆栈跟踪,因为请求实际发起的调用从未被捕获以供重放。 ### async / await:I/O 即逻辑 `` // 调用立即在逻辑中间触发。 async function registerUser(input) { const found = await db.findUser(input.email); if (found) throw new Error('Email in use'); return db.saveUser(input); } // 要测试它你必须运行它。当它在 // 生产环境失败时,没有任何东西被记录下来以供重放。 `` 你通过**执行它**来检查行为。失败的运行没有留下任何可以逐步追踪的痕迹。 ### pure-effect:逻辑将 I/O 作为数据返回 `` // 先读取它将要做什么。什么都没运行。 const flow = registerUserFlow(input); assert.equal(flow.cmd.name, 'cmdFindUser'); // 传入生产环境看到的结果,并沿着完全相同的路径 // 行走,无需触碰数据库。 const next = flow.next(recordedUser); assert.equal(next.cmd.name, 'cmdSaveUser'); `` 你通过**读取树**来检查它。同样的调用可以在生产环境中记录下来,然后在这里重放,无需任何基础设施。 ## 六个构件。一个下午就能学会。 每个 Effect 都是以下形状之一。它们组合成树,由解释器在系统的边界处遍历。 i. #### Success(value) OK 一个成功的计算结果。返回 `{ type: 'Success', value }`。任何管道步骤都可以返回一个来供给下一步。 ii. #### Failure(error) ERR 立即停止管道,短路剩余的步骤。可选的 `initialInput` 会被保留用于诊断。 iii. #### Command(cmd, next) 延迟的 I/O 一个以数据形式描述的副作用。`cmd` 是*将要*运行的函数;`next` 将其结果转换为下一个 Effect。 iv. #### Ask(next) 上下文 · 依赖注入 读取传递给 `runEffect` 的 `context` 对象(如租户、请求 ID 和配置),无需将其贯穿每个函数签名。 v. #### Retry(effect, opts) 韧性 用失败重试逻辑包装任何 Effect。配置 `attempts`、`delay`、`backoff`。当重试耗尽时:返回结构化的 Failure。 vi. #### Parallel(effects, next) 并发 通过 `Promise.all` 并发运行 Effect 树。Ask 上下文自动流入每个分支。第一个 Failure 会短路整个并行。 再加上组合器:**effectPipe(...fns)**:顺序执行 **runEffect(effect, ctx?)**:解释器 **configureEffect(opts)**:全局钩子 完整示例:用户注册流程 `` import { Success, Failure, Command, effectPipe, runEffect } from 'pure-effect'; // 纯函数,无 I/O,无导入,可立即测试。 function validateRegistration(input) { if (!input.email?.includes('@')) return Failure('Invalid email.'); if (input.password?.length < 8) return Failure('Password too short.'); return Success(input); } function ensureEmailAvailable(found) { return found ? Failure('Email already in use.') : Success(true); } // 命令:以数据形式描述的副作用,由解释器执行。 function findUserByEmail(email) { const cmdFindUser = () => db.findUserByEmail(email); return Command(cmdFindUser, (found) => Success(found)); } function saveUser(input) { const cmdSaveUser = () => db.saveUser(input); return Command(cmdSaveUser, (saved) => Success(saved)); } // 管道:组合成一个单一流程。 const registerUserFlow = (input) => effectPipe( validateRegistration, () => findUserByEmail(input.email), ensureEmailAvailable, () => saveUser(input) )(input); // 测试:对结构进行断言,无需 mock,无需 I/O。 const flow = registerUserFlow({ email: '[email protected]', password: 'password123' }); assert.equal(flow.cmd.name, 'cmdFindUser'); assert.equal(flow.next(null).cmd.name, 'cmdSaveUser'); // 运行:在边界处将树交给解释器。 const saved = await runEffect(registerUserFlow(input), { flowName: 'register' }); `` ## 开箱即用的功能。 无需数据库即可测试 ### 断言你的代码将要做什么。 管道返回惰性对象。遍历树并检查每一步:无需 mock、无需内存中的 fake、无需容器。 assert.equal(step.cmd.name, 'cmdFindUser'); assert.equal(step.type, 'Command'); 重放生产运行 ### 在本地逐步追踪失败的请求。 记录生产环境中每个命令返回的内容,然后将这些结果传回同一棵树,重走确切的路径,无需任何基础设施。 let step = flow; while (step.type === 'Command') step = step.next(recorded[step.cmd.name]); // 重放记录的路径,无 I/O 以数据形式重试 ### 测试韧性而无需等待。 用重试语义包装任何 Effect。因为配置是普通数据,测试可以直接对其进行断言:无需定时器、无需休眠、无脆弱的时序。 Retry(effect, { attempts: 3, delay: 200, backoff: 2 }); // 200·400·800 并行执行 ### 并发分支,有序结果。 `Promise.all` 语义,第一个失败即短路。Ask 上下文自动流入每个分支。 Parallel( [getUser(id), getPerms(id)], ([user, perms]) => Success({ user, perms }) ); 依赖注入 ### 上下文不污染函数签名。 从框架层解析租户、跟踪 ID 或配置。领域函数保持干净,只需 Ask。 Ask((ctx) => { // ctx.tenant 来自 runEffect return Command(...); }); OpenTelemetry ### 用于追踪的生命周期钩子。 `onRun`、`onStep` 和 `onBeforeCommand` 让你在不触及领域代码的情况下用 span 包装工作流。 configureEffect({ onRun: (eff, pipeline, name) => tracer.startActiveSpan(name, pipeline) }); ## 它适合哪里,不适合哪里。 Pure Effect 将一个有限操作描述为在运行之前就可以读取的树。这是它的优势,也是它的边界:它适用于请求形状的操作,而非后台进程。 库 描述 选择 pure-effect 如果…… Effect-TS 一个完整的功能性生态系统,包含纤程、流式处理、schema 验证和结构化并发。功能强大,学习曲线陡峭。 ……你需要可以在一个下午学会的可测试管道、重试和依赖注入。 fp-ts 将范畴论抽象(如 functor、monad 和 applicative)引入 TypeScript。在带来效果的同时也引入了大量词汇。 ……你想要效果即数据,但不需要学术词汇。 async/await + mocks 默认方案。对于小规模场景简单。当测试隔离变得重要时会出问题:mock 与真实驱动脱节,测试变成表演。 ……测试隔离是你代码库中的一个真正痛点。 ## AI 编写流程。你在它运行之前读取它。 AI 代码生成器输出 async/await:要看它做了什么你需要运行它,每次检查都会触及基础设施。因为 Pure Effect 流程是普通数据,你可以在任何东西执行之前读取它的控制流:它发出哪些命令、顺序是什么、走哪个分支。 ### async / await:运行以验证 `` // AI 处理了错误路径吗? // 它正确传递了上下文吗? // 你不运行它就不会知道。 async function registerUser(input) { const found = await db.findUser(input.email); if (found) throw new Error('Email in use'); return db.saveUser(input); // 有 await 吗?谁知道呢。 } `` 你通过执行来审计。**每次验证都会触及基础设施。** ### pure-effect:读取以验证 `` // AI 生成了这个流程。在它运行之前检查它。 const flow = registerUserFlow(input); assert.equal(flow.type, 'Command'); assert.equal(flow.cmd.name, 'cmdFindUser'); const step2 = flow.next(null); assert.equal(step2.cmd.name, 'cmdSaveUser'); // 控制流已确认。没有任何东西被执行。 `` 你通过读取树来审计。**无需数据库,无需网络。** 这确认了生成代码的**形状**,而不是它的正确性,但它可以在任何东西运行之前排除错误的代码路径。 **06**/ 安装 ## 六个原语。零依赖。*在你运行之前就能看到代码将要做什么。* $npm install pure-effect

相似文章

代数效应:给普通人的解释

Hacker News Top

这是一篇教育性博客文章,通过类比 try/catch 和 async/await 来解释编程中的代数效应概念,并讨论了它们与 React 及未来编程范式的潜在关联。