Brass TS — 在 TypeScript 中构建 Effect Runtime(第 4 部分)
Source: Dev.to
TL;DR
brass-http 不是 fetch 的包装器。
它是一个基于拥有 fiber、结构化并发和取消功能的运行时之上的 effectful HTTP client。
在之前的章节中,我们构建了受 ZIO 启发的 effect runtime 的基础:
- fiber 与调度
- 结构化并发
- 与 JavaScript 生态系统的集成
- 一个具备真实开发体验(DX)的 ZIO‑style HTTP 客户端
在本篇第四部分中,我们展示 当 HTTP 被建模为 effect 后会有什么变化——以及为何 重试、取消、流式传输和管道 等特性不再是 hack,而是 运行时属性。
📦 Repository:
Important mental shift
HTTP is not an async function.
HTTP is a description of work.
In Brass:
- nothing executes until interpreted
- retries are scheduled, not looped
- cancellation is structural, not best‑effort
Callout:
Effects delay execution. Promises don’t.
Effect 与 Promise
Promise‑based HTTP
fetch()
└─▶ immediately starts
└─▶ cannot be stopped
└─▶ side‑effects escape
Effect‑based HTTP (Brass)
HTTP Effect
│
│ (pure description)
▼
Interpreter (toPromise / Fiber / Stream)
│
▼
Runtime Scheduler
│
▼
Execution
创建一个具有效果的 HTTP 客户端
const http = httpClient({
baseUrl: "https://jsonplaceholder.typicode.com",
}).withRetry({
maxRetries: 3,
baseDelayMs: 200,
maxDelayMs: 2000,
});
🚫 尚未执行任何请求。
✅ 您仅仅描述了:
- 请求是如何构建的
- 重试的行为方式
- 错误是如何分类的
- 所有内容是如何与运行时调度器集成的
运行效果:toPromise
const effect = http.getJson("/posts/1");
const promise = toPromise(effect, {});
const result = await promise;
提示:
getJson不返回Promise。它返回一个Effect。
toPromise只是 一种解释器。
重试作为一等概念
const http = httpClient({ baseUrl }).withRetry({
maxRetries: 3,
retryOnStatus: (s) => s >= 500,
retryOnError: (e) => e._tag === "FetchError",
});
这不是包装器。
Retry:
- 不是
while循环或try/catch + setTimeout - 不是 Promise 递归
💡 重试是效果描述的一部分。
重试生命周期
HTTP Effect
│
▼
Attempt #1 ──┐
│ │
│ error │ schedule
▼ │ delay
Retry Policy │ (scheduler)
│ │
▼ │
Attempt #2 ──┘
│
▼
Attempt #N
│
▼
Success / Failure
提示:
重试是计划的,而不是循环的。
实际有效的取消
取消一个 effect 会取消所有内容。
多亏了 fibers 和结构化并发,取消单个 fiber 会在整个 HTTP 生命周期中传播。
Fiber
│
├─▶ HTTP Effect
│ ├─▶ fetch
│ ├─▶ retry delay
│ └─▶ response decode
│
└─▶ other effects
Cancel Fiber
│
▼
╳ fetch aborted
╳ retry timers cleared
╳ decode stopped
提示:
取消是结构性的,而不是尽力而为的。
原始 Wire 响应(完全控制)
const wire = await toPromise(http.get("/posts/1"), {});
console.log(wire.status);
console.log(wire.bodyText);
提示:
即使是原始 Wire 响应也支持重试、取消和调度。
Requests are data (optics FTW)
const req = mergeHeaders({ accept: "application/json" })(
setHeaderIfMissing("content-type", "application/json")({
method: "POST",
url: "/posts",
body: JSON.stringify({
userId: 1,
title: "Hello Brass",
body: "Testing POST from Brass HTTP client",
}),
})
);
const response = await http.request(req).toPromise({});
Base Request
│
▼
mergeHeaders
│
▼
setHeaderIfMissing
│
▼
Final Request
│
▼
http.request(effect)
Callout:
请求是值,而不是动作。
管道:HTTP 作为一系列效果的流
Request
│
▼
[ Enrich ]
│
▼
[ Retry Policy ]
│
▼
[ Fetch ]
│
▼
[ Decode ]
│
▼
Response / Wire
流式传输(设计就绪)
Producer (HTTP Body)
│
▼
Stream Effect
│
├─▶ Consumer A
│
└─▶ Consumer B
Cancel
│
▼
Stream stops
Callout:
流是副作用,而不是回调。
为什么这很重要?
Promises:
async + hope
Effects:
describe → schedule → execute → control
因为一旦 HTTP 成为一种 effect:
- 重试不再脆弱
- 取消变得可预测
- 测试变得轻而易举
- 组合变得自然
接下来
- 完整流式 API
- 将超时视为 effect
- 追踪
- 指标
- 资源作用域