Files
lsp-mcp/EFFECT.md
T
EFLKumo 35c8470e09 EFFECT.md: Effect 4 API shapes verified against repos/effect
Agents kept re-checking the same v4 signatures (renames from Effect 3,
Schedule, CLI flags, MCP, vitest, vscode-jsonrpc failure modes). Record
them once and point to the file from CLAUDE.md.
2026-09-20 15:36:30 +08:00

6.8 KiB

Effect 4 notes for this repo

Effect 4.0.0-rc.115 (pinned in package.json; repos/effect is the same version, check repos/effect/packages/effect/package.json if either is bumped). The API differs from Effect 3 in places, and the type stripper gives no hints, so agents kept re-verifying the same shapes. This file records what has been verified against the source so the next agent does not have to.

When something is not listed here, grep the source, not node_modules: grep -n "^export const NAME" repos/effect/packages/effect/src/MODULE.ts shows the signature and implementation; repos/effect/packages/effect/test/ has usage examples. Add what you learn.

Renames from Effect 3 (the things that bite)

  • Effect.async → Effect.callback; Effect.catchAll → Effect.catch; Effect.catchAllCause → Effect.catchCause.
  • Every unsafeX is xUnsafe: Deferred.doneUnsafe, Deferred.isDoneUnsafe, PubSub.publishUnsafe, SubscriptionRef.getUnsafe, Latch.openUnsafe, Scope.makeUnsafe.
  • Context.Tag classes are Context.Service<Self, Shape>()("id"); layers are built with Layer.effect(Tag, make).
  • Schema.Union([A, B]) and Schema.Literals(["a", "b"]) take an array. Filters are .check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0)), not .pipe(Schema.int()). .annotate({ description, message }). Schema.TaggedError<Self>()("Tag", fields); Schema.NumberFromString; Schema.fromJsonString(Schema.Json).
  • Duration.isGreaterThan / isLessThan (not greaterThan); Duration.format(d) prints 1s 500ms.
  • Effect.timeout / timeoutOption / timeoutOrElse; Stream.runHead returns an Option.
  • Effect.forkScoped(effect, { startImmediately?, uninterruptible? }); Effect.forkIn(scope); Effect.forkDetach.
  • Effect.tap accepts an effect or a function; Effect.when(condition) returns an Option.
  • Effect.fn("name")(function* (...) {}) for traced generator functions (used everywhere in src/lsp/).

Schedule (reshaped in v4)

  • Constructors: exponential(base, factor), spaced, fixed, recurs(n), forever, jittered, cron.
  • Limits: Schedule.upTo({ times, duration }) (replaces intersect(recurs(n))); Schedule.while(meta => boolean | Effect) replaces whileInput / whileOutput / recurWhile; Schedule.modifyDelay(meta => Duration.Input); Schedule.tap(meta => ...); Schedule.passthrough. Combinators: Schedule.max(a, b) recurs while both do, with the slower delay (Effect 3 intersect); Schedule.min recurs while either does, with the faster delay (union); Schedule.concat runs one then the other (andThen). There is no resetAfter: write a step with its own state instead (below).
  • Metadata given to those callbacks: { input, output, duration, attempt, start, now, elapsed, elapsedSincePrevious }. elapsedSincePrevious is the time between consecutive steps (between crashes, for a restart policy).
  • Custom schedules: Schedule.fromStepWithMetadata(Effect.sync(() => (meta: InputMetadata<Input>) => ...)), where the step returns Effect.succeed([output, delay]) to continue or Cause.done(output) to stop. Effect.sync runs once per retry, so closure state (a crash streak) is per run. See restarts in src/lsp/LanguageServer.ts.
  • Effect.retry takes { while, until, times, schedule }, a Schedule, or a builder ($) => .... Effect.retryOrElse(schedule, (lastError, output) => ...) runs the fallback once the schedule stops. The effect under retry/repeat sees Schedule.CurrentMetadata as a service.

Concurrency primitives (as expected, with v4 names)

  • Deferred.make<A, E>(), await, succeed, fail, done(exit), isDone; the *Unsafe variants are synchronous and return boolean (whether this call completed it). Waiters of Deferred.await resume synchronously inside doneUnsafe.
  • PubSub.unbounded<A>({ replay? }), publish, publishUnsafe, subscribe, Stream.fromPubSub, Stream.fromSubscription.
  • SubscriptionRef.make/get/set/update/changes; changes emits the current value first (replay 1).
  • Latch.make(open?) with .open, .close, .await, .release, .whenOpen(effect).
  • Semaphore.make(n), .withPermits(n)(effect); FiberSet.makeRuntime<R>() / makeRuntimePromise to run effects from callbacks.
  • ScopedCache.makeWith({ capacity, timeToLive: (exit) => Duration, lookup }), get, has, invalidate, keys, values, entries.
  • Cause.done(value) is the halt signal used by Pull, Schedule and Stream internals; Cause.hasInterruptsOnly(cause).

effect/unstable/cli

  • Command.make(name, { flag: Flag..., arg: Argument... }, Effect.fn(function* (config) {})), Command.withDescription, Command.withExamples([{ command, description }]), Command.run({ version }).
  • Flag.String/Int/Boolean/Directory(name, { mustExist })/KeyValuePair/Literals(name, [...]); Flag.withSchema(schema) (decodes from the flag's type, so Schema.NumberFromString for a string flag), withDescription, withDefault, withAlias, withMetavar("never|always|N"), optional (→ Option), map, orElse. Argument.String, Argument.variadic.
  • Help does not print default values; put them in the description. Schema failures print Invalid value for flag --x: "v". Expected: Schema validation failed: <message annotation or issue>.

effect/unstable/ai MCP (see src/Server.ts, src/Tools.ts, src/Resources.ts)

  • McpServer.layerStdio(info) / layerHttp({ ...info, path }) served with HttpRouter.serve; McpServer.registerToolkit(toolkit) after Toolkit.toLayer(handlers); McpServer.registerResource({ uri | template, name, description, mimeType, content, completion }).
  • Tool.make(name, { description, parameters, success, failure, dependencies }).annotate(Tool.Readonly, ...).
  • McpServer.McpServer service exposes notifications["notifications/message" | "notifications/resources/updated"].

@effect/vitest

  • it.effect / it.live tests run inside a Scope; it.layer(layer, { excludeTestServices: true })("name", (it) => ...) shares the layer across the block and, with that option, uses the real clock (Stream.debounce and timeouts hang under TestClock).
  • Real processes and Effect.sleep need real time, so every fixture-server test uses excludeTestServices or it.live.

vscode-jsonrpc (not Effect, but verified the same way)

  • Loss of the peer: connection.onClose fires on transport close and on dispose(); pending requests reject with ResponseError codes ConnectionInactive (-32096), PendingResponseRejected (-32097) or MessageWriteError (-32099).
  • Writing to a dead child's stdin rejects a promise nobody handles (EPIPE crashes the process). LanguageServer.ts writes through a PassThrough piped into stdin and listens for stdin errors instead.
  • connection.onRequest((method, params) => ...) registers a catch-all ("star") handler; handlers registered per type are consulted first.