AGENT SKILL

effect-ts

Use when working with Effect-TS patterns, services, layers, schemas, error handling, streams, concurrency, or writing/refactoring code that imports from 'effect'. Also covers Svelte 5 + Effect integration and conduit's ongoing Effect migration.

License 许可
MIT
所属仓库
dibstern/conduit
最近更新
2026-10-29
Stars
★ 15
Forks
2

01 前言 Front Matter

name
effect-ts
description
Use when working with Effect-TS patterns, services, layers, schemas, error handling, streams, concurrency, or writing/refactoring code that imports from 'effect'. Also covers Svelte 5 + Effect integration and conduit's ongoing Effect migration.

02 资源文件 14

03 SKILL.md 正文

Effect-TS Expert

Expert guidance for functional programming with the Effect library — typed errors, dependency injection, composability, concurrency, and testing patterns. Tailored for the conduit codebase (Svelte 5 + Node.js daemon, mid-migration to Effect).

Prerequisites Check

Before starting any Effect-related work, verify the Effect-TS source code exists at ~/src/personal/.effect.

If missing, stop immediately and inform the user. Clone it before proceeding:

git clone https://github.com/Effect-TS/effect.git ~/src/personal/.effect

Research Strategy

Effect-TS has many ways to accomplish the same task. Proactively research best practices using the Agent tool to spawn research agents when working with Effect patterns, especially for moderate to high complexity tasks.

Research Sources (Priority Order)

  1. Codebase Patterns First — Examine similar patterns in the current project before implementing. If Effect patterns exist in the codebase, follow them for consistency. If no patterns exist, skip this step.

  2. Effect Source Code — For complex type errors, unclear behavior, or implementation details, examine the Effect source at ~/src/personal/.effect/packages/effect/src/. This contains the core Effect logic and modules.

When to Research

HIGH Priority (Always Research):

  • Implementing Services, Layers, or complex dependency injection
  • Error handling with multiple error types or complex error hierarchies
  • Stream-based operations and reactive patterns
  • Resource management with scoped effects and cleanup
  • Concurrent/parallel operations and performance-critical code
  • Testing patterns, especially unfamiliar test scenarios

MEDIUM Priority (Research if Complex):

  • Refactoring imperative code (try-catch, promises) to Effect patterns
  • Adding new service dependencies or restructuring service layers
  • Custom error types or extending existing error hierarchies
  • Integrations with external systems (databases, APIs, third-party services)

Research Approach

  • Spawn multiple concurrent agents when investigating multiple related patterns
  • Focus on canonical, readable, and maintainable solutions over clever optimizations
  • Verify against existing codebase patterns for consistency (if patterns exist)
  • When multiple approaches are possible, research to find the most idiomatic Effect-TS solution

Core Principles

Error Handling

  • Use Effect's typed error system instead of throwing exceptions
  • Define descriptive error types with Data.TaggedError or Schema.TaggedError (serializable)
  • Use Effect.fail, Effect.catchTag, Effect.catchAll for error control flow
  • Categorize: expected rejections, domain errors, defects, interruptions, unknown/foreign
  • See ./references/critical-rules.md for forbidden patterns
  • See ./references/error-handling.md for comprehensive patterns

Dependency Injection

  • Implement DI using Services and Layers
  • Define services with Context.Tag or Effect.Service (simplified)
  • Compose layers with Layer.merge, Layer.provide, Layer.provideMerge
  • Provide layers at the composition root, not scattered through business logic
  • See ./references/layers.md for full patterns

Composability

  • Leverage Effect's composability for complex operations
  • Use appropriate constructors: Effect.succeed, Effect.fail, Effect.tryPromise, Effect.try
  • Apply proper resource management with scoped effects
  • Chain with Effect.flatMap, Effect.map, Effect.tap

Code Quality

  • Write type-safe code that leverages Effect's type system
  • Use Effect.gen for readable sequential code (preferred)
  • Use pipe for simple one-liner transformations
  • Prefer Effect.fn() for named functions — automatic telemetry and better stack traces
  • Implement proper testing patterns using @effect/vitest

Critical Rules

Read ./references/critical-rules.md before writing any Effect code. Key rules:

INEFFECTIVE: try-catch in Effect.gen

Effect failures are returned as exits, not thrown. try-catch inside Effect.gen will NOT catch Effect failures.

// WRONG — catches nothing
Effect.gen(function* () {
  try { const r = yield* someEffect } catch (e) { /* never runs */ }
})

// RIGHT
Effect.gen(function* () {
  const result = yield* Effect.catchTag(someEffect, "MyError", (e) => Effect.succeed(fallback))
})

AVOID: Type assertions (as never, as any, as unknown)

