缓存时间:
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