Brass TS — 在 TypeScript 中构建 Effect Runtime(第 4 部分)

发布: (2026年1月10日 GMT+8 23:23)
5 min read
原文: Dev.to

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
  • 追踪
  • 指标
  • 资源作用域
Back to Blog

相关文章

阅读更多 »

在 Nest.js 中设置 MonoRepo

Monorepos 与 Nest.js Monorepos 正在成为管理多个服务或共享库的后端团队的默认选择。Nest.js 的表现非常出色……