Show HN: Pure Effect – 无需数据库,在笔记本上复现生产环境bug
摘要
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
相似文章
代数效应:给普通人的解释
这是一篇教育性博客文章,通过类比 try/catch 和 async/await 来解释编程中的代数效应概念,并讨论了它们与 React 及未来编程范式的潜在关联。
Show HN: Mochi.js:专为 Bun 原生开发的高保真浏览器自动化库
Mochi.js 是一个新的开源浏览器自动化库,专为 Bun 运行时原生构建,旨在通过关系一致性、原生 Chromium 获取和行为合成来绕过检测机制。
Show HN: Nub – 类似 Bun 的用于 Node.js 的一体化工具包
Nub 是一个快速的一体化工具包,用于 Node.js,提供类似 Bun 的开发者体验,包括运行 TypeScript 文件、管理依赖项和 Node 版本,所有这些都集中在一个用 Rust 编写的 CLI 工具中。
我们狠狠撞上了重试问题,干脆开源了一个解决方案
Replaysafe 是一个开源的 npm 库,通过对操作进行指纹识别来确保幂等重试,防止 AI 智能体工作流中出现重复的副作用。它集成了 LangGraph、CrewAI 等流行框架。
Show HN: OpenGravity – 一款零安装、BYOK 的 Vanilla JS 版 Antigravity 克隆
OpenGravity 是 Google Antigravity 的零安装 Vanilla JS 克隆版,支持使用 Gemini 模型和 WebContainer API 进行 BYOK 代理式编码。旨在绕过速率限制,它提供了一个基于浏览器的 IDE,具备自主任务编排和本地文件同步功能。