opencode-effect-enforcer 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +38 -10
  2. package/docs/effect-4.0.0-rc.112.md +316 -0
  3. package/guidance/effect-first-development.md +30 -17
  4. package/guidance/progressive-disclosure-guidance.md +13 -0
  5. package/package.json +3 -2
  6. package/patterns/avoid-direct-tag-checks.md +8 -2
  7. package/patterns/avoid-react-hooks.md +18 -37
  8. package/patterns/effect-run-in-body.md +1 -1
  9. package/patterns/require-effect-concurrency.md +11 -0
  10. package/patterns/use-console-service.md +6 -1
  11. package/skills/effect-ai-language-model/SKILL.md +10 -16
  12. package/skills/effect-ai-prompt/SKILL.md +36 -2
  13. package/skills/effect-ai-provider/SKILL.md +13 -0
  14. package/skills/effect-ai-streaming/SKILL.md +81 -108
  15. package/skills/effect-ai-tool/SKILL.md +50 -87
  16. package/skills/effect-atom-rpc/SKILL.md +9 -2
  17. package/skills/effect-atom-state/SKILL.md +5 -0
  18. package/skills/effect-cache/SKILL.md +32 -0
  19. package/skills/effect-cli/SKILL.md +22 -3
  20. package/skills/effect-concurrency-testing/SKILL.md +7 -9
  21. package/skills/effect-domain-modeling/SKILL.md +208 -1169
  22. package/skills/effect-domain-predicates/SKILL.md +5 -6
  23. package/skills/effect-error-handling/SKILL.md +5 -4
  24. package/skills/effect-http-api/SKILL.md +12 -1
  25. package/skills/effect-http-client/SKILL.md +1 -1
  26. package/skills/effect-http-server/SKILL.md +14 -3
  27. package/skills/effect-layer-design/SKILL.md +22 -56
  28. package/skills/effect-mcp-server/SKILL.md +1 -1
  29. package/skills/effect-pattern-matching/SKILL.md +44 -11
  30. package/skills/effect-platform-abstraction/SKILL.md +1 -1
  31. package/skills/effect-platform-layers/SKILL.md +1 -1
  32. package/skills/effect-rpc-api/SKILL.md +8 -1
  33. package/skills/effect-rpc-client/SKILL.md +20 -6
  34. package/skills/effect-rpc-cluster/SKILL.md +44 -14
  35. package/skills/effect-rpc-server/SKILL.md +32 -5
  36. package/skills/effect-scheduling/SKILL.md +1 -1
  37. package/skills/effect-schema-composition/SKILL.md +69 -15
  38. package/skills/effect-schema-v4/SKILL.md +43 -1
  39. package/skills/effect-scope/SKILL.md +30 -0
  40. package/skills/effect-service-implementation/SKILL.md +10 -4
  41. package/skills/effect-socket/SKILL.md +5 -5
  42. package/skills/effect-sql/SKILL.md +22 -0
  43. package/skills/effect-stream/SKILL.md +32 -1
  44. package/skills/effect-testing/SKILL.md +39 -31
  45. package/skills/effect-workflow/SKILL.md +6 -0
  46. package/patterns/vm-in-wrong-file.md +0 -51
  47. package/skills/effect-react-vm/SKILL.md +0 -675
@@ -55,8 +55,7 @@ When you need an `Equivalence` instance (for use with combinators), derive it fr
55
55
  import { Schema, Array } from 'effect';
56
56
  import * as Equivalence from 'effect/Equivalence';
57
57
 
58
- declare const Task: Schema.Schema<any, any, never>;
59
- type Task = Schema.Schema.Type<typeof Task>;
58
+ class Task extends Schema.Class<Task>('Task')({ id: Schema.String }) {}
60
59
 
61
60
  // Derive from schema (structural equality)
62
61
  export const TaskEquivalence = Schema.toEquivalence(Task);
@@ -603,7 +602,7 @@ declare const appointments: Array<Appointment.Appointment>;
603
602
  * import { pipe } from "effect/Function"
604
603
  *
605
604
  * const tomorrow = DateTime.addDuration(
606
- * DateTime.unsafeNow(),
605
+ * DateTime.makeUnsafe('2026-09-06T00:00:00Z'),
607
606
  * Duration.days(1)
608
607
  * )
609
608
  *
@@ -612,7 +611,8 @@ declare const appointments: Array<Appointment.Appointment>;
612
611
  * Array.filter(Appointment.isScheduledBefore(tomorrow))
613
612
  * )
614
613
  */
615
- const tomorrow = DateTime.addDuration(DateTime.unsafeNow(), Duration.days(1));
614
+ // Deterministic fixture; in runtime code obtain the instant with yield* DateTime.now.
615
+ const tomorrow = DateTime.addDuration(DateTime.makeUnsafe('2026-09-06T00:00:00Z'), Duration.days(1));
616
616
 