Fix underlying type issues instead. Occasional assertions for poorly-typed external libraries are acceptable if documented.

RECOMMENDED: return yield* for errors

Makes termination explicit and prevents unreachable-code warnings:

Effect.gen(function* () {
  if (bad) {
    return yield* Effect.fail(new MyError({ reason: "bad" }))
  }
  return yield* doWork()
})

Quick Reference

The Effect Type

Effect<Success, Error, Requirements>
//     ^        ^       ^
//     |        |       └── Services/dependencies needed (Context)
//     |        └────────── Typed error channel
//     └─────────────────── Success value type

Creating Effects

Effect.succeed(value)           // Wrap success value
Effect.fail(error)              // Create failed effect
Effect.tryPromise(fn)           // Wrap promise-returning function (rejects → typed error)
Effect.try(fn)                  // Wrap synchronous throwing function
Effect.sync(fn)                 // Wrap synchronous non-throwing function
Effect.promise(fn)              // Wrap promise (rejects → defect — prefer tryPromise)

Composing Effects

Effect.flatMap(effect, fn)      // Chain effects
Effect.map(effect, fn)          // Transform success value
Effect.tap(effect, fn)          // Side effect without changing value
Effect.all([...effects])        // Run effects (concurrency configurable)
Effect.forEach(items, fn)       // Map over items with effects

// Collect ALL errors (not just first)
Effect.all([e1, e2, e3], { mode: "validate" })

// Partial success handling
Effect.partition([e1, e2, e3])  // Returns [failures, successes]

Error Handling

// Define typed errors with Data.TaggedError (preferred)
class UserNotFoundError extends Data.TaggedError("UserNotFoundError")<{
  userId: string
}> {}

// Direct yield of errors in gen (no Effect.fail wrapper needed)
Effect.gen(function* () {
  if (!user) return yield* new UserNotFoundError({ userId })
})

Effect.catchTag(effect, tag, fn) // Handle specific error tag
Effect.catchTags(effect, { ... }) // Handle multiple tags
Effect.catchAll(effect, fn)      // Handle all errors
Effect.result(effect)            // Convert to Exit value
Effect.either(effect)            // Convert to Either<E, A>
Effect.orElse(effect, alt)       // Fallback effect

Error Taxonomy

Category Examples Handling
Expected Rejections User cancel, deny Graceful exit, no retry
Domain Errors Validation, business rules Show to user, don't retry
Defects Bugs, assertions Log + alert, investigate
Interruptions Fiber cancel, timeout Cleanup, may retry
Unknown/Foreign Thrown exceptions Normalize at boundary

Pattern Matching (Match Module)

import { Match } from "effect"

// Type-safe exhaustive matching on tagged errors
const handleError = Match.type<AppError>().pipe(
  Match.tag("UserCancelledError", () => null),
  Match.tag("ValidationError", (e) => e.message),
  Match.tag("NetworkError", () => "Connection failed"),
  Match.exhaustive  // Compile error if case missing
)

// Cleaner than chained catchTag
Effect.catchAll(effect, (error) =>
  Match.value(error).pipe(
    Match.tag("A", handleA),
    Match.tag("B", handleB),
    Match.exhaustive
  )
)

Services and Layers

// Pattern 1: Context.Tag (implementation provided separately via Layer)
class MyService extends Context.Tag("MyService")<MyService, { ... }>() {}
const MyServiceLive = Layer.succeed(MyService, { ... })

// Pattern 2: Effect.Service (default implementation bundled)
class UserRepo extends Effect.Service<UserRepo>()("UserRepo", {
  effect: Effect.gen(function* () {
    const db = yield* Database
    return { findAll: db.query("SELECT * FROM users") }
  }),
  dependencies: [Database.Default],
  accessors: true  // Auto-generate method accessors
}) {}

// Pattern 3: Context.Reference (defaultable tags — 3.11.0+)
class SpecialNumber extends Context.Reference<SpecialNumber>()(
  "SpecialNumber",
  { defaultValue: () => 2048 }
) {}

Generator Pattern

// Effect.gen — recommended for sequential code
Effect.gen(function* () {
  const a = yield* effectA
  const b = yield* effectB
  return result
})

// Effect.fn — automatic tracing/telemetry (preferred for named functions)
const fetchUser = Effect.fn("fetchUser")(function* (id: string) {
  const db = yield* Database
  return yield* db.query(id)
})

Running Effects

Effect.runSync(effect)           // Sync, throws on async/error
Effect.runPromise(effect)        // Returns Promise<A>
Effect.runPromiseExit(effect)    // Returns Promise<Exit<A, E>>

