opencode-effect-enforcer 0.2.0

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 (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +278 -0
  3. package/guidance/effect-first-development.md +1247 -0
  4. package/guidance/post__effect-and-the-near-inexpressible-majesty-of-layers.md +490 -0
  5. package/guidance/post__parse-dont-validate.md +109 -0
  6. package/guidance/progressive-disclosure-guidance.md +38 -0
  7. package/package.json +63 -0
  8. package/patterns/avoid-any.md +37 -0
  9. package/patterns/avoid-data-tagged-error.md +34 -0
  10. package/patterns/avoid-direct-json.md +51 -0
  11. package/patterns/avoid-direct-tag-checks.md +54 -0
  12. package/patterns/avoid-expect-in-if.md +52 -0
  13. package/patterns/avoid-mutable-state.md +70 -0
  14. package/patterns/avoid-native-fetch.md +61 -0
  15. package/patterns/avoid-node-imports.md +86 -0
  16. package/patterns/avoid-non-null-assertion.md +44 -0
  17. package/patterns/avoid-object-type.md +46 -0
  18. package/patterns/avoid-option-getorthrow.md +39 -0
  19. package/patterns/avoid-platform-coupling.md +43 -0
  20. package/patterns/avoid-process-env.md +43 -0
  21. package/patterns/avoid-react-hooks.md +73 -0
  22. package/patterns/avoid-schema-suffix.md +45 -0
  23. package/patterns/avoid-sync-fs.md +68 -0
  24. package/patterns/avoid-try-catch.md +47 -0
  25. package/patterns/avoid-ts-ignore.md +38 -0
  26. package/patterns/avoid-untagged-errors.md +67 -0
  27. package/patterns/avoid-yield-ref.md +46 -0
  28. package/patterns/casting-awareness.md +46 -0
  29. package/patterns/context-tag-extends.md +84 -0
  30. package/patterns/effect-catchall-default.md +61 -0
  31. package/patterns/effect-promise-vs-trypromise.md +47 -0
  32. package/patterns/effect-run-in-body.md +58 -0
  33. package/patterns/imperative-loops.md +76 -0
  34. package/patterns/prefer-arr-sort.md +52 -0
  35. package/patterns/prefer-duration-values.md +56 -0
  36. package/patterns/prefer-effect-fn.md +161 -0
  37. package/patterns/prefer-match-over-switch.md +48 -0
  38. package/patterns/prefer-option-over-null.md +56 -0
  39. package/patterns/prefer-redacted-config.md +70 -0
  40. package/patterns/prefer-schema-class.md +54 -0
  41. package/patterns/require-effect-concurrency.md +83 -0
  42. package/patterns/stream-large-files.md +63 -0
  43. package/patterns/throw-in-effect-gen.md +62 -0
  44. package/patterns/use-clock-service.md +45 -0
  45. package/patterns/use-command-executor-service.md +54 -0
  46. package/patterns/use-console-service.md +54 -0
  47. package/patterns/use-filesystem-service.md +59 -0
  48. package/patterns/use-http-client-service.md +77 -0
  49. package/patterns/use-path-service.md +53 -0
  50. package/patterns/use-random-service.md +45 -0
  51. package/patterns/use-temp-file-scoped.md +66 -0
  52. package/patterns/vm-in-wrong-file.md +51 -0
  53. package/patterns/yield-in-for-loop.md +61 -0
  54. package/skills/effect-ai-chat/SKILL.md +472 -0
  55. package/skills/effect-ai-language-model/SKILL.md +652 -0
  56. package/skills/effect-ai-prompt/SKILL.md +752 -0
  57. package/skills/effect-ai-provider/SKILL.md +668 -0
  58. package/skills/effect-ai-streaming/SKILL.md +418 -0
  59. package/skills/effect-ai-tool/SKILL.md +1132 -0
  60. package/skills/effect-atom-rpc/SKILL.md +488 -0
  61. package/skills/effect-atom-state/SKILL.md +640 -0
  62. package/skills/effect-batching/SKILL.md +614 -0
  63. package/skills/effect-cache/SKILL.md +570 -0
  64. package/skills/effect-cli/SKILL.md +523 -0
  65. package/skills/effect-command-executor/SKILL.md +675 -0
  66. package/skills/effect-concurrency-testing/SKILL.md +612 -0
  67. package/skills/effect-config/SKILL.md +580 -0
  68. package/skills/effect-context-witness/SKILL.md +274 -0
  69. package/skills/effect-domain-modeling/SKILL.md +1212 -0
  70. package/skills/effect-domain-predicates/SKILL.md +867 -0
  71. package/skills/effect-error-handling/SKILL.md +1581 -0
  72. package/skills/effect-fiber/SKILL.md +731 -0
  73. package/skills/effect-filesystem/SKILL.md +624 -0
  74. package/skills/effect-graph/SKILL.md +571 -0
  75. package/skills/effect-http-api/SKILL.md +1760 -0
  76. package/skills/effect-http-client/SKILL.md +989 -0
  77. package/skills/effect-http-server/SKILL.md +920 -0
  78. package/skills/effect-incremental-migration/SKILL.md +362 -0
  79. package/skills/effect-layer-design/SKILL.md +642 -0
  80. package/skills/effect-managed-runtime/SKILL.md +395 -0
  81. package/skills/effect-mcp-server/SKILL.md +608 -0
  82. package/skills/effect-observability/SKILL.md +719 -0
  83. package/skills/effect-optics/SKILL.md +554 -0
  84. package/skills/effect-parallelization/SKILL.md +668 -0
  85. package/skills/effect-path/SKILL.md +296 -0
  86. package/skills/effect-pattern-matching/SKILL.md +914 -0
  87. package/skills/effect-platform-abstraction/SKILL.md +1175 -0
  88. package/skills/effect-platform-layers/SKILL.md +514 -0
  89. package/skills/effect-pubsub-event-bus/SKILL.md +384 -0
  90. package/skills/effect-react-composition/SKILL.md +986 -0
  91. package/skills/effect-react-vm/SKILL.md +675 -0
  92. package/skills/effect-rpc-api/SKILL.md +624 -0
  93. package/skills/effect-rpc-client/SKILL.md +666 -0
  94. package/skills/effect-rpc-cluster/SKILL.md +1623 -0
  95. package/skills/effect-rpc-server/SKILL.md +767 -0
  96. package/skills/effect-scheduling/SKILL.md +124 -0
  97. package/skills/effect-schema-composition/SKILL.md +975 -0
  98. package/skills/effect-schema-v4/SKILL.md +691 -0
  99. package/skills/effect-scope/SKILL.md +682 -0
  100. package/skills/effect-service-implementation/SKILL.md +656 -0
  101. package/skills/effect-socket/SKILL.md +703 -0
  102. package/skills/effect-sql/SKILL.md +781 -0
  103. package/skills/effect-stream/SKILL.md +765 -0
  104. package/skills/effect-testing/SKILL.md +1331 -0
  105. package/skills/effect-typeclass-design/SKILL.md +161 -0
  106. package/skills/effect-wide-events/Article.md +66 -0
  107. package/skills/effect-wide-events/SKILL.md +95 -0
  108. package/skills/effect-workflow/SKILL.md +810 -0
  109. package/src/agent-policy.ts +22 -0
  110. package/src/enforcer.ts +104 -0
  111. package/src/frontmatter.ts +34 -0
  112. package/src/guidance.ts +66 -0
  113. package/src/index.ts +38 -0
  114. package/src/pattern-catalog.ts +115 -0
  115. package/src/pattern-matcher.ts +178 -0
  116. package/src/pattern.ts +97 -0
  117. package/src/skills.ts +29 -0
  118. package/src/write-projection.ts +66 -0
@@ -0,0 +1,666 @@
1
+ ---
2
+ name: effect-rpc-client
3
+ description: Consume typed RPC services with Effect's RpcClient — protocol layers (HTTP, WebSocket, TCP, worker, in-memory), RpcSerialization codecs, per-call and ambient headers, streaming calls, interruption, reconnection, and RpcClientError handling. Use when calling an RpcGroup from a client, wiring a client transport + serialization stack, debugging RPC transport failures or reconnects, or testing RPC consumers with RpcTest.
4
+ ---
5
+
6
+ You are an Effect TypeScript expert specializing in consuming RPC services with `RpcClient` from `effect/unstable/rpc`.
7
+
8
+ This skill covers the client side only: building clients, transports, serialization, headers, streaming consumption, interruption, errors, and testing. Defining `Rpc`/`RpcGroup` contracts is the `effect-rpc-api` skill; serving them is the `effect-rpc-server` skill; cluster entity clients are the `effect-rpc-cluster` skill.
9
+
10
+ ## Effect Source Reference
11
+
12
+ 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 change between betas.
13
+
14
+ Key files:
15
+
16
+ - `packages/effect/src/unstable/rpc/RpcClient.ts` — `make`, `makeNoSerialization`, the `Protocol` service, `layerProtocolHttp`/`layerProtocolSocket`/`layerProtocolWorker` (+ `makeProtocol*`), `CurrentHeaders`, `withHeaders`, `ConnectionHooks`
17
+ - `packages/effect/src/unstable/rpc/RpcClientError.ts` — `RpcClientError` and `RpcClientDefect`
18
+ - `packages/effect/src/unstable/rpc/RpcSerialization.ts` — `json`, `ndjson`, `jsonRpc`, `ndJsonRpc`, `msgPack` codecs, their layers, the `Parser` interface and `includesFraming`
19
+ - `packages/effect/src/unstable/rpc/RpcTest.ts` — `makeClient` in-process test client
20
+ - `packages/effect/src/unstable/rpc/RpcWorker.ts` — `InitialMessage`, `layerInitialMessage`, `initialMessage`
21
+ - `packages/effect/src/unstable/rpc/RpcMessage.ts` — wire vocabulary: `Request`, `Ack`, `Interrupt`, `Chunk`, `Exit`, `Defect`, `Ping`/`Pong`, `ClientProtocolError`, `RequestId`
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
25
+
26
+ ## Core Model
27
+
28
+ A client is assembled from three replaceable layers plus your shared `RpcGroup` definition:
29
+
30
+ ```
31
+ RpcClient.make(group) typed methods, spans, request ids
32
+ requires: Protocol ← RpcClient.layerProtocolHttp / Socket / Worker
33
+ requires: RpcSerialization ← RpcSerialization.layerJson / Ndjson / MsgPack / JsonRpc
34
+ requires: transport service ← HttpClient | Socket.Socket | WorkerPlatform + Spawner
35
+ ```
36
+
37
+ `RpcClient.make(group)` returns an `Effect` producing an object with one method per rpc tag. Each method encodes the payload with the rpc's schema, sends a `Request` envelope through the `Protocol`, and decodes the server's `Exit` (or stream `Chunk`s) back into a typed `Effect` or `Stream`:
38
+
39
+ ```ts
40
+ // The generated client type — one method per tag
41
+ type RpcClient<Rpcs extends Rpc.Any, E = never> = {
42
+ readonly [Tag in Rpcs['_tag']]: (payload, options?) => Effect<Success, RpcError | E | MiddlewareErrors, R>;
43
+ // stream rpcs return Stream<A, E', R> instead (or Effect<Queue.Dequeue<...>> with asQueue)
44
+ };
45
+ ```
46
+
47
+ `E` is the transport error — `RpcClientError` for every real transport (`RpcClient.make`), `never` for `RpcTest.makeClient`.
48
+
49
+ Imports used throughout this skill (all rpc modules ship from the `effect` package — there is no `@effect/rpc` in v4):
50
+
51
+ ```ts
52
+ import { Cause, Context, Effect, Fiber, Layer, Queue, Schedule, Schema, Stream } from 'effect';
53
+ import {
54
+ Rpc,
55
+ RpcClient,
56
+ RpcGroup,
57
+ RpcMiddleware,
58
+ RpcSerialization,
59
+ RpcTest,
60
+ RpcWorker
61
+ } from 'effect/unstable/rpc';
62
+ import { RpcClientError } from 'effect/unstable/rpc/RpcClientError';
63
+ import { FetchHttpClient, Headers, HttpClient, HttpClientRequest } from 'effect/unstable/http';
64
+ import { Socket } from 'effect/unstable/socket';
65
+ // Platform transports:
66
+ import { NodeSocket, NodeWorker } from '@effect/platform-node';
67
+ import { BrowserWorker } from '@effect/platform-browser';
68
+ ```
69
+
70
+ Note the `RpcClientError` import: the `effect/unstable/rpc` barrel exports `RpcClientError` as a *namespace*; import the class from the module path `effect/unstable/rpc/RpcClientError` (this is what the upstream tests do).
71
+
72
+ ---
73
+
74
+ ## 1. Creating a Client — `RpcClient.make`
75
+
76
+ ```ts
77
+ const make: <Rpcs extends Rpc.Any, const Flatten extends boolean = false>(
78
+ group: RpcGroup.RpcGroup<Rpcs>,
79
+ options?: {
80
+ readonly spanPrefix?: string; // default 'RpcClient'; spans named `${spanPrefix}.${tag}`
81
+ readonly spanAttributes?: Record<string, unknown>;
82
+ readonly generateRequestId?: () => RequestId; // default: shared module-level numeric counter
83
+ readonly disableTracing?: boolean; // default false
84
+ readonly flatten?: Flatten; // default false
85
+ }
86
+ ) => Effect.Effect<
87
+ Flatten extends true ? RpcClient.RpcClient.Flat<Rpcs, RpcClientError> : RpcClient.RpcClient<Rpcs, RpcClientError>,
88
+ never,
89
+ RpcClient.Protocol | Rpc.MiddlewareClient<Rpcs> | Scope
90
+ >;
91
+ ```
92
+
93
+ Three things to internalize about the requirements:
94
+
95
+ - **`Protocol`** — provide exactly one `layerProtocol*` (section 4).
96
+ - **`Rpc.MiddlewareClient<Rpcs>`** — if any rpc in the group has a middleware declared with `requiredForClient: true`, you must provide its `RpcMiddleware.layerClient(...)` or the `Effect.provide` will not compile.
97
+ - **`Scope`** — the client registers a finalizer that interrupts all in-flight requests on close and makes later calls interrupt immediately. Build it inside `Layer.effect` (which supplies the layer's scope) or `Effect.scoped`.
98
+
99
+ The canonical wrapping is a `Context.Service` so consumers resolve the client by name:
100
+
101
+ ```ts
102
+ export class UsersClient extends Context.Service<
103
+ UsersClient,
104
+ RpcClient.RpcClient<RpcGroup.Rpcs<typeof UserRpcs>, RpcClientError>
105
+ >()('UsersClient') {
106
+ static readonly layer = Layer.effect(UsersClient)(RpcClient.make(UserRpcs)).pipe(
107
+ Layer.provide(AuthClient) // RpcMiddleware.layerClient for requiredForClient middleware
108
+ );
109
+ }
110
+ ```
111
+
112
+ `RpcClient.FromGroup<typeof UserRpcs, RpcClientError>` is shorthand for the same client type.
113
+
114
+ ### `flatten: true` — single-function client
115
+
116
+ The client becomes one function `(tag, payload, options?)` instead of a property-per-tag object. Useful for generic proxying (this is what `AtomRpc` uses internally):
117
+
118
+ ```ts
119
+ const client = yield* RpcClient.make(UserRpcs, { flatten: true });
120
+ const user = yield* client('GetUser', { id: 'u1' });
121
+ ```
122
+
123
+ ### `generateRequestId`
124
+
125
+ Request ids correlate requests with responses on a shared connection. The default is a process-wide incrementing `number`. Override only when ids need application-specific generation. The function must return `RequestId`, a branded `string | number`; construct it with `RequestId(1)` or `RequestId('1')` from `effect/unstable/rpc/RpcMessage`. `bigint` is not accepted.
126
+
127
+ ### Tracing
128
+
129
+ Unless `disableTracing: true`, every call runs in a span named `${spanPrefix}.${tag}` with `spanAttributes`, and the request envelope carries `traceId`/`spanId`/`sampled` so the server can continue the trace (whether it does depends on the server protocol — see the effect-rpc-server skill).
130
+
131
+ ---
132
+
133
+ ## 2. Calling RPCs — Per-Call Options and Error Channels
134
+
135
+ Each generated method takes the rpc's payload, then an options bag whose shape differs for stream vs non-stream rpcs:
136
+
137
+ ```ts
138
+ // Non-stream rpc
139
+ client.GetUser(
140
+ { id: 'u1' },
141
+ {
142
+ headers: { 'x-tenant': 't1' }, // Headers.Input — per-call headers
143
+ context: Context.empty(), // Context<never> — rarely needed (cluster plumbing)
144
+ discard: true // fire-and-forget: see below
145
+ }
146
+ );
147
+
148
+ // Stream rpc
149
+ client.StreamUsers(
150
+ { id: 'u1' },
151
+ {
152
+ headers: { 'x-tenant': 't1' },
153
+ context: Context.empty(),
154
+ asQueue: true, // return Effect<Queue.Dequeue<...>> instead of Stream
155
+ streamBufferSize: 16 // default 16 — client-side bounded buffer
156
+ }
157
+ );
158
+ ```
159
+
160
+ A void-payload rpc is callable with no arguments: `client.Ping()`, `client.GetInterrupts()`. The payload parameter is `Rpc.PayloadConstructor<R>` — the payload schema's constructor input, so `Schema.Class` payloads accept plain field objects. Tags containing dots — from `group.prefix('nested.')` or cluster `EntityProxy` groups — need bracket access: `client['nested.test']()` (or use `flatten: true`).
161
+
162
+ ### The full error channel
163
+
164
+ ```
165
+ Rpc.Error<R> your declared rpc error (decoded)
166
+ | MiddlewareError wire errors added by server middleware (e.g. Unauthorized)
167
+ | MiddlewareClientError errors a client-side middleware can produce
168
+ | RpcClientError transport failure (section 8)
169
+ ```
170
+
171
+ Server-side defects are *not* in the error channel — they arrive as `Cause.Die` in the failure cause. Inspect with `Effect.sandbox` / `Effect.catchCause`, e.g. `Cause.squash(cause)` after `Effect.sandbox` + `Effect.flip`.
172
+
173
+ The requirements channel of each call is the payload schema's `EncodingServices` plus the success/error schemas' `DecodingServices` (plus any middleware error schemas' `DecodingServices`) — `never` for plain schemas.
174
+
175
+ ### `discard: true`
176
+
177
+ Sends the request and skips response handling entirely: the method returns `Effect<void, RpcClientError | MiddlewareErrors>`. The rpc's *declared* error disappears from the channel, but transport and middleware errors remain. No client entry is registered, so the server's exit is dropped on the floor. Use for fire-and-forget commands; over HTTP the POST is still awaited (the response body is just ignored).
178
+
179
+ ---
180
+
181
+ ## 3. Headers
182
+
183
+ Three layers of headers, merged in this order (later wins on conflict):
184
+
185
+ 1. **Ambient** — the `RpcClient.CurrentHeaders` reference (default `Headers.empty`)
186
+ 2. **Per-call** — the `headers` option on the method call
187
+ 3. **Client middleware** — `RpcMiddleware.layerClient` can rewrite `request.headers` last, as the request passes through it
188
+
189
+ ```ts
190
+ // Region-scoped ambient headers — merged into CurrentHeaders for the wrapped effect
191
+ yield* program.pipe(
192
+ RpcClient.withHeaders({ authorization: `Bearer ${token}`, 'x-tenant': 't1' })
193
+ );
194
+
195
+ // withHeaders is dual: data-first works too
196
+ RpcClient.withHeaders(program, { 'x-tenant': 't1' });
197
+
198
+ // CurrentHeaders is a Context.Reference<Headers.Headers> — set directly if needed
199
+ Effect.updateService(program, RpcClient.CurrentHeaders, Headers.merge(Headers.fromInput({ a: 'b' })));
200
+ ```
201
+
202
+ **Header names are normalized to lowercase** by `Headers.fromInput`. If you send `{ userId: '123' }`, server middleware must read `headers.userid`. The upstream e2e fixture depends on exactly this.
203
+
204
+ ### Client-side middleware (auth headers and friends)
205
+
206
+ For middleware declared with `requiredForClient: true`, the client will not compile without a `layerClient`:
207
+
208
+ ```ts
209
+ export const AuthClient = RpcMiddleware.layerClient(AuthMiddleware, ({ next, request }) =>
210
+ next({
211
+ ...request,
212
+ headers: Headers.set(request.headers, 'authorization', `Bearer ${getToken()}`)
213
+ }));
214
+ ```
215
+
216
+ You must call `next(request)`; the middleware wraps the send, it does not replace it. Defining middleware services is covered by the effect-rpc-api skill.
217
+
218
+ ---
219
+
220
+ ## 4. Protocol Layers — Picking a Transport
221
+
222
+ Provide exactly one of these to satisfy `RpcClient.Protocol`:
223
+
224
+ | Layer | Requires | supportsAck | Notes |
225
+ |---|---|---|---|
226
+ | `RpcClient.layerProtocolHttp({ url, transformClient? })` | `RpcSerialization`, `HttpClient` | no | One POST per request. Framed serializations stream the response body, so streaming rpcs work over plain HTTP |
227
+ | `RpcClient.layerProtocolSocket({ retryTransientErrors?, onTransientError? })` | `RpcSerialization`, `Socket.Socket` | yes | Full duplex; pings every 5s; auto-reconnects (section 9). Works for WebSocket and raw TCP — the `Socket.Socket` layer decides which |
228
+ | `RpcClient.layerProtocolWorker(poolOptions)` | `Worker.WorkerPlatform`, `Worker.Spawner` | yes | Pool of workers; supports transferables and `RpcWorker.InitialMessage`; the only protocol layer with an error channel — `WorkerError` (section 10) |
229
+
230
+ Each has a `makeProtocol*` Effect counterpart (`makeProtocolHttp(client)`, `makeProtocolSocket(options?)`, `makeProtocolWorker(options)`) for inline composition — `makeProtocolSocket` additionally accepts a `retryPolicy: Schedule<any, SocketError>` that the layer constructor does **not** expose.
231
+
232
+ ### HTTP
233
+
234
+ ```ts
235
+ const ProtocolLive = RpcClient.layerProtocolHttp({
236
+ url: 'http://localhost:3000/rpc', // the full endpoint URL
237
+ // optional: rewrite the underlying HttpClient (retries, extra headers, url tweaks)
238
+ transformClient: HttpClient.mapRequest(HttpClientRequest.setHeader('x-api-key', key))
239
+ }).pipe(
240
+ Layer.provide(RpcSerialization.layerNdjson),
241
+ Layer.provide(FetchHttpClient.layer) // or NodeHttpClient.layerUndici etc.
242
+ );
243
+ ```
244
+
245
+ `url` is prepended to an empty path, so it must be the complete endpoint. `transformClient` receives the client *after* the url is attached; its request transformations run before the prepend (the upstream test uses `url: ''` + `transformClient: HttpClient.mapRequest(HttpClientRequest.appendUrl('/rpc'))`).
246
+
247
+ An empty HTTP response body fails the call with `RpcClientDefect` (`'Received empty HTTP response from RPC server'`) rather than hanging.
248
+
249
+ ### WebSocket
250
+
251
+ ```ts
252
+ // Node
253
+ const ProtocolLive = RpcClient.layerProtocolSocket().pipe(
254
+ Layer.provide(NodeSocket.layerWebSocket('ws://localhost:3000/rpc')),
255
+ Layer.provide(RpcSerialization.layerNdjson)
256
+ );
257
+
258
+ // Browser / anywhere with a global WebSocket
259
+ const BrowserProtocolLive = RpcClient.layerProtocolSocket().pipe(
260
+ Layer.provide(Socket.layerWebSocket('wss://api.example.com/rpc')),
261
+ Layer.provide(Socket.layerWebSocketConstructorGlobal),
262
+ Layer.provide(RpcSerialization.layerNdjson)
263
+ );
264
+ ```
265
+
266
+ `Socket.layerWebSocket(url, { closeCodeIsError?, openTimeout?, protocols? })` requires a `WebSocketConstructor`; `layerWebSocketConstructorGlobal` uses `globalThis.WebSocket`. `NodeSocket.layerWebSocket` bundles the constructor.
267
+
268
+ ### Raw TCP
269
+
270
+ ```ts
271
+ const TcpProtocolLive = RpcClient.layerProtocolSocket().pipe(
272
+ Layer.provide(NodeSocket.layerNet({ port: 9000 })),
273
+ Layer.provide(RpcSerialization.layerMsgPack)
274
+ );
275
+ ```
276
+
277
+ `Socket.Socket` layer construction, the `SocketError` taxonomy, and reconnect internals are covered by the effect-socket skill.
278
+
279
+ ### The `Protocol` service (custom transports)
280
+
281
+ `RpcClient.Protocol` is a `Context.Service` with this surface:
282
+
283
+ ```ts
284
+ {
285
+ readonly run: (clientId: number, f: (data: FromServerEncoded) => Effect<void>) => Effect<never>;
286
+ readonly send: (clientId: number, request: FromClientEncoded, transferables?) => Effect<void, RpcClientError>;
287
+ readonly supportsAck: boolean; // chunk acks → server-side stream backpressure
288
+ readonly supportsTransferables: boolean; // worker-only
289
+ }
290
+ ```
291
+
292
+ `FromServerEncoded` now also includes `RequestEncoded` for server-originated requests and notifications. The ordinary generated RPC client ignores these requests; reverse-protocol adapters such as MCP build a matching client contract to handle them. A notification has `isNotification: true`, and JSON-RPC omits its id.
293
+
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
+
296
+ ---
297
+
298
+ ## 5. Serialization — Pairing Codecs with Transports
299
+
300
+ `RpcSerialization` is the shared codec service. The load-bearing property is `includesFraming`: framed codecs can split a byte stream into messages; unframed codecs assume the transport delivers whole messages.
301
+
302
+ | Layer | Content-Type | Framed | Pair with |
303
+ |---|---|---|---|
304
+ | `RpcSerialization.layerJson` | `application/json` | no | HTTP (response decoded once, as an array); WebSocket (each ws message is one frame already) |
305
+ | `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` |
307
+ | `RpcSerialization.layerJsonRpc({ contentType? })` | `application/json` | no | JSON-RPC 2.0 interop over HTTP/WebSocket; maps rpc tag ↔ `method`, supports batch arrays |
308
+ | `RpcSerialization.layerNdJsonRpc({ contentType? })` | `application/json-rpc` | yes (newline) | JSON-RPC 2.0 over sockets |
309
+
310
+ Rules of thumb, verified by the upstream e2e matrix (http: ndjson/msgpack/ndJsonRpc; ws: ndjson/json/msgpack/jsonRpc; tcp: ndjson/msgpack/ndJsonRpc):
311
+
312
+ - **Raw TCP needs a framed codec** (`ndjson` / `msgPack` / `ndJsonRpc`) — plain `json` cannot find message boundaries in a byte stream.
313
+ - **WebSocket works with any codec** including plain `json`, because the ws transport frames messages itself.
314
+ - **HTTP + unframed (`json`/`jsonRpc`)**: the client buffers the whole response and expects a JSON array of response messages — streaming rpcs only flush when the response ends.
315
+ - **HTTP + framed (`ndjson`/`msgPack`/`ndJsonRpc`)**: the client incrementally parses the response body — streaming rpc chunks arrive as the server emits them, over a single POST.
316
+ - **Client and server must use the same codec.** A msgpack client against a json server garbles both directions.
317
+
318
+ `RpcSerialization.makeMsgPack(options)` customizes msgpackr (`useRecords`, `useFloat32`, ...); wrap with `Layer.succeed(RpcSerialization.RpcSerialization)(RpcSerialization.makeMsgPack({ useRecords: false }))` if the peer cannot handle msgpackr record extensions.
319
+
320
+ ---
321
+
322
+ ## 6. Consuming Streaming RPCs
323
+
324
+ A `stream: true` rpc produces a `Stream` on the client (no `Effect` wrapper — run it directly):
325
+
326
+ ```ts
327
+ const users = yield* client.StreamUsers({ id: '1' }).pipe(
328
+ Stream.take(5),
329
+ Stream.runCollect
330
+ );
331
+ ```
332
+
333
+ Mechanics worth knowing:
334
+
335
+ - Chunks are buffered client-side in a bounded queue (`streamBufferSize`, default 16).
336
+ - On transports with `supportsAck` (socket, worker), the client sends an `Ack` after delivering each chunk to the buffer — the server will not emit past the in-flight window, so slow consumers backpressure the producer end to end. Over HTTP there are no acks; the server streams freely.
337
+ - Ending consumption early (e.g. `Stream.take`, interruption) closes the stream's scope, which sends an `Interrupt` for the request — the server-side handler is interrupted and its finalizers run.
338
+ - The stream fails with the same error channel as effect rpcs (declared error | middleware errors | `RpcClientError`).
339
+
340
+ ### `asQueue: true`
341
+
342
+ Returns `Effect<Queue.Dequeue<A, E | Cause.Done>, never, Scope | ...>` instead of a `Stream` — use when you need manual chunk-level control:
343
+
344
+ ```ts
345
+ const program = Effect.scoped(
346
+ Effect.gen(function* () {
347
+ const queue = yield* client.StreamUsers({ id: '1' }, { asQueue: true });
348
+ const users: Array<User> = [];
349
+ // drain: Queue.take fails with Cause.Done when the stream ends
350
+ yield* Queue.take(queue).pipe(
351
+ Effect.flatMap((user) => Effect.sync(() => users.push(user))),
352
+ Effect.forever,
353
+ Effect.catchIf(Cause.isDone, () => Effect.void)
354
+ );
355
+ // closing the scope early interrupts the request server-side
356
+ return users;
357
+ })
358
+ );
359
+ ```
360
+
361
+ End-of-stream is signaled as `Cause.Done` in the queue's error channel, not as a value — guard with `Cause.isDone`, or `Effect.catchTag('Done', ...)` (`Done` is tagged; this is how the cluster `Runners` module drains upstream). The `Scope` requirement is how the request's lifetime is tied to your code — keep the scope open while consuming.
362
+
363
+ ---
364
+
365
+ ## 7. Interruption and Abort Propagation
366
+
367
+ Interruption is a first-class protocol message, not just a local cancel:
368
+
369
+ - **Effect rpcs** — interrupting the calling fiber interrupts the in-flight send and dispatches an `Interrupt` envelope to the server (best-effort, with a 1-second internal timeout so shutdown never hangs on a dead connection).
370
+ - **Stream rpcs** — the `Interrupt` is sent from a scope finalizer when the stream/queue scope closes for any reason (early `Stream.take`, fiber interrupt, error).
371
+ - **HTTP transport** — the protocol drops `Interrupt` messages (`send` ignores everything but `Request`). Interruption still propagates: interrupting the fiber aborts the underlying HTTP request, and the server interrupts the handler when the connection drops. The observable difference: there is no graceful interrupt message, just an aborted request.
372
+ - **Client scope close** — all in-flight requests resume as interrupted, `Interrupt`s are sent where the transport supports it, and any later call on the client returns `Effect.interrupt`.
373
+
374
+ ```ts
375
+ const fiber = yield* client.Never().pipe(Effect.forkChild);
376
+ yield* Effect.sleep('500 millis');
377
+ yield* Fiber.interrupt(fiber); // server-side handler's onInterrupt/finalizers run
378
+ ```
379
+
380
+ On the server, a client-initiated interrupt carries the `RpcSchema.ClientAbort` annotation in the cause so handlers can distinguish client cancel from server shutdown — see the effect-rpc-server skill.
381
+
382
+ ---
383
+
384
+ ## 8. Error Handling — the `RpcClientError` Taxonomy
385
+
386
+ `RpcClientError` is a `Schema.Error` with `_tag: 'RpcClientError'` and a `reason` union. Its `message` is `` `${reason._tag}: ${reason.message}` ``.
387
+
388
+ | `reason._tag` | Transport | Meaning |
389
+ |---|---|---|
390
+ | `'HttpError'` | HTTP | `HttpClientErrorSchema`: has `kind: 'EncodeError' \| 'DecodeError' \| 'TransportError' \| 'InvalidUrlError' \| 'StatusCodeError' \| 'EmptyBodyError'` and optional `cause` |
391
+ | `'SocketOpenError'` | socket | failed to (re)connect, includes ping timeouts |
392
+ | `'SocketReadError'` / `'SocketWriteError'` | socket | I/O failure on a live connection |
393
+ | `'SocketCloseError'` | socket | connection closed (has `code`) |
394
+ | `'WorkerSpawnError'` / `'WorkerSendError'` / `'WorkerReceiveError'` / `'WorkerUnknownError'` | worker | worker lifecycle failures |
395
+ | `'RpcClientDefect'` | any | protocol bug: undecodable message, empty HTTP response, non-array unframed response; has `message` + `cause` |
396
+
397
+ Handling strategy — recover from your domain errors by tag, treat `RpcClientError` as retryable-or-fatal at the edge:
398
+
399
+ ```ts
400
+ const getUser = (id: string) =>
401
+ client.GetUser({ id }).pipe(
402
+ Effect.catchTag('UserNotFound', () => Effect.succeed(guestUser)), // domain error
403
+ Effect.retry({
404
+ // transport-only retry with backoff
405
+ while: (e) => e._tag === 'RpcClientError' && e.reason._tag !== 'RpcClientDefect',
406
+ schedule: Schedule.exponential('200 millis').pipe(
407
+ Schedule.upTo({ times: 3 })
408
+ )
409
+ })
410
+ );
411
+ ```
412
+
413
+ Semantics to remember:
414
+
415
+ - **`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).
417
+ - **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
+ - `RpcTest.makeClient` clients have `E = never` — no transport error channel, only your rpc errors and middleware errors.
419
+
420
+ ---
421
+
422
+ ## 9. Connection Lifecycle, Reconnection, and `ConnectionHooks`
423
+
424
+ The socket protocol owns a long-lived connection loop:
425
+
426
+ - **Keepalive** — the client sends `Ping` every 5 seconds; a missing `Pong` by the next tick fails the connection with a `SocketOpenError` (kind `'Timeout'`) and triggers reconnect.
427
+ - **Reconnect policy** — the loop retries with `Schedule.min([Schedule.exponential("500 millis", 1.5), Schedule.spaced("5 seconds")])`, capped at 5s between attempts, forever, by default. Customize the schedule via `makeProtocolSocket({ retryPolicy })` + `Layer.effect(RpcClient.Protocol)(...)`; both constructors accept `retryTransientErrors` and `onTransientError`.
428
+ - **Failure broadcast** — on a connection error, all in-flight requests fail with `RpcClientError`, and subsequent `send`s fail fast with the same error until the socket reopens.
429
+ - **`retryTransientErrors: true`** — `SocketOpenError` failures (failure to connect, and ping timeouts, which are classified as open errors) are not broadcast: pending requests stay pending across reconnect attempts instead of failing. Read/write/close errors on an established connection still fail in-flight requests.
430
+ - **`onTransientError`** — called for every retried `SocketOpenError` (including ping timeouts) while `retryTransientErrors` is enabled. Its infallible, service-free effect is for logging/metrics; defects in the hook are logged and ignored.
431
+
432
+ ```ts
433
+ const ProtocolLive = RpcClient.layerProtocolSocket({ retryTransientErrors: true }).pipe(
434
+ Layer.provide(NodeSocket.layerWebSocket('ws://localhost:3000/rpc')),
435
+ Layer.provide(RpcSerialization.layerNdjson)
436
+ );
437
+ ```
438
+
439
+ ### `ConnectionHooks`
440
+
441
+ An optional `Context.Service` the socket and worker protocols pick up if present:
442
+
443
+ ```ts
444
+ const HooksLive = Layer.succeed(RpcClient.ConnectionHooks)({
445
+ onConnect: Effect.logInfo('rpc connected'), // socket: every (re)connect; worker: once after pool warm-up
446
+ onDisconnect: Effect.logWarning('rpc disconnected') // socket only; runs when a connection ends
447
+ });
448
+
449
+ // Provide it alongside the protocol layer:
450
+ const ProtocolWithHooks = ProtocolLive.pipe(Layer.provideMerge(HooksLive));
451
+ ```
452
+
453
+ Use `onConnect` to refresh auth state or re-subscribe after a reconnect. The HTTP protocol never invokes hooks (there is no connection).
454
+
455
+ ---
456
+
457
+ ## 10. Worker Transports and `RpcWorker.InitialMessage`
458
+
459
+ `layerProtocolWorker` runs the *server* inside workers and the client in the parent. Pool options come in two shapes:
460
+
461
+ ```ts
462
+ // Fixed-size pool
463
+ RpcClient.layerProtocolWorker({ size: 4, concurrency: 1, targetUtilization: 1 });
464
+
465
+ // Elastic pool
466
+ RpcClient.layerProtocolWorker({
467
+ minSize: 1,
468
+ maxSize: 8,
469
+ timeToLive: '60 seconds', // idle workers above minSize are released
470
+ concurrency: 1
471
+ });
472
+ ```
473
+
474
+ `concurrency` is requests-per-worker; a request checks a worker out of the pool until its `Exit` arrives. The protocol supports transferables (below) and acks. The pool spawns its minimum eagerly at startup via a background fiber — all `size` workers for the fixed pool, `minSize` for the elastic pool — and protocol initialization blocks until the first worker is ready (then `ConnectionHooks.onConnect` runs). That eager first spawn is why `layerProtocolWorker` is the only protocol layer with a non-`never` error channel: handle or `Layer.orDie` the `WorkerError` when composing the final layer. On worker crash, a `ClientProtocolError` is broadcast — every in-flight request on the protocol fails, not just the crashed worker's — and the worker is respawned (retried every second).
475
+
476
+ ```ts
477
+ // Node parent
478
+ const WorkerClient = UsersClient.layer.pipe(
479
+ Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })),
480
+ Layer.provide(NodeWorker.layer((id) => new WorkerThreads.Worker('./worker.js')))
481
+ );
482
+
483
+ // Browser parent
484
+ const BrowserClient = UsersClient.layer.pipe(
485
+ Layer.provide(RpcClient.layerProtocolWorker({ size: 1 })),
486
+ Layer.provide(BrowserWorker.layer(() => new Worker(new URL('./worker.ts', import.meta.url))))
487
+ );
488
+ ```
489
+
490
+ No `RpcSerialization` is needed — structured clone carries the messages. The worker side runs `RpcServer.layerProtocolWorkerRunner` (see the effect-rpc-server skill).
491
+
492
+ Transferables: to move binary data instead of copying it, wrap fields in the shared contract with `Transferable` from `effect/unstable/workers` — `Transferable.Uint8Array` transfers the backing `ArrayBuffer`, `Transferable.ImageData`/`Transferable.MessagePort` are prebuilt, and `Transferable.schema(s, (encoded) => [encoded.buffer])` wraps any schema with a custom transfer-list extractor. Transfer only happens on protocols with `supportsTransferables: true` — the worker protocol only.
493
+
494
+ ### Typed startup config — `RpcWorker.InitialMessage`
495
+
496
+ Send one schema-encoded value at spawn time, before any rpc traffic:
497
+
498
+ ```ts
499
+ class WorkerConfig extends Schema.Class<WorkerConfig>('WorkerConfig')({
500
+ apiUrl: Schema.String,
501
+ tenantId: Schema.String
502
+ }) {}
503
+
504
+ // Parent: provide alongside the worker protocol
505
+ const InitialMessageLive = RpcWorker.layerInitialMessage(
506
+ WorkerConfig,
507
+ Effect.succeed(new WorkerConfig({ apiUrl: '/api', tenantId: 't1' }))
508
+ );
509
+
510
+ const ClientLive = UsersClient.layer.pipe(
511
+ Layer.provide(RpcClient.layerProtocolWorker({ size: 4 })),
512
+ Layer.provide(InitialMessageLive),
513
+ Layer.provide(NodeWorker.layer(spawnWorker))
514
+ );
515
+ ```
516
+
517
+ The build effect runs once per spawned worker and its result is encoded with the schema's JSON codec (transferables are collected automatically). Inside the worker, read it with `RpcWorker.initialMessage(WorkerConfig)` — server-side detail, see the effect-rpc-server skill.
518
+
519
+ ---
520
+
521
+ ## 11. Testing — `RpcTest.makeClient`
522
+
523
+ In-process client + server pair over the no-serialization path. Requests, stream chunks, acks, interrupts, headers, and middleware all flow through the real machinery — no network, no codecs:
524
+
525
+ ```ts
526
+ import { it } from '@effect/vitest';
527
+
528
+ export class UsersClient extends Context.Service<
529
+ UsersClient,
530
+ RpcClient.RpcClient<RpcGroup.Rpcs<typeof UserRpcs>, RpcClientError>
531
+ >()('UsersClient') {
532
+ static readonly layer = Layer.effect(UsersClient)(RpcClient.make(UserRpcs)).pipe(
533
+ Layer.provide(AuthClient)
534
+ );
535
+ // Same service shape — swap the layer, not the call sites
536
+ static readonly layerTest = Layer.effect(UsersClient)(RpcTest.makeClient(UserRpcs)).pipe(
537
+ Layer.provide([UsersLive, AuthLive, AuthClient])
538
+ );
539
+ }
540
+
541
+ it.effect('GetUser', () =>
542
+ Effect.gen(function* () {
543
+ const client = yield* UsersClient;
544
+ const user = yield* client.GetUser({ id: '1' });
545
+ expect(user.id).toBe('1');
546
+ }).pipe(Effect.provide(UsersClient.layerTest)));
547
+ ```
548
+
549
+ 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
+
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.
552
+
553
+ ### `RpcClient.makeNoSerialization` (advanced)
554
+
555
+ The primitive under `RpcTest`: builds a client from a raw decoded-message channel and returns `{ client, write }`, where you implement `onFromClient` (deliver messages toward a server) and call `write` with `FromServer` messages. Extra options over `make`: `supportsAck` (default `true`). Use it to bridge a client onto an exotic in-process channel; otherwise prefer `RpcTest.makeClient`.
556
+
557
+ ---
558
+
559
+ ## Key Patterns
560
+
561
+ ### HTTP client service, end to end
562
+
563
+ ```ts
564
+ // client.ts — UserRpcs comes from the shared contract module (effect-rpc-api skill)
565
+ import { Context, Effect, Layer } from 'effect';
566
+ import { FetchHttpClient } from 'effect/unstable/http';
567
+ import { RpcClient, RpcGroup, RpcSerialization } from 'effect/unstable/rpc';
568
+ import { RpcClientError } from 'effect/unstable/rpc/RpcClientError';
569
+ import { AuthClient, UserRpcs } from './contract.ts';
570
+
571
+ export class UsersClient extends Context.Service<
572
+ UsersClient,
573
+ RpcClient.RpcClient<RpcGroup.Rpcs<typeof UserRpcs>, RpcClientError>
574
+ >()('UsersClient') {
575
+ static readonly layer = Layer.effect(UsersClient)(
576
+ RpcClient.make(UserRpcs, { spanPrefix: 'UsersClient' })
577
+ ).pipe(
578
+ Layer.provide(AuthClient),
579
+ Layer.provide(RpcClient.layerProtocolHttp({ url: 'https://api.example.com/rpc' })),
580
+ Layer.provide(RpcSerialization.layerNdjson), // framed → streaming rpcs work over HTTP
581
+ Layer.provide(FetchHttpClient.layer)
582
+ );
583
+ }
584
+
585
+ const program = Effect.gen(function* () {
586
+ const client = yield* UsersClient;
587
+ const user = yield* client.GetUser({ id: 'u1' });
588
+ yield* Effect.logInfo('got', user);
589
+ }).pipe(RpcClient.withHeaders({ 'x-request-source': 'cli' }));
590
+ ```
591
+
592
+ ### Resilient WebSocket client with reconnect hooks
593
+
594
+ ```ts
595
+ const ProtocolLive = RpcClient.layerProtocolSocket({ retryTransientErrors: true }).pipe(
596
+ Layer.provide(NodeSocket.layerWebSocket('wss://api.example.com/rpc')),
597
+ Layer.provide(RpcSerialization.layerNdjson),
598
+ Layer.provideMerge(
599
+ Layer.succeed(RpcClient.ConnectionHooks)({
600
+ onConnect: refreshAuthToken,
601
+ onDisconnect: Effect.logWarning('rpc disconnected')
602
+ })
603
+ )
604
+ );
605
+
606
+ export const UsersClientLive = UsersClient.layer.pipe(Layer.provide(ProtocolLive));
607
+ ```
608
+
609
+ ### Consume a streaming rpc with early exit and call-site retry
610
+
611
+ ```ts
612
+ const firstFive = client.StreamUsers({ id: '1' }).pipe(
613
+ Stream.take(5), // closing the stream sends Interrupt to the server
614
+ Stream.runCollect,
615
+ Effect.retry({
616
+ while: (e) => e._tag === 'RpcClientError',
617
+ schedule: Schedule.exponential('250 millis').pipe(
618
+ Schedule.upTo({ times: 5 })
619
+ )
620
+ })
621
+ );
622
+ ```
623
+
624
+ ### Fire-and-forget command with ambient tenant headers
625
+
626
+ ```ts
627
+ const enqueue = (job: JobInput) =>
628
+ client.EnqueueJob(job, { discard: true }).pipe(
629
+ // declared rpc error is gone; transport errors remain
630
+ Effect.catchTag('RpcClientError', (e) => Effect.logError('enqueue failed', e.reason))
631
+ );
632
+
633
+ yield* Effect.forEach(jobs, enqueue, { concurrency: 8 }).pipe(
634
+ RpcClient.withHeaders({ 'x-tenant': tenantId })
635
+ );
636
+ ```
637
+
638
+ ### Worker pool with typed startup config
639
+
640
+ ```ts
641
+ const WorkerClientLive = UsersClient.layer.pipe(
642
+ Layer.provide(RpcClient.layerProtocolWorker({ minSize: 1, maxSize: 4, timeToLive: '30 seconds' })),
643
+ Layer.provide(RpcWorker.layerInitialMessage(WorkerConfig, loadWorkerConfig)),
644
+ Layer.provide(BrowserWorker.layer(() => new Worker(new URL('./rpc-worker.ts', import.meta.url))))
645
+ );
646
+ ```
647
+
648
+ ---
649
+
650
+ ## Common Mistakes
651
+
652
+ 1. **Importing from `@effect/rpc`.** Gone in v4 — everything is `effect/unstable/rpc`. And `import { RpcClientError } from 'effect/unstable/rpc'` gives you a *namespace*; import the class from `effect/unstable/rpc/RpcClientError`.
653
+ 2. **Missing `Scope` for `RpcClient.make`.** The constructor is scoped. Build clients inside `Layer.effect(Tag)(RpcClient.make(group))` or under `Effect.scoped` — providing protocol layers alone will not discharge `Scope`.
654
+ 3. **Forgetting the client middleware layer.** A group with a `requiredForClient: true` middleware needs `Layer.provide(RpcMiddleware.layerClient(M, ...))` on the client (and in `RpcTest.makeClient`'s context). The compile error points at an unsatisfied `ForClient<M>` requirement — provide the layer, don't cast.
655
+ 4. **Wrong codec/transport pairing.** Raw TCP + `layerJson` corrupts framing — use `layerNdjson`/`layerMsgPack`/`layerNdJsonRpc`. Over HTTP, unframed `layerJson` buffers the whole response, so streaming rpcs stall until the request ends — use a framed codec. WebSocket alone is fine with any codec (the transport frames messages).
656
+ 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
+ 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
+ 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.
660
+ 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
+ 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
+ 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.
663
+ 12. **Waiting for a queue value to signal end-of-stream.** With `asQueue: true`, completion is `Cause.Done` in the `Queue.take` error channel, and the `Dequeue` lives in a `Scope` — closing that scope interrupts the request on the server.
664
+ 13. **Expecting a graceful interrupt over HTTP.** The HTTP protocol drops `Interrupt` messages; interruption aborts the in-flight POST instead. Server handlers are still interrupted, but only when the connection abort is observed.
665
+ 14. **`RpcSerialization` provided to the worker protocol.** Worker transports use structured clone — no serialization layer is needed (or used); `RpcSerialization` is for HTTP and socket protocols.
666
+ 15. **Wrong option key names on `make`.** They are `spanPrefix`, `spanAttributes`, `disableTracing`, `generateRequestId`, `flatten` — there is no `baseUrl`, `headers`, or `serialization` option on `RpcClient.make`; those live on the protocol/serialization layers and per-call options.