mcpField
E
effect-ts

effect-ts

github.com/kastalien-research开发9288 次安装42 周前更新
代码仓库

关于 effect-ts

面向使用 Effect-TS 构建 TypeScript 程序的开发者,讲解类型化 effect、并发、资源管理及常见 API 用法。指南还涵盖 MCP 服务的请求处理模式,并指出 AI 生成代码中容易出现的误用。

主要功能

  1. 01类型化错误与缺陷处理
  2. 02Fiber 并发与中断
  3. 03Layer 依赖注入
  4. 04作用域资源管理
  5. 05缓存、重试与调度
  6. 06MCP 请求处理示例

适用场景

  1. 01设计可处理类型化错误的程序
  2. 02管理并发任务及其生命周期
  3. 03确保连接和文件等资源释放
  4. 04实现重试、缓存和可观测性
  5. 05为 MCP 服务组织请求作用域

安装

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

$npx skills add https://github.com/kastalien-research/thoughtbox-dot-claude --skill effect-ts

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

详细介绍

Effect-TS 专家指南

Effect-TS 是一个函数式 TypeScript 库,提供带类型的效果、结构化并发和强大的运行时。本技能介绍正确的使用模式,并纠正大模型生成内容中的常见误解。

快速参考

import { Effect, Layer, Context, Fiber, Schedule, Cache, Scope } from "effect";
import { Schema, JSONSchema } from "@effect/schema";

核心类型签名:

Effect<Success, Error, Requirements>
//      ↑        ↑       ↑
//      |        |       └── Dependencies (provided via Layers)
//      |        └── Expected errors (typed, must be handled)
//      └── Success value

常见误解

大模型的输出常常包含错误的 API。 请使用此表进行纠正:

错误写法(AI 输出中常见)正确写法
Effect.cachedWithTTL(...)Cache.make({ timeToLive: Duration })
Effect.cachedInvalidateWithTTL(...)cache.invalidate(key) / cache.invalidateAll()
Effect.match(...)Effect.either + Either.match,或 Effect.catchTag
“线程本地存储”通过 FiberRef 实现的“纤程本地存储”
JSON Schema Draft 2020-12@effect/schema 生成 Draft-07
纤程被“取消”纤程被“终止”或“中断”
所有队列都有背压只有有界队列有背压;滑动队列和丢弃队列没有
--only=production--omit=dev(npm 7+)

错误处理:两种错误类型

Effect 区分:

  1. 预期错误(失败) - 在 E 通道中带有类型,必须处理
  2. 意外错误(缺陷) - 运行时错误,会被捕获,但不带类型
// Expected error - typed
const fetchUser = (id: string): Effect.Effect<User, UserNotFoundError | NetworkError> => ...

// Handle expected errors
const handled = pipe(
  fetchUser("123"),
  Effect.catchTag("UserNotFoundError", (e) => Effect.succeed(defaultUser)),
  Effect.catchTag("NetworkError", (e) => Effect.retry(schedule))
);

// Unexpected errors (defects) - captured by runtime
Effect.catchAllDefect(program, (defect) =>
  Console.error("Unexpected error", defect)
);

纤程与取消(对 MCP 至关重要)

纤程是轻量级虚拟线程,支持原生中断:

// Fork a fiber
const fiber = yield* Effect.fork(longRunningTask);

// Interrupt it (e.g., when MCP client disconnects)
yield* Fiber.interrupt(fiber);

// Structured concurrency: child fibers auto-terminate with parent
const parent = Effect.gen(function* () {
  yield* Effect.fork(backgroundTask);  // Auto-interrupted when parent ends
  yield* mainTask;
});

// Daemon fibers outlive their parent
yield* Effect.forkDaemon(longLivedBackgroundTask);

并发原语

Effect.race - 先完成者胜出,其余任务会被中断
// First to succeed wins; other is automatically interrupted
const result = yield* Effect.race(
  fetchFromCache,
  fetchFromDatabase
);
带并发控制的 Effect.all
// Process 50 documents with max 5 concurrent
const results = yield* Effect.all(documents.map(processDoc), {
  concurrency: 5  // NOT a "worker pool" - limits concurrent tasks
});
队列类型
// Bounded - applies back-pressure (offer suspends when full)
const bounded = yield* Queue.bounded<string>(100);

// Dropping - discards new items when full (no back-pressure)
const dropping = yield* Queue.dropping<string>(100);

// Sliding - discards oldest items when full (no back-pressure)
const sliding = yield* Queue.sliding<string>(100);

用于依赖注入的 Layer

Layer 用于构造服务,而不会泄漏依赖:

