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. 05MCPサーバーのリクエストスコープ構成

インストール

コマンドをコピーしてターミナルで実行します。

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

インストール後、Claude Code で/effect-tsを入力して読み込まれたか確認します。

詳細

Effect-TS エキスパートガイド

Effect-TS は、型付きエフェクト、構造化並行処理、堅牢なランタイムを提供する関数型 TypeScript ライブラリです。このスキルでは、正しい使用パターンを解説し、LLM が生成したコンテンツによく見られる誤解に対処します。

クイックリファレンス

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

よくある誤解

LLM の出力には、誤った 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 以降)

エラーハンドリング:2 種類のエラー

Effect は次の 2 種類のエラーを区別します。

  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 を使用してください
チームが関数型プログラミングに不慣れ学習曲線が急なため、NestJS を検討してください
バンドルサイズが重要Effect により、最低でも 15~25kb の gzip 圧縮後サイズが追加されます
既存の NestJS/TypeORM コードベースクラスベースの依存性注入とはインピーダンスミスマッチがあります

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;
    })
  );

リソース

コメント