// Production (with runtime)
const runtime = ManagedRuntime.make(AppLayer)
await runtime.runPromise(effect)
await runtime.dispose()          // Cleanup

Resource Management

Effect.acquireUseRelease(acquire, use, release)
Effect.scoped(effect)
Effect.addFinalizer(cleanup)

Duration

Effect accepts human-readable duration strings anywhere a DurationInput is expected:

Duration.toMillis("5 minutes")    // 300000
Duration.toMillis("100 millis")   // 100
// Units: nanos, micros, millis, seconds, minutes, hours, days, weeks

Scheduling

Effect.retry(effect, Schedule.exponential("100 millis"))
Effect.repeat(effect, Schedule.fixed("1 second"))
Schedule.compose(s1, s2)

Concurrency

Effect.fork(effect)              // Run in background fiber
Fiber.join(fiber)                // Wait for result
Fiber.interrupt(fiber)           // Cancel fiber
Effect.race(e1, e2)              // First to complete wins
Effect.all([...], { concurrency: 5 })  // Bounded parallel

Configuration

const port = Config.integer("PORT")
const host = Config.withDefault(Config.string("HOST"), "localhost")
const secret = Config.redacted("API_KEY")   // Masked in logs
const db = Config.nested("DATABASE")(Config.all({ host: Config.string("HOST"), port: Config.integer("PORT") }))

State Management

Ref.make(initial)                // Mutable reference (atomic)
Ref.get(ref) / Ref.set(ref, v)
Queue.bounded<T>(n)              // Producer/consumer
Deferred.make<E, A>()            // One-shot signal
Effect.makeSemaphore(n)          // Concurrency limiter

Array Operations

import { Array as Arr, Order } from "effect"

Arr.sort([3, 1, 2], Order.number)                    // [1, 2, 3]
Arr.sortWith(users, (u) => u.age, Order.number)       // Sort by field
Arr.sortBy(users, Order.mapInput(Order.number, (u: User) => u.age))  // Multi-criteria

Deprecations

  • BigDecimal.fromNumber → use BigDecimal.unsafeFromNumber (3.11.0+)
  • Schema.annotations() now removes previously set identifier annotations (3.17.10)

Conduit Migration Conventions

Conduit is mid-migration to Effect. Read docs/plans/2026-04-23-effect-ts-migration-plan.md for the full roadmap.

Migration Layers (in order)

  1. Schema + Errors — branded types, error hierarchy, RelayMessage union
  2. Resources — SQLite lifecycle, connection pool
  3. Concurrency — AbortSignal → Effect.interrupt, fiber supervision
  4. Dependency Injection — ServiceRegistry → Layers
  5. Handlers — request handlers as Effects
  6. Server — HTTP/WS server as Effect program
  7. Frontend — Svelte stores bridging to Effect runtime

Key Mappings

Current Pattern Effect Replacement
AbortSignal / AbortController Fiber.interrupt / Effect.scoped
try-catch + async/await Effect.gen + typed errors
RelayError class hierarchy Schema.TaggedError union
ServiceRegistry + constructor injection Context.Tag / Effect.Service + Layers
Promise.all() / Promise.allSettled() Effect.all() with concurrency options
TrackedService lifecycle Layer.scoped with acquireRelease
pino structured logging Effect.log + custom logger layer

Testing

Conduit uses Vitest with multiple config variants. When testing Effect code:

  • Default unit tests: pnpm test:unit
  • Run specific: pnpm vitest run <path>
  • Use @effect/vitest for it.effect, it.scoped, it.live
  • TestClock is active in it.effect — advance with TestClock.adjust()
  • See ./references/testing.md for comprehensive patterns

Verification Path

pnpm check          # tsgo type checking
pnpm lint           # biome
pnpm test:unit      # unit tests

Reference Files

Read these when working on specific topics:

  • ./references/critical-rules.md — Forbidden patterns and mandatory conventions
  • ./references/error-handling.md — Typed errors, defects, recovery, retry, timeout
  • ./references/schema.md — Validation, encoding/decoding, branded types, class schemas
  • ./references/layers.md — Dependency injection, service composition, Layer construction
  • ./references/concurrency.md — Fibers, synchronization primitives, interruption
  • ./references/streams.md — Stream creation, transformation, consumption, backpressure
  • ./references/testing.md — TestClock, test layers, mocking, @effect/vitest patterns
  • ./references/anti-patterns.md — Common mistakes and how to fix them
  • ./references/option-null.md — Option vs null boundary patterns
  • ./references/config.md — Configuration management, env vars, secrets
  • ./references/svelte-effect.md — Svelte 5 + Effect integration patterns
  • ./references/conduit-migration.md — Conduit-specific migration patterns and mappings

04 同仓库更多 Skills