mcpField
E
effect-advanced

effect-advanced

github.com/trancong12102开发746 次安装6 周前更新
代码仓库

关于 effect-advanced

effect-advanced 是构建生产级 Effect-TS 应用的规范型技能,聚焦类型化错误、依赖注入、资源作用域、并发及常见陷阱,而非完整 API 文档。

主要功能

  1. 01Effect.gen 业务逻辑规范
  2. 02类型化错误处理模式
  3. 03Service 与 Layer 架构
  4. 04作用域资源管理
  5. 05高级并发 API
  6. 06陷阱与参考指南

适用场景

  1. 01组织 Effect-TS 应用服务
  2. 02处理类型化失败与缺陷
  3. 03组合依赖 Layer
  4. 04管理资源生命周期与并发

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/trancong12102/agentskills --skill effect-advanced

安装后在 Claude Code 中输入/effect-advanced确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
concurrency.md
dependency-injection.md
error-handling.md
platform.md
resource-management.md
schema.md
stream.md
testing.md

详细介绍

Effect 高级用法:模式、约定与陷阱

本技能定义了构建生产级 Effect-TS 应用时应遵循的规则、约定和架构决策。它有意采用明确的立场,以避免常见陷阱并强制应用可扩展的模式。

如需详细的 API 文档,请使用其他合适的工具(文档查询、网络搜索等)——本技能侧重于以惯用方式使用 Effect 的方法和原因,而非完整的 API。

核心约定

使用 Effect.gen 编写业务逻辑

生成器读起来就像同步代码;对于超出简单组合的逻辑,强烈建议使用生成器,而不是冗长的 .pipe / .flatMap 链:

const program = Effect.gen(function* () {
  const config = yield* ConfigService;
  const user = yield* UserRepo.findById(config.userId);
  return user;
});

仅在数据转换流水线和简短的组合器链中使用 pipe。

切勿抛出异常——使用 Effect 的错误通道
不要使用……应使用
throw new Error()Effect.fail(new MyError())
对 Promise 使用 try/catchEffect.tryPromise({ try, catch })
回调 APIEffect.async((resume) => ...)
不可恢复的崩溃Effect.die(defect)
优先使用函数,而非方法

为提升可组合性和摇树优化效果,优先使用 Effect.map(e, f),而不是 e.pipe(Effect.map(f))。对于应用,扁平导入(import { Effect } from "effect")没有问题;对于库,命名空间导入(import * as Effect from "effect/Effect")更合适。

@effect/schema 已弃用

Schema 已合并到核心 effect 中。请直接从 "effect" 导入:

import { Schema } from "effect";
// NOT: import { Schema } from "@effect/schema"
在生产环境中使用 NodeRuntime.runMain

Effect.runPromise 无法妥善处理 SIGINT / SIGTERM:

import { NodeRuntime } from "@effect/platform-node";
NodeRuntime.runMain(program.pipe(Effect.provide(AppLayer)));

错误处理理念

失败与缺陷——根本区别
方面失败(预期)缺陷(意外)
APIEffect.fail(new MyError())Effect.die(new Error())
类型通道在 E 中跟踪永远不会出现在 E 中(never)
恢复方式catchTag、catchAll、retry仅在系统边界处处理
判断原则你打算在调用处处理它错误或不可能的状态
始终使用带标签的错误

普通 Error 或字符串形式的失败无法发挥 Effect 类型化错误通道的价值:

class UserNotFound extends Data.TaggedError("UserNotFound")<{
  readonly id: string;
}> {}

// Tagged errors are yieldable — no Effect.fail wrapper needed
const program = Effect.gen(function* () {
  const user = yield* db.findUser(id);
  if (!user) yield* new UserNotFound({ id });
  return user;
});
catchAll 不会捕获缺陷

这是错误处理中最常见的错误:

Effect.catchAll(program, handler); // catches E only — NOT defects
Effect.catchAllCause(program, handler); // catches everything (E + defects + interrupts)

仅在系统边界处使用 catchAllCause / catchAllDefect(顶层错误处理器、HTTP 响应映射器)。


依赖注入架构

Service → Layer → Provide(一次即可)
1. Define services with Context.Tag  →  "what do I need?"
2. Implement via Layers              →  "how is it built?"
3. Provide once at entry point       →  "wire it all together"
Service 方法的 R 必须为 never

依赖应属于 Layer 组合,而非方法签名:

// WRONG: leaks dependency to callers
findById: (id: string) => Effect.Effect<User, UserNotFound, Database>;

// RIGHT: Database is wired in the Layer
findById: (id: string) => Effect.Effect<User, UserNotFound>;
Layer 组合——了解这些运算符
操作使用场景行为
Layer.merge(A, B)独立服务并发构建二者
Layer.provide(downstream, upstream)A 为 B 提供依赖先构建 upstream
Layer.fresh(layer)强制创建新实例绕过记忆化