// Define a service
class Database extends Context.Tag("Database")<
  Database,
  { query: (sql: string) => Effect.Effect<Result> }
>() {}

// Create layer (dependencies handled at construction)
const DatabaseLive = Layer.effect(
  Database,
  Effect.gen(function* () {
    const config = yield* Config;  // Dependency injected here
    return {
      query: (sql) => Effect.tryPromise(() => runQuery(sql, config))
    };
  })
);

// Provide to program
const runnable = program.pipe(Effect.provide(DatabaseLive));

// For testing - swap implementation
const DatabaseTest = Layer.succeed(Database, {
  query: () => Effect.succeed(mockResult)
});

资源管理

Effect.ensuring - 始终运行终结器
const program = pipe(
  Effect.tryPromise(() => openConnection()),
  Effect.ensuring(Console.log("Cleanup"))  // Runs on success, failure, OR interrupt
);
acquireUseRelease 模式
const withConnection = Effect.acquireUseRelease(
  Effect.tryPromise(() => db.connect()),     // Acquire
  (conn) => Effect.tryPromise(() => conn.query("SELECT *")),  // Use
  (conn) => Effect.promise(() => conn.close())  // Release (always runs)
);
用于资源生命周期管理的 Scope
Effect.scoped(
  Effect.gen(function* () {
    const file = yield* openFile("data.txt");  // Acquired
    const data = yield* file.read();
    return data;
  })  // File automatically released when scope closes
);

缓存

不存在 Effect.cachedWithTTL。 请使用 Cache 模块:

import { Cache } from "effect";

const cache = yield* Cache.make({
  capacity: 100,
  timeToLive: Duration.minutes(5),
  lookup: (key: string) => fetchExpensiveData(key)
});

// Use the cache
const value = yield* cache.get("my-key");

// Invalidate
yield* cache.invalidate("my-key");
yield* cache.invalidateAll();

使用 Schedule 重试

import { Schedule } from "effect";

// Retry 3 times with exponential backoff
const policy = Schedule.exponential("100 millis").pipe(
  Schedule.intersect(Schedule.recurs(3))
);

const robust = Effect.retry(unstableOperation, policy);

// Retry until condition
const untilSuccess = Effect.retry(operation, {
  until: (err) => err.code === "RATE_LIMITED"
});

Schema 与 JSON Schema

@effect/schema 生成 JSON Schema Draft-07(而不是 2020-12):

import { Schema, JSONSchema } from "@effect/schema";

const User = Schema.Struct({
  id: Schema.String,
  age: Schema.Number.pipe(Schema.positive())
});

// Generate JSON Schema (Draft-07)
const jsonSchema = JSONSchema.make(User);
// { "$schema": "http://json-schema.org/draft-07/schema#", ... }

// Decode (parse)
const user = Schema.decodeUnknownSync(User)(rawData);

// Encode
const json = Schema.encodeSync(User)(user);

可观测性与 OpenTelemetry

Effect 原生集成了 OpenTelemetry:

import { NodeSdk } from "@effect/opentelemetry";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";

// Add tracing to any effect
const traced = Effect.withSpan("processRequest")(myEffect);

// Logging with context
yield* Effect.log("Processing request");
yield* Effect.annotateLogs("requestId", "abc-123");

// FiberRef for fiber-local context propagation
const RequestId = FiberRef.unsafeMake<string>("");
yield* FiberRef.set(RequestId, "req-456");

不适合使用 Effect 的场景

场景建议
简单的 MCP 工具(< 100 LOC)使用 FastMCP 或原生 SDK
团队不熟悉 FP学习曲线陡峭;可考虑 NestJS
包体积至关重要Effect 至少会增加 15-25kb 的 gzip 压缩体积
现有 NestJS/TypeORM 代码库与基于类的 DI 不兼容

MCP 服务模式

带类型错误的工具处理程序
const searchTool = Effect.gen(function* () {
  const args = yield* parseArgs(input);
  const db = yield* Database;
  const results = yield* db.query(args.query);
  return formatResults(results);
}).pipe(
  Effect.catchTag("ParseError", () =>
    Effect.fail({ code: -32602, message: "Invalid params" })
  ),
  Effect.catchTag("DatabaseError", () =>
    Effect.fail({ code: -32603, message: "Internal error" })
  )
);
请求作用域
// Each MCP request gets its own scope
const handleRequest = (request: MCPRequest) =>
  Effect.scoped(
    Effect.gen(function* () {
      // Resources acquired here auto-release when request completes
      const tempFile = yield* createTempFile();
      const result = yield* processRequest(request, tempFile);
      return result;
    })
  );

资源

评论

支持 Markdown