effect-ts
Effect-TS development guide for TypeScript, focused on Effect v4 (the recommended default) with full v3 (stable) support for existing codebases. Use when building, debugging, reviewing, or generating Effect code across its error, concurrency, service, streaming, schema, and platf
Install
npx skills add https://github.com/tenequm/skills/tree/main/skills/effect-ts
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install tenequm-skills@llmmart
git clone https://github.com/tenequm/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole tenequm/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Effect-TS
Effect is a TypeScript library for building production-grade software with typed errors, structured concurrency, dependency injection, and built-in observability.
Version Detection
Before writing Effect code, detect which version the user is on:
# Check installed version
cat package.json | grep '"effect"'
- v4.x (recommended, the direction Effect is heading):
Context.Service,Effect.catch,Effect.forkChild,Schema.TaggedErrorClass - v3.x (stable, still common in production):
Context.Tag,Effect.catchAll,Effect.fork,Data.TaggedError
Note: v4 beta briefly used a
ServiceMapmodule, renamed back toContexton 2026-04-07 (PR #1961). If you seeServiceMap.*in any doc or older beta code, it is the currentContext.*. Both v3 and v4 importContextfrom"effect"; the exports inside differ (Context.Servicein v4 vsContext.Tagin v3).
Prefer v4 for new projects - it's where Effect is going. In an existing codebase, match the installed version: don't rewrite v3 code in v4 syntax unless asked. If the version is genuinely unclear, default to v4 and say so. v4 is still in beta, so pin an exact version (4.0.0-beta.x) and expect occasional API churn.
Primary Documentation Sources
v4 (primary):
- https://github.com/Effect-TS/effect-smol (v4 source + migration guides)
- https://github.com/Effect-TS/effect-smol/blob/main/LLMS.md (v4 LLM guide)
v3 (for existing codebases):
- https://effect.website/docs (v3 stable docs)
- https://effect.website/llms.txt (LLM topic index)
- https://effect.website/llms-full.txt (full docs for large context)
Both versions:
- https://tim-smart.github.io/effect-io-ai/ (concise API list)
AI Guardrails: Critical Corrections
LLM outputs frequently contain incorrect Effect APIs. Verify every API against the reference docs before using it.
Common hallucinations (both versions):
| Wrong (AI often generates) | Correct |
|---|---|
Effect.cachedWithTTL(...) |
Cache.make({ capacity, timeToLive, lookup }) |
Effect.cachedInvalidateWithTTL(...) |
cache.invalidate(key) / cache.invalidateAll() |
Effect.mapError(effect, fn) |
Effect.mapError(fn) in pipe, or use Effect.catchTag |
import { Schema } from "@effect/schema" |
import { Schema } from "effect" (v3.10+ and all v4) |
import { JSONSchema } from "@effect/schema" |
import { JSONSchema } from "effect" (v3.10+) |
| JSON Schema Draft 2020-12 | Effect Schema generates Draft-07 |
| "thread-local storage" | "fiber-local storage" via FiberRef (v3) / Context.Reference (v4) |
| fibers are "cancelled" | fibers are "interrupted" |
| all queues have back-pressure | only bounded queues; sliding/dropping do not |
new MyError("message") |
new MyError({ message: "..." }) (Schema errors take objects) |
v3-specific hallucinations:
| Wrong | Correct (v3) |
|---|---|
Effect.Service (function call) |
class Foo extends Effect.Service<Foo>()("id", {}) |
Effect.match(effect, { ... }) |
Effect.match(effect, { onSuccess, onFailure }) |
Effect.provide(layer1, layer2) |
Effect.provide(Layer.merge(layer1, layer2)) |
v4-specific hallucinations (AI may mix v3/v4):
| Wrong (v3 API used in v4 code) | Correct (v4) |
|---|---|
Context.Tag("X") (v3 shape) |
Context.Service<X>(id) or class syntax |
ServiceMap.Service / ServiceMap.Reference |
Renamed back to Context.Service / Context.Reference on 2026-04-07 |
Effect.catchAll(fn) |
Effect.catch(fn) |
Effect.fork(effect) |
Effect.forkChild(effect) |
Effect.forkDaemon(effect) |
Effect.forkDetach(effect) |
Data.TaggedError |
Schema.TaggedErrorClass |
FiberRef.get(ref) |
yield* References.X (a Context.Reference) |
yield* ref (Ref as Effect) |
yield* Ref.get(ref) (Ref is no longer an Effect) |
yield* fiber (Fiber as Effect) |
yield* Fiber.join(fiber) (Fiber is no longer Effect) |
Logger.Default / Logger.Live |
Logger.layer (v4 naming convention) |
Schema.TaggedError |
Schema.TaggedErrorClass |
Schema.makeUnsafe(input) |
Schema.make(input) (throws SchemaError); also instance methods schema.makeOption(...), schema.makeEffect(...) |
ParseResult (from "effect") |
SchemaIssue module + SchemaError class; narrow with Schema.isSchemaError |
HttpApiEndpoint.get(n, p).pipe(HttpApiEndpoint.setPath(...), setPayload(...), setSuccess(...)) |
HttpApiEndpoint.get(n, p, { params, query, payload, success, error }) (object-option form) |
Otlp.layer({ url, serviceName }) |
OtlpTracer.layer({ url, resource: { serviceName } }) + OtlpSerialization.layerJson + FetchHttpClient.layer |
import { HttpApi } from "@effect/platform" (v4) |
import { HttpApi } from "effect/unstable/httpapi" |
| HttpApi endpoint schema errors are typed errors by default | In current v4 betas they default to defects unless transformed |
Read references/llm-corrections.md for the exhaustive corrections table.
Progressive Disclosure
Read only the reference files relevant to your task:
- Error modeling or typed failures →
references/error-modeling.md - Services, DI, or Layer wiring →
references/dependency-injection.md - Per-key dynamic layers (per-tenant resources,
LayerMap) →references/dependency-injection.md - Bridging Effect into non-Effect frameworks (Hono/Express,
ManagedRuntime) →references/dependency-injection.md - Retries, timeouts, or backoff →
references/retry-scheduling.md - Fibers, forking, or parallel work →
references/concurrency.md - Request batching, N+1 elimination, DataLoader pattern →
references/concurrency.md - Multi-provider fallback (
ExecutionPlan) →references/effect-ai.md/references/retry-scheduling.md - Streams, queues, or SSE →
references/streams.md - Framing streams (NDJSON / MessagePack encode-decode) →
references/streams.md - Running child processes / shelling out →
references/concurrency.md - Resource lifecycle or cleanup →
references/resource-management.md - Refreshable values (rotating credentials, polled config) →
references/resource-management.md - Reference-counted shared resources (
RcRef/RcMap) →references/resource-management.md - Schema validation or decoding →
references/schema.md - Branded / nominal types (
Brand) →references/schema.md - Logging, metrics, or tracing →
references/observability.md - HTTP clients or API calls →
references/http.md - HTTP API servers →
references/http.md(covers both client and server) - File uploads / multipart form-data →
references/http.md - LLM/AI integration →
references/effect-ai.md - Configuration, env vars, secrets →
references/configuration.md - SQL / database access →
references/sql.md - Command-line apps →
references/cli.md - Typed client/server RPC →
references/rpc.md - Sharded entities, durable workflows, event sourcing →
references/distributed.md - Transactional state (STM,
Tx*) →references/stm.md - Date/time handling →
references/datetime.md - Immutable nested updates (optics) →
references/optics.md - Graphs, dependency ordering, shortest paths, cycle detection →
references/graph.md - Pattern matching (
Match) →references/core-patterns.md - Pooling resources (
Pool) →references/resource-management.md - Fiber sets, SubscriptionRef, worker threads →
references/concurrency.md - Testing Effect code →
references/testing.md - Property-based testing / generating data from schemas →
references/testing.md - Migrating from async/await →
references/migration-async.md - Migrating from v3 to v4 →
references/migration-v4.md - Core types, gen, pipe, running →
references/core-patterns.md - Full wrong-vs-correct API table →
references/llm-corrections.md
Core Workflow
- Detect version from
package.jsonbefore writing any code - Clarify boundaries: identify where IO happens, keep core logic as
Effectvalues - Choose style: use
Effect.genfor sequential logic, pipelines for simple transforms. In v4, preferEffect.fn("name")for named functions - Model errors explicitly: type expected errors in the
Echannel; treat bugs as defects - Model dependencies with services and layers; keep interfaces free of construction logic
- Manage resources with
Scopewhen opening/closing things (files, connections, etc.) - Provide layers and run effects only at program edges (
NodeRuntime.runMainorManagedRuntime) - Verify APIs exist before using them - consult https://tim-smart.github.io/effect-io-ai/ or source docs
Starter Function Set
Start with these ~20 functions (the official recommended set):
Creating effects: Effect.succeed, Effect.fail, Effect.sync, Effect.tryPromise
Composition: Effect.gen (+ Effect.fn in v4), Effect.andThen, Effect.map, Effect.tap, Effect.all
Running: Effect.runPromise, NodeRuntime.runMain (preferred for entry points)
Error handling: Effect.catchTag, Effect.catch (v4) / Effect.catchAll (v3), Effect.orDie
Resources: Effect.acquireRelease, Effect.acquireUseRelease, Effect.scoped
Dependencies: Effect.provide, Effect.provideService
Key modules: Effect, Schema, Layer, Option, Result (v4) / Either (v3), Array, Match
DI (v4): Context.Service, Context.Reference, Layer.effect, Effect.fn("name")
DI (v3): Context.Tag, Context.Reference
Import Patterns
Always use barrel imports from "effect":
import { Context, Effect, Schema, Layer, Option, Stream } from "effect"
For companion packages, import from the package name. v3 and v4 differ here:
// v4 (recommended) - platform transports still separate, but HttpApi / observability
// moved under effect/unstable/*
import { NodeRuntime } from "@effect/platform-node"
import { FetchHttpClient } from "effect/unstable/http"
import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"
import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"
// v3 (stable) companion packages
import { NodeRuntime } from "@effect/platform-node"
import { HttpClient } from "@effect/platform"
import { NodeSdk } from "@effect/opentelemetry"
Avoid deep module imports (effect/Effect) unless your bundler requires it for tree-shaking.
Output Standards
- Show imports in every code example
- Prefer
Effect.gen(imperative) for multi-step logic; pipelines for transforms - In v4, use
Effect.fn("name")instead of bareEffect.genfor named functions; useEffect.fnUntracedfor internal helpers that don't need a span/stack-frame - Never call
Effect.runPromise/Effect.runSyncinside library code - only at program edges - Use
NodeRuntime.runMainfor CLI/server entry points (handles SIGINT gracefully) - Use
ManagedRuntimewhen integrating Effect into non-Effect frameworks (Hono, Express, etc.) - Always
return yield*when raising an error in a generator (ensures TS understands control flow) - Avoid point-free/tacit usage: write
Effect.map((x) => fn(x))notEffect.map(fn)(generics get erased) - Keep dependency graphs explicit (services, layers, tags)
- State the
Effect<A, E, R>shape when it helps design decisions
Agent Quality Checklist
Before outputting Effect code, verify:
- Every API exists (check against tim-smart API list or source docs)
- Imports are from
"effect"(not@effect/schema,@effect/io, etc.) - Version matches the user's codebase (v3 vs v4 syntax)
- Expected errors are typed in
E; unexpected failures are defects -
run*is called only at program edges, not inside library code - Resources opened with
acquireReleaseare wrapped inEffect.scoped - Layers are provided before running (no missing
Rrequirements) - Generator bodies use
yield*(notyieldwithout*) - Error raises in generators use
return yield*pattern
Files (skills)
-
evals
-
evals.json 3.9 KB
{ "skill_name": "effect-ts", "evals": [ { "id": 1, "prompt": "Write an Effect service that wraps a flaky HTTP API. It should retry on network errors with exponential backoff (max 3 retries), fail fast on auth errors, and expose a typed interface via Context.Tag. Include the layer and a usage example.", "expected_output": "A complete Effect service with typed errors (NetworkError, AuthError), a Context.Tag-based interface, a Layer with retry logic using Schedule.exponential + Schedule.recurs, conditional retry (only on NetworkError), and a runnable usage example.", "files": [], "expectations": [ "Imports from 'effect' package (not invented packages)", "Defines at least 2 distinct typed error classes (e.g., NetworkError and AuthError)", "Uses Context.Tag (v3) or Context.Service (v4) for the service interface", "Uses Schedule.exponential for exponential backoff", "Limits retries to 3 (Schedule.recurs(3) or equivalent)", "Only retries network errors, not auth errors (conditional retry logic)", "Includes a Layer definition (Layer.succeed, Layer.effect, or similar)", "Includes a runnable usage example (Effect.runPromise or NodeRuntime.runMain)", "Does not use non-existent Effect APIs (e.g., Effect.retryN, Effect.cachedWithTTL)" ] }, { "id": 2, "prompt": "I need to consume an SSE endpoint that streams JSON events and process each event through a validation schema. Show me how to do this with Effect Stream, including proper resource cleanup if the connection drops.", "expected_output": "An Effect Stream that connects to an SSE endpoint via HttpClient, parses SSE data lines, validates each event with Schema.decodeUnknown, handles errors, and uses Scope/acquireRelease for cleanup on disconnect.", "files": [], "expectations": [ "Uses Effect Stream module for SSE consumption", "Validates events with Schema (Schema.decodeUnknown or Schema.decodeUnknownEffect)", "Uses acquireRelease, addFinalizer, unwrapScoped, or AbortController for resource cleanup", "Defines typed error classes for connection and parse failures", "Parses SSE format (data: lines, event separation on blank lines)", "Does not use non-existent APIs (e.g., Stream.fromSSE, Stream.fromEventSource, Stream.acquireRelease)", "Handles errors with catchTag or similar typed error handling" ] }, { "id": 3, "prompt": "Migrate this v3 Effect code to v4:\n```typescript\nimport { Context, Data, Effect, Layer } from 'effect'\nclass AppError extends Data.TaggedError('AppError')<{ message: string }> {}\nclass Config extends Context.Tag('Config')<Config, { readonly endpoint: string }>() {}\nconst ConfigLive = Layer.succeed(Config, { endpoint: 'https://api.example.com' })\nconst program = Effect.gen(function*() {\n const config = yield* Config\n return yield* Effect.tryPromise({ try: () => fetch(config.endpoint), catch: () => new AppError({ message: 'failed' }) })\n}).pipe(Effect.catchAll((e) => Effect.log(e.message)))\nEffect.runPromise(program.pipe(Effect.provide(ConfigLive)))\n```", "expected_output": "Migrated code using Context.Service (the v4 API) instead of Context.Tag (v3), Schema.TaggedErrorClass instead of Data.TaggedError, Effect.catch instead of Effect.catchAll, and correct v4 import patterns. Should note all changes made.", "files": [], "expectations": [ "Replaces Context.Tag (v3) with Context.Service (v4)", "Replaces Data.TaggedError with Schema.TaggedErrorClass (the v4 API)", "Replaces Effect.catchAll with Effect.catch (the v4 rename)", "Imports Context from 'effect'", "Includes a changelog or migration notes explaining what changed", "No v3-only APIs remain in code (Context.Tag, Data.TaggedError, Effect.catchAll)", "Code is logically complete and would compile with effect v4" ] } ] }
-
-
references
-
cli.md 2.3 KB
# CLI `effect/unstable/cli` builds command-line apps where handlers are Effects — flags and arguments are parsed and typed, and the whole program runs through the Effect runtime. (Unstable module, `@since 4.0.0`.) ```typescript import { Command, Flag, Argument } from "effect/unstable/cli" ``` ## A Minimal Command ```typescript import { Console, Effect } from "effect" import { Command, Flag } from "effect/unstable/cli" import { NodeRuntime, NodeServices } from "@effect/platform-node" const greet = Command.make( "greet", { name: Flag.string("name"), loud: Flag.boolean("loud") }, (config) => Effect.gen(function*() { const msg = `Hello, ${config.name}!` yield* Console.log(config.loud ? msg.toUpperCase() : msg) }) ) Command.run(greet, { version: "1.0.0" }).pipe( Effect.provide(NodeServices.layer), NodeRuntime.runMain ) ``` `Command.make(name, config?, handler?)` — `config` maps option names to `Flag.*` / `Argument.*` params (and can nest: `server: { host, port }`); the handler receives the parsed, typed values and returns an Effect. > `Command.run` takes `{ version }` only — the command's name comes from the command itself, **not** a `{ name, version }` object. Use `Command.runWith(command, { version })(argv)` to pass args explicitly. ## Flags and Arguments ```typescript const port = Flag.integer("port").pipe(Flag.withDefault(3000)) const verbose = Flag.boolean("verbose").pipe(Flag.withAlias("v")) const config = Flag.file("config").pipe(Flag.optional) const env = Flag.choice("env", ["dev", "prod"]) const file = Argument.string("file") const rest = Argument.variadic(Argument.string("paths")) ``` `Flag.*` constructors: `string`, `boolean`, `integer`, `float`, `date`, `choice`, `path` / `file` / `directory`, `redacted`, `fileText` / `fileParse` / `fileSchema`, `keyValuePair`. Modifiers: `withDefault`, `withAlias`, `withDescription`, `optional`. `Argument.*` mirrors these and adds `Argument.variadic`. Both are built on the shared `Param` layer. ## Subcommands ```typescript const root = Command.make("mytool").pipe( Command.withSubcommands([greet, otherCommand]) ) ``` `Command.run` / `runWith` require the `FileSystem | Path | Terminal | ...` environment, all provided by `NodeServices.layer`; finish with `NodeRuntime.runMain` so SIGINT is handled. -
concurrency.md 10.6 KB
# Concurrency Effect uses fiber-based structured concurrency. Fibers are lightweight virtual threads managed by the Effect runtime. ## Forking Fibers ### v3 ```typescript import { Effect, Fiber } from "effect" // Fork as child (interrupted when parent ends) const fiber = yield* Effect.fork(myEffect) // Fork as daemon (outlives parent) const fiber = yield* Effect.forkDaemon(longRunning) // Fork tied to a Scope const fiber = yield* Effect.forkScoped(background) ``` ### v4 ```typescript import { Effect, Fiber } from "effect" // Fork as child const fiber = yield* Effect.forkChild(myEffect) // Fork detached (outlives parent) const fiber = yield* Effect.forkDetach(longRunning) // Fork tied to a Scope (unchanged) const fiber = yield* Effect.forkScoped(background) // New options const fiber = yield* Effect.forkChild(myEffect, { startImmediately: true, // begin executing immediately uninterruptible: true // cannot be interrupted }) ``` > **`forkDetach` parents to the global scope**, so the fiber survives even `runtime.dispose()` on a `ManagedRuntime`. For a background fiber (a poller, a subscriber) that must die when its runtime is torn down, use `Effect.forkScoped` inside the runtime's scope instead — `forkDetach` there leaks the fiber past disposal. ## Joining and Interrupting ```typescript // Wait for a fiber to complete const result = yield* Fiber.join(fiber) // Interrupt a fiber yield* Fiber.interrupt(fiber) // v4: Fiber is NOT an Effect - must use Fiber.join explicitly // v3: yield* fiber was allowed (Fiber was an Effect subtype) ``` ## Parallel Execution ```typescript // Process items with bounded concurrency const results = yield* Effect.all( items.map((item) => processItem(item)), { concurrency: 5 } ) // forEach variant const results = yield* Effect.forEach( items, (item) => processItem(item), { concurrency: 10 } ) ``` ## Racing ```typescript // First to succeed wins, loser is interrupted const result = yield* Effect.race(fetchFromCache, fetchFromDb) // Race multiple effects const result = yield* Effect.raceAll([ fetchFromCdn1, fetchFromCdn2, fetchFromCdn3 ]) ``` ## Interruption Interruption is cooperative, not preemptive. Fibers check for interruption at yield points. ```typescript // Register cleanup on interruption const withCleanup = myEffect.pipe( Effect.onInterrupt(() => Effect.log("Interrupted! Cleaning up...")) ) // Make a region uninterruptible const critical = Effect.uninterruptible( Effect.gen(function*() { yield* beginTransaction() yield* doWork() yield* commitTransaction() }) ) // Interruptible region inside an uninterruptible one const mixed = Effect.uninterruptible( Effect.gen(function*() { yield* criticalSetup() yield* Effect.interruptible(longComputation) yield* criticalTeardown() }) ) ``` ## Queue Bounded queues provide back-pressure; dropping/sliding queues do not. ```typescript import { Queue } from "effect" // Bounded queue (back-pressure: offer suspends when full) const queue = yield* Queue.bounded<string>(100) // Dropping queue (discards new items when full) const queue = yield* Queue.dropping<string>(100) // Sliding queue (discards oldest items when full) const queue = yield* Queue.sliding<string>(100) // Offer and take yield* Queue.offer(queue, "hello") const item = yield* Queue.take(queue) // Take all available const items = yield* Queue.takeAll(queue) ``` ## Semaphore ```typescript // v3 import { Effect } from "effect" const semaphore = yield* Effect.makeSemaphore(3) // Limit concurrency (method-style on the returned semaphore) const limited = semaphore.withPermits(1)(expensiveOp) ``` ```typescript // v4 — Effect.makeSemaphore is removed. Use the Semaphore module. import { Semaphore } from "effect" const semaphore = yield* Semaphore.make(3) // withPermits is data-first in v4: pass the semaphore as the first arg. const limited = Semaphore.withPermits(semaphore, 1)(expensiveOp) ``` ## Deferred (one-shot signal) ```typescript import { Deferred, Effect } from "effect" const deferred = yield* Deferred.make<string, never>() // Complete the deferred (can only be done once) yield* Deferred.succeed(deferred, "done") // Wait for completion // v3: yield* deferred (Deferred was an Effect subtype) // v4: must use Deferred.await const value = yield* Deferred.await(deferred) ``` ## Structured Concurrency Pattern: Background Worker ```typescript const withWorker = Effect.scoped( Effect.gen(function*() { // Background fiber tied to scope - auto-interrupted on scope exit yield* Effect.forkScoped( Effect.repeat( processQueue, Schedule.spaced("100 millis") ) ) // Main work continues... yield* handleRequests() }) ) ``` ## Managing Dynamic Sets of Fibers `FiberHandle` (one fiber), `FiberMap` (keyed) and `FiberSet` (unkeyed) manage fibers whose set changes at runtime. All three are **scoped** — closing the surrounding scope interrupts every managed fiber, and fibers self-remove on completion. `FiberMap.run(map, key, effect)` forks `effect` under `key`, interrupting any prior fiber at that key (pass `{ onlyIfMissing: true }` to keep the existing one). ```typescript import { Effect, FiberMap } from "effect" const program = Effect.scoped( Effect.gen(function*() { const fibers = yield* FiberMap.make<string>() // Start (or restart) a background fiber per connection id yield* FiberMap.run(fibers, "conn-1", handleConnection(conn1)) yield* FiberMap.run(fibers, "conn-2", handleConnection(conn2)) yield* serveRequests() // All managed fibers interrupted when this scope closes }) ) ``` ## SubscriptionRef (Observable State) `SubscriptionRef` is a `Ref` whose updates can be observed as a `Stream`. `.changes` emits the current value first, then every subsequent update — useful for reactive state, config hot-reload, or fan-out to watchers. ```typescript import { Effect, Stream, SubscriptionRef } from "effect" const program = Effect.gen(function*() { const ref = yield* SubscriptionRef.make(0) // Subscribe in the background; receives 0, then each update yield* Effect.forkScoped( Stream.runForEach(SubscriptionRef.changes(ref), (n) => Effect.log(`value is now ${n}`) ) ) yield* SubscriptionRef.set(ref, 1) yield* SubscriptionRef.update(ref, (n) => n + 1) // 2 }) ``` ## Request Batching & Caching (Request / RequestResolver) When the same data is fetched repeatedly (the classic N+1 problem), model each fetch as a typed `Request` and resolve a batch of them through a `RequestResolver`. Effect automatically deduplicates identical requests in flight and batches those collected within the same step, then `Effect.request` reads a single result. The resolver receives all pending entries at once — issue one bulk query, then complete each entry. ```typescript import { Console, Effect, Exit, Request, RequestResolver } from "effect" interface GetUser extends Request.Request<string, Error> { readonly _tag: "GetUser" readonly id: number } const GetUser = Request.tagged<GetUser>("GetUser") // runAll receives the full batch of pending entries; complete each with an Exit const UserResolver = RequestResolver.make<GetUser>( Effect.fnUntraced(function*(entries) { const ids = entries.map((e) => e.request.id) const rows = yield* bulkFetchUsers(ids) // ONE query for the whole batch for (const entry of entries) { yield* Request.complete(entry, Exit.succeed(rows[entry.request.id])) } }) ) const program = Effect.gen(function*() { // These two run in the same step -> batched into one runAll call const [a, b] = yield* Effect.all( [Effect.request(GetUser({ id: 1 }), UserResolver), Effect.request(GetUser({ id: 2 }), UserResolver)], { concurrency: "unbounded" } ) yield* Console.log([a, b]) }) ``` `Request.tagged<T>(tag)` builds the constructor; `Request.Class` is the class form. `RequestResolver.make(runAll)` is the basic batched resolver; `RequestResolver.makeGrouped` partitions entries by a computed key, and `RequestResolver.fromEffect` lifts a per-request effect. Group several resolvers behind a service so callers just `yield* Effect.request(...)`. ## Worker Threads (`effect/unstable/workers`) There is **no `Worker.makePool` / high-level worker-pool API** in v4. The intended way to offload work to worker threads is **RPC-over-worker**: define an `RpcGroup` (see `references/rpc.md`), run an `RpcServer` inside the worker, and create a pooled client with `RpcClient.layerProtocolWorker({ size })`. Calling a typed RPC method dispatches it onto a worker in the pool. ```typescript import { Layer } from "effect" import { RpcClient } from "effect/unstable/rpc" import { NodeWorker } from "@effect/platform-node" import { Worker as NodeWorkerThread } from "node:worker_threads" // Client side: a pool of worker threads; RPC calls run on a worker const WorkerClientLive = MyRpcClient.layer.pipe( Layer.provide(RpcClient.layerProtocolWorker({ size: 4 })), Layer.provide( NodeWorker.layer(() => new NodeWorkerThread(new URL("./worker.ts", import.meta.url))) ) ) // Inside ./worker.ts: RpcServer.layerProtocolWorkerRunner + your handler layer // + NodeWorkerRunner.layer (see references/rpc.md for the server side). ``` The low-level `Worker` / `WorkerRunner` modules exist but are platform primitives, not an ergonomic pool — prefer the RPC transport above. ## Child Processes (`effect/unstable/process`) Define commands with `ChildProcess.make` and run them through the `ChildProcessSpawner` service. `ChildProcess.make` supports positional args, a template-literal form, and options (`cwd`, `env`, `extendEnv`); `ChildProcess.pipeTo` composes a shell-style pipeline between two command values. Provide the spawner from your platform (`NodeServices.layer` on Node). ```typescript import { Effect, String } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" const gitLog = Effect.gen(function*() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner // Collect the whole output as a string const nodeVersion = yield* spawner.string( ChildProcess.make("node", ["--version"]) ).pipe(Effect.map(String.trim)) // Line-oriented output, with a piped command: git log ... | head -n 5 const subjects = yield* spawner.lines( ChildProcess.make("git", ["log", "--pretty=format:%s", "-n", "20"]).pipe( ChildProcess.pipeTo(ChildProcess.make("head", ["-n", "5"])) ) ) return { nodeVersion, subjects } }) ``` Use `spawner.string` / `spawner.lines` to collect completed output; use `spawner.spawn` to get a `ChildProcessHandle` and stream stdout/stderr while the process is still running. On Node, provide `NodeServices.layer` (from `@effect/platform-node`) to satisfy the `ChildProcessSpawner` requirement. -
configuration.md 3.3 KB
# Configuration (Config / ConfigProvider) Effect's `Config` module reads configuration (env vars by default) as typed, validated values inside the type system. A `Config<T>` *is* an `Effect<T, ConfigError>`, so you consume it with `yield*` and it resolves against the `ConfigProvider` in the fiber's services. Always import from `"effect"`. ```typescript import { Config, Effect } from "effect" ``` > v4 note: in v4 `Config` is schema-backed — every constructor delegates to `Config.schema`, and several constructors (`Config.schema`, `Config.int`, `Config.finite`, `Config.literals`) are `@since 4.0.0`. The consumption model (`yield* Config.string(...)`) is the same across v3 and v4. ## Reading Values ```typescript const program = Effect.gen(function*() { const host = yield* Config.string("HOST") const port = yield* Config.port("PORT") // validates 1-65535 const debug = yield* Config.boolean("DEBUG") // "true"/"yes"/"on"/"1" -> true const timeout = yield* Config.duration("TIMEOUT") // "10 seconds" -> Duration const apiKey = yield* Config.redacted("API_KEY") // Redacted<string> (won't print) return { host, port, debug, timeout, apiKey } }) ``` Common constructors: `Config.string`, `Config.nonEmptyString`, `Config.number`, `Config.int`, `Config.boolean`, `Config.duration`, `Config.port`, `Config.url`, `Config.date`, `Config.literal` / `Config.literals`, `Config.logLevel`, `Config.redacted`. ## Defaults, Optional, Nesting ```typescript // Fall back to a default (only on MISSING data, not on validation failure) const port = yield* Config.port("PORT").pipe(Config.withDefault(8080)) // Option<T> when absent const proxy = yield* Config.option(Config.string("PROXY_URL")) // Group related config and namespace it (reads DATABASE_HOST / DATABASE_PORT) const db = yield* Config.all({ host: Config.string("HOST"), port: Config.number("PORT") }).pipe(Config.nested("DATABASE")) ``` `Config.all` accepts a struct (object) or a tuple/iterable of configs and combines them. `withDefault` / `option` only recover from *missing* data — a present-but-invalid value (wrong type, out of range) still fails with a `ConfigError`. ## Schema-Validated Config `Config.schema` decodes raw config through an Effect `Schema`, giving full validation and transformation: ```typescript import { Config, Schema } from "effect" const AppConfig = Config.schema( Schema.Struct({ host: Schema.String, port: Schema.Int }), "APP" ) ``` ## Providing a ConfigProvider The default provider reads from `process.env`. Override it (tests, embedded config) by providing a `ConfigProvider` via its layer. There is **no `ConfigProvider.fromJson`** — use `ConfigProvider.fromUnknown(obj)` for an in-memory object or `ConfigProvider.fromEnv({ env })` for an explicit env map. ```typescript import { Config, ConfigProvider, Effect, Layer } from "effect" const TestConfig = ConfigProvider.layer( ConfigProvider.fromUnknown({ HOST: "localhost", PORT: "8080", DEBUG: "yes" }) ) const runnable = program.pipe(Effect.provide(TestConfig)) ``` Other provider helpers: `ConfigProvider.fromEnv`, `ConfigProvider.fromDotEnv`, `ConfigProvider.fromDir`, plus combinators `orElse`, `nested`, `constantCase`, `mapInput`. The provider is a `Context.Reference` service, so you can also set it inline with `Effect.provideService(ConfigProvider.ConfigProvider, provider)`. -
core-patterns.md 5.8 KB
# Core Patterns ## The Effect Type ```typescript Effect<Success, Error, Requirements> // ^ ^ ^ // | | └── Dependencies needed (provided via Layers) // | └── Expected errors (typed, must be handled) // └── Success value ``` An `Effect` is a lazy description of a program. Nothing runs until you call `run*` at the edge. ## Creating Effects ```typescript import { Effect } from "effect" // From a pure value const succeed = Effect.succeed(42) // From a failure const fail = Effect.fail(new Error("boom")) // From synchronous code that might throw const sync = Effect.sync(() => JSON.parse(rawJson)) // From a Promise (typed error on rejection) const async = Effect.tryPromise({ try: () => fetch(url), catch: (err) => new FetchError({ cause: err }) }) // From a Promise that never rejects const safe = Effect.promise(() => fs.readFile(path)) ``` ## Composition with Effect.gen Use `Effect.gen` for imperative-style multi-step logic: ```typescript const program = Effect.gen(function*() { const user = yield* fetchUser(id) const posts = yield* fetchPosts(user.id) return { user, posts } }) ``` **v4: Use `Effect.fn` for named functions** (adds automatic span + better stack traces): ```typescript // v4 only const fetchUserPosts = Effect.fn("fetchUserPosts")( function*(id: string): Effect.fn.Return<UserPosts, ApiError> { const user = yield* fetchUser(id) const posts = yield* fetchPosts(user.id) return { user, posts } }, Effect.catch((e) => Effect.log(`Failed: ${e}`)) ) ``` **`Effect.fn` vs `Effect.fnUntraced`:** `Effect.fn("name")(...)` creates a tracing span and records a stack-frame boundary on every call. When you do not need spans or stack frames (most internal helpers), use `Effect.fnUntraced(function*() { ... })` instead — same generator ergonomics, no span/frame overhead. Reach for the named `Effect.fn` only where the observability is worth it (request handlers, top-level operations). Both accept extra `pipe`-style combinators after the generator body. ## Composition with Pipes Use pipes for simple linear transforms: ```typescript const program = fetchUser(id).pipe( Effect.map((user) => user.name), Effect.tap((name) => Effect.log(`Found: ${name}`)), Effect.catchTag("NotFound", () => Effect.succeed("anonymous")) ) ``` ## Running Effects **Entry points** - use platform-specific `runMain`: ```typescript import { NodeRuntime } from "@effect/platform-node" const main = Effect.gen(function*() { yield* Effect.log("Starting...") // your program here }) NodeRuntime.runMain(main) // handles SIGINT gracefully ``` **One-off execution** (scripts, tests): ```typescript await Effect.runPromise(myEffect) // throws on failure const exit = await Effect.runPromiseExit(myEffect) // returns Exit Effect.runSync(myEffect) // sync only, throws on async ``` **Integrating with non-Effect frameworks** (Hono, Express, etc.) - use `ManagedRuntime`: ```typescript import { ManagedRuntime } from "effect" // Create once at startup const runtime = ManagedRuntime.make(MyAppLayer) // Use in route handlers app.get("/users/:id", async (c) => { const result = await runtime.runPromise(fetchUser(c.req.param("id"))) return c.json(result) }) ``` ## Effect.all - Parallel/Sequential Collection ```typescript // Sequential (default) const results = yield* Effect.all([effectA, effectB, effectC]) // Parallel (bounded) const results = yield* Effect.all([effectA, effectB, effectC], { concurrency: 5 }) // Fully parallel const results = yield* Effect.all([effectA, effectB, effectC], { concurrency: "unbounded" }) // With error accumulation (collect all errors, not just first) const results = yield* Effect.all([effectA, effectB], { concurrency: "unbounded", mode: "validate" }) ``` ## Key Combinators | Combinator | Purpose | |------------------|--------------------------------------------------------| | `Effect.map` | Transform the success value | | `Effect.flatMap` | Chain effects (success of first feeds into next) | | `Effect.tap` | Side-effect on success without changing the value | | `Effect.andThen` | Like flatMap but also accepts plain values/functions | | `Effect.all` | Combine multiple effects (sequential or parallel) | | `Effect.forEach` | Map over an iterable with an effectful function | | `Effect.if` | Branch on a boolean condition | | `Effect.match` | Pattern match on success/failure | | `Effect.zip` | Combine two effects into a tuple | | `Effect.orDie` | Convert typed error to defect (unrecoverable) | ## Pattern Matching with `Match` `Match` builds type-safe, exhaustive matchers. Start from `Match.type<T>()` (matcher over a type) or `Match.value(x)` (match a concrete value), pipe in cases, then finish with `Match.exhaustive` (compile error if a case is missed) or `Match.orElse(fallback)`. The combinators are the same in v3 and v4. ```typescript import { Match } from "effect" type Event = | { readonly _tag: "Click"; readonly x: number } | { readonly _tag: "Key"; readonly key: string } // Match on the discriminant tag, exhaustively const render = Match.type<Event>().pipe( Match.tag("Click", ({ x }) => `click at ${x}`), Match.tag("Key", ({ key }) => `key ${key}`), Match.exhaustive ) render({ _tag: "Click", x: 10 }) // "click at 10" // Predicate / partial matching with a fallback const classify = (n: number) => Match.value(n).pipe( Match.when((x) => x < 0, () => "negative"), Match.when(0, () => "zero"), Match.orElse(() => "positive") ) ``` `Match.tag` narrows on `_tag` (works with `Data.TaggedError` / `Schema.TaggedErrorClass` errors); `Match.when` accepts a literal, a predicate, or a partial-shape pattern. -
datetime.md 2.6 KB
# DateTime `DateTime` is Effect's timezone-aware date/time type. It has two variants, both carrying an absolute instant (`epochMillis`): - `DateTime.Utc` (`_tag: "Utc"`) - an instant with no zone - `DateTime.Zoned` (`_tag: "Zoned"`) - an instant plus a `TimeZone` ```typescript import { DateTime, Effect } from "effect" ``` > v4 naming: the unsafe constructors use `Unsafe` as a **suffix** (not a v3-style `unsafe*` prefix): `DateTime.makeUnsafe`, `DateTime.nowUnsafe`, `DateTime.makeZonedUnsafe`, `DateTime.zoneMakeNamedUnsafe`, `DateTime.fromDateUnsafe`. The safe `make` / `makeZoned` / `zoneMakeNamed` return `Option`, not `Effect`. ## Getting "now" ```typescript const program = Effect.gen(function*() { const utc = yield* DateTime.now // Effect<DateTime.Utc> (uses the Clock) return utc }) // Outside Effect (impure): const nowImpure = DateTime.nowUnsafe() ``` Prefer `yield* DateTime.now` inside effects — it reads the `Clock` service, so it is controllable in tests via `TestClock`. ## Constructing ```typescript // Safe: returns Option const maybe = DateTime.make("2026-06-04T12:00:00Z") // Option<Utc> // Unsafe: throws on invalid input const utc = DateTime.makeUnsafe("2026-06-04T12:00:00Z") const fromDate = DateTime.fromDateUnsafe(new Date()) ``` ## Arithmetic ```typescript const utc = DateTime.makeUnsafe("2026-06-04T12:00:00Z") const later = utc.pipe(DateTime.addDuration("5 minutes")) // stays Utc const tomorrow = utc.pipe(DateTime.add({ days: 1 })) // calendar-aware const startOfDay = utc.pipe(DateTime.startOf("day")) ``` `addDuration` / `subtractDuration` shift by a `Duration`; `add` / `subtract` take calendar parts (`{ days, months, hours, ... }`). These are variant-preserving — a `Utc` stays `Utc`. ## Time Zones ```typescript const utc = DateTime.makeUnsafe("2026-06-04T12:00:00Z") // Attach a named zone -> Zoned const london = DateTime.setZone(utc, DateTime.zoneMakeNamedUnsafe("Europe/London")) // Run an effect with an ambient current zone const withZone = program.pipe( DateTime.withCurrentZone(DateTime.zoneMakeNamedUnsafe("America/New_York")) ) ``` `setZone` always returns a `Zoned`. `withCurrentZone` / `nowInCurrentZone` use a `CurrentTimeZone` service (provide it once via `DateTime.layerCurrentZoneNamed("...")`). ## Formatting ```typescript DateTime.formatIso(utc) // "2026-06-04T12:00:00.000Z" DateTime.formatIsoDate(utc) // "2026-06-04" DateTime.format(london, { dateStyle: "full", timeStyle: "short" }) // Intl-based, zone-aware ``` Other formatters: `formatLocal`, `formatUtc`, `formatIntl`, `formatIsoOffset`, `formatIsoZoned`. -
dependency-injection.md 8.3 KB
# Dependency Injection Effect's DI system tracks dependencies through the type system. The `R` parameter in `Effect<A, E, R>` lists required services. The compiler enforces that all dependencies are provided before running. ## Defining Services ### v3: Context.Tag ```typescript import { Context, Effect, Layer } from "effect" class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>() {} ``` ### v4: Context.Service ```typescript import { Context, Effect, Layer } from "effect" class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>()( "myapp/Database" // include package path for uniqueness ) {} ``` Note the argument order difference: v3's `Context.Tag` takes `id` first and types second; v4's `Context.Service` takes types first and `id` on the returned constructor. The module name is the same (`Context`); only the exported factory differs. > v4 briefly exported this under a `ServiceMap` module; it was renamed back to `Context` on 2026-04-07 (PR #1961). Older beta docs or code may still say `ServiceMap.Service` / `ServiceMap.Reference` — treat as `Context.Service` / `Context.Reference`. ## Building Layers Layers construct services and wire their dependencies: ```typescript // Pure implementation (no dependencies) const DatabaseLive = Layer.succeed(Database, { query: (sql) => Effect.tryPromise(() => pgClient.query(sql)) }) // Effectful construction (with dependencies) const DatabaseLive = Layer.effect( Database, Effect.gen(function*() { const config = yield* AppConfig const pool = yield* createPool(config.dbUrl) return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) } }) ) // Scoped (with resource lifecycle) // v3 const DatabaseLive = Layer.scoped( Database, Effect.gen(function*() { const pool = yield* Effect.acquireRelease( createPool(), (pool) => Effect.promise(() => pool.end()) ) return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) } }) ) // v4 — Layer.scoped is removed. Use Layer.effect; it strips Scope from the // requirements automatically when the inner effect uses acquireRelease. const DatabaseLive = Layer.effect( Database, Effect.gen(function*() { const pool = yield* Effect.acquireRelease( createPool(), (pool) => Effect.promise(() => pool.end()) ) return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) } }) ) ``` ## v4: Context.Service with make ```typescript import { Context, Effect, Layer } from "effect" class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>()( "myapp/Database", { make: Effect.gen(function*() { const config = yield* AppConfig return { query: (sql) => Effect.tryPromise(() => pgClient.query(sql)) } }) } ) { // Build layer explicitly from make (v4 does NOT auto-generate layers) static readonly layer = Layer.effect(this, this.make).pipe( Layer.provide(AppConfig.layer) ) } ``` ## Composing Layers ```typescript // Merge independent layers const AppLayer = Layer.merge(DatabaseLive, CacheLive) // Wire dependencies between layers const FullLayer = Layer.provide(ServiceLayer, DatabaseLive) // ServiceLayer depends on Database, DatabaseLive provides it // Compose multiple with provideMerge const FullApp = DatabaseLive.pipe( Layer.provideMerge(CacheLive), Layer.provideMerge(LoggerLive) ) ``` ## Providing Dependencies ```typescript // Provide a full layer const runnable = program.pipe(Effect.provide(AppLayer)) // Provide a single service inline const runnable = program.pipe( Effect.provideService(Database, { query: mockQuery }) ) ``` ## Accessing Services ### In generators (preferred) ```typescript const program = Effect.gen(function*() { const db = yield* Database const results = yield* db.query("SELECT * FROM users") return results }) ``` ### v4: Service.use (one-liner access) ```typescript // v4 only const program = Database.use((db) => db.query("SELECT * FROM users")) ``` Prefer `yield*` in generators over `.use()` because it makes dependencies explicit and avoids accidentally leaking service requirements. ## Testing with Layer Swaps ```typescript // Production layer const DatabaseLive = Layer.effect(Database, /* real implementation */) // Test layer const DatabaseTest = Layer.succeed(Database, { query: (sql) => Effect.succeed([{ id: 1, name: "test" }]) }) // In tests, provide the test layer const result = await Effect.runPromise( program.pipe(Effect.provide(DatabaseTest)) ) ``` ## v4: References (Services with Defaults) For configuration values and feature flags that have sensible defaults: ```typescript // v3 class LogLevel extends Context.Reference<LogLevel>()("LogLevel", { defaultValue: () => "info" as const }) {} // v4 const LogLevel = Context.Reference<"info" | "warn" | "error">("LogLevel", { defaultValue: () => "info" as const }) ``` References can be `yield*`-ed like services but have a default if not provided. ## Layer Naming Convention - v3: `DatabaseLive`, `Database.Default` - v4: `Database.layer`, `Database.layerTest`, `Database.layerConfig` ## v4: Per-Key Dynamic Layers (`LayerMap`) When you need one instance of a service *per key* — a connection pool per tenant, a client per region — build a `LayerMap.Service`. Its `lookup` builds the layer for a key on first access, caches it, and releases it after `idleTimeToLive`. Downstream code stays key-agnostic (`yield* DatabasePool`); the correct instance is chosen by whichever `MyMap.get(key)` layer is provided. ```typescript import { Context, Effect, Layer, LayerMap } from "effect" class DatabasePool extends Context.Service<DatabasePool, { readonly query: (sql: string) => Effect.Effect<ReadonlyArray<unknown>> }>()("app/DatabasePool") { // one layer per tenant, cleaned up on scope close static readonly layer = (tenantId: string) => Layer.effect( DatabasePool, Effect.acquireRelease( Effect.sync(() => DatabasePool.of({ query: (sql) => Effect.succeed([]) })), () => Effect.log(`Closing pool for ${tenantId}`) ) ) } class PoolMap extends LayerMap.Service<PoolMap>()("app/PoolMap", { lookup: (tenantId: string) => DatabasePool.layer(tenantId), idleTimeToLive: "1 minute" }) {} const queryUsers = Effect.gen(function*() { const pool = yield* DatabasePool // tenant-agnostic return yield* pool.query("SELECT id FROM users") }) const program = queryUsers.pipe( Effect.provide(PoolMap.get("acme")), // builds/caches the "acme" pool Effect.provide(PoolMap.layer) ) // PoolMap.invalidate("acme") forces a rebuild on next access. ``` ## Bridging Effect into Non-Effect Frameworks (`ManagedRuntime`) To call Effect from an imperative framework (Hono, Express, a webhook handler), build **one** `ManagedRuntime` from your application layer at startup and reuse it — never construct a runtime per request, and never chain two `runPromise` calls where one Effect would do. Carry per-request state through a fiber-local `Context.Reference` set with `Effect.provideService` inside the run, **not** as an extra "bag" parameter threaded through every function. This keeps the request context off the `R` channel (it stays `never` at the edge) and prevents cross-request leakage. Pass a plain object into the reference — never the framework's own request/context object. ```typescript import { Context, Effect, Layer, ManagedRuntime } from "effect" interface RequestInfo { readonly requestId: string; readonly userId: string } const RequestInfo = Context.Reference<RequestInfo>("app/RequestInfo", { defaultValue: () => ({ requestId: "", userId: "" }) }) const runtime = ManagedRuntime.make(AppLayer) // once, at startup // In a Hono handler: app.post("/charge", async (c) => { const info: RequestInfo = { requestId: c.req.header("x-request-id") ?? "", userId: c.get("userId") } const result = await runtime.runPromise( chargeUser.pipe(Effect.provideService(RequestInfo, info)) ) return c.json(result) }) // On shutdown: await runtime.dispose() ``` Anti-patterns to avoid: two sequential `runPromise` calls in one handler, a `ChargeOpts`-style parameter bag instead of a `Context.Reference`, telemetry/logging buried inside a `tryPromise` thunk, and re-running `setup()` without disposing the previous runtime (leaks the old runtime and any detached fibers). -
distributed.md 4.7 KB
# Distributed & Durable Execution Effect's distributed-systems modules — Cluster (sharded stateful entities), Workflow (durable, resumable execution), and EventLog (event sourcing) — all live under `effect/unstable/*` and are explicitly **unstable** (APIs may shift between minor releases). Reach for them only when the architecture calls for it; most apps do not. All three follow the same convention: the **tag/name is the first argument** (`Entity.make(tag, ...)`, `Workflow.make(tag, ...)`, `Event.make({ tag, ... })`). ## Cluster: Sharded Entities An `Entity` is a stateful actor addressed by `entityId`; `Sharding` routes messages to the shard owning that id. Messages are typed `Rpc` definitions (see `references/rpc.md`). ```typescript import { Effect, Schema } from "effect" import { Entity, ShardingConfig } from "effect/unstable/cluster" import { Rpc } from "effect/unstable/rpc" class User extends Schema.Class<User>("User")({ id: Schema.Number, name: Schema.String }) {} // Tag first, then an array of Rpc message definitions const UserEntity = Entity.make("UserEntity", [ Rpc.make("GetUser", { payload: { id: Schema.Number }, success: User }) ]) const UserEntityLayer = UserEntity.toLayer({ GetUser: (envelope) => Effect.succeed(new User({ id: envelope.payload.id, name: `User ${envelope.payload.id}` })) }) // In tests, address an entity with an in-memory client: const program = Effect.gen(function*() { const makeClient = yield* Entity.makeTestClient(UserEntity, UserEntityLayer) const client = yield* makeClient("user-1") // addressed by entityId return yield* client.GetUser({ id: 1 }) // message by rpc tag }).pipe(Effect.provide(ShardingConfig.layer({}))) ``` In production, provide a real `Sharding` layer + a runner backend (SQL/Http/Socket) instead of `makeTestClient`, and obtain the client via `UserEntity.client`. ## Workflow: Durable Execution A `Workflow` is execution that survives process restarts — steps (`Activity`) are checkpointed so a resumed workflow replays completed steps instead of re-running them. ```typescript import { Effect, Layer, Schema } from "effect" import { Workflow, WorkflowEngine } from "effect/unstable/workflow" const IncrementWorkflow = Workflow.make("IncrementWorkflow", { payload: { value: Schema.Number }, success: Schema.Number, idempotencyKey: ({ value }) => String(value) }) const IncrementLayer = IncrementWorkflow.toLayer( ({ value }) => Effect.succeed(value + 1) ) const program = Effect.gen(function*() { return yield* IncrementWorkflow.execute({ value: 1 }) // 2 }).pipe( Effect.provide( IncrementLayer.pipe(Layer.provideMerge(WorkflowEngine.layerMemory)) ) ) ``` `Workflow.make(tag, { payload, success?, error?, idempotencyKey?, ... })` returns a workflow with `.execute`, `.poll`, `.interrupt`, `.resume`, `.toLayer`, `.withCompensation`. It also supports class form: `class MyWorkflow extends Workflow.make(...) {}` (the tag-first signature landed in beta.75). Inside a workflow, wrap side-effecting steps in `Activity.make({ name, execute, ... })` and use `Activity.retry({ times })`, `DurableClock.sleep`, and `DurableDeferred` for durable waits. `DurableQueue` provides a persistent work queue. Use `WorkflowEngine.layerMemory` for dev/tests; a cluster-backed engine for production. ## EventLog: Event Sourcing `EventLog` is a full event-sourcing stack: define event groups, write events, and run handlers against a journal (in-memory, IndexedDB, or SQL), with optional encryption and remote replication. ```typescript import { Effect, Layer, Schema } from "effect" import * as EventGroup from "effect/unstable/eventlog/EventGroup" import * as EventJournal from "effect/unstable/eventlog/EventJournal" import * as EventLog from "effect/unstable/eventlog/EventLog" const UserEvents = EventGroup.empty.add({ tag: "UserCreated", primaryKey: (payload) => payload.id, payload: Schema.Struct({ id: Schema.String }) }) const schema = EventLog.schema(UserEvents) const HandlersLive = EventLog.group(UserEvents, (handlers) => handlers.handle("UserCreated", ({ payload }) => Effect.log(`created ${payload.id}`) ) ).pipe(Layer.provide(EventLog.layerRegistry)) const program = Effect.gen(function*() { const log = yield* EventLog.EventLog yield* log.write({ schema, event: "UserCreated", payload: { id: "user-1" } }) }).pipe( Effect.provide( EventLog.layer(schema, HandlersLive).pipe(Layer.provide(EventJournal.layerMemory)) ) ) ``` Build events with `EventGroup.empty.add({ tag, primaryKey, payload })` (or standalone `Event.make({ tag, ... })`), register handlers with `EventLog.group`, and assemble with `EventLog.layer(schema, handlers)` over a journal backend (`EventJournal.layerMemory` / `layerIndexedDb` / SQL). -
effect-ai.md 7.4 KB
# Effect AI The `@effect/ai` packages provide a provider-agnostic interface for language models. Write AI logic once, swap providers at runtime. **Status:** Unstable / Alpha (marked "Unstable" in official docs). APIs may change between releases. ## Packages ### v3 | Package | Purpose | |-----------------------------|--------------------------------------------------| | `@effect/ai` | Core abstractions (LanguageModel, Tool, Toolkit, Chat, McpServer) | | `@effect/ai-openai` | OpenAI provider | | `@effect/ai-anthropic` | Anthropic provider | | `@effect/ai-amazon-bedrock` | Amazon Bedrock provider (v3 only) | | `@effect/ai-google` | Google Gemini provider (v3 only) | | `@effect/ai-openrouter` | OpenRouter provider | ### v4 The core AI module is consolidated into `effect/unstable/ai` (no separate `@effect/ai` package). Provider packages still ship separately and currently include only `@effect/ai-anthropic`, `@effect/ai-openai`, `@effect/ai-openai-compat`, and `@effect/ai-openrouter` — Bedrock and Google providers are not yet ported to v4. ```typescript // v4 imports import { Chat, LanguageModel, McpSchema, McpServer, Tool, Toolkit } from "effect/unstable/ai" ``` ## Basic Text Generation ```typescript // v3 import { LanguageModel } from "@effect/ai" import { OpenAiLanguageModel } from "@effect/ai-openai" // v4 — equivalent imports: // import { LanguageModel } from "effect/unstable/ai" // import { OpenAiLanguageModel } from "@effect/ai-openai" const program = Effect.gen(function*() { const response = yield* LanguageModel.generateText({ prompt: "Explain Effect-TS in one sentence" }) return response.text }) // Provide the OpenAI layer const main = program.pipe( Effect.provide(OpenAiLanguageModel.layer({ model: "gpt-4o" })), Effect.provide(OpenAiClient.layer({ apiKey: env.OPENAI_API_KEY })) ) ``` ## Structured Output (Schema-Validated) ```typescript const SentimentResult = Schema.Struct({ sentiment: Schema.Literal("positive", "negative", "neutral"), confidence: Schema.Number }) const analyze = LanguageModel.generateObject({ prompt: `Analyze sentiment: "${text}"`, schema: SentimentResult }) // Returns Effect<{ sentiment, confidence }, AiError, LanguageModel> // Output is Schema-validated at runtime ``` ## Streaming ```typescript const stream = LanguageModel.streamText({ prompt: "Write a story about..." }) // Returns Stream<TextChunk, AiError, LanguageModel> // Per-chunk processing (for token billing, SSE forwarding, etc.) const processed = stream.pipe( Stream.tap((chunk) => incrementTokenCount(chunk)), Stream.map((chunk) => chunk.text) ) ``` ## Tool Use `Tool.make` defines only the *schema* of a tool — name, description, parameters, success/failure types. The runtime *handler* is attached separately via `Toolkit.toLayer`, which produces a Layer that the LanguageModel call requires. ```typescript import { Effect, Schema } from "effect" import { Tool, Toolkit } from "effect/unstable/ai" // v4 (or "@effect/ai" for v3) // Define a tool — note: NO `handler` field on Tool.make const GetWeather = Tool.make("GetWeather", { description: "Get current weather for a location", parameters: Schema.Struct({ location: Schema.String }), success: Schema.Struct({ temperature: Schema.Number, condition: Schema.String }) }) // Group tools into a toolkit const MyToolkit = Toolkit.make(GetWeather) // Attach handlers via toLayer (handler keys match the tool names) const MyToolkitLayer = MyToolkit.toLayer({ GetWeather: ({ location }) => fetchWeather(location) }) // Use with LanguageModel — provide the toolkit layer const program = LanguageModel.generateText({ prompt: "What's the weather in San Francisco?", toolkit: MyToolkit }).pipe(Effect.provide(MyToolkitLayer)) ``` ## Chat (Stateful Conversations) The Chat module returns a Service whose instance carries `generateText`, `streamText`, and `generateObject` methods. It threads conversation history through a Ref automatically — there is no static `Chat.send`. ```typescript import { Chat } from "effect/unstable/ai" // v4 (or "@effect/ai" for v3) const program = Effect.gen(function*() { const chat = yield* Chat.empty // also: Chat.fromPrompt(initial), Chat.makePersisted(...) const r1 = yield* chat.generateText({ prompt: "Hello, who are you?" }) const r2 = yield* chat.generateText({ prompt: "What did I just say?" }) // r2 sees the prior turn — history is appended in the chat's internal state return [r1.text, r2.text] }) ``` ## MCP Server (v4) Effect v4's AI modules include built-in MCP server support: ```typescript // v4 only import { McpServer, McpSchema } from "effect/unstable/ai" // Define MCP tools using Effect's Schema and service patterns ``` ## Provider Pattern The key benefit: write AI logic against the abstract `LanguageModel` interface, then swap providers via layers: ```typescript // Business logic - no provider dependency const summarize = (text: string) => LanguageModel.generateText({ prompt: `Summarize: ${text}` }) // Production: OpenAI const prod = summarize(text).pipe( Effect.provide(OpenAiLanguageModel.layer({ model: "gpt-4o" })) ) // Development: local model via OpenRouter const dev = summarize(text).pipe( Effect.provide(OpenRouterLanguageModel.layer({ model: "llama-3" })) ) // Testing: mock const test = summarize(text).pipe( Effect.provideService(LanguageModel, { generateText: () => Effect.succeed({ text: "mock summary" }) }) ) ``` ## Provider Fallback (ExecutionPlan) `ExecutionPlan` declares an ordered sequence of attempts, each providing its own `Layer` (e.g. a different model/provider) with optional `attempts` and retry `schedule`. `Effect.withExecutionPlan` runs the effect against each step until one succeeds or the plan is exhausted — ideal for primary -> backup model fallback. ```typescript import { Effect, ExecutionPlan, Schedule } from "effect" import type { Layer } from "effect" import type { LanguageModel } from "effect/unstable/ai" declare const primary: Layer.Layer<LanguageModel.LanguageModel> declare const backup: Layer.Layer<LanguageModel.LanguageModel> const ThePlan = ExecutionPlan.make( // try the primary model twice, 3s apart { provide: primary, attempts: 2, schedule: Schedule.spaced("3 seconds") }, // then fall back to the backup model (one attempt when attempts/schedule omitted) { provide: backup } ) declare const summarize: Effect.Effect<string, never, LanguageModel.LanguageModel> const resilient = Effect.withExecutionPlan(summarize, ThePlan) ``` Each step's `provide` accepts a `Layer` or `Context`; `Stream.withExecutionPlan` is the streaming equivalent. Not AI-specific — use it anywhere you need ordered, layer-swapping fallback (primary -> replica DB, region failover). ## Observability Effect AI integrates with Effect's built-in tracing. Each model call produces spans with: - Model name and provider - Input/output token counts - Duration - Error details Use `Effect.withSpan` to create parent spans for multi-step AI workflows: ```typescript const aiWorkflow = Effect.gen(function*() { const summary = yield* LanguageModel.generateText({ prompt: text }) const analysis = yield* LanguageModel.generateObject({ prompt: summary.text, schema: AnalysisSchema }) return analysis }).pipe(Effect.withSpan("ai.analyze-document")) ``` -
error-modeling.md 5 KB
# Error Modeling Effect has a two-tier error model. Understanding the distinction is critical for writing correct Effect code. ## Expected Errors vs Defects **Expected errors** (typed in `E` channel): recoverable failures your program anticipates. These MUST be handled before running. **Defects** (untyped): unexpected bugs, programmer mistakes, invariant violations. These crash the fiber and are NOT in the type signature. ```typescript // Expected error - appears in the type // v---- typed error const fetchUser: Effect<User, NotFoundError | NetworkError> // Defect - NOT in the type (thrown at runtime) const bad = Effect.sync(() => { throw new Error("bug") }) // ^--- Effect<never, never> - defect is invisible in types ``` ## Defining Errors **v3: `Data.TaggedError`** ```typescript import { Data } from "effect" class NotFoundError extends Data.TaggedError("NotFoundError")<{ readonly id: string }> {} class NetworkError extends Data.TaggedError("NetworkError")<{ readonly cause: unknown }> {} ``` **v4: `Schema.TaggedErrorClass`** ```typescript import { Schema } from "effect" class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", { id: Schema.String }) {} class NetworkError extends Schema.TaggedErrorClass<NetworkError>()("NetworkError", { cause: Schema.Defect() // v4 beta.76+: Schema.Defect is a constructor function, call it }) {} ``` ## Handling Errors ### catchTag - handle a specific error by its _tag ```typescript // v3 const handled = program.pipe( Effect.catchTag("NotFoundError", (e) => Effect.succeed(defaultUser)) ) // v4 - also accepts arrays const handled = program.pipe( Effect.catchTag(["NotFoundError", "NetworkError"], (e) => Effect.succeed(fallback)) ) ``` ### catchAll (v3) / catch (v4) - handle all errors ```typescript // v3 const handled = program.pipe( Effect.catchAll((error) => Effect.succeed(fallback)) ) // v4 const handled = program.pipe( Effect.catch((error) => Effect.succeed(fallback)) ) ``` ### catchTags - handle multiple errors with different handlers ```typescript const handled = program.pipe( Effect.catchTags({ NotFoundError: (e) => Effect.succeed(defaultUser), NetworkError: (e) => Effect.retry(program, Schedule.exponential("1 second")) }) ) ``` ### orDie - convert expected error to defect Use when an error is logically impossible or unrecoverable at this point: ```typescript const critical = program.pipe(Effect.orDie) // Error channel becomes `never` - any failure is a defect ``` ### mapError - transform errors ```typescript const mapped = program.pipe( Effect.mapError((e) => new AppError({ cause: e })) ) ``` ## Defect Handling (Advanced) > **`Effect.catch` / `Effect.catchTag` only handle typed `E`-channel failures.** Defects (thrown bugs, `Effect.die`) and interruptions pass straight through them — a common wrong assumption is that `catch` swallows everything. To handle defects use `Effect.catchDefect`; to handle the full `Cause` (failures + defects + interrupts) use `Effect.catchCause` (see below). Also beware `Effect.catch(() => Effect.void)`: it silently discards the typed error, which turns a real failure into an invisible no-op and is a frequent source of "it just does nothing" debugging dead-ends — log or re-raise instead of returning `void`. Defects are for debugging, not normal recovery. Use sparingly: ```typescript // v3 const withDefectHandling = program.pipe( Effect.catchAllDefect((defect) => Effect.log(`Bug: ${defect}`)) ) // v4 const withDefectHandling = program.pipe( Effect.catchDefect((defect) => Effect.log(`Bug: ${defect}`)) ) ``` ### Sandbox - expose defects as Cause for inspection ```typescript import { Cause, Effect } from "effect" // v3 — uses Effect.catchAllCause const diagnosed = program.pipe( Effect.sandbox, Effect.catchAllCause((cause) => { console.log(Cause.pretty(cause)) return Effect.succeed(fallback) }) ) // v4 — renamed to Effect.catchCause const diagnosedV4 = program.pipe( Effect.sandbox, Effect.catchCause((cause) => { console.log(Cause.pretty(cause)) return Effect.succeed(fallback) }) ) ``` ## v4: Reason-Based Errors v4 adds `catchReason` for errors with a tagged `reason` field: ```typescript // v4 only const handled = program.pipe( Effect.catchReason("AiError", "RateLimitError", (reason) => Effect.retry(program, Schedule.exponential("1 second")) ) ) ``` ## Pattern: Error Hierarchy Design For services with multiple error types, use a discriminated union: ```typescript // v3 class ApiError extends Data.TaggedError("ApiError")<{ readonly kind: "network" | "auth" | "not_found" readonly message: string }> {} // Better: separate tags for type-safe catchTag class NetworkError extends Data.TaggedError("NetworkError")<{ cause: unknown }> {} class AuthError extends Data.TaggedError("AuthError")<{ message: string }> {} class NotFoundError extends Data.TaggedError("NotFoundError")<{ id: string }> {} type ApiError = NetworkError | AuthError | NotFoundError ``` -
graph.md 5 KB
# Graph (`effect/Graph`) `Graph` is Effect's built-in graph data structure - indexed **nodes** and **edges** that both carry your own data, in **directed** or **undirected** form. Reach for it to model dependency graphs, build/task ordering, routing and shortest paths, cycle detection, or any network of relationships without hand-rolling BFS/DFS/Dijkstra. Import from `"effect"`: ```typescript import { Graph } from "effect" ``` Nodes and edges are identified by `Graph.NodeIndex` / `Graph.EdgeIndex`, which are **plain numbers** - stable identifiers, not array offsets. A removed index is not reused. ## Building a Graph Graphs are immutable. Construct one by mutating a scoped-mutable draft inside the builder callback; `addNode` returns the new node's index, which you pass to `addEdge`. ```typescript import { Graph } from "effect" // Directed graph of string nodes and string edge labels const graph = Graph.directed<string, string>((mutable) => { const a = Graph.addNode(mutable, "A") const b = Graph.addNode(mutable, "B") const c = Graph.addNode(mutable, "C") Graph.addEdge(mutable, a, b, "A->B") Graph.addEdge(mutable, b, c, "B->C") }) // Undirected variant: Graph.undirected<N, E>((mutable) => { ... }) ``` Update an existing immutable graph with `Graph.mutate` (returns a new graph; the original is untouched): ```typescript const withExtra = Graph.mutate(graph, (mutable) => { const d = Graph.addNode(mutable, "D") Graph.addEdge(mutable, 2, d, "C->D") // 2 = index of node "C" }) ``` Other mutations (all inside a builder/`mutate` callback): `updateNode`, `updateEdge`, `removeNode`, `removeEdge`, `mapNodes`, `mapEdges`, `filterNodes`, `filterEdges`, `filterMapNodes`, `filterMapEdges`, `reverse`. ## Reading a Graph ```typescript import { Graph } from "effect" Graph.nodeCount(graph) // number of nodes Graph.edgeCount(graph) // number of edges Graph.getNode(graph, 0) // Option<N> for the node at that index Graph.hasNode(graph, 0) // boolean Graph.getEdge(graph, 0) // Option<Edge<E>> Graph.findNode(graph, (n) => n === "B") // Option<NodeIndex> Graph.findEdges(graph, (e) => e.startsWith("A")) ``` `Graph.nodes(graph)` and `Graph.edges(graph)` return **walkers** you iterate with the walker helpers below. Neighbor queries: `Graph.neighbors`, `Graph.successors` (outgoing), `Graph.predecessors` (incoming). `Graph.neighborsDirected` is deprecated (beta.80) - use `successors`/`predecessors`. ## Traversal `dfs`, `bfs`, `topo` (topological sort, Kahn's algorithm), and `dfsPostOrder` return a lazy **walker**. Turn it into values with `Graph.indices` (node indices), `Graph.values` (node data), or `Graph.entries` (`[index, data]` pairs). ```typescript import { Graph } from "effect" // Depth-first from a start node for (const idx of Graph.indices(Graph.dfs(graph, { start: [0] }))) { console.log(idx) } // Breadth-first; omit start to walk all nodes for (const data of Graph.values(Graph.bfs(graph))) { console.log(data) } // Topological order (only valid on a DAG) const order = Array.from(Graph.indices(Graph.topo(graph))) ``` `dfs`/`bfs` accept `{ start, direction }` where `direction` is `"outgoing"` (default) or `"incoming"`; `topo` accepts `{ initials }`. ## Analysis ```typescript import { Graph } from "effect" Graph.isAcyclic(graph) // boolean - true if no cycles (a DAG) Graph.isBipartite(graph) // boolean Graph.connectedComponents(graph) // Array<Array<NodeIndex>> Graph.stronglyConnectedComponents(graph) // Array<Array<NodeIndex>> (directed) ``` ## Shortest Paths `dijkstra` finds the cheapest path between two nodes; `cost` maps each edge's data to a **non-negative** weight. It returns `Option<PathResult<E>>` - `Option.none()` when the target is unreachable. ```typescript import { Graph } from "effect" const weighted = Graph.directed<string, number>((mutable) => { const a = Graph.addNode(mutable, "A") const b = Graph.addNode(mutable, "B") const c = Graph.addNode(mutable, "C") Graph.addEdge(mutable, a, b, 3) Graph.addEdge(mutable, b, c, 4) }) const result = Graph.dijkstra(weighted, { source: 0, target: 2, cost: (edgeData) => edgeData }) if (result._tag === "Some") { console.log(result.value.path) // [0, 1, 2] console.log(result.value.distance) // 7 console.log(result.value.costs) // [3, 4] - edge data along the path } ``` Related solvers: `Graph.astar` (heuristic-guided), `Graph.bellmanFord` (allows negative edge weights), `Graph.floydWarshall` (all-pairs shortest paths). ## Visualization `Graph.toGraphViz(graph, options)` and `Graph.toMermaid(graph, options)` render the graph as DOT or Mermaid diagram source - useful for debugging or docs. ## When to Use Prefer `Graph` when relationships between entities are the core of the problem (ordering, reachability, routing, cycle detection). For a plain adjacency you only ever look up once, a `Map`/`Record` is simpler. `Graph` is a data structure, not an `Effect` - its operations are pure and synchronous; wrap them in `Effect.sync` only if you need them inside an effectful pipeline. -
http.md 9.4 KB
# HTTP Client and Server Import paths differ between versions: - **v3:** `@effect/platform` for client + API, `@effect/platform-node` for Node transports. - **v4:** `effect/unstable/http` for the client and transport helpers, `effect/unstable/httpapi` for the schema-first API layer. `@effect/platform-node` still provides Node-specific `NodeHttpServer` / `NodeHttpClient`. ## HTTP Client (v3 — @effect/platform) ### Making Requests ```typescript import { HttpClient, HttpClientRequest, HttpClientResponse } from "@effect/platform" const program = Effect.gen(function*() { const client = yield* HttpClient.HttpClient // GET request const response = yield* client.execute( HttpClientRequest.get("https://api.example.com/users") ) // POST with JSON body const response = yield* client.execute( HttpClientRequest.post("https://api.example.com/users").pipe( HttpClientRequest.jsonBody({ name: "Alice", age: 30 }) ) ) // With headers const response = yield* client.execute( HttpClientRequest.get("https://api.example.com/data").pipe( HttpClientRequest.setHeader("Authorization", `Bearer ${token}`) ) ) }) ``` ### Response Handling ```typescript // Filter by status (fail on non-2xx) const okResponse = yield* client.execute(request).pipe( HttpClientResponse.filterStatusOk ) // Parse JSON body with Schema validation const users = yield* client.execute(request).pipe( HttpClientResponse.filterStatusOk, HttpClientResponse.schemaBodyJson(Schema.Array(User)) ) // Get raw text const text = yield* response.text // Get raw JSON const json = yield* response.json ``` ### Retry with HttpClient ```typescript // v3 — Schedule.compose still works const resilientClient = client.pipe( HttpClient.retryTransient({ schedule: Schedule.exponential("200 millis").pipe( Schedule.compose(Schedule.recurs(3)) ) }) ) ``` > v4 note: `Schedule.compose` is removed. Use `Schedule.take(n)` instead — see `retry-scheduling.md`. ### Platform Layer Provide the HTTP client layer for your runtime: ```typescript import { NodeHttpClient } from "@effect/platform-node" const main = program.pipe( Effect.provide(NodeHttpClient.layer) ) ``` ## HTTP Server (v3 — @effect/platform) ### Schema-First API Definition ```typescript import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform" // Define endpoints (chained setters - v3 style) const getUser = HttpApiEndpoint.get("getUser", "/users/:id").pipe( HttpApiEndpoint.setPath(Schema.Struct({ id: Schema.String })), HttpApiEndpoint.setSuccess(User) ) const createUser = HttpApiEndpoint.post("createUser", "/users").pipe( HttpApiEndpoint.setPayload(CreateUserBody), HttpApiEndpoint.setSuccess(User) ) // Group endpoints const UsersApi = HttpApiGroup.make("users").pipe( HttpApiGroup.add(getUser), HttpApiGroup.add(createUser) ) // Build the API const MyApi = HttpApi.make("my-api").pipe( HttpApi.addGroup(UsersApi) ) ``` ### Implement Handlers (v3) ```typescript import { HttpApiBuilder } from "@effect/platform" const UsersLive = HttpApiBuilder.group(MyApi, "users", (handlers) => handlers.pipe( HttpApiBuilder.handle("getUser", ({ path }) => Effect.gen(function*() { const db = yield* Database return yield* db.findUser(path.id) }) ), HttpApiBuilder.handle("createUser", ({ payload }) => Effect.gen(function*() { const db = yield* Database return yield* db.createUser(payload) }) ) ) ) ``` ### Serve (v3) ```typescript import { HttpApiBuilder, HttpMiddleware } from "@effect/platform" import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" import { createServer } from "node:http" const ServerLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( Layer.provide(HttpApiBuilder.api(MyApi)), Layer.provide(UsersLive), Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) ) NodeRuntime.runMain(Layer.launch(ServerLive)) ``` ### OpenAPI / Swagger (v3) ```typescript import { HttpApiSwagger } from "@effect/platform" const ServerLive = HttpApiBuilder.serve(HttpMiddleware.logger).pipe( Layer.provide(HttpApiSwagger.layer()), // adds /docs Layer.provide(HttpApiBuilder.api(MyApi)), // ... ) ``` ## HTTP API Server (v4 — effect/unstable/httpapi) v4 replaces the chained endpoint setters with an **object-option** form and moves all the HttpApi modules to `effect/unstable/httpapi`. Transport helpers live in `effect/unstable/http`. `HttpApiScalar` replaces `HttpApiSwagger` as the canonical docs UI (Swagger still exists). ### Define endpoints with object options ```typescript import { Schema } from "effect" import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi" const User = Schema.Struct({ id: Schema.Number, name: Schema.String, email: Schema.String }) class UserNotFound extends Schema.TaggedErrorClass<UserNotFound>()("UserNotFound", { id: Schema.Number }) {} // Endpoints take (name, path, options) — no chained setters const Users = HttpApiGroup.make("users") .add( HttpApiEndpoint.get("list", "/", { query: { search: Schema.optional(Schema.String) }, success: Schema.Array(User) }) ) .add( HttpApiEndpoint.get("getById", "/:id", { params: { id: Schema.NumberFromString }, success: User, error: UserNotFound }) ) .add( HttpApiEndpoint.post("create", "/", { payload: Schema.Struct({ name: Schema.String, email: Schema.String }), success: User }) ) export const Api = HttpApi.make("api").add(Users) ``` ### Implement handlers with `Effect.fn` ```typescript import { Effect, Layer } from "effect" import { HttpApiBuilder } from "effect/unstable/httpapi" const UsersApiHandlers = HttpApiBuilder.group( Api, "users", Effect.fn(function*(handlers) { const db = yield* Database return handlers .handle("list", ({ urlParams }) => db.listUsers(urlParams.search)) .handle("getById", ({ path }) => db.findUser(path.id)) .handle("create", ({ payload }) => db.createUser(payload)) }) ) ``` ### Serve + docs UI ```typescript import { FetchHttpClient } from "effect/unstable/http" import { HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi" import { NodeHttpServer, NodeRuntime } from "@effect/platform-node" import { createServer } from "node:http" const DocsRoute = HttpApiScalar.layer(Api, { path: "/docs" }) const ApiRoutes = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(Layer.provide([UsersApiHandlers])) const ServerLive = Layer.mergeAll(ApiRoutes, DocsRoute).pipe( Layer.provide(NodeHttpServer.layer(createServer, { port: 3000 })) ) NodeRuntime.runMain(Layer.launch(ServerLive)) ``` ### HttpApi schema errors default to defects (v4, since PR #2057) Endpoint parse/schema failures no longer appear in the typed error channel by default — they surface as defects. If you want typed error responses (e.g. `400 Bad Request` with a structured body), transform the failure through `HttpApiSchema` helpers or add an explicit `error` schema on the endpoint: ```typescript import { HttpApiSchema } from "effect/unstable/httpapi" class BadInput extends Schema.TaggedErrorClass<BadInput>()("BadInput", { message: Schema.String }) {} HttpApiEndpoint.post("create", "/", { payload: CreateUserBody.pipe(HttpApiSchema.withBadRequest(BadInput)), success: User, error: BadInput }) ``` Without this transform, bad payloads cause a defect (500-style) instead of a typed error. This is a deliberate 2026-04-20 change (commit `8e04bfc9`). ### File uploads (Multipart, v4) For `multipart/form-data` (file uploads), brand the endpoint payload with `HttpApiSchema.asMultipart()` and type file fields with the schemas from `effect/unstable/http/Multipart` (`SingleFileSchema`, `FilesSchema`, `PersistedFileSchema`). Files are persisted to disk and arrive as `PersistedFile` (with `.path`, `.name`, `.contentType`). ```typescript import { Schema } from "effect" import { HttpApiEndpoint, HttpApiSchema } from "effect/unstable/httpapi" import { Multipart } from "effect/unstable/http" const upload = HttpApiEndpoint.post("upload", "/upload", { payload: Schema.Struct({ title: Schema.String, file: Multipart.SingleFileSchema // a single PersistedFile; FilesSchema for many }).pipe(HttpApiSchema.asMultipart()), success: Schema.Struct({ path: Schema.String }) }) // handler: ({ payload }) => ... payload.file.path / payload.file.name ``` Tune limits with `Context.Reference`s from `Multipart` (`MaxFileSize`, `MaxParts`, `MaxFieldSize`, `FieldMimeTypes`) provided into the server layer. Outside HttpApi, a raw `HttpServerRequest` exposes `request.multipart` (buffered) and `request.multipartStream` (streaming), plus `HttpServerRequest.schemaBodyMultipart(schema)`. Use `HttpApiSchema.asMultipartStream()` for streaming large uploads instead of buffering. ## Integrating Effect with Hono Use `ManagedRuntime` to bridge Effect into Hono routes: ```typescript import { Hono } from "hono" import { ManagedRuntime } from "effect" // Build runtime once from your app layer const runtime = ManagedRuntime.make( Layer.mergeAll(DatabaseLive, CacheLive, LoggerLive) ) const app = new Hono() app.get("/users/:id", async (c) => { const result = await runtime.runPromise( Effect.gen(function*() { const db = yield* Database return yield* db.findUser(c.req.param("id")) }) ) return c.json(result) }) ``` Layers are created once and reused across all requests. Do NOT call `Layer.provide` + `Effect.runPromise` per request - that rebuilds layers every time. -
llm-corrections.md 10.7 KB
# LLM Corrections: Wrong vs Correct Effect APIs This is the exhaustive reference for preventing hallucinated Effect code. Check this before using any API you're unsure about. ## Non-Existent APIs (frequently hallucinated) | Hallucinated API | What to use instead | |---------------------------------------|--------------------------------------------------------------| | `Effect.cachedWithTTL` | `Cache.make({ capacity, timeToLive, lookup })` | | `Effect.cachedInvalidateWithTTL` | `cache.invalidate(key)` / `cache.invalidateAll()` | | `Effect.retryN(n)` | `Effect.retry(Schedule.recurs(n))` | | `Effect.retryWithBackoff` | `Effect.retry(Schedule.exponential("100 millis"))` | | `Effect.timeout(effect, ms)` | `Effect.timeout(effect, "5 seconds")` (Duration string) | | `Effect.timeoutTo` | `Effect.timeoutTo(effect, { duration, onTimeout })` | | `Effect.parallel` | `Effect.all([...], { concurrency: "unbounded" })` | | `Effect.race(a, b)` (array form) | `Effect.race(a, b)` takes exactly two effects | | `Effect.raceAll([...])` | `Effect.raceAll(effects)` (single iterable argument) | | `Effect.withTimeout` | `Effect.timeout("5 seconds")` in a pipe | | `Effect.bracket` | `Effect.acquireUseRelease(acquire, use, release)` | | `Effect.ensuring(effect, finalizer)` | `Effect.ensuring(finalizer)` in a pipe | | `Effect.supervised` | Use `Effect.forkScoped` or manual fiber management | | `Effect.blocking` | `Effect.sync` or `Effect.tryPromise` (no separate blocking) | | `Stream.fromSSE` | Build from `HttpClient` response + chunk parsing | | `Layer.fromEffect` | `Layer.effect(Tag, effect)` | | `Layer.fromFunction` | `Layer.succeed(Tag, implementation)` | | `Layer.fromService` | `Layer.effect(Tag, Effect.gen(function*() { ... }))` | | `Schema.nullable` | `Schema.NullOr(schema)` | | `Schema.optional` (standalone) | `Schema.optional(schema)` for struct fields only | | `Schema.makeUnsafe(input)` (v4) | `Schema.make(input)` - throws `SchemaError` on invalid input; also instance methods `schema.makeOption(...)`, `schema.makeEffect(...)` | | `ServiceMap.Service` / `ServiceMap.Reference` (v4) | Renamed back to `Context.Service` / `Context.Reference` on 2026-04-07 (PR #1961). Import `Context` from `"effect"`. | | `Otlp.layer({ url, serviceName })` (v4) | Canonical split form: `OtlpTracer.layer({ url, resource: { serviceName } })` + `OtlpSerialization.layerJson` + `FetchHttpClient.layer`. The aggregator `Otlp.layer` exists but uses `baseUrl` + `resource.serviceName`, not `url` + `serviceName`. | | Chained `HttpApiEndpoint.get(n, p).pipe(HttpApiEndpoint.setPath(...), setPayload(...), setSuccess(...))` (v4) | Object-option form: `HttpApiEndpoint.get(name, path, { params, query, payload, success, error })` | | `HttpApiEndpoint` parse failures surface as typed errors (v4) | In current v4 betas, endpoint schema failures default to **defects** unless transformed via `HttpApiSchema` helpers. Use `HttpApiSchemaError` when you need typed error responses. | | `Layer.scoped(Tag, eff)` (v4) | Removed. `Layer.effect(Tag, eff)` strips `Scope` from the requirements automatically. | | `Effect.async((resume) => ...)` (v4) | Renamed to `Effect.callback`. Register signature is `(resume, signal: AbortSignal) => void \| Effect<void, never, R>`. | | `Effect.makeSemaphore(n)` (v4) | Removed from `Effect`. Use `Semaphore.make(n)` from the `Semaphore` module. `withPermits` is data-first: `Semaphore.withPermits(sem, n)(eff)`. | | `Effect.logSpan(label)(eff)` (v3 + v4) | Never existed. Use `Effect.withLogSpan(label)(eff)`. | | `Schedule.compose(other)` (v4) | Removed. Use `Schedule.take(n)` to bound by attempt count, or `Effect.retry(eff, { schedule, times })`. | | `Schedule.once` (v4) | Removed. Use `Schedule.recurs(0)` (run once, no retry) or `Schedule.recurs(1)`. | | `Tool.make("name", { ..., handler: ... })` | `Tool.make` defines schema only — there is no `handler` field. Attach handlers via `MyToolkit.toLayer({ ToolName: (params) => effect })`. | | `Chat.make()` / `Chat.send(session, msg)` | No such exports. Use `yield* Chat.empty` (or `Chat.fromPrompt`, `Chat.makePersisted`) and call methods on the instance: `chat.generateText({ prompt })`, `chat.streamText({ prompt })`, `chat.generateObject({ prompt, schema })`. | | `@effect/ai-amazon-bedrock` / `@effect/ai-google` (v4) | Not yet ported to v4. v4 ships only `@effect/ai-anthropic`, `@effect/ai-openai`, `@effect/ai-openai-compat`, `@effect/ai-openrouter`. | | `cause: Schema.Defect` / `error: Schema.Error` as bare field values (v4) | Since beta.76 `Schema.Defect` and `Schema.Error` are **constructor functions** — call them: `Schema.Defect()` / `Schema.Error()`. Stack control moved to an option: `Schema.Error({ includeStack: true })` (the old `Schema.ErrorWithStack` / `Schema.DefectWithStack` are removed). | | `Random.nextUUIDv4()` (v4) | Removed — `Random` is not cryptographically secure. Use the `Crypto` service: `yield* Crypto.Crypto` then `crypto.randomUUIDv4` / `crypto.randomUUIDv7`. | ## Wrong Import Paths | Wrong | Correct | |----------------------------------------------|--------------------------------------------| | `import { Schema } from "@effect/schema"` | `import { Schema } from "effect"` | | `import { JSONSchema } from "@effect/schema"`| `import { JSONSchema } from "effect"` | | `import { ParseResult } from "@effect/schema"` | `import { ParseResult } from "effect"` (v3). In v4 `ParseResult` is gone — use the `SchemaIssue` module for issue inspection and the `SchemaError` class (re-exported from `Schema`) as the thrown/failed value. Narrow with `Schema.isSchemaError(e)`, then inspect `e.issue` via `SchemaIssue.makeFormatterStandardSchemaV1()(e.issue).issues`. | | `import { Effect } from "@effect/io"` | `import { Effect } from "effect"` | | `import { Layer } from "@effect/io"` | `import { Layer } from "effect"` | | `import { Stream } from "@effect/stream"` | `import { Stream } from "effect"` | | `import { HttpClient } from "@effect/platform"` | `import { HttpClient } from "@effect/platform"` (v3) or `"effect/unstable/http"` (v4) | | `import { HttpApi, HttpApiEndpoint, HttpApiGroup } from "@effect/platform"` (v4) | `import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi"` | | `import { Otlp } from "effect/unstable/observability"` + single `Otlp.layer(...)` | `import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability"` + `import { FetchHttpClient } from "effect/unstable/http"` | | `import { RateLimiter } from "effect"` (v4) | `import { RateLimiter } from "effect/unstable/persistence"`. v4 exposes a Service (`RateLimiter.layer` + `RateLimiter.layerStoreMemory`) and the `makeWithRateLimiter` accessor — there is no top-level `RateLimiter.make`/`withCost`. | ## Wrong Patterns ### Running effects inside effects (DO NOT) ```typescript // WRONG - calling runPromise inside an Effect pipeline const bad = Effect.gen(function*() { const result = Effect.runPromise(someEffect) // NO! return result }) // CORRECT - compose effects, run only at the edge const good = Effect.gen(function*() { const result = yield* someEffect return result }) ``` ### Missing scope for acquireRelease (DO NOT) ```typescript // WRONG - acquireRelease without scoped const bad = Effect.gen(function*() { const conn = yield* Effect.acquireRelease( openConn(), (conn) => closeConn(conn) ) return yield* conn.query("SELECT 1") }) // CORRECT - wrap in Effect.scoped const good = Effect.scoped( Effect.gen(function*() { const conn = yield* Effect.acquireRelease( openConn(), (conn) => closeConn(conn) ) return yield* conn.query("SELECT 1") }) ) ``` ### Generator yield without star (DO NOT) ```typescript // WRONG - yield without * const bad = Effect.gen(function*() { const x = yield someEffect // Missing *! This yields the Effect object itself }) // CORRECT - yield* const good = Effect.gen(function*() { const x = yield* someEffect }) ``` ### Not returning on error raise (DO NOT) ```typescript // WRONG - error doesn't stop execution in TS's view const bad = Effect.gen(function*() { if (!input) { yield* Effect.fail(new InputError({ message: "missing" })) } // TS thinks this code runs even after fail ^ yield* doWork(input) // input might be undefined }) // CORRECT - return yield* to signal control flow const good = Effect.gen(function*() { if (!input) { return yield* Effect.fail(new InputError({ message: "missing" })) } yield* doWork(input) // TS knows input is defined here }) ``` ### Point-free/tacit function passing (DO NOT) ```typescript // WRONG - generics get erased, overloads break const bad = Effect.map(myEffect, JSON.parse) // CORRECT - explicit lambda preserves types const good = Effect.map(myEffect, (x) => JSON.parse(x)) ``` ## Terminology Corrections | Wrong term | Correct term | |--------------------------|--------------------------------------------| | "cancel a fiber" | "interrupt a fiber" | | "thread-local storage" | "fiber-local storage" (FiberRef / Reference) | | "worker pool" | `concurrency` option on `Effect.all/forEach` | | "dependency container" | "Layer"; `Context.Tag` (v3) or `Context.Service` (v4) — both live in the `Context` module | | "middleware" (for layers)| "Layer" (layers compose, not chain) | | "async effect" | Effects are lazy by default, both sync/async | | "Observable" / "RxJS" | Effect is single-shot (like lazy Promise), not multi-shot | ## Myths to Not Repeat 1. **"Effect is 500x slower"** - Only for trivial `1+1` micro-ops. For real IO-bound services, fiber overhead is unmeasurable 2. **"Huge bundle size"** - v3 minimum ~25KB gzipped; v4 minimum ~6.3KB gzipped. Tree-shaking friendly 3. **"Generators are slow"** - Effect internals are NOT built on generators. Generator API is equally performant to async/await 4. **"Same as RxJS"** - Effect's base type is single-shot (like a lazy Promise), not multi-shot like Observables -
migration-async.md 4 KB
# Migrating from async/await to Effect Mechanical conversion patterns for moving Promise-based code to Effect. ## Promise -> Effect ```typescript // BEFORE: async/await async function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`) if (!response.ok) throw new Error("Not found") return response.json() } // AFTER: Effect (v3) const fetchUser = (id: string): Effect.Effect<User, FetchError> => Effect.tryPromise({ try: () => fetch(`/api/users/${id}`).then((r) => { if (!r.ok) throw new Error("Not found") return r.json() }), catch: (err) => new FetchError({ cause: err }) }) // AFTER: Effect (v4 with Effect.fn) const fetchUser = Effect.fn("fetchUser")( function*(id: string): Effect.fn.Return<User, FetchError> { return yield* Effect.tryPromise({ try: () => fetch(`/api/users/${id}`).then((r) => { if (!r.ok) throw new Error("Not found") return r.json() }), catch: (err) => new FetchError({ cause: err }) }) } ) ``` ## try/catch -> typed errors ```typescript // BEFORE async function processPayment(amount: number) { try { const result = await chargeCard(amount) return result } catch (err) { if (err instanceof InsufficientFunds) { return { status: "declined" } } throw err // re-throw unknown errors } } // AFTER const processPayment = (amount: number) => chargeCard(amount).pipe( Effect.catchTag("InsufficientFunds", () => Effect.succeed({ status: "declined" as const }) ) // Unknown errors become defects automatically ) ``` ## Promise.all -> Effect.all ```typescript // BEFORE const [user, posts] = await Promise.all([ fetchUser(id), fetchPosts(id) ]) // AFTER (parallel) const [user, posts] = yield* Effect.all( [fetchUser(id), fetchPosts(id)], { concurrency: "unbounded" } ) // With bounded concurrency const results = yield* Effect.all( urls.map((url) => fetchUrl(url)), { concurrency: 5 } ) ``` ## Callback APIs ```typescript // BEFORE function readFile(path: string): Promise<string> { return new Promise((resolve, reject) => { fs.readFile(path, "utf-8", (err, data) => { if (err) reject(err) else resolve(data) }) }) } // AFTER (v3) — Effect.async const readFile = (path: string): Effect.Effect<string, FileError> => Effect.async((resume) => { fs.readFile(path, "utf-8", (err, data) => { if (err) resume(Effect.fail(new FileError({ cause: err }))) else resume(Effect.succeed(data)) }) }) // AFTER (v4) — Effect.async is renamed to Effect.callback. The register // receives a second positional `signal: AbortSignal` argument for cancellation // (ignore it when not needed). const readFile = (path: string): Effect.Effect<string, FileError> => Effect.callback((resume, signal) => { fs.readFile(path, "utf-8", (err, data) => { if (err) resume(Effect.fail(new FileError({ cause: err }))) else resume(Effect.succeed(data)) }) }) ``` ## Class with state -> Service + Ref ```typescript // BEFORE class Counter { private count = 0 increment() { this.count++ } getCount() { return this.count } } // AFTER class Counter extends Context.Tag("Counter")<Counter, { readonly increment: Effect.Effect<void> readonly getCount: Effect.Effect<number> }>() {} const CounterLive = Layer.effect(Counter, Effect.gen(function*() { const ref = yield* Ref.make(0) return { increment: Ref.update(ref, (n) => n + 1), getCount: Ref.get(ref) } }) ) ``` ## Key Principles 1. **Effects are lazy**: nothing runs until `run*` is called. Don't mix `await` and `yield*` 2. **Errors are typed**: convert thrown exceptions to typed errors at boundaries with `Effect.tryPromise` 3. **Dependencies are explicit**: extract shared state/clients into services with `Context.Tag` (v3) / `Context.Service` (v4) 4. **run* at edges only**: libraries return `Effect` values. Only call `runPromise` / `runMain` at the program boundary 5. **Incremental adoption**: you can wrap individual functions with `Effect.tryPromise` and gradually expand -
migration-v4.md 18.9 KB
# Migrating from Effect v3 to v4 Effect v4 (beta, February 2026) is a major release. The core programming model (Effect, Layer, Schema, Stream) is unchanged, but naming, imports, and some APIs have changed significantly. Source: https://github.com/Effect-TS/effect-smol/blob/main/MIGRATION.md ## Installation ```bash npm install effect@beta # Companion packages must match versions: npm install @effect/platform-node@beta @effect/opentelemetry@beta ``` ## Structural Changes ### Unified Versioning All packages share a single version (e.g., `effect@4.0.0-beta.X`, `@effect/sql-pg@4.0.0-beta.X`). ### Package Consolidation Many packages merged into `effect`. Remaining separate: `@effect/platform-*`, `@effect/sql-*`, `@effect/ai-*`, `@effect/opentelemetry`, `@effect/vitest`. ### Unstable Modules New `effect/unstable/*` paths may break in minor releases: `ai`, `cli`, `cluster`, `devtools`, `http`, `httpapi`, `observability`, `rpc`, `sql`, `workflow`, `workers`, etc. ### Bundle Size ~70KB (v3) -> ~20KB (v4) for Effect + Stream + Schema. Minimal: ~6.3KB gzipped. ## Services: Context.Tag -> Context.Service v4 briefly introduced a `ServiceMap` module for service definitions. On 2026-04-07 (PR #1961) it was renamed back to `Context`. Any doc or older beta code that says `ServiceMap.Service` / `ServiceMap.Reference` should be read as the current `Context.Service` / `Context.Reference`. | v3 | v4 | |---------------------------------------|-------------------------------------------| | `Context.GenericTag<T>(id)` | `Context.Service<T>(id)` | | `Context.Tag(id)<Self, Shape>()` | `Context.Service<Self, Shape>()(id)` | | `Effect.Tag(id)<Self, Shape>()` | `Context.Service<Self, Shape>()(id)` | | `Effect.Service<Self>()(id, opts)` | `Context.Service<Self>()(id, { make })` | | `Context.Reference<Self>()(id, opts)` | `Context.Reference<T>(id, opts)` | ```typescript // v3 class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>() {} // v4 class Database extends Context.Service<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>()( "myapp/Database" ) {} ``` **Static accessors removed.** Use `Service.use()` or `yield*` in generators: ```typescript // v3: Notifications.notify("hello") (proxy accessor) // v4: Notifications.use((n) => n.notify("hello")) // v4 preferred: yield* Notifications in Effect.gen ``` **Layer naming:** `.Default` / `.Live` -> `.layer`, `.layerTest`, `.layerConfig` **No auto-generated layers in v4.** Build explicitly with `Layer.effect(this, this.make)`. ## Error Handling Renames | v3 | v4 | |--------------------------|--------------------------------| | `Effect.catchAll` | `Effect.catch` | | `Effect.catchAllCause` | `Effect.catchCause` | | `Effect.catchAllDefect` | `Effect.catchDefect` | | `Effect.catchSome` | `Effect.catchFilter` | | `Effect.catchSomeCause` | `Effect.catchCauseFilter` | | `Effect.catchSomeDefect` | Removed | | `Effect.catchTag` | Unchanged (also accepts arrays)| | `Effect.catchTags` | Unchanged | **New in v4:** `Effect.catchReason`, `Effect.catchReasons`, `Effect.catchEager` ## Forking Renames | v3 | v4 | |-------------------------------|---------------------| | `Effect.fork` | `Effect.forkChild` | | `Effect.forkDaemon` | `Effect.forkDetach` | | `Effect.forkScoped` | Unchanged | | `Effect.forkIn` | Unchanged | | `Effect.forkAll` | Removed | | `Effect.forkWithErrorHandler` | Removed | Fork options: `{ startImmediately?: boolean, uninterruptible?: boolean | "inherit" }` ## FiberRef -> Context.Reference `FiberRef`, `FiberRefs`, `FiberRefsPatch`, `Differ` are removed. Fiber-local state is now handled by `Context.Reference` — the same mechanism used for services with default values. Built-in fiber-local values are exported from the `References` namespace. | v3 | v4 | |---------------------------------|------------------------------------| | `FiberRef.currentLogLevel` | `References.CurrentLogLevel` | | `FiberRef.currentConcurrency` | `References.CurrentConcurrency` | | `FiberRef.get(ref)` | `yield* References.X` | | `Effect.locally(e, ref, value)` | `Effect.provideService(e, Ref, v)` | ## Either -> Result | v3 | v4 | |-------------------|--------------------| | `Either` | `Result` | | `Either.right(x)` | `Result.ok(x)` | | `Either.left(e)` | `Result.err(e)` | | `Effect.either` | `Effect.result` | ## Yieldable (Types No Longer Effect Subtypes) In v3, `Ref`, `Deferred`, `Fiber`, `Option`, `Either`, `Config` etc. were structural subtypes of `Effect`. In v4, they implement `Yieldable` instead - `yield*` still works in generators, but they can't be passed directly to Effect combinators. ```typescript // v3: yield* ref -> reads the ref value // v4: yield* Ref.get(ref) // v3: yield* fiber -> joins the fiber // v4: yield* Fiber.join(fiber) // v3: yield* deferred -> awaits the deferred // v4: yield* Deferred.await(deferred) // v3: Effect.map(option, fn) -> worked because Option was Effect // v4: Effect.map(option.asEffect(), fn) -> explicit conversion needed ``` ## Runtime<R> Removed | v3 | v4 | |----------------------------------|---------------------------------------| | `Effect.runtime<R>()` | `Effect.services<R>()` | | `Runtime.runFork(runtime)(eff)` | `Effect.runForkWith(services)(eff)` | ## Effect.fn (New in v4) Preferred way to write functions returning Effects. Adds automatic span + better stack traces: ```typescript const fetchUser = Effect.fn("fetchUser")( function*(id: string): Effect.fn.Return<User, NotFoundError> { const db = yield* Database return yield* db.findUser(id) }, // Additional combinators (no .pipe needed) Effect.catch((e) => Effect.log(`Error: ${e}`)) ) ``` ## Schema Changes (Major) ### Renames | v3 | v4 | |-------------------------------|---------------------------------------| | `Schema.TaggedError` | `Schema.TaggedErrorClass` | | `Schema.decodeUnknown` | `Schema.decodeUnknownEffect` | | `Schema.decode` | `Schema.decodeEffect` | | `Schema.encode` | `Schema.encodeEffect` | | `Schema.decodeUnknownEither` | `Schema.decodeUnknownExit` | | `Schema.Literal("a","b")` | `Schema.Literals(["a","b"])` | | `Schema.Union(A, B)` | `Schema.Union([A, B])` | | `Schema.Tuple(A, B)` | `Schema.Tuple([A, B])` | | `Schema.pick("a")` | `.mapFields(Struct.pick(["a"]))` | | `Schema.omit("a")` | `.mapFields(Struct.omit(["a"]))` | | `Schema.partial` | `.mapFields(Struct.map(Schema.optional))` | | `Schema.extend(B)` | `.mapFields(Struct.assign(fieldsB))` | | `Schema.filter(pred)` | `.check(Schema.makeFilter(pred))` | | `Schema.positive()` | `Schema.isGreaterThan(0)` | | `Schema.int()` | `Schema.isInt()` | | `Schema.minLength(n)` | `Schema.isMinLength(n)` | ### Transform syntax change ```typescript // v3 Schema.transform(FromSchema, ToSchema, { decode, encode }) // v4 FromSchema.pipe( Schema.decodeTo(ToSchema, SchemaTransformation.transform({ decode, encode })) ) ``` ### optionalWith changes | v3 options | v4 | |-------------------------------|-----------------------------------------------------------| | `{ exact: true }` | `Schema.optionalKey(schema)` | | `{ default }` | `schema.pipe(Schema.withDecodingDefaultType(...))` | | `{ exact: true, default }` | `schema.pipe(Schema.withDecodingDefaultTypeKey(...))` | > The non-`Type` variants (`withDecodingDefault`, `withDecodingDefaultKey`) also exist in v4 but apply the default to the **Encoded** side. v3's `optionalWith({ default })` applied the default on the Type (decoded) side, so the `*Type*` variants are the correct migration target. ### Equality `Equal.equals` performs deep structural comparison by default in v4. `Schema.Data` is removed (unnecessary). ## Other API Removals & Renames | v3 | v4 | |------------------------------------------|-----------------------------------------------------------------------------------| | `Layer.scoped(Tag, eff)` | `Layer.effect(Tag, eff)` — strips `Scope` from requirements automatically | | `Effect.async((resume) => ...)` | `Effect.callback((resume, signal) => ...)` — `signal: AbortSignal` is positional | | `Effect.makeSemaphore(n)` | `Semaphore.make(n)` (import `Semaphore` from `"effect"`) | | `semaphore.withPermits(n)(eff)` | `Semaphore.withPermits(semaphore, n)(eff)` — data-first | | `Schedule.compose(Schedule.recurs(n))` | `Schedule.take(n)` (bound by attempt count) or `Effect.retry(_, { schedule, times })` | | `Schedule.once` | `Schedule.recurs(0)` | | `import { RateLimiter } from "effect"` | `import { RateLimiter } from "effect/unstable/persistence"` — Service-based API; no `withCost`, use `tokens` option | ## Quick Checklist for v3 -> v4 1. Replace `Context.Tag` / `Effect.Tag` / `Effect.Service` with `Context.Service` 2. Replace `Effect.catchAll` with `Effect.catch` (and similar renames) 3. Replace `Effect.fork` with `Effect.forkChild`, `Effect.forkDaemon` with `Effect.forkDetach` 4. Replace `FiberRef.*` with `Context.Reference` / `References.*` 5. Replace `yield* ref` with `yield* Ref.get(ref)`, same for Fiber/Deferred 6. Replace `Data.TaggedError` with `Schema.TaggedErrorClass` 7. Update Schema API calls (variadic to array, filter renames, transform syntax) 8. Replace `Effect.either` with `Effect.result` 9. Update layer naming (`.Default` -> `.layer`) 10. Use `Effect.fn("name")` for new functions 11. Replace `Layer.scoped` with `Layer.effect` 12. Replace `Effect.async` with `Effect.callback` 13. Replace `Effect.makeSemaphore` with `Semaphore.make`; switch `withPermits` to data-first 14. Replace `Schedule.compose(Schedule.recurs(n))` with `Schedule.take(n)`; replace `Schedule.once` with `Schedule.recurs(0)` 15. Move `RateLimiter` import from `"effect"` to `"effect/unstable/persistence"` and switch to its Service-based API ## v4 beta additions (through beta.92) These landed in later betas and are worth knowing if you are currently on an older beta. The skill tracks `effect@4.0.0-beta.92`; companion packages share that single version. ### beta.55-58 (through 2026-04-28) - `Effect.abortSignal` for bridging AbortController-based APIs (beta.57, PR #2085) - `@effect/sql-pglite` package wrapping `@electric-sql/pglite` (beta.57, PR #2073) - `Effectable` module for lifting existing types into Effect (beta.55-ish) - `Socket.make` constructor (beta.57, PR #2078) - `RpcGroup.omit` for deriving subsets of RPC groups - `AtomRpc.query` requires an explicit serialization option for serializable atoms (PR #2040) - **HttpApi schema errors now default to defects** unless transformed (PR #2057, 2026-04-20). See `references/http.md` for how to surface them as typed errors via `HttpApiSchema` transforms. - `Schema.withDecodingDefaultType` / `...TypeKey` added alongside the Encoded-side variants (PR #2013, 2026-04-10) - `AsyncResult.builder` (`effect/unstable/reactivity`) gained `.onInterrupt(...)` and an `.exhaustive()` finalizer; `.onDefect` / `.onFailure` typing refined so `.exhaustive()` is only callable when every case (success, error, initial, defect, interrupt) is handled (beta.58, PR #2097, 2026-04-28). Use `.exhaustive(): Out` instead of `.render(): Out | null` when you want a non-nullable result. - Stream -> `Uint8Array` conversion and HTTP body consumption now use fewer buffer copies - internal perf only, no public API change (beta.58, PR #2098, 2026-04-27). ### beta.59-78 (2026-04-29 through 2026-06) Breaking / behavioral: - **`Schema.Error` / `Schema.Defect` are now constructor functions**, not constants — write `Schema.Error()` / `Schema.Defect()`. `ErrorWithStack` / `DefectWithStack` folded into `{ includeStack: true }`. `Schema.Defect()` now models defects as `unknown` with a JSON-encoded form, so non-`Error` objects no longer round-trip unchanged (beta.76, PR #2318). - **`Random.nextUUIDv4` removed** — `Random` is not cryptographically secure. Use the new platform-agnostic `Crypto` service's `randomUUIDv4` / `randomUUIDv7` (beta.68, PR #2180). - **`Effect.Yieldable` export removed** (beta.66, PR #2163). The Yieldable *concept* still applies (Ref/Deferred/Fiber/Option/Config implement it); only the re-export off `Effect` is gone. - `Types.MergeRecord` removed -> use `Types.MergeLeft` (beta.75, PR #2298). - `SchemaParser.makeUnsafe` -> `SchemaParser.make` (beta.67, PR #2172). - `Schema.asserts` signature changed to `asserts(schema, input)`; `Schema.Codec.ToAsserts` removed (beta.68, PR #2221). - `Model.Generated` -> `Model.GeneratedByDb` (beta.68, PR #2207). See `references/sql.md`. - `Workflow.make` now takes the tag as its **first** argument and supports `class X extends Workflow.make(...) {}` (beta.75, PR #2294). See `references/distributed.md`. - `Inspectable.stringifyCircular` removed (beta.60, PR #2119). Fixes / additions worth knowing: - `catch*` combinators no longer silently drop unhandled error types — the residual error channel is now preserved (beta.71, PR #2257). - `Effect.firstSuccessOf` ported from v3 (beta.61, PR #2120); `Effect.acquireDisposable` added (beta.63, PR #2123). - `Schedule.tap` added — observe full schedule metadata without altering inputs/outputs (beta.71, PR #2252). - `Stream.broadcastN` for fixed-size stream broadcasts (beta.68, PR #2210); `Channel.decodeText` UTF-8-across-chunk fix (beta.68, PR #2209). - `HttpApiTest` module added for testing HttpApi servers (beta.63, PR #2136); `HttpApiSecurity.http` for custom schemes (beta.73, PR #2291). - `Schema.DurationFromString` (beta.60, PR #2117); `Schema.isGUID` + RFC 9562 max-UUID support in `Schema.isUUID` (beta.76, PR #2320). - OTLP observability now reads `OTEL_*` environment variables and prefers them over explicit `OtlpResource.fromConfig` options (beta.77, PRs #2325/#2326). - `Config.literals` convenience constructor for `Schema.Literals` (beta.60, PR #2116). ### beta.79-92 (2026-06 through 2026-07) Breaking / behavioral: - **`SchemaError` now extends `Data.TaggedError`** (beta.84, PR #2407) — it is also a native `Error` with `_tag: "SchemaError"`, catchable via `Effect.catchTag`. The `SchemaParser` Promise APIs reject an `Error` whose `cause` is the `SchemaIssue.Issue`; the `is`/`asserts`/`Promise`/`Sync`/`Result`/`Option`/`make`/`makeOption` adapters now distinguish schema issues from non-schema causes. - **`Schema.Void` now models ignored `void` return values** (beta.89, PR #2475) — it accepts any present value and discards it as `undefined`. Use `Schema.Undefined` when you need to match `undefined` exactly. - **`Config.make` low-level constructor removed** (beta.84, PR #2383) — use the config constructors/combinators or `ConfigProvider.make`. `ConfigProvider.fromDir` now returns `undefined` when neither file nor dir exists, so you can chain `orElse` fallbacks. - **`Config.withDefault` recovery narrowed** (beta.81, PRs #2387/#2388) — it now only recovers from *missing* data for literal/union schemas; present-but-invalid values and filter failures propagate the validation error instead of falling back to the default. `Config.schema` also treats a missing array value as missing data so `withDefault` applies (beta.90, PR #2483). - **`Effect.try` accepts a thunk directly** (beta.84, PR #2415), matching `Effect.tryPromise`; `tryPromise` only creates an `AbortController` when the thunk declares an `AbortSignal` parameter. - **`RpcGroup.toHandlers` is now definition-first** (beta.84, PR #2423); RpcClient HTTP requests fail with a *defect* when the response stream closes before a terminal response (beta.86, PR #2461). - **Schema arbitrary-derivation metadata migrated** (beta.79, PR #2348) — custom filter annotations use `arbitrary: { constraint }` instead of `toArbitraryConstraint`, bucketed constraints are flattened (`string.minLength` -> `minLength`, `number.isInteger` -> `integer`), `ctx.constraints` -> `ctx.constraint`, and `Schema.toArbitrary(schema, { report: true })` returns `{ value, report }`. Plain `Schema.toArbitrary(schema)` (as shown in `references/testing.md`) is unaffected. - `keepDeclarations` option removed from `Schema.toCodecStringTree` (beta.86, PR #2452). - `Graph.neighborsDirected` deprecated in favor of `Graph.successors` / `Graph.predecessors` (beta.80, PR #2376). Fixes / additions worth knowing: - `Effect.transposeOption` turns `Option<Effect<A, E, R>>` into `Effect<Option<A>, E, R>` (beta.84, PR #2420); `Effect.fromOption` gained custom error callbacks (beta.89, PR #2479). - `Random.choice` selects a random element from an iterable (beta.85, PR #2425); `Latch.isOpen` queries latch state (beta.88, PR #2428). - `String.configCase` for configuration-key casing, plus a numeric-segment fix in `camelCase`/`pascalCase` (beta.91, PR #2488). - HTTP API streaming response support (beta.81, PR #2270); malformed JSON request bodies now return 400 (unreleased, PR #2492). - `Statement.valuesUnprepared` returns unprepared SQL rows as arrays (beta.86, PR #2462); `Schema.toCodecArrayFromSingle` added (beta.86, PR #2442); the original input schema is now exposed on `Schema.toType`/`toEncoded`/`toCodecJson`/`toCodecStringTree` via a `.schema` property (beta.87, PR #2468). - `RequestResolver` interruption fixed (beta.91, PR #2485); `Schedule.andThenResult` now emits `self` outputs as `Failure` and `other` as `Success` (beta.91, PR #2497); `Schema.toTaggedUnion(...).isAnyOf` narrowing fixed for custom discriminant keys (beta.81, PR #2386). - Adaptive consume + feedback operations on the unstable persistent `RateLimiterStore` API (in-memory + Redis, 429 Retry-After feedback) (beta.88, PR #2472). - `OtlpTracer` now renders causes in exception events (beta.89, PR #2480); excess-property handling fixed in schema-backed class constructors (beta.92, PR #2499). - `@effect/ai-anthropic`: non-streaming responses no longer throw on tool-call `caller` metadata (emits `null` for `caller.toolId`, beta.88, PR #2450). -
observability.md 5.7 KB
# Observability Effect has built-in structured logging, metrics, and distributed tracing. For exporting telemetry, use `effect/unstable/observability` (v4/new projects) or `@effect/opentelemetry` (v3/existing OTel setups). ## Structured Logging ```typescript import { Effect } from "effect" yield* Effect.log("Processing request") yield* Effect.logInfo("User found", userId) yield* Effect.logWarning("Rate limit approaching") yield* Effect.logError("Failed to fetch", error) yield* Effect.logDebug("Cache miss for key:", key) // Add context to all logs in scope const withContext = myEffect.pipe( Effect.annotateLogs("requestId", requestId), Effect.annotateLogs("userId", userId) ) // Log spans (structured timing) const timed = Effect.withLogSpan("database.query")(queryEffect) ``` ## Distributed Tracing ```typescript // Add a span to any effect const traced = myEffect.pipe( Effect.withSpan("processPayment", { attributes: { amount, currency } }) ) // Nested spans create parent-child relationships automatically const program = Effect.gen(function*() { yield* fetchUser(id).pipe(Effect.withSpan("fetch.user")) yield* validatePayment().pipe(Effect.withSpan("validate.payment")) yield* processCharge().pipe(Effect.withSpan("process.charge")) }).pipe(Effect.withSpan("handlePayment")) // Produces: handlePayment -> fetch.user, validate.payment, process.charge // Annotate spans const annotated = myEffect.pipe( Effect.annotateSpans("http.status_code", 200) ) ``` **v4: `Effect.fn` adds spans automatically:** ```typescript // v4 - the name string becomes a span const fetchUser = Effect.fn("fetchUser")(function*(id: string) { // ... automatically wrapped in a "fetchUser" span }) ``` ## Metrics ```typescript import { Metric } from "effect" // Counter const requestCount = Metric.counter("http.requests.total") yield* Metric.increment(requestCount) // Counter with tags const errorCount = Metric.counter("http.errors.total") yield* Metric.increment(errorCount).pipe( Effect.tagMetrics("status", "500"), Effect.tagMetrics("method", "POST") ) // Gauge const activeConnections = Metric.gauge("connections.active") yield* Metric.set(activeConnections, 42) // Histogram const latency = Metric.histogram("http.request.duration_ms", Metric.Histogram.exponential({ start: 1, factor: 2, count: 10 }) ) yield* Metric.record(latency, durationMs) ``` ## OpenTelemetry Export ### v3: @effect/opentelemetry ```typescript import { NodeSdk } from "@effect/opentelemetry" import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http" import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics" const OtelLayer = NodeSdk.layer(() => ({ resource: { serviceName: "my-service" }, spanProcessor: new BatchSpanProcessor(new OTLPTraceExporter({ url: "http://localhost:4318/v1/traces" })), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: "http://localhost:4318/v1/metrics" }) }) })) // Provide to your program const main = program.pipe(Effect.provide(OtelLayer)) ``` ### v4: effect/unstable/observability (fetch-based, no OTel SDK dependency) v4 ships its own lightweight OTLP exporters under `effect/unstable/observability`. The canonical setup is to compose split modules — `OtlpTracer.layer`, `OtlpLogger.layer` — with an `OtlpSerialization` encoding layer and an `HttpClient` transport. The whole stack depends on `FetchHttpClient.layer` from `effect/unstable/http`; there is no OpenTelemetry SDK dependency. ```typescript import { Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability" const Tracing = OtlpTracer.layer({ url: "http://localhost:4318/v1/traces", resource: { serviceName: "my-service", serviceVersion: "1.0.0" } }) const Logging = OtlpLogger.layer({ url: "http://localhost:4318/v1/logs", resource: { serviceName: "my-service" } }) export const Observability = Layer.merge(Tracing, Logging).pipe( Layer.provide(OtlpSerialization.layerJson), // or layerProtobuf Layer.provide(FetchHttpClient.layer) ) const main = program.pipe(Effect.provide(Observability)) ``` An aggregator `Otlp.layer` also exists that bundles tracer + logger + metrics, but note its argument shape: ```typescript import { Otlp } from "effect/unstable/observability" // aggregator form - baseUrl, NOT url; serviceName under resource, NOT top-level const All = Otlp.layer({ baseUrl: "http://localhost:4318", resource: { serviceName: "my-service" } }).pipe( Layer.provide(OtlpSerialization.layerJson), Layer.provide(FetchHttpClient.layer) ) ``` Do NOT use `Otlp.layer({ url, serviceName })` — neither field exists in that shape. Prefer the split-module form above; it is the pattern in `ai-docs/src/08_observability/20_otlp-tracing.ts`. The v3 `@effect/opentelemetry` NodeSdk path is not available in v4 — if you need the OpenTelemetry SDK stack you should stay on v3. ## Testing Observability ```typescript import { InMemorySpanExporter, SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base" const TestOtelLayer = NodeSdk.layer(() => ({ resource: { serviceName: "test" }, spanProcessor: new SimpleSpanProcessor(new InMemorySpanExporter()) })) ``` ## Log Level Configuration ```typescript import { Logger, LogLevel } from "effect" // Set minimum log level const withLogLevel = program.pipe( Logger.withMinimumLogLevel(LogLevel.Info) ) // Custom logger const jsonLogger = Logger.make(({ logLevel, message, annotations }) => { console.log(JSON.stringify({ level: logLevel.label, message, ...annotations })) }) const withCustomLogger = program.pipe( Effect.provide(Logger.replace(Logger.defaultLogger, jsonLogger)) ) ``` -
optics.md 1.8 KB
# Optics `Optic` provides composable, type-safe access into immutable nested data — read, replace, and modify deeply nested fields without hand-written spreads. The v4 module is **instance/method-chain based** (a departure from v3's standalone-combinator `@fp-ts/optic` style): build an optic from `Optic.id<S>()` and chain `.key(...)` / `.at(...)`. ```typescript import { Optic } from "effect" ``` ## Building and Using an Optic ```typescript interface State { readonly user: { readonly profile: { readonly age: number } } } // Focus a deeply nested field const ageOptic = Optic.id<State>().key("user").key("profile").key("age") const s: State = { user: { profile: { age: 30 } } } ageOptic.get(s) // 30 const s2 = ageOptic.replace(31, s) // { user: { profile: { age: 31 } } } const s3 = ageOptic.modify((n) => n + 1)(s) // modify returns (s) => s ``` `Optic.id<S>()` is the identity optic over `S`. Chain to focus deeper: - `.key(k)` - a struct field (always present) -> a `Lens` - `.at(k)` - a map/array entry that may be absent -> an `Optional` (fails to focus if missing) - `.optionalKey(k)`, `.pick(...)`, `.omit(...)`, `.tag(...)` / `.refine(...)`, `.check(...)`, `.compose(other)` ## Reading vs. Fallible Focus `.get(s)` exists on lenses/isos (always-present focus). For optics that may fail to focus (`.at`, prisms), use `.getResult(s)`, which returns a `Result` (v4 renamed `Either` -> `Result`): ```typescript import { Optic, Result } from "effect" const firstTag = Optic.id<{ tags: ReadonlyArray<string> }>().key("tags").at(0) const r = firstTag.getResult({ tags: ["a", "b"] }) // Result<string, string> Result.isSuccess(r) // true ``` `.replace` and `.modify` never throw: if the optic can't focus the target, they return the original `S` unchanged. -
resource-management.md 7.5 KB
# Resource Management Effect guarantees deterministic resource cleanup through `Scope`. Resources are released in LIFO order, even on failure or interruption. ## acquireRelease (Scoped Resource) Define a resource with its cleanup: ```typescript import { Effect } from "effect" const managedConnection = Effect.acquireRelease( // Acquire Effect.tryPromise(() => pool.connect()), // Release (always runs - success, failure, or interruption) (conn) => Effect.promise(() => conn.release()) ) // MUST wrap in Effect.scoped to trigger cleanup const program = Effect.scoped( Effect.gen(function*() { const conn = yield* managedConnection return yield* conn.query("SELECT * FROM users") }) ) ``` ## acquireUseRelease (Inline Pattern) When you want acquire, use, and release in one expression: ```typescript const result = yield* Effect.acquireUseRelease( // Acquire Effect.tryPromise(() => pool.connect()), // Use (conn) => Effect.tryPromise(() => conn.query("SELECT 1")), // Release (conn) => Effect.promise(() => conn.release()) ) // No Effect.scoped needed - cleanup is handled inline ``` ## Scope and LIFO Ordering Multiple resources are released in reverse acquisition order: ```typescript const program = Effect.scoped( Effect.gen(function*() { const db = yield* acquireDb() // acquired first const cache = yield* acquireCache() // acquired second const file = yield* acquireFile() // acquired third // ... use all three }) // Cleanup order: file -> cache -> db (LIFO) ) ``` ## Scoped Layers Layers can manage resource lifecycles: ```typescript // v3 const DatabaseLayer = Layer.scoped( Database, Effect.gen(function*() { const pool = yield* Effect.acquireRelease( Effect.tryPromise(() => createPool()), (pool) => Effect.promise(() => pool.end()) ) return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) } }) ) // v4 — Layer.scoped is removed. Layer.effect strips Scope from R automatically. const DatabaseLayer = Layer.effect( Database, Effect.gen(function*() { const pool = yield* Effect.acquireRelease( Effect.tryPromise(() => createPool()), (pool) => Effect.promise(() => pool.end()) ) return { query: (sql) => Effect.tryPromise(() => pool.query(sql)) } }) ) // Pool is created when the layer is built // Pool is released when the program exits ``` ## Background Fibers in Scopes ```typescript const program = Effect.scoped( Effect.gen(function*() { // This fiber is automatically interrupted when scope closes yield* Effect.forkScoped( Effect.repeat(healthCheck, Schedule.spaced("30 seconds")) ) // Main work yield* serveRequests() }) ) ``` ## ensuring - Always-Run Finalizer For cleanup that doesn't need the acquired resource: ```typescript const withCleanup = myEffect.pipe( Effect.ensuring(Effect.log("Done, regardless of outcome")) ) ``` ## addFinalizer - Manual Scope Registration ```typescript const program = Effect.scoped( Effect.gen(function*() { yield* Effect.addFinalizer((exit) => Effect.log(`Exiting with: ${exit._tag}`) ) // ... your logic }) ) ``` ## Pooling Resources with `Pool` When a resource is expensive to create and you need many of them concurrently (DB connections, clients), pool them instead of acquiring per-use. `Pool.make` builds a fixed-size pool; `Pool.makeWithTTL` an elastic one that shrinks idle items. Both the pool and `Pool.get` require a `Scope` — the pool's lifetime is the scope it is built in. ```typescript import { Effect, Pool } from "effect" const acquireConn = Effect.acquireRelease( openConnection(), (conn) => Effect.promise(() => conn.close()) ) const program = Effect.scoped( Effect.gen(function*() { // Fixed-size pool of 10 connections, all created within this scope const pool = yield* Pool.make({ acquire: acquireConn, size: 10 }) // Borrow inside its own scope so the item returns to the pool promptly const rows = yield* Effect.scoped( Pool.get(pool).pipe( Effect.flatMap((conn) => conn.query("SELECT 1")) ) ) return rows }) ) ``` `Pool.get` hands back a **scoped** resource: wrap the borrow in `Effect.scoped` (or a child scope) so it is released back to the pool deterministically when that scope closes. Use `Pool.makeWithTTL({ acquire, min, max, timeToLive })` for elastic sizing, and `Pool.invalidate(pool, item)` to discard a known-bad item. ## Refreshable Values with `Resource` `Resource` caches the latest result of an acquisition and lets you refresh it — either on a `Schedule` (`Resource.auto`) or on demand (`Resource.manual` + `Resource.refresh`). Use it for values that go stale and must be re-fetched without tearing down dependents: rotating credentials, polled remote config, cached tokens. Distinct from `Pool` (many concurrent instances) and `Cache` (keyed lookups) — a `Resource` is one value that periodically renews. ```typescript import { Effect, Resource, Schedule } from "effect" const program = Effect.scoped( Effect.gen(function*() { // Re-acquire the token every 50 minutes in the background const token = yield* Resource.auto( fetchAuthToken, // Effect<Token, AuthError> Schedule.spaced("50 minutes") ) // Read the current cached value (fails with the stored error if the last acquire failed) const current = yield* Resource.get(token) // Force an immediate refresh when needed yield* Resource.refresh(token) }) ) ``` `Resource.auto(acquire, policy)` forks the refresh loop into the surrounding scope; `Resource.manual(acquire)` skips the schedule so you control refresh timing entirely via `Resource.refresh`. Both require a `Scope`. ## Reference-Counted Resources (`RcRef` / `RcMap`) When several concurrent consumers should *share* a single expensive resource that is acquired on first use and released once the last user is done, use an `RcRef` (single resource) or `RcMap` (one per key). Each `get` increments a reference count scoped to the current `Scope`; when the count hits zero the resource is released, optionally after an `idleTimeToLive` grace period so a quick re-acquire reuses it. ```typescript import { Effect, RcRef, RcMap } from "effect" // Single shared resource: one connection behind many borrowers const shared = Effect.gen(function*() { const ref = yield* RcRef.make({ acquire: Effect.acquireRelease(openConnection, closeConnection), idleTimeToLive: "5 seconds" }) // Each borrow is scoped; the connection is opened once and reused yield* Effect.scoped(Effect.gen(function*() { const conn = yield* RcRef.get(ref) yield* useConnection(conn) })) }) // Keyed variant: one shared resource per key (e.g. per host) const perKey = Effect.gen(function*() { const clients = yield* RcMap.make({ lookup: (host: string) => Effect.acquireRelease(connect(host), disconnect), idleTimeToLive: "30 seconds" }) yield* Effect.scoped(Effect.gen(function*() { const client = yield* RcMap.get(clients, "api.example.com") yield* client.ping() })) }) ``` `RcMap.make` also accepts a `capacity`; exceeding it fails with `Cause.ExceededCapacityError`. Prefer `RcRef`/`RcMap` over `Pool` when consumers should transparently *share* one live instance rather than each borrow a distinct one from a pool. ## v4: Scope Changes In v4, `Scope` remains conceptually the same. Key change: `Effect.forkScoped` behavior is unchanged, but `Effect.fork` is renamed to `Effect.forkChild` (which is NOT scope-tied - use `Effect.forkScoped` for scope-tied fibers). -
retry-scheduling.md 6.8 KB
# Retry and Scheduling Effect's `Schedule` module provides composable, typed retry and repetition policies. ## Basic Retry ```typescript import { Effect, Schedule } from "effect" // Retry up to 3 times const retried = Effect.retry(unstableOp, Schedule.recurs(3)) // Retry with exponential backoff const retried = Effect.retry(unstableOp, Schedule.exponential("100 millis")) // Retry with exponential backoff + jitter + max retries (v3) const policy = Schedule.exponential("200 millis", 2).pipe( Schedule.compose(Schedule.recurs(5)), Schedule.jittered ) const retried = Effect.retry(unstableOp, policy) // v4 — Schedule.compose is removed. Use Schedule.take(n) to bound by attempts: const policyV4 = Schedule.exponential("200 millis", 2).pipe( Schedule.take(5), Schedule.jittered ) const retriedV4 = Effect.retry(unstableOp, policyV4) // Version-agnostic alternative: pass `times` in the retry options object const retriedAny = Effect.retry(unstableOp, { schedule: Schedule.exponential("200 millis", 2).pipe(Schedule.jittered), times: 5 }) ``` ## Built-In Schedules | Schedule | Behavior | |---------------------------------|-----------------------------------------------| | `Schedule.recurs(n)` | Retry/repeat up to n times | | `Schedule.spaced("1 second")` | Fixed spacing between iterations | | `Schedule.exponential("100 millis")` | Exponential backoff (doubles each time) | | `Schedule.exponential("100 millis", 1.5)` | Custom growth factor | | `Schedule.fibonacci("100 millis")` | Fibonacci backoff | | `Schedule.fixed("5 seconds")` | Fixed interval (accounts for elapsed time) | | `Schedule.forever` | Repeat indefinitely | | `Schedule.once` (v3 only) | Run once more — v4: use `Schedule.recurs(0)` | | `Schedule.jittered` | Add randomness (combine with other schedules) | | `Schedule.take(n)` | Bound any schedule to n iterations (v4 idiom) | | `Schedule.cron("0 * * * *")` | Cron-based scheduling | ## Composing Schedules ```typescript // Sequential: first policy, then second (recurs 3 times, then exponential) const sequential = Schedule.recurs(3).pipe( Schedule.andThen(Schedule.exponential("1 second")) ) // Intersection: both constraints must be satisfied // (exponential backoff, but max 5 retries) // v3: Schedule.compose const bounded = Schedule.exponential("100 millis").pipe( Schedule.compose(Schedule.recurs(5)) ) // v4: Schedule.compose is removed. Use Schedule.take(n) for the same intent. const boundedV4 = Schedule.exponential("100 millis").pipe( Schedule.take(5) ) // Union: either constraint can trigger const either = Schedule.spaced("1 second").pipe( Schedule.either(Schedule.recurs(10)) ) ``` ## Conditional Retry (only on specific errors) ```typescript // Retry only on retryable errors // Pass `times` in the options object (works in both v3 and v4); the schedule // itself just controls delay shape. const retried = Effect.retry(fetchFromApi, { schedule: Schedule.exponential("200 millis").pipe(Schedule.jittered), times: 4, while: (error) => error._tag === "NetworkError" || error._tag === "RateLimitError" }) // Or using until (inverse condition) const retriedUntil = Effect.retry(fetchFromApi, { schedule: Schedule.exponential("200 millis").pipe(Schedule.jittered), times: 4, until: (error) => error._tag === "AuthError" // stop retrying on auth errors }) ``` ## Repetition (success-based) ```typescript // Repeat an effect 5 times const repeated = Effect.repeat(pollStatus, Schedule.recurs(5)) // Repeat every second forever const polling = Effect.repeat(checkHealth, Schedule.spaced("1 second")) // Repeat until a condition is met const waitForReady = Effect.repeat(checkStatus, { until: (status) => status === "ready" }) ``` ## Timeout ```typescript // Timeout after 5 seconds (returns Option - None on timeout) const withTimeout = Effect.timeout(slowOp, "5 seconds") // Timeout with fallback const withFallback = Effect.timeoutTo(slowOp, { duration: "5 seconds", onTimeout: () => Effect.succeed(defaultValue) }) // Timeout that fails const withError = Effect.timeoutFail(slowOp, { duration: "5 seconds", onTimeout: () => new TimeoutError({ message: "operation timed out" }) }) ``` ## Combining Retry + Timeout ```typescript const resilient = fetchFromUpstream(params).pipe( Effect.timeout("10 seconds"), // Version-agnostic retry shape — `times` caps the attempts and works in v3 + v4. Effect.retry({ schedule: Schedule.exponential("200 millis").pipe(Schedule.jittered), times: 3 }), Effect.withSpan("upstream.fetch") ) ``` ## HttpClient.retryTransient (v3 @effect/platform) For HTTP calls, `@effect/platform` provides a built-in retry for transient failures (connection errors, 429, 503): ```typescript import { HttpClient } from "@effect/platform" // v3 — Schedule.compose still works const resilientClient = HttpClient.retryTransient({ schedule: Schedule.exponential("200 millis").pipe( Schedule.compose(Schedule.recurs(3)) ) }) // v4 (effect/unstable/http) — use Schedule.take(n) instead of compose const resilientClientV4 = HttpClient.retryTransient({ schedule: Schedule.exponential("200 millis").pipe(Schedule.take(3)) }) ``` ## Ordered Fallback (ExecutionPlan) When retrying the *same* effect isn't enough and you need to fall back to a different resource (backup provider, replica DB, alternate region), use `ExecutionPlan` instead of a bare `Schedule`. Each step provides its own `Layer` with optional `attempts` + `schedule`, and `Effect.withExecutionPlan(effect, plan)` walks the steps until one succeeds. See `references/effect-ai.md` for a full example. ## RateLimiter ```typescript // v3 — top-level module with make/withCost import { RateLimiter } from "effect" const limiter = yield* RateLimiter.make({ limit: 10, interval: "1 second" }) const limited = RateLimiter.withCost(limiter, 1)(fetchFromApi(params)) ``` ```typescript // v4 — moved to effect/unstable/persistence and uses a Service-based API. // There is no withCost; per-call cost is the `tokens` option on consume. import { Effect, Layer } from "effect" import { RateLimiter } from "effect/unstable/persistence" const program = Effect.gen(function*() { const withLimiter = yield* RateLimiter.makeWithRateLimiter return yield* fetchFromApi(params).pipe( withLimiter({ key: "fetchFromApi", limit: 10, window: "1 second", algorithm: "fixed-window", onExceeded: "delay", tokens: 1 // per-call cost }) ) }) // Provide the in-memory store + RateLimiter service const Live = RateLimiter.layer.pipe(Layer.provide(RateLimiter.layerStoreMemory)) const main = program.pipe(Effect.provide(Live)) ``` -
rpc.md 2.8 KB
# RPC `effect/unstable/rpc` provides typed, Schema-validated client/server RPC. Define requests once as an `RpcGroup`; implement them as a handler layer on the server; call them as typed methods on the client. Transport (HTTP, WebSocket, worker) and serialization (JSON, NDJSON, MsgPack) are pluggable layers. (Unstable module, `@since 4.0.0`.) ```typescript import { Rpc, RpcGroup, RpcServer, RpcClient, RpcSerialization } from "effect/unstable/rpc" ``` ## Define a Group ```typescript import { Schema } from "effect" import { Rpc, RpcGroup } from "effect/unstable/rpc" class User extends Schema.Class<User>("User")({ id: Schema.String, name: Schema.String }) {} const UserRpcs = RpcGroup.make( Rpc.make("GetUser", { payload: { id: Schema.String }, success: User }), Rpc.make("ListUsers", { success: Schema.Array(User), stream: true }) ) ``` `Rpc.make(tag, { payload?, success?, error?, stream?, primaryKey? })` — `payload` accepts struct fields or a Schema; `stream: true` makes the result a stream of `success`. ## Implement Handlers (server) ```typescript import { Effect, Layer, Stream } from "effect" import { RpcServer } from "effect/unstable/rpc" const UsersLive = UserRpcs.toLayer( UserRpcs.of({ GetUser: ({ id }) => Effect.succeed(new User({ id, name: "Ada" })), ListUsers: () => Stream.fromIterable([new User({ id: "1", name: "Ada" })]) }) ) // Server = handlers + protocol + serialization const ServerLive = RpcServer.layer(UserRpcs).pipe( Layer.provide(UsersLive), Layer.provide(RpcSerialization.layerJson) // + a protocol layer, e.g. RpcServer.layerProtocolHttp({ path: "/rpc" }) ) ``` `group.toLayer(handlers)` builds the handler layer; `group.of({...})` is an identity helper that gives the handler object its types. Handler keys are the rpc tags. ## Call from the Client ```typescript import { Effect } from "effect" import { RpcClient } from "effect/unstable/rpc" const program = Effect.gen(function*() { const client = yield* RpcClient.make(UserRpcs) const user = yield* client.GetUser({ id: "1" }) // typed call by tag return user }) // Wire the client transport: e.g. // RpcClient.layerProtocolHttp({ url }) + RpcSerialization.layerJson + HttpClient.HttpClient ``` `RpcClient.make(group)` returns a client whose methods are keyed by rpc tag. Streaming rpcs return a `Stream`. ## Transports and Testing - Serialization: `RpcSerialization.layerJson` / `layerNdjson` / `layerMsgPack` / `layerJsonRpc()`. - Server protocols: `RpcServer.layerProtocolHttp`, `layerProtocolWebsocket`, `layerProtocolSocketServer`, `layerProtocolWorkerRunner`. - Client protocols: `RpcClient.layerProtocolHttp`, `layerProtocolWorker({ size })` (a worker-thread pool — see `references/concurrency.md`). - Testing without a transport: `RpcTest.makeClient(group)` runs the group in-memory. -
schema.md 7.2 KB
# Schema Effect Schema provides bidirectional validation: decode (external input -> typed data) and encode (typed data -> wire format). Always import from `"effect"`, not `"@effect/schema"`. ```typescript import { Schema } from "effect" ``` ## Defining Schemas ```typescript const User = Schema.Struct({ id: Schema.String, name: Schema.String, age: Schema.Number, email: Schema.optionalWith(Schema.String, { exact: true }) // v3 // v4: Schema.optionalKey(Schema.String) }) // Infer the TypeScript type type User = typeof User.Type // { id: string; name: string; age: number; email?: string } ``` ## Common Schema Types | Schema | Decoded Type | |------------------------------|---------------------------------| | `Schema.String` | `string` | | `Schema.Number` | `number` | | `Schema.Boolean` | `boolean` | | `Schema.Literal("a", "b")` | `"a" \| "b"` (v3 variadic) | | `Schema.Literals(["a","b"])`| `"a" \| "b"` (v4 array) | | `Schema.Array(Schema.String)`| `string[]` | | `Schema.NullOr(Schema.String)` | `string \| null` | | `Schema.Union(A, B)` (v3) | `A \| B` | | `Schema.Union([A, B])` (v4) | `A \| B` | | `Schema.Record(Schema.String, Schema.Number)` | `Record<string, number>` (v4) | | `Schema.NumberFromString` | string on wire, number decoded | | `Schema.DateFromString` | string on wire, Date decoded | ## Decoding (Parse) ```typescript // Sync (throws on failure) const user = Schema.decodeUnknownSync(User)(rawData) // Effect-based (typed error) // v3: const user = yield* Schema.decodeUnknown(User)(rawData) // v4: const user = yield* Schema.decodeUnknownEffect(User)(rawData) // Returns Exit instead of throwing // v3: Schema.decodeUnknownEither(User)(rawData) // v4: Schema.decodeUnknownExit(User)(rawData) ``` ## Encoding ```typescript // Sync const json = Schema.encodeSync(User)(user) // Effect-based // v3: yield* Schema.encode(User)(user) // v4: yield* Schema.encodeEffect(User)(user) ``` ## Construction (v4) Every v4 schema exposes three constructor methods. v3's `Schema.makeUnsafe` no longer exists. ```typescript // Throws SchemaError on invalid input const user = User.make({ id: "1", name: "Alice", age: 30 }) // Returns Option<Type> const maybeUser = User.makeOption({ id: "1", name: "Alice", age: 30 }) // Returns Effect<Type, SchemaError> const userEffect = User.makeEffect({ id: "1", name: "Alice", age: 30 }) ``` ## Error Shape (v4) Decode/encode/construct failures are `SchemaError` instances carrying a `SchemaIssue.Issue` on the `.issue` field. Narrow thrown errors with `Schema.isSchemaError` and format issues via the `SchemaIssue` module. v3's `ParseResult` module is gone. Since beta.84, `SchemaError` extends `Data.TaggedError` — it is a native `Error` with `_tag: "SchemaError"`, so within Effect code you can also branch on it with `Effect.catchTag("SchemaError", ...)` instead of a `try`/`catch`. The `SchemaParser` Promise adapters reject an `Error` whose `cause` is the underlying `SchemaIssue.Issue`. ```typescript import { Schema, SchemaIssue } from "effect" try { Schema.decodeUnknownSync(User)(badInput) } catch (e) { if (Schema.isSchemaError(e) && SchemaIssue.isIssue(e.issue)) { const issues = SchemaIssue.makeFormatterStandardSchemaV1()(e.issue).issues console.error(issues) } } ``` ## Filters and Validation ### v3 ```typescript const PositiveAge = Schema.Number.pipe( Schema.positive(), Schema.int() ) const ShortString = Schema.String.pipe( Schema.minLength(1), Schema.maxLength(100) ) ``` ### v4 ```typescript const PositiveAge = Schema.Number.check( Schema.isGreaterThan(0), Schema.isInt() ) const ShortString = Schema.String.check( Schema.isMinLength(1), Schema.isMaxLength(100) ) ``` ## Tagged Error Classes ### v3 ```typescript import { Data } from "effect" class NotFoundError extends Data.TaggedError("NotFoundError")<{ readonly id: string }> {} ``` ### v4 ```typescript import { Schema } from "effect" class NotFoundError extends Schema.TaggedErrorClass<NotFoundError>()("NotFoundError", { id: Schema.String }) {} ``` ## JSON Schema Generation Effect Schema generates **JSON Schema Draft-07** (not 2020-12): ```typescript import { JSONSchema, Schema } from "effect" const jsonSchema = JSONSchema.make(User) // { "$schema": "http://json-schema.org/draft-07/schema#", ... } ``` ## Struct Operations ### v3 ```typescript const Picked = User.pipe(Schema.pick("id", "name")) const Omitted = User.pipe(Schema.omit("age")) const Extended = User.pipe(Schema.extend(Schema.Struct({ role: Schema.String }))) const Partial = User.pipe(Schema.partial) ``` ### v4 ```typescript import { Struct } from "effect" const Picked = User.mapFields(Struct.pick(["id", "name"])) const Omitted = User.mapFields(Struct.omit(["age"])) const Extended = User.mapFields(Struct.assign({ role: Schema.String })) const Partial = User.mapFields(Struct.map(Schema.optional)) ``` ## Transforms ### v3 ```typescript const BoolFromString = Schema.transform( Schema.Literal("on", "off"), Schema.Boolean, { decode: (s) => s === "on", encode: (b) => b ? "on" : "off" } ) ``` ### v4 ```typescript import { SchemaTransformation } from "effect" const BoolFromString = Schema.Literals(["on", "off"]).pipe( Schema.decodeTo( Schema.Boolean, SchemaTransformation.transform({ decode: (s) => s === "on", encode: (b) => b ? "on" : "off" }) ) ) ``` ## Branded Types (nominal typing) Brands give primitives compile-time identity so a `UserId` can't be passed where an `OrderId` is expected, even though both are `string`. Two routes: **Inside a schema** — `Schema.brand` tags the decoded type: ```typescript const UserId = Schema.Number.pipe(Schema.brand("UserId")) type UserId = typeof UserId.Type // number & Brand<"UserId"> ``` **Standalone** — the `Brand` module builds constructors independent of Schema: ```typescript import { Brand, Schema } from "effect" type UserId = number & Brand.Brand<"UserId"> // nominal: no runtime validation, just a type-level tag const UserId = Brand.nominal<UserId>() const id = UserId(123) // UserId // check: validate from schema checks (throws on failure; .option / .result / .is don't) type PositiveInt = number & Brand.Brand<"PositiveInt"> const PositiveInt = Brand.check<PositiveInt>(Schema.isInt(), Schema.isGreaterThan(0)) const ok = PositiveInt(5) // throws if invalid const maybe = PositiveInt.option(-1) // Option<PositiveInt> (None here) // make: validate from a custom predicate (return true/undefined to pass, a string to reject) type Even = number & Brand.Brand<"Even"> const Even = Brand.make<Even>((n) => n % 2 === 0 || `${n} is not even`) ``` `Brand.all(A, B)` combines constructors. (Note: v4 uses `Brand.check`/`Brand.make`, not v3's `Brand.refined`/`Brand.error`.) Use `Schema.brand` when the value already flows through decoding; reach for the standalone `Brand` module for domain primitives constructed in plain code. ## TypeScript Configuration Effect Schema requires `strict: true` in tsconfig. Optionally enable `exactOptionalPropertyTypes: true` for precise optional field handling. -
sql.md 4.9 KB
# SQL Effect's SQL toolkit gives typed, composable database access with tagged-template queries, Schema-validated decoding, models, and migrations. Core modules live under `effect/unstable/sql/*`; concrete drivers ship as `@effect/sql-*` packages. > Key fact: a tagged-template query **is an Effect** — `Statement<A> extends Effect<ReadonlyArray<A>, SqlError>`. You `yield*` it directly; there is no `.execute()`. ## Running Queries ```typescript import { Effect } from "effect" import { SqlClient } from "effect/unstable/sql/SqlClient" interface User { readonly id: number readonly name: string } const program = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient const id = 1 const users = yield* sql<User>`SELECT * FROM users WHERE id = ${id}` // ReadonlyArray<User> yield* sql`INSERT INTO users ${sql.insert({ name: "Alice" })}` return users }) ``` Interpolated values (`${id}`) are bound parameters, not string-concatenated — safe from injection. ### Statement helpers (on `sql`) `sql.insert(record | records)`, `sql.update(record)`, `sql.updateValues(rows, alias)`, `sql.in(values)` / `sql.in(col, values)`, `sql.and(clauses)`, `sql.or(clauses)`, `sql.csv(values)`, `sql.unsafe(raw, params?)`, `sql.literal(raw)`, `sql.onDialect({ pg, sqlite, ... })`. ### Transactions ```typescript yield* sql.withTransaction( Effect.gen(function*() { yield* sql`INSERT INTO accounts ${sql.insert({ id: 1, balance: 100 })}` yield* sql`UPDATE accounts SET balance = balance - 50 WHERE id = ${1}` }) ) // nested withTransaction uses savepoints ``` ## Schema-Validated Queries (`SqlSchema`) Derive queries that decode rows through an Effect `Schema`. Constructors: `SqlSchema.findAll`, `SqlSchema.findNonEmpty`, `SqlSchema.findOne`, `SqlSchema.findOneOption`, `SqlSchema.void` (there is **no `single`**). ```typescript import { Effect, Schema } from "effect" import { SqlClient } from "effect/unstable/sql/SqlClient" import * as SqlSchema from "effect/unstable/sql/SqlSchema" const getUserById = Effect.gen(function*() { const sql = yield* SqlClient.SqlClient return SqlSchema.findOne({ Request: Schema.Number, Result: Schema.Struct({ id: Schema.Number, name: Schema.String }), execute: (id) => sql`SELECT id, name FROM users WHERE id = ${id}` }) }) // findOne fails with NoSuchElementError if no row; findOneOption returns Option ``` ## Models and Repositories `Model.Class` (from `effect/unstable/schema`) defines a table-backed schema with insert/update/json variants. Use `Model.GeneratedByDb` for DB-generated columns (e.g. auto-increment ids) — note this replaced `Model.Generated` (renamed in beta.68). `SqlModel.makeRepository` derives CRUD operations. ```typescript import { Effect, Schema } from "effect" import { Model } from "effect/unstable/schema" import * as SqlModel from "effect/unstable/sql/SqlModel" const UserId = Schema.Number.pipe(Schema.brand("UserId")) class User extends Model.Class<User>("User")({ id: Model.GeneratedByDb(UserId), name: Schema.String, createdAt: Model.DateTimeInsertFromDate, updatedAt: Model.DateTimeUpdateFromDate }) {} const makeUserRepo = SqlModel.makeRepository(User, { tableName: "users", spanPrefix: "UserRepo", idColumn: "id" }) // yields { insert, insertVoid, update, updateVoid, findById, delete } ``` `SqlModel` exports only `makeRepository` and `makeResolvers` (both return Effects requiring `SqlClient`). ## Migrations (`Migrator`) `Migrator.make({ ... })` runs pending migrations in a transaction. Loaders: `Migrator.fromFileSystem(dir)`, `fromGlob`, `fromBabelGlob`, `fromRecord`. Driver packages expose a ready layer (e.g. `PgMigrator.layer`). ## Providing a Driver Concrete `@effect/sql-*` packages: `@effect/sql-pg`, `-pglite`, `-mysql2`, `-mssql`, `-clickhouse`, `-libsql`, `-d1`, and SQLite variants (`-sqlite-node`, `-sqlite-bun`, `-sqlite-wasm`, ...). Each layer provides both its specific client and the generic `SqlClient`. ```typescript import { Effect, Redacted } from "effect" import { PgClient } from "@effect/sql-pg" const PgLive = PgClient.layer({ host: "localhost", port: 5432, database: "app", username: "postgres", password: Redacted.make("secret"), // password/url are Redacted maxConnections: 10 }) const runnable = program.pipe(Effect.provide(PgLive)) ``` Use `PgClient.layerConfig(...)` to source the connection from `Config` instead of literals. ## Errors (`SqlError`) Every statement fails with `SqlError` (`_tag: "SqlError"`) carrying a `reason`. **Match on `error.reason._tag`, not the top-level `_tag`** (which is always `"SqlError"`): ```typescript program.pipe( Effect.catchTag("SqlError", (e) => e.reason._tag === "UniqueViolation" ? Effect.succeed("duplicate") : Effect.fail(e) ) ) ``` Reasons include `ConnectionError`, `UniqueViolation` (carries `constraint`), `ConstraintError`, `DeadlockError`, `SerializationError`, `*TimeoutError`, `SqlSyntaxError`, `Authentication`/`AuthorizationError`. Each exposes `isRetryable` for retry policies. -
stm.md 2.4 KB
# Software Transactional Memory (STM) Effect's transactional layer lets you compose atomic updates across multiple mutable cells with no locks and no race conditions. Transactions are **optimistic with retry** (MVCC-style: versions are checked at commit; on conflict the whole block re-runs), not lock-based. The driver is `Effect.tx` — there is **no `Effect.atomic` and no `Effect.transaction`**. Transactional cells are the `Tx*` modules (`TxRef`, `TxQueue`, `TxHashMap`, `TxChunk`, `TxSemaphore`, ...). ```typescript import { Effect, TxRef } from "effect" ``` ## Atomic Updates Across Cells ```typescript const transfer = Effect.gen(function*() { const from = yield* TxRef.make(100) const to = yield* TxRef.make(0) // Both writes commit together, or neither does yield* Effect.tx( Effect.gen(function*() { yield* TxRef.update(from, (n) => n - 50) yield* TxRef.update(to, (n) => n + 50) }) ) return [yield* TxRef.get(from), yield* TxRef.get(to)] // [50, 50] }) ``` - `Effect.tx(effect)` is the transaction **boundary** — the outermost call commits/rolls back atomically. Nested `Effect.tx` calls compose into the same journal (they do not start a separate transaction). - `TxRef` operations: `TxRef.make`, `TxRef.get`, `TxRef.set`, `TxRef.update`, `TxRef.modify`. Inside a `tx` block they read/write the transaction journal; outside one they run in a singleton transaction. ## Waiting for a Condition (`Effect.txRetry`) To block until some transactional state changes, call `Effect.txRetry`. It marks the transaction for retry and suspends until one of the `TxRef`s it read is modified, then re-runs the block: ```typescript import { Effect, TxRef } from "effect" // Wait until the queue ref is non-empty, then take one item const takeWhenReady = (queue: TxRef.TxRef<ReadonlyArray<string>>) => Effect.tx( Effect.gen(function*() { const items = yield* TxRef.get(queue) if (items.length === 0) { return yield* Effect.txRetry // suspend; re-run when `queue` changes } yield* TxRef.set(queue, items.slice(1)) return items[0] }) ) ``` ## Transactional Collections Beyond `TxRef`, the same atomicity applies to: `TxQueue`, `TxPubSub`, `TxSubscriptionRef`, `TxHashMap`, `TxHashSet`, `TxChunk`, `TxPriorityQueue`, `TxDeferred`, `TxSemaphore`, `TxReentrantLock`. Compose operations on any mix of these inside a single `Effect.tx` for all-or-nothing semantics. -
streams.md 5 KB
# Streams Effect Streams are pull-based, lazily evaluated sequences of values with typed errors and resource safety. ## Creating Streams ```typescript import { Stream } from "effect" // From an iterable const fromArray = Stream.fromIterable([1, 2, 3]) // From a single effect const fromEffect = Stream.fromEffect(fetchUser(id)) // From repeated effect on a schedule const polling = Stream.fromEffectSchedule( checkStatus, Schedule.spaced("1 second") ) // Paginated API const pages = Stream.paginate(1, (page) => [ fetchPage(page), page < totalPages ? Option.some(page + 1) : Option.none() ]) // From an async iterable const fromAsync = Stream.fromAsyncIterable( asyncIterator, (err) => new StreamError({ cause: err }) ) // Callback-based (SSE, WebSocket, etc.) const fromCallback = Stream.async<string, never>((emit) => { source.on("data", (chunk) => emit.single(chunk)) source.on("end", () => emit.end()) source.on("error", (err) => emit.fail(new StreamError({ cause: err }))) }) ``` ## Transforming Streams ```typescript const transformed = myStream.pipe( Stream.map((x) => x * 2), Stream.filter((x) => x > 10), Stream.take(5), Stream.tap((x) => Effect.log(`Processing: ${x}`)) ) // Effectful transform const enriched = Stream.mapEffect(myStream, (item) => fetchDetails(item.id), { concurrency: 5 } ) // FlatMap (one-to-many) const expanded = Stream.flatMap(usersStream, (user) => Stream.fromIterable(user.posts) ) ``` ## Consuming Streams ```typescript // Collect all into an array const items = yield* Stream.runCollect(myStream) // Fold/reduce const sum = yield* Stream.runFold(myStream, 0, (acc, x) => acc + x) // Process each item yield* Stream.runForEach(myStream, (item) => processItem(item)) // Run to completion, discard results yield* Stream.runDrain(myStream) // Using Sink for reusable consumption patterns import { Sink } from "effect" const count = yield* Stream.run(myStream, Sink.count) ``` ## Queue-to-Stream Bridge ```typescript import { Queue, Stream } from "effect" const queue = yield* Queue.bounded<string>(100) // Convert queue to stream (stream pulls from queue) const stream = Stream.fromQueue(queue) // Process stream in background, offer to queue from producers yield* Effect.forkScoped( Stream.runForEach(stream, (msg) => handleMessage(msg)) ) // Producers offer to queue yield* Queue.offer(queue, "new message") ``` ## PubSub (Fan-out) ```typescript import { PubSub, Stream } from "effect" const pubsub = yield* PubSub.bounded<Event>(100) // Subscribe returns a Queue (each subscriber gets all messages) const subscription = yield* PubSub.subscribe(pubsub) const stream = Stream.fromQueue(subscription) // Publish yield* PubSub.publish(pubsub, { type: "user_created", userId: "123" }) ``` ## Chunked Processing Streams process data in chunks for efficiency: ```typescript // Group into fixed-size chunks const chunked = Stream.grouped(myStream, 100) // Group by time window const windowed = Stream.groupedWithin(myStream, 100, "5 seconds") ``` ## Error Handling in Streams ```typescript // v3 — Schedule.compose const resilient = myStream.pipe( Stream.retry(Schedule.exponential("1 second").pipe( Schedule.compose(Schedule.recurs(3)) )), Stream.catchAll((error) => Stream.fromIterable(fallbackData)) ) // v4 — Schedule.compose is removed. Use Schedule.take(n). const resilientV4 = myStream.pipe( Stream.retry(Schedule.exponential("1 second").pipe(Schedule.take(3))), Stream.catchAll((error) => Stream.fromIterable(fallbackData)) ) ``` ## SSE / Streaming HTTP Pattern For bridging HTTP SSE streams into Effect Streams: ```typescript // Using HttpClient to consume SSE const sseStream = Effect.gen(function*() { const client = yield* HttpClient.HttpClient const response = yield* client.execute( HttpClientRequest.get(url) ) return response.stream.pipe( Stream.decodeText(), Stream.splitLines, Stream.filter((line) => line.startsWith("data: ")), Stream.map((line) => JSON.parse(line.slice(6))) ) }) ``` ## Framing Streams (NDJSON / MessagePack) To encode or decode a stream of structured values over a byte stream, pipe it through a `Channel` from the `effect/unstable/encoding` modules with `Stream.pipeThroughChannel`. `Ndjson` frames newline-delimited JSON; `Msgpack` frames MessagePack; `Sse` handles server-sent events. Each module exposes `decode`/`encode` (raw) and `decodeSchema`/`encodeSchema` (validate against a `Schema`) channels. ```typescript import { Schema, Stream } from "effect" import { Ndjson } from "effect/unstable/encoding" const Event = Schema.Struct({ id: Schema.Number, kind: Schema.String }) // Decode a byte stream of newline-delimited JSON into typed values const events = byteStream.pipe( Stream.pipeThroughChannel(Ndjson.decodeSchema(Event)) ) // Encode typed values back into an NDJSON byte stream const bytes = typedStream.pipe( Stream.pipeThroughChannel(Ndjson.encodeSchema(Event)) ) ``` For RPC transport framing (a different use), NDJSON/MessagePack are provided as serialization layers — see `references/rpc.md`. -
testing.md 4.7 KB
# Testing Effect Code ## @effect/vitest (Recommended) ```typescript import { it } from "@effect/vitest" import { Effect } from "effect" // Effect-based test it.effect("fetches user", () => Effect.gen(function*() { const user = yield* fetchUser("123") expect(user.name).toBe("Alice") }) ) // With layers it.effect("fetches user with db", () => Effect.gen(function*() { const user = yield* fetchUser("123") expect(user.name).toBe("Alice") }).pipe(Effect.provide(TestDatabaseLayer)) ) // With shared layer across tests const { it: testWithDb } = it.layer(TestDatabaseLayer) testWithDb.effect("test 1", () => /* ... */) testWithDb.effect("test 2", () => /* ... */) ``` ## Layer-Based Test Doubles Replace real services with test implementations via layers: ```typescript // Production service class Database extends Context.Tag("Database")<Database, { readonly query: (sql: string) => Effect.Effect<unknown[]> }>() {} // Test layer - deterministic mock const TestDatabase = Layer.succeed(Database, { query: (sql) => Effect.succeed([{ id: 1, name: "test-user" }]) }) // Stateful test double const TestDatabase = Layer.effect(Database, Effect.gen(function*() { const store = yield* Ref.make<Map<string, unknown[]>>(new Map()) return { query: (sql) => Effect.gen(function*() { const data = yield* Ref.get(store) return data.get(sql) ?? [] }) } }) ) ``` ## TestClock (Deterministic Time) Control time in tests for schedule/timeout/delay testing: ```typescript import { TestClock, TestContext } from "effect" it.effect("retries 3 times with backoff", () => Effect.gen(function*() { // Fork the effect under test (v4: Effect.forkChild instead of Effect.fork) const fiber = yield* Effect.fork( Effect.retry(failingOp, { schedule: Schedule.exponential("1 second"), times: 3 }) ) // Advance time to trigger retries yield* TestClock.adjust("1 second") // 1st retry yield* TestClock.adjust("2 seconds") // 2nd retry yield* TestClock.adjust("4 seconds") // 3rd retry const result = yield* Fiber.join(fiber) // Assert result... }).pipe(Effect.provide(TestContext.TestContext)) ) ``` ## Testing Patterns ### Assert on typed errors ```typescript it.effect("fails with NotFound", () => Effect.gen(function*() { const exit = yield* Effect.exit(fetchUser("nonexistent")) expect(exit).toEqual(Exit.fail(new NotFoundError({ id: "nonexistent" }))) }) ) ``` ### Assert on Exit ```typescript import { Exit } from "effect" it.effect("returns success exit", () => Effect.gen(function*() { const exit = yield* Effect.exit(myEffect) expect(Exit.isSuccess(exit)).toBe(true) }) ) ``` ### Test with timeout ```typescript it.effect("completes within 5 seconds", () => myEffect.pipe( Effect.timeout("5 seconds"), Effect.map((result) => expect(result).toBeDefined()) ) ) ``` ### Override a single service ```typescript it.effect("uses custom logger", () => program.pipe( Effect.provideService(Logger, { log: (msg) => Effect.sync(() => messages.push(msg)) }) ) ) ``` ## Property-Based Testing (FastCheck + Schema Arbitrary) `Schema.toArbitrary(schema)` derives a `fast-check` `Arbitrary` directly from any schema, so you can generate valid sample data and run property tests without writing generators by hand. Effect re-exports fast-check as `FastCheck` from `effect/testing`. ```typescript import { Schema } from "effect" import { FastCheck } from "effect/testing" const Person = Schema.Struct({ name: Schema.String, age: Schema.Number.check(Schema.isGreaterThanOrEqualTo(0)) }) const PersonArb = Schema.toArbitrary(Person) // Generate a sample const sample = FastCheck.sample(PersonArb, 1)[0] // Property: every generated value round-trips through encode -> decode FastCheck.assert( FastCheck.property(PersonArb, (person) => { const decoded = Schema.decodeUnknownSync(Person)( Schema.encodeSync(Person)(person) ) expect(decoded).toEqual(person) }) ) ``` Derived arbitraries respect schema checks (`isGreaterThan`, `isMinLength`, etc.), so generated data is always valid input. Combine with `it.effect` to property-test effectful logic over generated inputs. ## Common Testing Mistakes 1. **Forgetting TestContext**: `TestClock.adjust` requires `TestContext.TestContext` to be provided 2. **Not forking before adjusting time**: `TestClock.adjust` only affects already-running fibers. Fork the effect first, then adjust 3. **Testing implementation instead of behavior**: Use Layer swaps to test service behavior, not internal implementation details 4. **Missing `Effect.exit` for error assertions**: Use `Effect.exit` to inspect the Exit value instead of catching errors
-
-
CHANGELOG.md 7.2 KB
# Changelog All notable changes to this skill will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/2.0.0/), and this skill adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [0.6.5] - 2026-09-09 ### Changed - Description condensed to fit the repo's 250-character limit. ## [0.6.4] - 2026-08-21 ### Changed - Declared ClawHub browse categories (`development`) and topics in `metadata`, so the release pipeline publishes them instead of leaving the skill in the `other` category. ### Removed - `skill-card.md`. The ClawHub CLI strips a root `skill-card.md` from every publish and the registry generates its own card, so the authored file never reached ClawHub. ## [0.6.3] - 2026-08-07 ### Changed - Rewrote the frontmatter description: dropped the "Comprehensive" opener and the 14-subsystem enumeration, folded the distinguishing import specifiers into a natural clause, and removed the trailing trigger dump. ## [0.6.2] - 2026-07-22 ### Added - skill-card.md release record following NVIDIA's skill-card format - metadata.openclaw audited against ClawHub spec (removed ANTHROPIC_API_KEY envVar declared but never referenced in skill content) ## [0.6.1] - 2026-07-10 ### Changed - CHANGELOG preamble pinned to Keep a Changelog 2.0.0 (format unchanged; KaC 2.0.0 keeps existing changelogs valid). ## [0.6.0] - 2026-07-01 ### Added - `migration-v4.md`: extended the "v4 beta additions" ledger from beta.78 to beta.92 (SchemaError now a tagged `Data.TaggedError`; `Schema.Void` void-return semantics; `Config.make` removed + `Config.withDefault`/`Config.schema` recovery changes; `Effect.try` thunk form; `RpcGroup.toHandlers` definition-first; `Effect.transposeOption`; `Effect.fromOption` callbacks; `Random.choice`; `Latch.isOpen`; `String.configCase`; `Statement.valuesUnprepared`; `Schema.toCodecArrayFromSingle`; `Graph.successors`/`predecessors`; arbitrary-derivation metadata migration; HTTP API streaming responses). - `concurrency.md`: child-process execution via `effect/unstable/process` (`ChildProcess`, `ChildProcessSpawner`; `spawner.string`/`lines`/`spawn`, `ChildProcess.pipeTo`). - `dependency-injection.md`: `LayerMap.Service` for per-key dynamic layers, and a `ManagedRuntime` bridge pattern for non-Effect frameworks (Hono/Express) with fiber-local request context via `Context.Reference`. - `resource-management.md`: `RcRef`/`RcMap` reference-counted shared resources. - `streams.md`: stream framing/encoding via `Stream.pipeThroughChannel` + `Ndjson`/`Msgpack` (`effect/unstable/encoding`). - `graph.md` (new reference): the `Graph` module - directed/undirected graphs, traversal (`dfs`/`bfs`/`topo`), analysis (`isAcyclic`, connected/strongly-connected components), and shortest paths (`dijkstra`/`astar`/`bellmanFord`/`floydWarshall`). - `schema.md`: note that `SchemaError` is a tagged `Data.TaggedError` (catchable via `Effect.catchTag`). - SKILL.md progressive-disclosure index entries for the new concepts. ### Changed - `llm-corrections.md` + SKILL.md: corrected `Schema.makeOption`/`makeEffect` to instance methods (`schema.makeOption(...)`); softened the unverified "PR #2057" attribution for HttpApi-errors-as-defects to a source-grounded statement. ### Fixed - `error-modeling.md`: warn that `Effect.catch`/`catchTag` don't catch defects or interrupts (use `catchDefect`/`catchCause`) and that `Effect.catch(() => Effect.void)` silently swallows the typed error. - `concurrency.md`: clarify that `Effect.forkDetach` parents to the global scope (survives `runtime.dispose()`); use `Effect.forkScoped` for runtime-tied background fibers. Verified against: effect@4.0.0-beta.92 ## [0.5.0] - 2026-06-09 ### Added - `concurrency.md`: Request / RequestResolver section (batching, dedup, N+1 elimination via `Effect.request`). - `effect-ai.md` + `retry-scheduling.md`: `ExecutionPlan` for ordered multi-provider fallback (`Effect.withExecutionPlan`). - `http.md`: Multipart file-upload section (`HttpApiSchema.asMultipart`, `Multipart.SingleFileSchema`/`FilesSchema`, limit references). - `testing.md`: property-based testing via `Schema.toArbitrary` + `FastCheck` (from `effect/testing`). - `schema.md`: branded / nominal types (`Brand.nominal`/`check`/`make`/`all`; notes v4 has no `refined`/`error`). - `resource-management.md`: `Resource` (refreshable scoped value via `Resource.auto`/`manual`/`refresh`). - `core-patterns.md` + SKILL.md: `Effect.fnUntraced` guidance (use it for helpers that don't need a span/stack-frame; reserve named `Effect.fn` for traced operations). - SKILL.md progressive-disclosure index entries for the new concepts. ### Changed - Repositioned the skill to recommend Effect v4 as the default while keeping full v3 support. The stance is now "prefer v4 for new projects; in an existing codebase match the installed version, default to v4 when unclear." - `SKILL.md` description now leads with v4 (the recommended default) instead of presenting v3/v4 as co-equal. - Version Detection: lists v4 first, replaces the v3-default guidance, keeps the beta caveat (pin an exact `4.0.0-beta.x`). - Primary documentation sources grouped as v4 (primary) / v3 (existing codebases) / both. - Flipped `v3 / v4` orderings to `v4 / v3` in the starter function set, key modules, DI list, and import patterns so v4 reads as primary. ## [0.4.0] - 2026-06-04 ### Added - CHANGELOG; upstream tracking established. - `references/configuration.md` - Config / ConfigProvider (typed env config, defaults, redacted secrets, schema-validated config, custom providers). - `references/sql.md` - SQL toolkit (SqlClient tagged-template queries, SqlSchema, Model/SqlModel repositories, Migrator, driver layers, SqlError reasons). - `references/cli.md` - building command-line apps (Command, Flag, Argument, subcommands). - `references/rpc.md` - typed client/server RPC (RpcGroup, RpcServer, RpcClient, transports, serialization). - `references/distributed.md` - Cluster (sharded entities), Workflow (durable execution), EventLog (event sourcing). - `references/stm.md` - software transactional memory (`Effect.tx`, `TxRef`, `Tx*` collections, `Effect.txRetry`). - `references/datetime.md` - DateTime (Utc/Zoned, arithmetic, zones, formatting). - `references/optics.md` - Optic (immutable nested reads/updates, method-chain API). - `core-patterns.md`: `Match` pattern-matching section. - `resource-management.md`: `Pool` resource-pooling section. - `concurrency.md`: FiberHandle/FiberMap/FiberSet, SubscriptionRef, and worker-threads (RPC-over-worker) sections. - `llm-corrections.md`: `Schema.Defect`/`Schema.Error` are constructor functions in v4; `Random.nextUUIDv4` removed (use `Crypto` service). ### Fixed - `error-modeling.md`: `cause: Schema.Defect` -> `cause: Schema.Defect()` (Schema.Defect is a constructor function since beta.76). ### Changed - Bumped tracked `effect` from `4.0.0-beta.58` to `4.0.0-beta.78`. - `migration-v4.md`: refreshed the "v4 beta additions" log to cover beta.59-78 (Schema.Error/Defect constructors, Random.nextUUIDv4 removal, `Effect.Yieldable`/`Types.MergeRecord` removals, `catch*` error-channel fix, HttpApiTest, and more). - `SKILL.md`: extended the progressive-disclosure index with the new references; added SQL/CLI/RPC/Config to the description. Verified against: effect@4.0.0-beta.78 -
LICENSE.txt 8.9 KB
Apache License Version 2.0, January 2004 https://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS -
SKILL.md 13.5 KB
--- name: effect-ts description: Effect-TS guide for TypeScript, v4 default with v3 support. Use when writing, debugging, or reviewing Effect code across errors, concurrency, services, streams, and schema, or when code imports from 'effect' or any '@effect/*' package. metadata: version: "0.6.5" categories: "development" topics: "effect, typescript, functional-programming, concurrency, error-handling" upstream: "effect@4.0.0-beta.92" openclaw: homepage: https://github.com/tenequm/skills/tree/main/skills/effect-ts emoji: "🌀" envVars: - name: OPENAI_API_KEY required: false description: OpenAI API key for Effect AI examples using the OpenAI provider. --- # Effect-TS Effect is a TypeScript library for building production-grade software with typed errors, structured concurrency, dependency injection, and built-in observability. ## Version Detection Before writing Effect code, detect which version the user is on: ```bash # Check installed version cat package.json | grep '"effect"' ``` - **v4.x** (recommended, the direction Effect is heading): `Context.Service`, `Effect.catch`, `Effect.forkChild`, `Schema.TaggedErrorClass` - **v3.x** (stable, still common in production): `Context.Tag`, `Effect.catchAll`, `Effect.fork`, `Data.TaggedError` > Note: v4 beta briefly used a `ServiceMap` module, renamed back to `Context` on 2026-04-07 (PR #1961). If you see `ServiceMap.*` in any doc or older beta code, it is the current `Context.*`. Both v3 and v4 import `Context` from `"effect"`; the exports inside differ (`Context.Service` in v4 vs `Context.Tag` in v3). **Prefer v4 for new projects** - it's where Effect is going. In an existing codebase, match the installed version: don't rewrite v3 code in v4 syntax unless asked. If the version is genuinely unclear, default to v4 and say so. v4 is still in beta, so pin an exact version (`4.0.0-beta.x`) and expect occasional API churn. ## Primary Documentation Sources **v4 (primary):** - https://github.com/Effect-TS/effect-smol (v4 source + migration guides) - https://github.com/Effect-TS/effect-smol/blob/main/LLMS.md (v4 LLM guide) **v3 (for existing codebases):** - https://effect.website/docs (v3 stable docs) - https://effect.website/llms.txt (LLM topic index) - https://effect.website/llms-full.txt (full docs for large context) **Both versions:** - https://tim-smart.github.io/effect-io-ai/ (concise API list) ## AI Guardrails: Critical Corrections LLM outputs frequently contain incorrect Effect APIs. Verify every API against the reference docs before using it. **Common hallucinations (both versions):** | Wrong (AI often generates) | Correct | |----------------------------------------------|---------------------------------------------------------------| | `Effect.cachedWithTTL(...)` | `Cache.make({ capacity, timeToLive, lookup })` | | `Effect.cachedInvalidateWithTTL(...)` | `cache.invalidate(key)` / `cache.invalidateAll()` | | `Effect.mapError(effect, fn)` | `Effect.mapError(fn)` in pipe, or use `Effect.catchTag` | | `import { Schema } from "@effect/schema"` | `import { Schema } from "effect"` (v3.10+ and all v4) | | `import { JSONSchema } from "@effect/schema"`| `import { JSONSchema } from "effect"` (v3.10+) | | JSON Schema Draft 2020-12 | Effect Schema generates **Draft-07** | | "thread-local storage" | "fiber-local storage" via `FiberRef` (v3) / `Context.Reference` (v4) | | fibers are "cancelled" | fibers are "interrupted" | | all queues have back-pressure | only **bounded** queues; sliding/dropping do not | | `new MyError("message")` | `new MyError({ message: "..." })` (Schema errors take objects) | **v3-specific hallucinations:** | Wrong | Correct (v3) | |------------------------------------|-----------------------------------------------------| | `Effect.Service` (function call) | `class Foo extends Effect.Service<Foo>()("id", {})` | | `Effect.match(effect, { ... })` | `Effect.match(effect, { onSuccess, onFailure })` | | `Effect.provide(layer1, layer2)` | `Effect.provide(Layer.merge(layer1, layer2))` | **v4-specific hallucinations (AI may mix v3/v4):** | Wrong (v3 API used in v4 code) | Correct (v4) | |-----------------------------------|------------------------------------------------------| | `Context.Tag("X")` (v3 shape) | `Context.Service<X>(id)` or class syntax | | `ServiceMap.Service` / `ServiceMap.Reference` | Renamed back to `Context.Service` / `Context.Reference` on 2026-04-07 | | `Effect.catchAll(fn)` | `Effect.catch(fn)` | | `Effect.fork(effect)` | `Effect.forkChild(effect)` | | `Effect.forkDaemon(effect)` | `Effect.forkDetach(effect)` | | `Data.TaggedError` | `Schema.TaggedErrorClass` | | `FiberRef.get(ref)` | `yield* References.X` (a `Context.Reference`) | | `yield* ref` (Ref as Effect) | `yield* Ref.get(ref)` (Ref is no longer an Effect) | | `yield* fiber` (Fiber as Effect) | `yield* Fiber.join(fiber)` (Fiber is no longer Effect) | | `Logger.Default` / `Logger.Live` | `Logger.layer` (v4 naming convention) | | `Schema.TaggedError` | `Schema.TaggedErrorClass` | | `Schema.makeUnsafe(input)` | `Schema.make(input)` (throws `SchemaError`); also instance methods `schema.makeOption(...)`, `schema.makeEffect(...)` | | `ParseResult` (from `"effect"`) | `SchemaIssue` module + `SchemaError` class; narrow with `Schema.isSchemaError` | | `HttpApiEndpoint.get(n, p).pipe(HttpApiEndpoint.setPath(...), setPayload(...), setSuccess(...))` | `HttpApiEndpoint.get(n, p, { params, query, payload, success, error })` (object-option form) | | `Otlp.layer({ url, serviceName })` | `OtlpTracer.layer({ url, resource: { serviceName } })` + `OtlpSerialization.layerJson` + `FetchHttpClient.layer` | | `import { HttpApi } from "@effect/platform"` (v4) | `import { HttpApi } from "effect/unstable/httpapi"` | | HttpApi endpoint schema errors are typed errors by default | In current v4 betas they default to **defects** unless transformed | **Read `references/llm-corrections.md` for the exhaustive corrections table.** ## Progressive Disclosure Read only the reference files relevant to your task: - Error modeling or typed failures → `references/error-modeling.md` - Services, DI, or Layer wiring → `references/dependency-injection.md` - Per-key dynamic layers (per-tenant resources, `LayerMap`) → `references/dependency-injection.md` - Bridging Effect into non-Effect frameworks (Hono/Express, `ManagedRuntime`) → `references/dependency-injection.md` - Retries, timeouts, or backoff → `references/retry-scheduling.md` - Fibers, forking, or parallel work → `references/concurrency.md` - Request batching, N+1 elimination, DataLoader pattern → `references/concurrency.md` - Multi-provider fallback (`ExecutionPlan`) → `references/effect-ai.md` / `references/retry-scheduling.md` - Streams, queues, or SSE → `references/streams.md` - Framing streams (NDJSON / MessagePack encode-decode) → `references/streams.md` - Running child processes / shelling out → `references/concurrency.md` - Resource lifecycle or cleanup → `references/resource-management.md` - Refreshable values (rotating credentials, polled config) → `references/resource-management.md` - Reference-counted shared resources (`RcRef`/`RcMap`) → `references/resource-management.md` - Schema validation or decoding → `references/schema.md` - Branded / nominal types (`Brand`) → `references/schema.md` - Logging, metrics, or tracing → `references/observability.md` - HTTP clients or API calls → `references/http.md` - HTTP API servers → `references/http.md` (covers both client and server) - File uploads / multipart form-data → `references/http.md` - LLM/AI integration → `references/effect-ai.md` - Configuration, env vars, secrets → `references/configuration.md` - SQL / database access → `references/sql.md` - Command-line apps → `references/cli.md` - Typed client/server RPC → `references/rpc.md` - Sharded entities, durable workflows, event sourcing → `references/distributed.md` - Transactional state (STM, `Tx*`) → `references/stm.md` - Date/time handling → `references/datetime.md` - Immutable nested updates (optics) → `references/optics.md` - Graphs, dependency ordering, shortest paths, cycle detection → `references/graph.md` - Pattern matching (`Match`) → `references/core-patterns.md` - Pooling resources (`Pool`) → `references/resource-management.md` - Fiber sets, SubscriptionRef, worker threads → `references/concurrency.md` - Testing Effect code → `references/testing.md` - Property-based testing / generating data from schemas → `references/testing.md` - Migrating from async/await → `references/migration-async.md` - Migrating from v3 to v4 → `references/migration-v4.md` - Core types, gen, pipe, running → `references/core-patterns.md` - Full wrong-vs-correct API table → `references/llm-corrections.md` ## Core Workflow 1. **Detect version** from `package.json` before writing any code 2. **Clarify boundaries**: identify where IO happens, keep core logic as `Effect` values 3. **Choose style**: use `Effect.gen` for sequential logic, pipelines for simple transforms. In v4, prefer `Effect.fn("name")` for named functions 4. **Model errors explicitly**: type expected errors in the `E` channel; treat bugs as defects 5. **Model dependencies** with services and layers; keep interfaces free of construction logic 6. **Manage resources** with `Scope` when opening/closing things (files, connections, etc.) 7. **Provide layers** and run effects only at program edges (`NodeRuntime.runMain` or `ManagedRuntime`) 8. **Verify APIs exist** before using them - consult https://tim-smart.github.io/effect-io-ai/ or source docs ## Starter Function Set Start with these ~20 functions (the official recommended set): **Creating effects:** `Effect.succeed`, `Effect.fail`, `Effect.sync`, `Effect.tryPromise` **Composition:** `Effect.gen` (+ `Effect.fn` in v4), `Effect.andThen`, `Effect.map`, `Effect.tap`, `Effect.all` **Running:** `Effect.runPromise`, `NodeRuntime.runMain` (preferred for entry points) **Error handling:** `Effect.catchTag`, `Effect.catch` (v4) / `Effect.catchAll` (v3), `Effect.orDie` **Resources:** `Effect.acquireRelease`, `Effect.acquireUseRelease`, `Effect.scoped` **Dependencies:** `Effect.provide`, `Effect.provideService` **Key modules:** `Effect`, `Schema`, `Layer`, `Option`, `Result` (v4) / `Either` (v3), `Array`, `Match` **DI (v4):** `Context.Service`, `Context.Reference`, `Layer.effect`, `Effect.fn("name")` **DI (v3):** `Context.Tag`, `Context.Reference` ## Import Patterns Always use barrel imports from `"effect"`: ```typescript import { Context, Effect, Schema, Layer, Option, Stream } from "effect" ``` For companion packages, import from the package name. v3 and v4 differ here: ```typescript // v4 (recommended) - platform transports still separate, but HttpApi / observability // moved under effect/unstable/* import { NodeRuntime } from "@effect/platform-node" import { FetchHttpClient } from "effect/unstable/http" import { HttpApi, HttpApiEndpoint, HttpApiGroup, HttpApiBuilder, HttpApiScalar } from "effect/unstable/httpapi" import { OtlpLogger, OtlpSerialization, OtlpTracer } from "effect/unstable/observability" // v3 (stable) companion packages import { NodeRuntime } from "@effect/platform-node" import { HttpClient } from "@effect/platform" import { NodeSdk } from "@effect/opentelemetry" ``` Avoid deep module imports (`effect/Effect`) unless your bundler requires it for tree-shaking. ## Output Standards - Show imports in every code example - Prefer `Effect.gen` (imperative) for multi-step logic; pipelines for transforms - In v4, use `Effect.fn("name")` instead of bare `Effect.gen` for named functions; use `Effect.fnUntraced` for internal helpers that don't need a span/stack-frame - Never call `Effect.runPromise` / `Effect.runSync` inside library code - only at program edges - Use `NodeRuntime.runMain` for CLI/server entry points (handles SIGINT gracefully) - Use `ManagedRuntime` when integrating Effect into non-Effect frameworks (Hono, Express, etc.) - Always `return yield*` when raising an error in a generator (ensures TS understands control flow) - Avoid point-free/tacit usage: write `Effect.map((x) => fn(x))` not `Effect.map(fn)` (generics get erased) - Keep dependency graphs explicit (services, layers, tags) - State the `Effect<A, E, R>` shape when it helps design decisions ## Agent Quality Checklist Before outputting Effect code, verify: - [ ] Every API exists (check against tim-smart API list or source docs) - [ ] Imports are from `"effect"` (not `@effect/schema`, `@effect/io`, etc.) - [ ] Version matches the user's codebase (v3 vs v4 syntax) - [ ] Expected errors are typed in `E`; unexpected failures are defects - [ ] `run*` is called only at program edges, not inside library code - [ ] Resources opened with `acquireRelease` are wrapped in `Effect.scoped` - [ ] Layers are provided before running (no missing `R` requirements) - [ ] Generator bodies use `yield*` (not `yield` without `*`) - [ ] Error raises in generators use `return yield*` pattern
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.