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.
6.8 KiB
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
unsafeXisxUnsafe:Deferred.doneUnsafe,Deferred.isDoneUnsafe,PubSub.publishUnsafe,SubscriptionRef.getUnsafe,Latch.openUnsafe,Scope.makeUnsafe. Context.Tagclasses areContext.Service<Self, Shape>()("id"); layers are built withLayer.effect(Tag, make).Schema.Union([A, B])andSchema.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(notgreaterThan);Duration.format(d)prints1s 500ms.Effect.timeout/timeoutOption/timeoutOrElse;Stream.runHeadreturns anOption.Effect.forkScoped(effect, { startImmediately?, uninterruptible? });Effect.forkIn(scope);Effect.forkDetach.Effect.tapaccepts an effect or a function;Effect.when(condition)returns anOption.Effect.fn("name")(function* (...) {})for traced generator functions (used everywhere insrc/lsp/).
Schedule (reshaped in v4)
- Constructors:
exponential(base, factor),spaced,fixed,recurs(n),forever,jittered,cron. - Limits:
Schedule.upTo({ times, duration })(replacesintersect(recurs(n)));Schedule.while(meta => boolean | Effect)replaceswhileInput/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 3intersect);Schedule.minrecurs while either does, with the faster delay (union);Schedule.concatruns one then the other (andThen). There is noresetAfter: write a step with its own state instead (below). Metadatagiven to those callbacks:{ input, output, duration, attempt, start, now, elapsed, elapsedSincePrevious }.elapsedSincePreviousis the time between consecutive steps (between crashes, for a restart policy).- Custom schedules:
Schedule.fromStepWithMetadata(Effect.sync(() => (meta: InputMetadata<Input>) => ...)), where the step returnsEffect.succeed([output, delay])to continue orCause.done(output)to stop.Effect.syncruns once perretry, so closure state (a crash streak) is per run. Seerestartsinsrc/lsp/LanguageServer.ts. Effect.retrytakes{ while, until, times, schedule }, aSchedule, or a builder($) => ....Effect.retryOrElse(schedule, (lastError, output) => ...)runs the fallback once the schedule stops. The effect under retry/repeat seesSchedule.CurrentMetadataas a service.
Concurrency primitives (as expected, with v4 names)
Deferred.make<A, E>(),await,succeed,fail,done(exit),isDone; the*Unsafevariants are synchronous and returnboolean(whether this call completed it). Waiters ofDeferred.awaitresume synchronously insidedoneUnsafe.PubSub.unbounded<A>({ replay? }),publish,publishUnsafe,subscribe,Stream.fromPubSub,Stream.fromSubscription.SubscriptionRef.make/get/set/update/changes;changesemits 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>()/makeRuntimePromiseto 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 byPull,ScheduleandStreaminternals;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, soSchema.NumberFromStringfor 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 withHttpRouter.serve;McpServer.registerToolkit(toolkit)afterToolkit.toLayer(handlers);McpServer.registerResource({ uri | template, name, description, mimeType, content, completion }).Tool.make(name, { description, parameters, success, failure, dependencies }).annotate(Tool.Readonly, ...).McpServer.McpServerservice exposesnotifications["notifications/message" | "notifications/resources/updated"].
@effect/vitest
it.effect/it.livetests run inside aScope;it.layer(layer, { excludeTestServices: true })("name", (it) => ...)shares the layer across the block and, with that option, uses the real clock (Stream.debounceand timeouts hang underTestClock).- Real processes and
Effect.sleepneed real time, so every fixture-server test usesexcludeTestServicesorit.live.
vscode-jsonrpc (not Effect, but verified the same way)
- Loss of the peer:
connection.onClosefires on transport close and ondispose(); pending requests reject withResponseErrorcodesConnectionInactive(-32096),PendingResponseRejected(-32097) orMessageWriteError(-32099). - Writing to a dead child's stdin rejects a promise nobody handles (EPIPE crashes the process).
LanguageServer.tswrites through aPassThroughpiped 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.