617
617
  const beforeTomorrow = pipe(
618
618
  appointments,
@@ -791,8 +791,7 @@ const areSame = Equal.equals(t1, t2);
791
791
  ```typescript
792
792
  import { Schema, Array } from 'effect';
793
793
 
794
- declare const Task: Schema.Schema<any, any, never>;
795
- type Task = Schema.Schema.Type<typeof Task>;
794
+ class Task extends Schema.Class<Task>('Task')({ id: Schema.String }) {}
796
795
 
797
796
  export const Equivalence = Schema.toEquivalence(Task);
798
797
 
@@ -1083,7 +1083,7 @@ const secondaryService: Effect.Effect<Data, SecondaryServiceError> =
1083
1083
 
1084
1084
  // Try primary, fallback to secondary
1085
1085
  // Effect<Data, SecondaryServiceError, Dependencies>
1086
- const program = primaryService.pipe(Effect.orElse(() => secondaryService));
1086
+ const program = primaryService.pipe(Effect.catch(() => secondaryService));
1087
1087
  ```
1088
1088
 
1089
1089
  ### Retry with Schedule
@@ -1205,10 +1205,11 @@ const program = loadConfig.pipe(
1205
1205
 
1206
1206
  // With custom defect message
1207
1207
  const program2 = loadConfig.pipe(
1208
- Effect.orDieWith(
1208
+ Effect.mapError(
1209
1209
  (error) =>
1210
1210
  new Error(`Fatal: Configuration failed to load: ${error._tag}`)
1211
- )
1211
+ ),
1212
+ Effect.orDie
1212
1213
  );
1213
1214
  ```
1214
1215
 
@@ -1387,7 +1388,7 @@ Model ambiguous HTTP responses (where the body structure differs for success vs
1387
1388
 
1388
1389
  ```typescript
1389
1390
  import { Effect, Schema } from 'effect';
1390
- import { HttpClientResponse } from 'effect/unstable/HttpClient';
1391
+ import { HttpClientResponse } from 'effect/unstable/http';
1391
1392
 
1392
1393
  class TokenSuccess extends Schema.Class<TokenSuccess>('TokenSuccess')({
1393
1394
  access_token: AccessToken,
@@ -14,7 +14,7 @@ Key reference files:
14
14
  - `packages/effect/HTTPAPI.md` — canonical HttpApi documentation
15
15
  - `packages/effect/src/unstable/httpapi/*.ts` — module sources
16
16
  - `packages/effect/typetest/unstable/httpapi/*.tst.ts` — type-level contracts
17
- - `packages/platform-node/test/HttpApi.test.ts` — comprehensive runtime tests
17
+ - `packages/platform/node/test/HttpApi.test.ts` — comprehensive runtime tests
18
18
  - `ai-docs/src/51_http-server/` — server walkthrough with fixtures
19
19
  - `ai-docs/src/50_http-client/` — HttpClient walkthrough
20
20
 
@@ -1170,6 +1170,17 @@ Top-level group endpoints are at the root: `buildUrl.health()`. With `disableCod
1170
1170
 
1171
1171
  ## OpenAPI Documentation
1172
1172
 
1173
+ As of rc.112 built-in OpenAPI documentation responses are generated lazily on
1174
+ the first request, rather than while the route layer is built. A generation
1175
+ defect is not permanently cached: a later request retries generation. Include a
1176
+ documentation-route request in integration checks if generation must be verified;
1177
+ successful server startup alone no longer proves it. Explicit `OpenApi.fromApi`
1178
+ remains a direct synchronous generation call.
1179
+
1180
+ JSON Schema import/conversion now rejects unsupported references, validation
1181
+ keywords, and unrepresentable dialect conversions instead of weakening them.
1182
+ See `effect-schema-v4` before importing third-party schemas into an API contract.
1183
+
1173
1184
  ### Scalar UI
1174
1185
 
1175
1186
  ```ts
@@ -24,7 +24,7 @@ Key files:
24
24
  - `packages/effect/src/unstable/http/Url.ts` — immutable helpers over the native `URL`
25
25
  - `packages/effect/src/unstable/http/Cookies.ts` — cookie model, `fromSetCookie`, `toCookieHeader`, `getValue`
26
26
  - `packages/effect/src/unstable/http/Headers.ts` — header model, `Input` forms, `CurrentRedactedNames`
27
- - `packages/platform-node/src/NodeHttpClient.ts` — Node transports: undici, node:http, fetch re-export
27
+ - `packages/platform/node/src/NodeHttpClient.ts` — Node transports: undici, node:http, fetch re-export
28
28
  - `packages/effect/test/unstable/http/HttpClient.test.ts` — retryTransient, withRateLimiter, abort semantics
29
29
  - `ai-docs/src/50_http-client/10_basics.ts` — canonical "wrap a configured client in a service" lesson
30
30
 
@@ -24,9 +24,9 @@ Key files:
24
24
  - `packages/effect/src/unstable/http/HttpBody.ts` — body variants (`Empty`/`Raw`/`Uint8Array`/`FormData`/`Stream`) and constructors
25
25
  - `packages/effect/src/unstable/http/Headers.ts`, `Cookies.ts`, `Multipart.ts` — header/cookie/multipart models and limits
26
26
  - `packages/effect/src/unstable/http/HttpStaticServer.ts` — static file serving
27
- - `packages/platform-node/src/NodeHttpServer.ts` — Node server adapter, `layer`, `layerTest`, graceful shutdown
28
- - `packages/platform-bun/src/BunHttpServer.ts` — Bun equivalent
29
- - `packages/platform-node/test/NodeHttpServer.test.ts` — the best end-to-end reference for real route/middleware/multipart wiring
27
+ - `packages/platform/node/src/NodeHttpServer.ts` — Node server adapter, `layer`, `layerTest`, graceful shutdown
28
+ - `packages/platform/bun/src/BunHttpServer.ts` — Bun equivalent
29
+ - `packages/platform/node/test/NodeHttpServer.test.ts` — the best end-to-end reference for real route/middleware/multipart wiring
30
30
 
31
31
  ## Core Model
32
32
 
@@ -197,6 +197,10 @@ const byteStream = request.stream; // Stream<Uint8Array, HttpServerError> (singl
197
197
 
198
198
  Cap accepted body sizes with the `MaxBodySize` reference (re-exported from `HttpIncomingMessage`, default `undefined` = unlimited):
199
199
 
200
+ On Node in rc.112, `remoteAddress` returns `Option.none()` after Node has cleared
201
+ the incoming message's socket. Preserve absence; do not dereference the native
202
+ socket after request cleanup. The same behavior applies to Node client responses.
203
+
200
204
  ```ts
201
205
  import { FileSystem } from 'effect';
202
206
 
@@ -650,6 +654,13 @@ yield* HttpServer.serveEffect(httpEffect);
650
654
 
651
655
  ## 8. WebSocket Upgrades
652
656
 
657
+ In `@effect/platform-bun` rc.112, outgoing WebSocket messages are compressed when
658
+ per-message deflate is configured **and negotiated**. The server option
659
+ `websocket.compressionThreshold` sets the minimum byte size (default `1024`);
660
+ smaller messages stay uncompressed. Configure it alongside
661
+ `websocket.perMessageDeflate` on `BunHttpServer.layer` rather than pre-compressing
662
+ application payloads. See `packages/platform/bun/src/BunHttpServer.ts`.
663
+
653
664
  `request.upgrade` yields a `Socket` (from `effect/unstable/socket`) once the connection is upgraded. Both `NodeHttpServer` and `BunHttpServer` handle the platform `upgrade` events for you — just write a normal route:
654
665
 
655
666
  ```ts
@@ -554,69 +554,35 @@ const services = Layer.mergeAll(UserServiceLayer, OrderServiceLayer).pipe(
554
554
 
555
555
  If downstream code does not need the dependency, use `Layer.provide` and keep it hidden. Do not preserve every intermediate service by default.
556
556
 
557
- ## Pattern: SynchronizedRef + Deferred State Machine
557
+ ## Pattern: Share Work With a Cache
558
558
 
559
- For services that need atomic state transitions with concurrent callers, use `SynchronizedRef.modifyEffect` combined with `Deferred` for result sharing:
559
+ For lazy one-shot result sharing, allocate `Effect.cached(work)` once inside the
560
+ service's layer. It shares in-flight work and the completed result (including
561
+ failure). Do not reallocate the cache on each method call.
560
562
 
563
+ <!-- typecheck -->
561
564
  ```typescript
562
- import { Deferred, Effect, Fiber, Scope, SynchronizedRef } from 'effect';
563
-
564
- type State<A, E> =
565
- | { readonly _tag: 'Idle' }
566
- | {
567
- readonly _tag: 'Running';
568
- readonly done: Deferred.Deferred<A, E>;
569
- readonly fiber: Fiber.Fiber<A, E>;
570
- }
571
- | { readonly _tag: 'Pending'; readonly done: Deferred.Deferred<A, E> };
572
-
573
- const make = <A, E>(scope: Scope.Scope) => {
574
- const ref = SynchronizedRef.makeUnsafe<State<A, E>>({ _tag: 'Idle' });
575
-
576
- const run = (work: Effect.Effect<A, E>) =>
577
- SynchronizedRef.modifyEffect(
578
- ref,
579
- Effect.fnUntraced(function* (state) {
580
- switch (state._tag) {
581
- case 'Running':
582
- // Already running — share the existing result
583
- return [Deferred.await(state.done), state];
584
- case 'Idle': {
585
- // Start new work
586
- const done = yield* Deferred.make<A, E>();
587
- const fiber = yield* Effect.forkIn(
588
- work.pipe(Effect.intoDeferred(done)),
589
- scope
590
- );
591
- return [
592
- Deferred.await(done),
593
- { _tag: 'Running' as const, done, fiber }
594
- ];
595
- }
596
- case 'Pending': {
597
- // Queued — share the pending result
598
- return [Deferred.await(state.done), state];
599
- }
600
- }
601
- })
602
- ).pipe(Effect.flatten);
603
-
604
- return { run };
605
- };
606
- ```
565
+ import { Context, Effect, Layer } from 'effect';
607
566
 
608
- Key properties:
567
+ class Settings extends Context.Service<Settings, {
568
+ readonly load: Effect.Effect<string>;
569
+ }>()('app/Settings') {}
609
570
 
610
- - **`SynchronizedRef.modifyEffect`** — atomically reads state, runs an effect, and updates state in one operation. No other caller can interleave.
611
- - **`Deferred`** — shares the result of in-flight work with concurrent callers who arrive while it's running.
612
- - **`Effect.forkIn(work, scope)`** — ties the worker fiber to the service scope, not the calling fiber.
613
- - The state machine pattern ensures at most one concurrent execution of `work`, with all callers sharing the same result.
571
+ const layer = Layer.effect(Settings, Effect.gen(function* () {
572
+ const load = yield* Effect.cached(Effect.succeed('settings'));
573
+ return Settings.of({ load });
574
+ }));
575
+ ```
614
576
 
615
- Use this pattern when:
577
+ For refresh use `Effect.cachedInvalidateWithTTL(work, Duration.infinity)` and
578
+ expose the returned invalidation effect. For keyed retention use `Cache`,
579
+ `ScopedCache`, `RcMap`, or `LayerMap` according to resource lifetime (see
580
+ `effect-cache`). `LayerMap.contextEffectOption` in rc.112 atomically retains an
581
+ already-cached layer context without allocating a missing key.
616
582
 
617
- - Multiple concurrent callers may trigger the same expensive operation
618
- - Only one execution should run at a time
619
- - All callers should receive the same result
583
+ When the requirement is a restartable worker, latest-wins scheduling, or queued
584
+ state transitions, use a dedicated coordinator with explicit states and a
585
+ supervised fiber lifetime (see `effect-fiber`). A cache is not a job scheduler.
620
586
 
621
587
  ## Naming Convention
622
588
 
@@ -53,7 +53,7 @@ Layer.mergeAll(
53
53
  )
54
54
  ```
55
55
 
56
- Every server runner now requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback. Streamable HTTP rejects an explicit unsupported `MCP-Protocol-Version` header with `400`.
56
+ Every server runner requires a non-empty `protocols` option. Put the preferred fallback revision first; an exact initialization offer is selected when present, otherwise the first adapter is used where the transport permits fallback. In rc.112, `initialize` negotiates from its body even if the client sends an unsupported default `MCP-Protocol-Version` header. The header is checked only on subsequent requests; unsupported explicit versions there still return `400`. Do not reject the initialization request in custom middleware before body negotiation.
57
57
 
58
58
  ## Protocol Revisions
59
59
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: effect-pattern-matching
3
- description: Master Effect pattern matching using Data.TaggedEnum, $match, $is, Match.typeTags, and Effect.match. Avoid manual _tag checks and Effect.result patterns. Use this skill when working with discriminated unions, ADTs, or conditional logic based on tagged types.
3
+ description: Match schema tagged unions with match/matchOrElse and guards, trusted Data.TaggedEnum values with $match/$is, and Effect outcomes with Effect.match. Use for discriminated unions, ADTs, or conditional logic based on tagged types.
4
4
  ---
5
5
 
6
6
  # Effect Pattern Matching Skill
@@ -28,9 +28,44 @@ Reference this for:
28
28
  - Declarative, not imperative
29
29
  - Pipeline-friendly composition
30
30
 
31
- ## Pattern 1: Data.TaggedEnum for ADTs
31
+ ## Schema-First Matching (rc.112)
32
32
 
33
- Use `Data.TaggedEnum` instead of manual tagged unions.
33
+ For domain/wire models, prefer class variants combined with
34
+ `Schema.Union([...]).pipe(Schema.toTaggedUnion('kind'))`. Use `.match` for
35
+ exhaustiveness, `.guards` for narrowing decoded values, and `.matchOrElse` for
36
+ intentional partial matching. Direct `Schema.TaggedUnion` creates canonical
37
+ `_tag` object variants. Both expose data-first and data-last `matchOrElse` forms;
38
+ the `toTaggedUnion` fallback excludes handled variants, while direct
39
+ `Schema.TaggedUnion.matchOrElse` types its fallback as the full union.
40
+
41
+ <!-- typecheck -->
42
+ ```typescript
43
+ import * as Schema from 'effect/Schema';
44
+
45
+ const State = Schema.TaggedUnion({
46
+ Loading: {},
47
+ Ready: { data: Schema.Array(Schema.String) },
48
+ Failed: { message: Schema.String }
49
+ });
50
+ type State = typeof State.Type;
51
+
52
+ const getData = State.matchOrElse({ Ready: (state) => state.data }, () => []);
53
+ const label = State.match({
54
+ Loading: () => 'Loading',
55
+ Ready: ({ data }) => `${data.length} items`,
56
+ Failed: ({ message }) => message
57
+ });
58
+ ```
59
+
60
+ TypeScript correctly narrows literal discriminator checks. Prefer these helpers
61
+ for exhaustive, reusable branching, not because manual checks cannot narrow.
62
+ For the full class-based lifecycle example see `effect-domain-modeling`.
63
+
64
+ ## Pattern 1: Data.TaggedEnum for Trusted ADTs
65
+
66
+ Use `Data.TaggedEnum` for trusted, non-schema ADTs. It supplies constructors and
67
+ tag checks but does not parse unknown input; avoid a parallel Data model for an
68
+ existing schema union.
34
69
 
35
70
  ### The Problem: Manual Tagged Unions
36
71
 
@@ -415,14 +450,12 @@ type LoadState = Data.TaggedEnum<{
415
450
  }>;
416
451
  const LoadState = Data.taggedEnum<LoadState>();
417
452
 
418
- const getData = (state: LoadState): string[] =>
419
- pipe(
420
- state,
421
- // Type guard refines to Ready
422
- LoadState.$is('Ready'),
423
- // Now can access .data safely
424
- (ready) => (ready ? ready.data : [])
425
- );
453
+ const getData = LoadState.$match({
454
+ Loading: () => [],
455
+ Ready: ({ data }) => data,
456
+ Error: () => []
457
+ });
458
+ // A guard returns boolean; piping through $is would lose the original value.
426
459
  ```
427
460
 
428
461
  ## Pattern 5: Use Option.match Instead of \_tag Checks
@@ -16,7 +16,7 @@ Reference this for:
16
16
  - Path source: `packages/effect/src/Path.ts`
17
17
  - Crypto source: `packages/effect/src/Crypto.ts`
18
18
  - Socket source: `packages/effect/src/unstable/socket/`
19
- - Platform layers: `packages/platform-node/`, `packages/platform-bun/`, and `packages/platform-browser/`
19
+ - Platform layers: `packages/platform/node/`, `packages/platform/bun/`, and `packages/platform/browser/`
20
20
  - Migration guide: `MIGRATION.md`
21
21
  - Effect source: `packages/effect/src/`
22
22
 
@@ -292,7 +292,7 @@ const TestContext = Layer.mergeAll(
292
292
  Terminal.make({
293
293
  columns: Effect.succeed(80),
294
294
  rows: Effect.succeed(24),
295
- readInput: Effect.dieMessage('readInput not used in this test'),
295
+ readInput: Effect.die('readInput not used in this test'),
296
296
  readLine: Effect.succeed('test input'),
297
297
  display: () => Effect.void
298
298
  })
@@ -9,6 +9,13 @@ This skill covers the **contract layer**: the definitions that client and server
9
9
 
10
10
  ## Effect Source Reference
11
11
 
12
+ In rc.112 schema encoding is selected by the transport's `codecFor`, rather
13
+ than being fixed to canonical JSON for every protocol. Preserve the schemas and
14
+ their decoding/encoding requirements in shared contracts; custom protocol
15
+ implementations must supply `codecFor` (see `effect-rpc-client` /
16
+ `effect-rpc-server`). Existing JSON, NDJSON, JSON-RPC, and MsgPack formats keep
17
+ their wire representation; the new SchemaBinary transport has its own codecs.
18
+
12
19
  The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these modules are under `unstable` and move between betas.
13
20
 
14
21
  Key files:
@@ -20,7 +27,7 @@ Key files:
20
27
  - `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire envelopes: `Request`, `Ack`, `Interrupt`, `Eof`, `Ping`, `ResponseChunk`, `ResponseExit`, `ExitEncoded`, `RequestId`
21
28
  - `packages/effect/src/unstable/rpc/RpcClientError.ts` — the transport error type referenced when typing shared client aliases
22
29
  - `packages/effect/src/unstable/rpc/index.ts` — public exports of the rpc namespace
23
- - `packages/platform-node/test/fixtures/rpc-schemas.ts` — the best real-world contract fixture: rpcs, streaming, middleware, deferred responses
30
+ - `packages/platform/node/test/fixtures/rpc-schemas.ts` — the best real-world contract fixture: rpcs, streaming, middleware, deferred responses
24
31
  - `packages/effect/test/rpc/Rpc.test.ts` — `exitSchema`, custom defect schemas, `getStreamSchemas` semantics
25
32
 
26
33
  ## Core Model
@@ -20,8 +20,8 @@ Key files:
20
20
  - `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage`, `layerInitialMessage`, `initialMessage`
21
21
  - `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire vocabulary: `Request`, `Ack`, `Interrupt`, `Chunk`, `Exit`, `Defect`, `Ping`/`Pong`, `ClientProtocolError`, `RequestId`
22
22
  - `packages/effect/src/unstable/socket/Socket.ts` — `Socket` service, `layerWebSocket`, `WebSocketConstructor`
23
- - `packages/platform-node/test/RpcServer.test.ts` + `test/fixtures/rpc-e2e.ts` + `test/fixtures/rpc-schemas.ts` — the best end-to-end reference: every transport × serialization combination, headers, streams, interrupts, defects
24
- - `packages/platform-browser/test/RpcWorker.test.ts` + `test/fixtures/rpc-worker.ts` — worker transport end to end
23
+ - `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-e2e.ts` + `test/fixtures/rpc-schemas.ts` — the best end-to-end reference: every transport × serialization combination, headers, streams, interrupts, defects
24
+ - `packages/platform/browser/test/RpcWorker.test.ts` + `test/fixtures/rpc-worker.ts` — worker transport end to end
25
25
 
26
26
  ## Core Model
27
27
 
@@ -293,6 +293,13 @@ const TcpProtocolLive = RpcClient.layerProtocolSocket().pipe(
293
293
 
294
294
  Build your own with `RpcClient.Protocol.make((writeResponse, clientIds) => Effect<Omit<Service, 'run'>>)` — it buffers server responses per client until that client's `run` loop is installed. You rarely need this; prefer `RpcTest` for in-memory wiring (section 11).
295
295
 
296
+ As of rc.112 the returned service must include `codecFor`, normally forwarded
297
+ from the selected `RpcSerialization` service. It chooses the schema codec for
298
+ RPC payloads/results while the protocol handles envelopes. JSON-compatible
299
+ custom transports can use `codecFor: Schema.toCodecJson`. Preserve the schema's
300
+ decoding and encoding service requirements; see `effect-rpc-server` for the
301
+ full `RpcSerialization.CodecFor` contract.
302
+
296
303
  ---
297
304
 
298
305
  ## 5. Serialization — Pairing Codecs with Transports
@@ -303,7 +310,14 @@ Build your own with `RpcClient.Protocol.make((writeResponse, clientIds) => Effec
303
310
  |---|---|---|---|
304
311
  | `RpcSerialization.layerJson` | `application/json` | no | HTTP (response decoded once, as an array); WebSocket (each ws message is one frame already) |
305
312
  | `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | anything — the safe default; enables streaming over HTTP |
306
- | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary transports; smallest payloads; uses msgpackr `useRecords: true` |
313
+ | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary framing with JSON-compatible schema codecs; msgpackr `useRecords: true` |
314
+ | `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | schema-derived binary framing and payload codecs; pair on both peers |
315
+
316
+ `layerSchemaBinary({ maxFrameSize?, fingerprintPayloads? })` defaults to a
317
+ 16 MiB frame limit and no payload fingerprint. Envelopes are fingerprinted and
318
+ dictionary-enabled; payload fingerprints opt into strict layout agreement.
319
+ Existing built-in formats retain their wire encoding in rc.112. Workers use
320
+ `Schema.toCodecJson` with structured clone without a serialization layer.
307
321
  | `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop over HTTP/WebSocket; maps rpc tag ↔ `method`, supports batch arrays |
308
322
  | `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes (newline) | JSON-RPC 2.0 over sockets |
309
323
 
@@ -413,7 +427,7 @@ const getUser = (id: string) =>
413
427
  Semantics to remember:
414
428
 
415
429
  - **`ClientProtocolError` fails everything in flight.** When a socket dies or a worker crashes, every pending request on that connection fails with the same `RpcClientError`. Requests are *not* replayed after reconnect — retry at the call site.
416
- - **Server defects are `Cause.Die`, not typed failures.** `Effect.catchTag` will not see them; use `Effect.sandbox`/`Effect.catchAllCause`. What survives the wire depends on the rpc's `defect` schema (see the effect-rpc-api skill).
430
+ - **Server defects are `Cause.Die`, not typed failures.** `Effect.catchTag` will not see them; use `Effect.sandbox`/`Effect.catchCause`. What survives the wire depends on the rpc's `defect` schema (see the effect-rpc-api skill).
417
431
  - **Whole-connection defects** (server-side fatal defects when the server runs without `disableFatalDefects`) arrive as a `Defect` message and kill every in-flight request on the connection as `Cause.Die`.
418
432
  - `RpcTest.makeClient` clients have `E = never` — no transport error channel, only your rpc errors and middleware errors.
419
433
 
@@ -548,7 +562,7 @@ it.effect('GetUser', () =>
548
562
 
549
563
  Signature: `RpcTest.makeClient(group, options?: { flatten? })` with required context `Scope | Rpc.ToHandler<Rpcs> | Rpc.Middleware<Rpcs> | Rpc.MiddlewareClient<Rpcs>` — i.e. the handler layers, any *server* middleware layers, **and** any client middleware layers. Forgetting the client middleware layer is the classic confusing type error.
550
564
 
551
- The test client's `E` is `never`: transport errors cannot occur, so tests exercise only your declared errors, middleware errors, and defects. To also test serialization and transport semantics, build a real client+server pair against `NodeHttpServer.layerTest` exactly as `packages/platform-node/test/RpcServer.test.ts` does.
565
+ The test client's `E` is `never`: transport errors cannot occur, so tests exercise only your declared errors, middleware errors, and defects. To also test serialization and transport semantics, build a real client+server pair against `NodeHttpServer.layerTest` exactly as `packages/platform/node/test/RpcServer.test.ts` does.
552
566
 
553
567
  ### `RpcClient.makeNoSerialization` (advanced)
554
568
 
@@ -656,7 +670,7 @@ const WorkerClientLive = UsersClient.layer.pipe(
656
670
  5. **Mismatched client/server serialization.** Both sides share one `RpcSerialization` choice; there is no negotiation. Garbled `RpcClientDefect: Error decoding ...` errors usually mean the codecs differ.
657
671
  6. **Reading mixed-case header names server-side.** `Headers.fromInput` lowercases keys: send `{ userId: '123' }`, read `headers.userid`. Same for `withHeaders` and middleware `Headers.set`.
658
672
  7. **Expecting `discard: true` to be error-free.** It only removes the rpc's *declared* error; `RpcClientError` and middleware errors remain, and over HTTP the POST round-trip is still awaited.
659
- 8. **Catching server defects with `catchTag`.** Handler `Effect.die`s arrive as `Cause.Die`, not typed failures. Use `Effect.sandbox`/`Effect.catchAllCause`, and remember one server fatal defect can fail *all* in-flight requests on the connection.
673
+ 8. **Catching server defects with `catchTag`.** Handler `Effect.die`s arrive as `Cause.Die`, not typed failures. Use `Effect.sandbox`/`Effect.catchCause`, and remember one server fatal defect can fail *all* in-flight requests on the connection.
660
674
  9. **Assuming requests survive a reconnect.** A socket/worker failure fails every in-flight call with `RpcClientError`; after reconnect nothing is replayed. Add `Effect.retry` at call sites; `retryTransientErrors: true` only keeps requests pending across *connection-establishment* failures.
661
675
  10. **Passing `retryPolicy` to `layerProtocolSocket`.** The layer accepts `retryTransientErrors` and `onTransientError`, but not `retryPolicy`. For a custom reconnect schedule use `Layer.effect(RpcClient.Protocol)(RpcClient.makeProtocolSocket({ retryPolicy, retryTransientErrors, onTransientError }))`.
662
676
  11. **Treating a flattened client like an object client.** With `flatten: true` you call `client('GetUser', payload)`; `client.GetUser(payload)` is not a function. Pick one shape per client.
@@ -36,9 +36,9 @@ Key files:
36
36
  - `packages/effect/src/unstable/workflow/WorkflowProxy.ts` + `WorkflowProxyServer.ts` — workflow ↔ RPC/HTTP bridge
37
37
  - `packages/effect/src/unstable/cluster/ClusterWorkflowEngine.ts` — production workflow engine backed by sharding + storage
38
38
  - `packages/effect/src/unstable/reactivity/AtomRpc.ts` — reactive RPC client for Atom UIs (see also `effect-atom-rpc` skill)
39
- - `packages/platform-node/src/NodeClusterHttp.ts` / `NodeClusterSocket.ts` — Node "all-in-one" cluster layers
40
- - `packages/platform-bun/src/BunClusterHttp.ts` / `BunClusterSocket.ts` — Bun equivalents
41
- - `packages/platform-node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — best end-to-end reference for real RPC wiring
39
+ - `packages/platform/node/src/NodeClusterHttp.ts` / `NodeClusterSocket.ts` — Node "all-in-one" cluster layers
40
+ - `packages/platform/bun/src/BunClusterHttp.ts` / `BunClusterSocket.ts` — Bun equivalents
41
+ - `packages/platform/node/test/RpcServer.test.ts` + `test/fixtures/rpc-{schemas,e2e}.ts` — best end-to-end reference for real RPC wiring
42
42
  - `packages/effect/test/cluster/TestEntity.ts` + `test/cluster/Entity.test.ts` — best reference for Entity + makeTestClient
43
43
 
44
44
  ## Imports
@@ -606,7 +606,10 @@ client.Subscribe({ topic: 't' }, {
606
606
  });
607
607
  ```
608
608
 
609
- `discard: true` removes the error channel — the request is sent and acknowledged; the result and any failure are discarded. Use for fire-and-forget commands (especially against persistent entities).
609
+ `discard: true` skips response decoding and removes response-side failures;
610
+ transport and required client-middleware failures can still occur. A successful
611
+ send is not proof that the server completed the operation. Cluster persistent
612
+ messages additionally have their documented storage/delivery outcomes.
610
613
 
611
614
  `asQueue: true` is useful when you need finer control than a `Stream` gives you — e.g., you want to take only one chunk, then drop it. The end-of-stream signal is `Cause.Done` in the queue's error channel.
612
615
 
@@ -685,15 +688,21 @@ const ConnectionHooksLayer = Layer.succeed(RpcClient.ConnectionHooks, {
685
688
 
686
689
  ### `RpcSchema.ClientAbort`
687
690
 
688
- When a client interrupts a streaming subscription, the server-side handler's `onInterrupt` finalizer sees a `Cause` carrying the `ClientAbort` annotation. Use it to distinguish client cancel from server shutdown:
691
+ When a client interrupts a subscription, inspect interrupt reasons in the exit
692
+ cause for the `ClientAbort` annotation. `onInterrupt` receives interruptor IDs,
693
+ not a Cause; use `onExit` to distinguish client cancel from server shutdown:
689
694
 
695
+ <!-- typecheck -->
690
696
  ```ts
691
697
  import { RpcSchema } from 'effect/unstable/rpc';
692
- import { Cause, Context } from 'effect';
693
-
694
- const subscribeHandler = stream.pipe(
695
- Effect.onInterrupt((cause) => {
696
- const isClientAbort = Context.has(cause, RpcSchema.ClientAbort);
698
+ import { Cause, Effect, Exit, Stream } from 'effect';
699
+ import * as Arr from 'effect/Array';
700
+
701
+ declare const stream: Stream.Stream<string>;
702
+ const subscribeHandler = stream.pipe(Stream.runDrain,
703
+ Effect.onExit((exit) => {
704
+ const isClientAbort = Exit.isFailure(exit) && Arr.some(exit.cause.reasons, (reason) =>
705
+ Cause.isInterruptReason(reason) && reason.annotations.has(RpcSchema.ClientAbort.key));
697
706
  return Effect.logInfo('subscribe ended', { isClientAbort });
698
707
  })
699
708
  );
@@ -782,10 +791,17 @@ The choice of serialization is load-bearing because of *framing*. Some transport
782
791
  | Layer | Content-Type | Framed? | Use for | Notes |
783
792
  |---|---|---|---|---|
784
793
  | `RpcSerialization.layerJson` | `application/json` | no | `layerProtocolHttp` | Default JSON over request/response |
785
- | `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | `layerProtocolWebsocket`, sockets, http+stream | Newline-delimited JSON; required for streaming |
794
+ | `RpcSerialization.layerNdjson` | `application/ndjson` | yes (newline) | `layerProtocolWebsocket`, sockets, http+stream | Newline-delimited JSON; one of several streaming formats |
786
795
  | `RpcSerialization.layerJsonRpc()` | `application/json` (configurable) | no | JSON-RPC 2.0 interop | Maps `_tag` to `method`; preserves batched arrays |
787
796
  | `RpcSerialization.layerNdJsonRpc()` | `application/json-rpc` (configurable) | yes (newline) | JSON-RPC 2.0 over sockets | |
788
- | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary transports | Smallest wire size; native binary; uses `useRecords: true` |
797
+ | `RpcSerialization.layerMsgPack` | `application/msgpack` | yes (msgpack frames) | binary transports | JSON-compatible schema codecs; uses `useRecords: true` |
798
+ | `RpcSerialization.layerSchemaBinary(options?)` | `application/vnd.effect.rpc+schema-binary` | yes | binary transports, framed HTTP | schema-aware binary payloads and envelopes |
799
+
800
+ In rc.112 serializations and both protocol services require `codecFor`.
801
+ Custom protocols forward it from the serialization; custom JSON-compatible
802
+ protocols can use `Schema.toCodecJson`. Cluster network codecs now follow the
803
+ transport while persistent message storage remains JSON. See `effect-rpc-server`
804
+ for the complete contract, binary options, and compatibility constraints.
789
805
 
790
806
  `RpcSerialization.makeMsgPack(options?)` lets you customize msgpackr (`useRecords`, `useFloat32`, etc.).
791
807
 
@@ -793,7 +809,7 @@ Picking the wrong one is a real bug:
793
809
 
794
810
  - `layerJson` over a websocket → no framing → the first chunk past the first message is misinterpreted
795
811
  - `layerMsgPack` against a JSON-only HTTP client → garbled responses
796
- - `layerNdjson` against `layerProtocolHttp` → works, but framing is wasted; clients have to wait for the response to end
812
+ - `layerNdjson` against `layerProtocolHttp` → streams incrementally through a bounded response queue (default 16), without RPC acks
797
813
 
798
814
  ## Testing — `RpcTest.makeClient`
799
815
 
@@ -1347,6 +1363,15 @@ The generated **RPC** payload wraps the original payload as `{ entityId: string,
1347
1363
 
1348
1364
  As of beta.106, `EventLogEncryption.encrypt` returns `{ iv, encryptedEntry }` for every input entry, and `EventLogMessage.WriteEntries.encryptedEntries` carries `{ entryId, iv, encryptedEntry }` values. A fresh AES-GCM IV is generated per entry. This changes the encrypted replication wire format: upgrade encrypted event-log clients and servers together rather than performing a mixed-version rolling deployment.
1349
1365
 
1366
+ In rc.112, `EventJournal.withRemoteUncommited` (spelling intentional) receives a
1367
+ non-empty readonly entry array in its callback and returns `Effect<Option<A>, ...>`.
1368
+ No pending entries means `None` and the callback is not invoked; `Some(result)`
1369
+ means it ran. Update adapters/tests that assumed every flush calls the writer.
1370
+ EventLog retries transient remote write failures with exponential backoff from
1371
+ 200 ms (factor 1.5), capped at 10 seconds, so pending local entries synchronize
1372
+ after recovery. Preserve journal acknowledgment/idempotency semantics in custom
1373
+ remotes rather than adding an independent retry loop.
1374
+
1350
1375
  ### `WorkflowProxy` — workflow → RPC / HTTP
1351
1376
 
1352
1377
  ```ts
@@ -1372,6 +1397,11 @@ To namespace the generated rpcs, pass `prefix` as the **second** argument: `Work
1372
1397
 
1373
1398
  These proxies are how you give a frontend or an external system a typed RPC/HTTP surface that drives durable workflows, without leaking workflow-engine internals.
1374
1399
 
1400
+ In rc.112 workflow discard endpoints return a `Schema.String` execution ID
1401
+ instead of `void`, for both RPC and HTTP. Call the generated `<Name>Discard` RPC
1402
+ normally to receive it; passing the RPC client's `{ discard: true }` option
1403
+ discards even that ID. Entity discard endpoints keep their separate contract.
1404
+
1375
1405
  ## Cluster + workflow integration — `ClusterWorkflowEngine`
1376
1406
 
1377
1407
  The in-memory `WorkflowEngine.layerMemory` is for testing only. For production, use `ClusterWorkflowEngine.layer`, which wires the workflow engine into the cluster's `Sharding` + `MessageStorage`:
@@ -1620,4 +1650,4 @@ const usersByName = Effect.gen(function*() {
1620
1650
  - For production cluster, use `NodeClusterSocket.layer` / `NodeClusterHttp.layer` (or the Bun equivalents) unless you specifically need to assemble layers manually.
1621
1651
  - Size `maxResidentEntities` and `unprocessedMessageBatchSize` deliberately for the runner's memory and storage throughput.
1622
1652
  - Match transport ↔ serialization: HTTP → `layerJson`; sockets/websocket/streaming → `layerNdjson` or `layerMsgPack`.
1623
- - Pattern-match on `client.GetUser(...).pipe(Effect.catchTag('UserNotFound', ...), Effect.catchFilter(...))` for typed recovery; reserve broad `Effect.catchAll` for the runtime boundary.
1653
+ - Pattern-match on `client.GetUser(...).pipe(Effect.catchTag('UserNotFound', ...), Effect.catchFilter(...))` for typed recovery; reserve broad `Effect.catch` for an explicit boundary recovery policy.