重要:Layer.merge 不会按顺序构建。如果 B 依赖 A,请使用 Layer.provide,不要使用 Layer.merge。

在入口点调用一次 Effect.provide

分散的 provide 调用会造成隐藏依赖和 Layer 重复:

// WRONG: provide scattered throughout codebase
const getUser = UserRepo.findById(id).pipe(Effect.provide(DbLayer));

// RIGHT: compose and provide once
const main = program.pipe(Effect.provide(AppLayer));
NodeRuntime.runMain(main);

资源与作用域规则

使用 acquireRelease 时必须使用 Effect.scoped

忘记使用 Effect.scoped 是资源管理中最常见的陷阱——资源会不断累积,直到程序退出:

// WRONG: scope never closes, connection leaks
const result = yield * getDbConnection;

// RIGHT: scope closes when block completes
const result =
  yield *
  Effect.scoped(
    Effect.gen(function* () {
      const conn = yield* getDbConnection;
      return yield* conn.query("SELECT 1");
    }),
  );
发布终结器始终会运行

无论成功、失败还是中断,都保证运行。终结器会接收 Exit 值,以便有条件地执行清理。

在一个作用域中管理多个资源
Effect.scoped(
  Effect.gen(function* () {
    const conn = yield* Effect.acquireRelease(openConn(), closeConn);
    const file = yield* Effect.acquireRelease(openFile(), closeFile);
    // both released when scope closes, in REVERSE acquisition order
  }),
);

并发模型

优先使用高级 API,而非原始 fork
API使用场景
Effect.all([], { concurrency: N })有界并行执行
Effect.forEach(items, fn, { concurrency: N })工作池模式
Effect.race(a, b)谁先完成就采用谁,其他任务会被中断
Effect.timeout(e, dur)为任意效果设置截止时间

只有在高级 API 无法满足需求时,才使用 Effect.fork / Fiber。

Fork 变体 — 了解其生命周期
函数作用域清理方式
Effect.fork父级作用域随父级自动中断
Effect.forkDaemon全局作用域不会自动清理 — 必须手动清理
Effect.forkScoped最近的 Scope与资源生命周期绑定

注意: 如果忘记中断 forkDaemon,会导致 fiber 泄漏。


常见陷阱

  1. 未执行的效果 — 创建 Effect 却不 yield 或运行它,是一种静默错误。在生成器中调用 Effect.log("msg"),除非使用 yield*,否则什么也不会发生。

  2. catchAll 无法捕获缺陷 — 在系统边界使用 catchAllCause,以完整了解失败情况。

  3. 缺少 Effect.scoped — 不设置作用域边界就使用 acquireRelease,会导致资源一直泄漏到程序退出。

  4. 零散使用 Effect.provide — 组合所有层,并在入口点统一提供。

  5. 对重载函数使用 Point-free 写法 — Effect.map(myOverloadedFn) 会静默擦除泛型。请使用显式 lambda:Effect.map((x) => myOverloadedFn(x))。

  6. 多次调用 Effect.async 的 resume — resume 必须且只能调用一次。多次调用会导致未定义行为。

  7. orDie 会掩盖错误 — 它会将类型化失败转换为非类型化缺陷。应当妥善处理错误。

  8. 用 Layer.merge 处理有依赖关系的服务 — merge 不会按顺序构造。某个层需要另一个层的输出时,应使用 Layer.provide。

  9. Fiber.join 与 Fiber.await — 在某些边界情况下,join 可能导致终结器过早执行。涉及资源安全时,优先使用 await。

  10. 对无限流使用 runCollect — 如果之前没有调用 take,绝不要调用它。操作永远不会终止,还会消耗无界内存。

  11. 在作用域测试中使用 it.effect — 需要 Scope 的效果必须使用 it.scoped,而不是 it.effect,否则会出现类型错误。


参考文件

处理特定问题时,请阅读相关参考文件:

文件阅读时机
references/error-handling.md带标签的错误、Cause、缺陷恢复、错误映射模式
references/dependency-injection.md服务、Layers、组合、记忆化、provide 模式
references/concurrency.mdFibers、fork 变体、Deferred、Semaphore、结构化并发
references/resource-management.mdScope、acquireRelease、Layer 资源、fork 与 scope 的交互
references/schema.mdSchema 定义、转换、品牌类型、递归 Schema
references/stream.mdStream 运算符、分块、背压、带资源的 Stream
references/testing.md@effect/vitest、TestClock、Layer 模拟、Config 模拟
references/platform.mdHTTP 客户端、FileSystem、Command、运行时、框架集成

评论

支持 Markdown