opencode-effect-enforcer 0.2.5 → 0.2.8

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 (52) hide show
  1. package/README.md +42 -140
  2. package/docs/effect-4.0.0-rc.116-changelog.md +2654 -0
  3. package/docs/effect-4.0.0-rc.116.md +102 -0
  4. package/guidance/effect-first-development.md +28 -311
  5. package/guidance/progressive-disclosure-guidance.md +5 -19
  6. package/package.json +2 -2
  7. package/patterns/avoid-direct-tag-checks.md +1 -1
  8. package/patterns/avoid-process-env.md +4 -4
  9. package/patterns/context-tag-extends.md +4 -4
  10. package/patterns/prefer-arr-sort.md +1 -1
  11. package/patterns/prefer-redacted-config.md +10 -10
  12. package/patterns/prefer-schema-class.md +1 -1
  13. package/patterns/require-effect-concurrency.md +1 -1
  14. package/skills/effect-ai-chat/SKILL.md +2 -2
  15. package/skills/effect-ai-language-model/SKILL.md +36 -4
  16. package/skills/effect-ai-prompt/SKILL.md +1 -1
  17. package/skills/effect-ai-provider/SKILL.md +23 -12
  18. package/skills/effect-ai-tool/SKILL.md +13 -0
  19. package/skills/effect-atom-rpc/SKILL.md +7 -1
  20. package/skills/effect-atom-state/SKILL.md +8 -2
  21. package/skills/effect-cache/SKILL.md +10 -1
  22. package/skills/effect-cli/SKILL.md +105 -94
  23. package/skills/effect-command-executor/SKILL.md +7 -1
  24. package/skills/effect-config/SKILL.md +67 -44
  25. package/skills/effect-domain-modeling/SKILL.md +3 -3
  26. package/skills/effect-error-handling/SKILL.md +2 -2
  27. package/skills/effect-fiber/SKILL.md +2 -2
  28. package/skills/effect-filesystem/SKILL.md +34 -4
  29. package/skills/effect-http-api/SKILL.md +17 -2
  30. package/skills/effect-http-client/SKILL.md +11 -2
  31. package/skills/effect-http-server/SKILL.md +28 -11
  32. package/skills/effect-layer-design/SKILL.md +6 -2
  33. package/skills/effect-mcp-server/SKILL.md +21 -4
  34. package/skills/effect-observability/SKILL.md +2 -2
  35. package/skills/effect-optics/SKILL.md +1 -1
  36. package/skills/effect-parallelization/SKILL.md +2 -2
  37. package/skills/effect-pattern-matching/SKILL.md +1 -1
  38. package/skills/effect-platform-abstraction/SKILL.md +3 -3
  39. package/skills/effect-rpc-api/SKILL.md +3 -3
  40. package/skills/effect-rpc-client/SKILL.md +14 -13
  41. package/skills/effect-rpc-cluster/SKILL.md +15 -12
  42. package/skills/effect-rpc-server/SKILL.md +11 -13
  43. package/skills/effect-scheduling/SKILL.md +7 -0
  44. package/skills/effect-schema-composition/SKILL.md +12 -4
  45. package/skills/effect-schema-v4/SKILL.md +75 -20
  46. package/skills/effect-scope/SKILL.md +4 -4
  47. package/skills/effect-socket/SKILL.md +161 -658
  48. package/skills/effect-sql/SKILL.md +50 -12
  49. package/skills/effect-stream/SKILL.md +19 -24
  50. package/skills/effect-testing/SKILL.md +35 -22
  51. package/skills/effect-workflow/SKILL.md +13 -2
  52. package/src/guidance.ts +0 -1
@@ -1,703 +1,206 @@
1
1
  ---
2
2
  name: effect-socket
3
- description: Build bidirectional socket transports with effect/unstable/socket — the Socket run/writer surface, WebSocket-backed sockets, TCP/Unix-domain clients via NodeSocket, SocketServer accept loops, channel adapters, the SocketError taxonomy, and reconnect patterns. Use when connecting to or serving raw TCP, Unix-domain, or WebSocket endpoints, framing socket bytes with Stream/Channel (NDJSON), building reconnecting socket clients, or providing socket transports to RPC/devtools layers.
3
+ description: Build pull-based TCP, Unix, TLS, and WebSocket transports with Effect Socket readers and writers. Use for bidirectional connections, framing, reconnects, socket servers, or RPC socket transports.
4
4
  ---
5
5
 
6
- You are an Effect TypeScript expert specializing in bidirectional socket programming with `effect/unstable/socket` and the Node/Bun platform socket adapters.
6
+ # Effect Socket
7
7
 
8
- These modules live under `effect/unstable/socket` — there is no `@effect/platform/Socket` in v4. Platform implementations ship from `@effect/platform-node` (`NodeSocket`, `NodeSocketServer`) and `@effect/platform-bun` (`BunSocket`, `BunSocketServer`), both re-exporting the same shared implementation from `@effect/platform-node-shared`.
8
+ ## Source reference
9
9
 
10
- ## Effect Source Reference
10
+ Read `packages/effect/src/unstable/socket/{Socket,SocketServer}.ts` at
11
+ `effect@4.0.0-rc.116` in the Effect reference. Platform adapters live in
12
+ `packages/platform/node-shared/src/{NodeSocket,NodeSocketServer}.ts`; Node and
13
+ Bun expose these as `NodeSocket` / `BunSocket` and their server counterparts.
14
+ Load `effect-scope` and `effect-fiber` for lifecycle ownership,
15
+ `effect-stream` for framing, and `effect-scheduling` for reconnect policy.
11
16
 
12
- The Effect v4 source is at `~/.local/share/opencode/repos/github.com/Effect-TS/effect@main/`. Read it directly when in doubt — these unstable modules change between betas.
17
+ ## Connection ownership
13
18
 
14
- Key files:
19
+ `Socket.Socket` is both the service tag and the interface. A client socket is
20
+ a connection recipe. Acquiring `socket.reader` establishes a connection and
21
+ returns a `Reader` with `pull` and `upgrade`. Its acquisition scope owns the
22
+ connection. A pull yields a non-empty batch of `string | Uint8Array` frames.
23
+ Every termination, including clean close, fails with `SocketError`.
15
24
 
16
- - `packages/effect/src/unstable/socket/Socket.ts` — `Socket` interface and service tag, `make`, `CloseEvent`, the `SocketError` taxonomy, channel adapters (`toChannel`, `toChannelString`, `toChannelMap`, `makeChannel`), WebSocket constructors (`makeWebSocket`, `fromWebSocket`, `layerWebSocket`, `WebSocketConstructor`), `fromTransformStream`
17
- - `packages/effect/src/unstable/socket/SocketServer.ts` — `SocketServer` service contract, `Address` (`TcpAddress` / `UnixAddress`), `SocketServerError`
18
- - `packages/platform/node-shared/src/NodeSocket.ts` — `makeNet`, `fromDuplex`, `makeNetChannel`, `layerNet`, `NetSocket` service, `NodeWS` (`ws` re-export)
19
- - `packages/platform/node-shared/src/NodeSocketServer.ts` — TCP/Unix server `make`/`layer`, WebSocket server `makeWebSocket`/`layerWebSocket`, `IncomingMessage` service
20
- - `packages/platform/node/src/NodeSocket.ts` — re-exports shared module; adds `layerWebSocketConstructor`, `layerWebSocketConstructorWS`, `layerWebSocket`
21
- - `packages/platform/bun/src/BunSocket.ts` — same shared re-export; Bun-global WebSocket constructor layers
22
- - `packages/platform/node/test/NodeSocket.test.ts` — loopback echo server, WebSocket client semantics, transform-stream sockets
23
- - `packages/effect/src/unstable/devtools/DevToolsClient.ts` — production example of NDJSON-framed request/response over a `Socket`
24
- - `packages/effect/src/unstable/rpc/RpcClient.ts` (`makeProtocolSocket`) — production example of socket reconnect with retry schedules
25
+ `socket.writer` is an infallible scoped acquisition of a `Writer` object:
25
26
 
26
- ## Core Model
27
+ - `writer.write(frameOrCloseEvent)` sends one frame or a close request.
28
+ - `writer.writeAll(nonEmptyBatch)` batches outgoing frames with native backpressure.
29
+ - Both write operations may fail with `SocketError`.
30
+ - Writes while disconnected wait for a reader to establish the connection.
31
+ - Releasing the writer scope half-closes transports that support it.
27
32
 
28
- A `Socket` is a **recipe for one bidirectional connection**, not a live handle. The underlying transport (TCP socket, WebSocket, duplex stream) is acquired each time you execute `run`/`runString`/`runRaw` and released when that run ends — so retrying the run loop reconnects. Run loops must not be executed concurrently on the same `Socket` value — this is not enforced: concurrent runs each open their own transport and clobber the single internal writer slot, so writes silently target the last-opened connection.
33
+ One owner acquires the reader. Process each batch sequentially by default;
34
+ bounded concurrency is a deliberate application decision. Place a handshake
35
+ after reader acquisition and before the first pull so it runs per connection.
36
+ There are no callback-driven `run`, `runString`, or `runRaw` methods on a socket.
29
37
 
38
+ <!-- typecheck -->
30
39
  ```ts
31
- interface Socket {
32
- // Read loop + connection driver. Completes/fails when the connection ends.
33
- readonly run: <_, E = never, R = never>(
34
- handler: (data: Uint8Array) => Effect.Effect<_, E, R> | void,
35
- options?: { readonly onOpen?: Effect.Effect<void> | undefined }
36
- ) => Effect.Effect<void, Socket.SocketError | E, R>;
37
- readonly runString: (handler: (data: string) => ..., options?) => ...;
38
- readonly runRaw: (handler: (data: string | Uint8Array) => ..., options?) => ...;
39
-
40
- // Scoped writer: a function you call per outgoing chunk.
41
- readonly writer: Effect.Effect<
42
- (chunk: Uint8Array | string | Socket.CloseEvent) => Effect.Effect<void, Socket.SocketError>,
43
- never,
44
- Scope.Scope
45
- >;
46
- }
47
- ```
48
-
49
- How to think about it:
50
-
51
- - **`run*` drives everything.** It opens the connection, registers the handler for incoming frames, and only returns when the connection closes (or the open/read fails). Writes made through `writer` suspend on an internal latch until a `run` is active and the connection is open — calling `write` with no concurrent `run` waits forever.
52
- - **The handler can return `void` or an Effect.** Returning `void` is the synchronous fast path (e.g. `Queue.offerUnsafe`). Returning an Effect forks it into a per-run `FiberSet` — handler effects run **concurrently and unordered**, and a failed handler effect fails the whole run loop with `E`.
53
- - **`writer` is scoped.** Closing the writer's scope ends the writable side: Node TCP calls `socket.end()` (half-close), transform-stream sockets close the writable stream, WebSocket writers do nothing on release (the WS itself closes when the run ends).
54
- - **`Socket` is a `Context.Service`** (`Socket.Socket` tag), so layers like `NodeSocket.layerNet` or `Socket.layerWebSocket` provide it to programs that just `yield* Socket.Socket`.
55
-
56
- Imports used throughout:
57
-
58
- ```ts
59
- import { Effect, Layer, Queue, Schedule, Schema, Scope, Stream } from 'effect';
60
- import { Socket, SocketServer } from 'effect/unstable/socket';
61
- import { Ndjson } from 'effect/unstable/encoding';
62
- import { NodeSocket, NodeSocketServer } from '@effect/platform-node';
63
- // Bun equivalents (same API, shared implementation):
64
- // import { BunSocket, BunSocketServer } from '@effect/platform-bun';
65
- ```
66
-
67
- ---
68
-
69
- ## 1. The Socket Surface: run, runString, runRaw, writer
70
-
71
- ### Reading
72
-
73
- ```ts
74
- const program = Effect.gen(function*() {
75
- const socket = yield* Socket.Socket;
76
-
77
- // Binary frames (strings arriving on the wire are UTF-8 encoded for you)
78
- yield* socket.run((data: Uint8Array) => Effect.log(`got ${data.length} bytes`));
79
-
80
- // Text frames (binary frames are decoded per-frame with TextDecoder)
81
- yield* socket.runString((text) => Effect.log(text));
82
-
83
- // Raw frames — exactly what the transport delivered (string | Uint8Array)
84
- yield* socket.runRaw((data) => {
85
- // returning void = synchronous handling, preserves frame order
86
- });
87
- });
88
- ```
89
-
90
- The `onOpen` option runs an effect once the connection is open, before the run loop settles — the idiomatic place for subscribe/hello messages, since it re-runs on every reconnect. `onOpen` must be infallible (`Effect<void>`, error channel `never`), so wrap fallible writes with `Effect.orDie` (or `Effect.ignore` if a failed hello should not kill the run):
91
-
92
- ```ts
93
- yield* socket.run(handleFrame, {
94
- onOpen: Effect.orDie(write(JSON.stringify({ type: 'subscribe', channel: 'trades' })))
95
- });
96
- ```
97
-
98
- ### Ordered processing of incoming frames
99
-
100
- Effectful handlers are forked concurrently. When ordering matters, push synchronously into a `Queue` and consume it in your own fiber (this is exactly what `Socket.toChannelMap` does internally):
101
-
102
- ```ts
103
- const queue = yield* Queue.unbounded<Uint8Array>();
104
- yield* Effect.forkChild(
105
- socket.run((data) => {
106
- Queue.offerUnsafe(queue, data); // sync — no Effect returned
107
- })
108
- );
109
- ```
110
-
111
- ### Writing
112
-
113
- `writer` yields a write function; each call returns an Effect that completes when the chunk is flushed (Node: the `write` callback) and fails with `SocketError` on write failure:
114
-
115
- ```ts
116
- yield* Effect.gen(function*() {
117
- const write = yield* socket.writer;
118
- yield* write(new TextEncoder().encode('Hello'));
119
- yield* write('strings are fine too');
120
- yield* write(new Socket.CloseEvent(1000, 'done')); // request close
121
- }).pipe(Effect.scoped);
122
- ```
123
-
124
- `Socket.CloseEvent` is `new Socket.CloseEvent(code = 1000, reason?)`. On a WebSocket it calls `ws.close(code, reason)`; on TCP it destroys the connection — codes above 1000 destroy with a local `Error`, failing the *local* run loop with `SocketReadError`. Close codes are a WebSocket concept and are never transmitted over raw TCP: the peer just observes an ordinary EOF or ECONNRESET.
125
-
126
- Write effects suspend on the internal latch until the connection is open, so it is safe to start writing fibers before (or while) `run` connects — but a `run` must eventually be active or the writes suspend forever.
127
-
128
- ### Building a Socket by hand
129
-
130
- `Socket.make({ runRaw, run?, runString?, writer })` derives the missing read loops from `runRaw` (UTF-8 encoding/decoding per frame). A custom `writer` receives `Uint8Array | string | Socket.CloseEvent` and must branch with `Socket.isCloseEvent(chunk)` to separate close requests from data (every built-in writer does). You rarely need it directly — prefer `fromWebSocket`, `NodeSocket.fromDuplex`, or `Socket.fromTransformStream`.
131
-
132
- ---
133
-
134
- ## 2. The SocketError Taxonomy
135
-
136
- All socket failures surface as a single tagged error `SocketError` (`_tag: 'SocketError'`) wrapping a `reason` union:
137
-
138
- | `reason._tag` | When | Extra fields |
139
- |---|---|---|
140
- | `SocketOpenError` | connect/handshake failed | `kind: 'Unknown' \| 'Timeout'`, `cause` |
141
- | `SocketReadError` | transport error while reading | `cause` |
142
- | `SocketWriteError` | write callback failed | `cause` |
143
- | `SocketCloseError` | connection closed | `code: number`, `closeReason?: string` |
144
-
145
- ```ts
146
- program.pipe(
147
- Effect.catchTag('SocketError', (error) => {
148
- switch (error.reason._tag) {
149
- case 'SocketOpenError':
150
- return error.reason.kind === 'Timeout' ? retryLater : giveUp;
151
- case 'SocketCloseError':
152
- return error.reason.code === 1000 ? Effect.void : reconnect;
153
- case 'SocketReadError':
154
- case 'SocketWriteError':
155
- return reconnect;
156
- }
157
- })
158
- );
159
- ```
160
-
161
- Useful helpers:
162
-
163
- - `Socket.isSocketError(u)` / `Socket.SocketError.is(u)` — runtime guards
164
- - `Socket.SocketErrorReason` — `Schema.Union` of the four reasons (reusable in your own schemas)
165
- - `Socket.SocketCloseError.filterClean(isClean)` — a `Filter` for `Effect.catchFilter` that succeeds only for close errors whose code passes `isClean`:
166
-
167
- ```ts
168
- // Treat a clean close (1000) as normal completion, keep everything else as failure
169
- socket.run(handler).pipe(
170
- Effect.catchFilter(
171
- Socket.SocketCloseError.filterClean((code) => code === 1000),
172
- () => Effect.void
173
- )
174
- );
175
- ```
176
-
177
- Close-code semantics per transport:
178
-
179
- - **WebSocket** (`fromWebSocket`/`makeWebSocket`): *every* close — including a clean 1000 — fails the run with `SocketCloseError` by default (`Socket.defaultCloseCodeIsError` returns `true` for all codes). Pass `closeCodeIsError: (code) => code !== 1000` to make clean closes complete the run with `void`.
180
- - **TCP** (`NodeSocket.makeNet`/`fromDuplex`): a graceful peer FIN (`'end'` event) completes the run **successfully** with `void`; transport errors fail with `SocketReadError`; a close without a prior `'end'`/`'error'` (e.g. a local destroy via `write(CloseEvent)`) fails with `SocketCloseError` code `1000` (`1006` if errored — in practice preempted by `SocketReadError`, since `'error'` fires first and the run keeps the first settlement). There is no `closeCodeIsError` option here.
181
-
182
- ---
183
-
184
- ## 3. WebSocket Clients
185
-
186
- ### Constructor service
187
-
188
- WebSocket creation is abstracted behind the `Socket.WebSocketConstructor` service so the same code runs in browsers, Node, and Bun:
189
-
190
- ```ts
191
- // Browser / any runtime with a global WebSocket
192
- Socket.layerWebSocketConstructorGlobal;
193
-
194
- // Node: global WebSocket when available, otherwise the `ws` package
195
- NodeSocket.layerWebSocketConstructor;
196
- // Node: always the `ws` package
197
- NodeSocket.layerWebSocketConstructorWS;
40
+ import { Effect } from 'effect';
41
+ import { Socket } from 'effect/unstable/socket';
198
42
 
199
- // Bun: global WebSocket
200
- BunSocket.layerWebSocketConstructor;
43
+ const echo = Effect.fn('Socket.echo')(function* (socket: Socket.Socket) {
44
+ const { pull } = yield* socket.reader;
45
+ const writer = yield* socket.writer;
46
+ yield* pull.pipe(Effect.flatMap(writer.writeAll), Effect.forever);
47
+ }, Effect.scoped);
201
48
  ```
202
49
 
203
- ### Creating the socket
204
-
205
- ```ts
206
- const socket = yield* Socket.makeWebSocket('wss://example.com/feed', {
207
- closeCodeIsError: (code) => code !== 1000, // default: every close is an error
208
- openTimeout: '5 seconds', // default 10000 ms; SocketOpenError kind 'Timeout' on expiry
209
- protocols: ['v1.protocol'] // optional subprotocols
210
- });
211
- // Effect<Socket, never, WebSocketConstructor> — connection NOT opened yet
212
- ```
50
+ `Socket.readerBytes(socket)` and `Socket.readerString(socket, encoding?)`
51
+ acquire a converted pull directly, rather than returning a Reader object.
52
+ The string adapter decodes each frame independently. For TCP, use a byte stream
53
+ and streaming `Stream.decodeText` to preserve split UTF-8 characters.
213
54
 
214
- The URL can be `string | Effect<string>`. An effectful URL is re-evaluated on **every** (re)connect — use it to refresh auth tokens in query strings between retries.
215
-
216
- `Socket.fromWebSocket(acquire, options?)` is the lower-level form taking a scoped `Effect<globalThis.WebSocket, SocketError, R>` — `NodeSocketServer.makeWebSocket` uses it to wrap server-accepted sockets.
217
-
218
- ### Layers
55
+ ## WebSocket clients
219
56
 
57
+ <!-- typecheck -->
220
58
  ```ts
221
- // Core: requires WebSocketConstructor in context
222
- Socket.layerWebSocket(url, options?); // Layer<Socket, never, WebSocketConstructor>
223
-
224
- // Platform conveniences: constructor already provided
225
- NodeSocket.layerWebSocket(url, options?); // Layer<Socket, never, never>
226
- BunSocket.layerWebSocket(url, options?); // Layer<Socket, never, never>
227
- ```
228
-
229
- ### Behavior details (verified in source/tests)
230
-
231
- - The run loop waits for the `open` event before settling; incoming `Blob` frames are converted to `Uint8Array` automatically; text frames arrive as strings (use `run` to get bytes, `runString` to get text).
232
- - When the run ends, the WebSocket is closed with code 1000 (the acquire's release in `makeWebSocket`).
233
- - Handler effects can access the live `globalThis.WebSocket` via the `Socket.WebSocket` service (`yield* Socket.WebSocket`).
234
- - An `error` event before open fails the run with `SocketOpenError`; after open, with `SocketReadError`.
59
+ import { Duration, Effect } from 'effect';
60
+ import { Socket } from 'effect/unstable/socket';
235
61
 
236
- ```ts
237
- // Full client wiring
238
- const program = Effect.gen(function*() {
62
+ const receive = Effect.gen(function* () {
239
63
  const socket = yield* Socket.makeWebSocket('wss://example.com/feed', {
240
- closeCodeIsError: (code) => code !== 1000
64
+ openTimeout: Duration.seconds(5),
65
+ highWaterMark: 64 * 1024,
66
+ protocols: ['v1']
241
67
  });
242
- const messages = yield* Queue.unbounded<Uint8Array>();
243
- yield* Effect.forkChild(
244
- socket.run((data) => {
245
- Queue.offerUnsafe(messages, data); // sync — preserves frame order
246
- })
247
- );
248
- yield* Effect.gen(function*() {
249
- const write = yield* socket.writer;
250
- yield* write('hello');
251
- }).pipe(Effect.scoped);
252
- return yield* Queue.take(messages);
253
- }).pipe(Effect.provide(NodeSocket.layerWebSocketConstructor));
254
- ```
255
-
256
- ---
257
-
258
- ## 4. TCP and Unix-Domain Clients (NodeSocket)
259
-
260
- `NodeSocket.makeNet(options)` opens a `net.createConnection` as a `Socket`. Options are Node's `NetConnectOpts` plus `openTimeout`:
261
-
262
- ```ts
263
- // TCP
264
- const tcp = yield* NodeSocket.makeNet({
265
- host: 'localhost', // Net.TcpNetConnectOpts
266
- port: 9000,
267
- openTimeout: '3 seconds' // optional — no default; without it, connects wait forever
268
- });
269
-
270
- // Unix-domain socket
271
- const unix = yield* NodeSocket.makeNet({ path: '/tmp/app.sock' });
272
- ```
273
-
274
- `makeNet` returns `Effect<Socket>` — infallible, because connection happens per `run`. Connect failures surface from the run loop as `SocketOpenError`.
275
-
276
- An explicitly supplied zero timeout is not treated as absence. `openTimeout: 0` or `openTimeout: Duration.zero` makes `NodeSocket.makeNet` / `fromDuplex` time out immediately with `SocketOpenError` kind `"Timeout"`. Omit `openTimeout` to allow an unbounded connect wait.
277
-
278
- ```ts
279
- // As a layer
280
- NodeSocket.layerNet({ port: 9000 }); // Layer<Socket, SocketError>
281
-
282
- // As a Channel directly (see section 5)
283
- NodeSocket.makeNetChannel<IE>({ port: 9000 });
284
- ```
285
-
286
- `NodeSocket.fromDuplex(open, options?)` adapts any Node `Duplex` (child process stdio, TLS sockets via `tls.connect`, PTYs):
287
-
288
- ```ts
289
- import * as Tls from 'node:tls';
290
-
291
- const tlsSocket = yield* NodeSocket.fromDuplex(
292
- Effect.sync(() => Tls.connect({ host: 'example.com', port: 443 })),
293
- { openTimeout: '5 seconds' }
294
- );
295
- ```
296
-
297
- The `open` effect is scoped — it re-runs per `run` call, and its scope closes when the run ends. Inside handler effects, the raw `net.Socket` is available as the `NodeSocket.NetSocket` service (e.g. to read `remoteAddress` or call `setKeepAlive`).
298
-
299
- These constructors work identically from `BunSocket` (same shared implementation over `node:net`).
300
-
301
- ---
302
-
303
- ## 5. Sockets as Channels and Streams
304
-
305
- A `Socket` converts to a bidirectional `Channel` — output is incoming frames, input is outgoing frames:
306
-
307
- ```ts
308
- // Binary: incoming string frames are UTF-8 encoded to bytes
309
- Socket.toChannel(socket);
310
- // Channel<NonEmptyReadonlyArray<Uint8Array>, SocketError | IE, void,
311
- // NonEmptyReadonlyArray<Uint8Array | string | CloseEvent>, IE>
312
-
313
- // Text: incoming binary frames decoded per-frame (optional encoding arg; dual)
314
- Socket.toChannelString(socket);
315
- Socket.toChannelString(socket, 'utf-8');
316
-
317
- // Fix the upstream error type explicitly (channels are invariant in IE)
318
- Socket.toChannelWith<MyError>()(socket);
319
-
320
- // Custom frame mapping
321
- Socket.toChannelMap(socket, (data) => decodeFrame(data));
322
-
323
- // From the Socket service in context
324
- Socket.makeChannel<IE>(); // Channel<..., ..., ..., ..., ..., unknown, Socket>
325
-
326
- // Node shortcut: TCP channel without an intermediate Socket value
327
- NodeSocket.makeNetChannel<IE>({ port: 9000 });
328
-
329
- // WebSocket shortcut (requires WebSocketConstructor)
330
- Socket.makeWebSocketChannel<IE>(url, { closeCodeIsError });
331
- ```
332
-
333
- `Stream.pipeThroughChannel` turns request/response over a socket into a stream pipeline — the loopback test does exactly this against an echo server:
334
-
335
- ```ts
336
- const channel = NodeSocket.makeNetChannel({ port });
337
-
338
- const output = yield* Stream.make('Hello', 'World').pipe(
339
- Stream.encodeText, // Stream<string> → Stream<Uint8Array>
340
- Stream.pipeThroughChannel(channel), // write upstream, read downstream
341
- Stream.decodeText(), // streaming UTF-8 decode (safe across chunk splits)
342
- Stream.mkString
343
- );
344
- // 'HelloWorld'
345
- ```
346
-
347
- When the upstream stream ends, the channel's write side completes (Node: `.end()` half-close), and the channel keeps emitting until the peer closes. Prefer `Stream.decodeText` over `runString`/`toChannelString` for TCP byte streams — only `decodeText` reassembles multi-byte UTF-8 characters split across chunks (see Common Mistakes).
348
-
349
- See the effect-stream skill for the full Stream/Channel surface.
350
-
351
- ---
352
-
353
- ## 6. Framing: NDJSON over a Socket
354
-
355
- TCP delivers a byte stream, not messages — you must frame. `Ndjson.duplex*` from `effect/unstable/encoding` wraps a socket channel so you write/read typed values:
356
-
357
- ```ts
358
- import { Ndjson } from 'effect/unstable/encoding';
359
-
360
- const Request = Schema.Struct({ id: Schema.Number, op: Schema.String });
361
- const Response = Schema.Struct({ id: Schema.Number, result: Schema.String });
362
-
363
- const program = Effect.gen(function*() {
364
- const socket = yield* NodeSocket.makeNet({ port: 9000 });
365
- const requests = yield* Queue.unbounded<typeof Request.Type>();
366
-
367
- yield* Stream.fromQueue(requests).pipe(
368
- Stream.pipeThroughChannel(
369
- Ndjson.duplexSchema(Socket.toChannel(socket), {
370
- inputSchema: Request, // encodes outgoing values
371
- outputSchema: Response // decodes incoming lines
372
- })
373
- ),
374
- Stream.runForEach((response) => Effect.log('response', response)),
375
- Effect.forkScoped
376
- );
377
-
378
- yield* Queue.offer(requests, { id: 1, op: 'ping' });
379
- });
380
- ```
381
-
382
- Variants (all dual, options `{ ignoreEmptyLines?: boolean }`):
383
-
384
- - `Ndjson.duplex(channel)` — untyped `unknown` values over a byte channel
385
- - `Ndjson.duplexSchema(channel, { inputSchema, outputSchema })` — typed, byte channel (use for TCP)
386
- - `Ndjson.duplexString` / `Ndjson.duplexSchemaString` — string-channel versions (fine for WebSocket text frames; `DevToolsClient` composes `Ndjson.duplexSchemaString(Socket.toChannelString(socket), ...)`)
387
-
388
- Error channel gains `NdjsonError` (and `Schema.SchemaError` for the schema variants). `Msgpack.duplex`/`duplexSchema` provide analogous binary framing, but require `Uint8Array<ArrayBuffer>` channels — `Socket.toChannel`'s plain `Uint8Array` output does not directly satisfy them (a cast or mapping is needed) — and `Msgpack.duplex` is not dual. One-way encoding/decoding (`Ndjson.encode()`, `Ndjson.decode()`) composes with `Stream.pipeThroughChannel` as shown in the effect-stream skill.
389
-
390
- ---
391
-
392
- ## 7. Accepting Connections: SocketServer
393
-
394
- `SocketServer.SocketServer` is a `Context.Service` with two members:
395
-
396
- ```ts
397
- interface SocketServerService {
398
- readonly address: SocketServer.Address; // TcpAddress | UnixAddress, known at construction
399
- readonly run: <R, E, _>(
400
- handler: (socket: Socket.Socket) => Effect.Effect<_, E, R>
401
- ) => Effect.Effect<never, SocketServer.SocketServerError, R>;
402
- }
403
- ```
404
-
405
- ### TCP / Unix-domain (Node and Bun)
406
-
407
- ```ts
408
- // Scoped construction — listening starts immediately; closes with the scope
409
- const server = yield* NodeSocketServer.make({ port: 0 }); // port 0 = ephemeral
410
- const unixServer = yield* NodeSocketServer.make({ path: '/tmp/app.sock' });
411
-
412
- // Or as a layer
413
- NodeSocketServer.layer({ port: 9000, host: '127.0.0.1' });
414
- // Layer<SocketServer, SocketServerError>
415
- ```
416
-
417
- Options are Node's `Net.ServerOpts & Net.ListenOptions`. Read the bound port from the address:
418
-
419
- ```ts
420
- const port = (server.address as SocketServer.TcpAddress).port;
421
- ```
422
-
423
- ### The accept loop
424
-
425
- `run(handler)` returns `Effect<never, ...>` — it never completes, so fork it. The handler is forked per connection; an errored handler is **logged** (via the `UnhandledLogLevel` reference: "Unhandled error in SocketServer"), never propagated to the accept loop. Connections accepted between `make` and `run` are queued and handled once `run` starts.
426
-
427
- ```ts
428
- const EchoServer = Effect.gen(function*() {
429
- const server = yield* NodeSocketServer.make({ port: 0 });
430
-
431
- yield* server.run(
432
- Effect.fnUntraced(function*(socket) {
433
- const write = yield* socket.writer; // needs Scope —
434
- yield* socket.run(write); // echo: write fn doubles as read handler
435
- }, Effect.scoped) // — eliminate it per connection
436
- ).pipe(Effect.forkScoped);
437
-
438
- return server;
439
- });
440
- ```
441
-
442
- Wrap handlers that use `socket.writer` (or any scoped resource) in `Effect.scoped`: the server captures the handler's context at `run` time **minus `Scope`**, so a handler still requiring `Scope` dies at runtime. Inside TCP handlers the raw `net.Socket` is provided as `NodeSocket.NetSocket`.
443
-
444
- ### WebSocket servers (`ws`-backed)
445
-
446
- ```ts
447
- NodeSocketServer.makeWebSocket({ port: 8080 }); // ws.ServerOptions
448
- NodeSocketServer.layerWebSocket({ port: 8080 });
449
- ```
450
-
451
- Handlers receive the same `Socket.Socket` interface, plus two extra services in context: `NodeSocketServer.IncomingMessage` (the Node `http.IncomingMessage` — URL, headers, auth) and `Socket.WebSocket` (the raw `ws` socket). To attach WebSockets to an existing Effect HTTP server route instead, use `HttpServerRequest.upgrade` — see the effect-http-server skill.
452
-
453
- `BunSocketServer` re-exports the identical API (Bun runs it over `node:net` compatibility).
454
-
455
- `SocketServerError` wraps a `reason` of `SocketServerOpenError` (bind/listen failures) or `SocketServerUnknownError`, each carrying `cause`.
456
-
457
- ---
458
-
459
- ## 8. Transform-Stream Sockets and In-Memory Testing
460
-
461
- `Socket.fromTransformStream` adapts a web `ReadableStream`/`WritableStream` pair — the basis for in-memory sockets in tests, WebTransport-style APIs, or piping through `CompressionStream`:
462
-
463
- ```ts
464
- const socket = yield* Socket.fromTransformStream(
465
- Effect.succeed({
466
- readable: someReadableStream, // ReadableStream<Uint8Array | string>
467
- writable: someWritableStream // WritableStream<Uint8Array>
468
- }),
469
- { closeCodeIsError: (code) => code !== 1000 }
470
- );
471
- ```
472
-
473
- In-memory test socket from the test suite — feed the read side from a `Stream`, capture writes in an array:
474
-
475
- ```ts
476
- const readable = Stream.make('A', 'B', 'C').pipe(
477
- Stream.toReadableStream()
478
- );
479
- const chunks: Array<string> = [];
480
- const decoder = new TextDecoder();
481
- const writable = new WritableStream<Uint8Array>({
482
- write(chunk) {
483
- chunks.push(decoder.decode(chunk));
484
- }
485
- });
486
-
487
- const socket = yield* Socket.fromTransformStream(
488
- Effect.succeed({ readable, writable }),
489
- { closeCodeIsError: () => false }
490
- );
491
- ```
492
-
493
- When the readable ends, the run fails with `SocketCloseError` code 1000 — which `closeCodeIsError: () => false` converts to successful completion. Writing a `Socket.CloseEvent` ends the run with `SocketCloseError` carrying that code (subject to `closeCodeIsError`), without closing the writable stream. String writes are UTF-8 encoded before reaching the writable. Releasing the writer scope closes the writable stream.
494
-
495
- For integration-level tests, prefer a real loopback server: `NodeSocketServer.make({ port: 0 })` + `NodeSocket.makeNet({ port: address.port })` (the pattern in `NodeSocket.test.ts`) — it exercises actual framing and close semantics.
496
-
497
- ---
498
-
499
- ## 9. Reconnect and Retry Patterns
500
-
501
- Because each `run` acquires a fresh connection, **reconnect = retry the run loop**. This is exactly how `RpcClient.makeProtocolSocket` works in production: acquire the writer once (writes latch while disconnected), wrap the run in `Effect.suspend` to reset per-attempt state, and `Effect.retry` with a capped backoff:
502
-
503
- ```ts
504
- const feed = Effect.gen(function*() {
505
- const socket = yield* Socket.Socket;
506
- const write = yield* socket.writer; // once — survives reconnects
507
-
508
- yield* Effect.suspend(() => {
509
- // reset per-connection state here (parsers, sequence numbers, ...)
510
- return socket.run(handleFrame, {
511
- onOpen: Effect.orDie(write(JSON.stringify({ type: 'subscribe', channel: 'trades' })))
512
- });
513
- }).pipe(
514
- // a clean run completion is also a disconnect — fail so retry kicks in
515
- Effect.flatMap(() =>
516
- Effect.fail(
517
- new Socket.SocketError({
518
- reason: new Socket.SocketCloseError({ code: 1000 })
519
- })
520
- )
521
- ),
522
- Effect.tapError((error) => Effect.logWarning('socket disconnected', error)),
523
- Effect.retry(
524
- // RpcClient's defaultRetryPolicy: exponential from 500ms, capped at 5s
525
- Schedule.min([
526
- Schedule.exponential('500 millis', 1.5),
527
- Schedule.spaced('5 seconds')
528
- ])
529
- )
68
+ const pull = yield* Socket.readerString(socket);
69
+ const writer = yield* socket.writer;
70
+ yield* writer.write('subscribe');
71
+ yield* pull.pipe(
72
+ Effect.flatMap((batch) => Effect.forEach(batch, Effect.logInfo)),
73
+ Effect.forever
530
74
  );
531
- }).pipe(Effect.provide(NodeSocket.layerWebSocket('wss://example.com/feed')));
75
+ }).pipe(Effect.scoped, Effect.provide(Socket.layerWebSocketConstructorGlobal));
532
76
  ```
533
77
 
534
- Notes:
535
-
536
- - `onOpen` re-runs on every reconnect — put (re)subscription handshakes there, not before the retry loop.
537
- - To retry only transient failures, filter: `Effect.retry({ schedule, while: (e) => e.reason._tag !== 'SocketOpenError' || e.reason.kind === 'Timeout' })` — or match whatever taxonomy subset is transient for your protocol.
538
- - For liveness detection on quiet connections, race the run loop against an application-level ping timeout and fail with a synthetic `SocketOpenError({ kind: 'Timeout' })` — see `makePinger` in `RpcClient.ts`.
539
- - An effectful URL passed to `makeWebSocket` is re-evaluated per attempt — refresh tokens there.
540
-
541
- ---
78
+ Use `NodeSocket.layerWebSocketConstructor` or
79
+ `NodeSocket.layerWebSocketConstructorWS` on Node and
80
+ `BunSocket.layerWebSocketConstructor` on Bun. Platform `layerWebSocket(url, options)`
81
+ conveniences supply the constructor. Core `Socket.layerWebSocket` requires it.
82
+ Constructors accept compatible browser, Bun, and Node implementations without
83
+ casts; opening-handshake headers are available where the platform supports them.
542
84
 
543
- ## 10. Sockets as Transports for Higher Layers
85
+ An effectful URL is reevaluated on each connection. `fromWebSocket(acquire, options)`
86
+ wraps a scoped transport acquisition. Pausable transports pause at `highWaterMark`
87
+ (default 64 KiB) and resume when drained. Browser WebSockets cannot pause and
88
+ fail with `SocketReadError` when the configured bound is exceeded. Text-frame
89
+ buffering counts UTF-8 bytes, not string length.
544
90
 
545
- Sockets are the transport substrate for several Effect subsystems — wire them with one layer each:
91
+ ## TCP, Unix, and TLS
546
92
 
547
93
  ```ts
548
- // RPC over raw TCP: server side (see effect-rpc-server skill)
549
- RpcServer.layerProtocolSocketServer; // requires SocketServer + RpcSerialization
550
- // e.g. Layer.provide(NodeSocketServer.layer({ port: 9000 }))
94
+ import { NodeSocket } from '@effect/platform-node';
551
95
 
552
- // RPC client over any Socket (see effect-rpc-client skill)
553
- RpcClient.layerProtocolSocket({ retryTransientErrors: true });
554
- // e.g. Layer.provide(NodeSocket.layerNet({ port: 9000 })) or NodeSocket.layerWebSocket(url)
96
+ const tcp = NodeSocket.makeNet({ host: 'localhost', port: 9000, openTimeout: '3 seconds' });
97
+ const unix = NodeSocket.makeNet({ path: '/tmp/app.sock' });
98
+ const tls = NodeSocket.makeTls({ host: 'example.com', port: 443, servername: 'example.com' });
555
99
  ```
556
100
 
557
- - WebSocket upgrades inside an HTTP server (`HttpServerRequest.upgrade` returns a `Socket`) — see the effect-http-server skill.
558
- - Cluster runners communicate over sockets via `NodeClusterSocket`/`BunClusterSocket` — see the effect-rpc-cluster skill.
559
- - DevTools telemetry runs NDJSON over a `Socket` (`effect/unstable/devtools`), provided by e.g. `NodeSocket.layerWebSocket('ws://localhost:34437')`.
560
-
561
- ---
101
+ `makeNet` / `makeTls` return recipes; connection failures occur during reader
102
+ acquisition. `layerNet`, `layerTls`, `makeNetChannel`, and `makeTlsChannel` provide
103
+ service/channel equivalents. Prefer `makeTls` over hand-wrapping `tls.connect`.
104
+ `fromDuplex` remains the adapter for an existing scoped Node duplex source.
105
+ Omitted TCP `openTimeout` is unbounded; zero means an immediate timeout.
562
106
 
563
- ## Key Patterns
107
+ For STARTTLS acquire the reader, complete the protocol handshake, then call
108
+ `reader.upgrade(options)`. TLS keys and passphrases are redacted. Server-role
109
+ upgrades require both key and certificate; unsupported transports and invalid
110
+ credentials fail with `SocketUpgradeError` inside `SocketError`.
564
111
 
565
- ### Loopback echo server + stream client (the canonical test)
112
+ ## Errors, close, and reconnect
566
113
 
567
- ```ts
568
- import { Effect, Stream } from 'effect';
569
- import { Socket, SocketServer } from 'effect/unstable/socket';
570
- import { NodeSocket, NodeSocketServer } from '@effect/platform-node';
571
-
572
- const program = Effect.gen(function*() {
573
- // Server: echo every frame back
574
- const server = yield* NodeSocketServer.make({ port: 0 });
575
- yield* server.run(
576
- Effect.fnUntraced(function*(socket) {
577
- const write = yield* socket.writer;
578
- yield* socket.run(write);
579
- }, Effect.scoped)
580
- ).pipe(Effect.forkScoped);
581
-
582
- // Client: pipe a stream through the connection
583
- const port = (server.address as SocketServer.TcpAddress).port;
584
- const channel = NodeSocket.makeNetChannel({ port });
585
-
586
- return yield* Stream.make('Hello', 'World').pipe(
587
- Stream.encodeText,
588
- Stream.pipeThroughChannel(channel),
589
- Stream.decodeText(),
590
- Stream.mkString
591
- );
592
- }).pipe(Effect.scoped);
593
- ```
114
+ `SocketError.reason` is one of `SocketOpenError`, `SocketReadError`,
115
+ `SocketWriteError`, `SocketCloseError`, or `SocketUpgradeError`. Narrow with
116
+ `Effect.catchReason`, schema matching, or the supplied guards. Open failures
117
+ distinguish `Unknown` and `Timeout`; close reasons carry `code` and `closeReason`.
118
+ Handle a normal close at the owning protocol boundary when completion is intended.
119
+ Close-code predicate options and `SocketCloseError.filterClean` are removed.
594
120
 
595
- ### Typed NDJSON request/response client (DevToolsClient pattern)
121
+ Reconnect by retrying the **scoped reader acquisition and consumption**, not just
122
+ the already-acquired pull. This releases each connection before the next attempt.
123
+ Retry only connection failures classified as transient for the protocol; replayed
124
+ handshakes and commands must be idempotent. Accepted server connections cannot
125
+ reconnect. Use a new socket value for an independent concurrent client connection.
596
126
 
127
+ <!-- typecheck -->
597
128
  ```ts
598
- import { Effect, Queue, Schedule, Schema, Stream } from 'effect';
599
- import { Ndjson } from 'effect/unstable/encoding';
129
+ import { Duration, Effect, Schedule } from 'effect';
600
130
  import { Socket } from 'effect/unstable/socket';
601
- import { NodeSocket } from '@effect/platform-node';
602
131
 
603
- const Request = Schema.Struct({ id: Schema.Number, op: Schema.String });
604
- const Response = Schema.Struct({ id: Schema.Number, result: Schema.String });
605
-
606
- const makeClient = Effect.gen(function*() {
607
- const socket = yield* NodeSocket.makeNet({ port: 9000, openTimeout: '3 seconds' });
608
- const requests = yield* Queue.unbounded<typeof Request.Type>();
609
-
610
- // One fiber owns the connection: writes from the queue, reads responses
611
- yield* Stream.fromQueue(requests).pipe(
612
- Stream.pipeThroughChannel(
613
- Ndjson.duplexSchema(Socket.toChannel(socket), {
614
- inputSchema: Request,
615
- outputSchema: Response
616
- })
617
- ),
618
- Stream.runForEach((response) => Effect.log('response', response)),
619
- Effect.retry(
620
- Schedule.min([
621
- Schedule.exponential('500 millis', 1.5),
622
- Schedule.spaced('5 seconds')
623
- ])
624
- ),
625
- Effect.forkScoped
132
+ const consume = Effect.fn('Socket.consume')(function* (socket: Socket.Socket) {
133
+ const pull = yield* Socket.readerString(socket);
134
+ const writer = yield* socket.writer;
135
+ yield* writer.write('subscribe'); // protocol-specific idempotent handshake
136
+ yield* pull.pipe(
137
+ Effect.flatMap((batch) => Effect.forEach(batch, Effect.logInfo)),
138
+ Effect.forever
626
139
  );
140
+ }, Effect.scoped);
627
141
 
628
- return {
629
- send: (request: typeof Request.Type) => Queue.offer(requests, request)
630
- };
631
- });
632
- ```
633
-
634
- ### Per-connection NDJSON server
635
-
636
- ```ts
637
- const NdjsonServer = Effect.gen(function*() {
638
- const server = yield* NodeSocketServer.make({ port: 9000 });
639
-
640
- yield* server.run(
641
- Effect.fnUntraced(function*(socket) {
642
- const responses = yield* Queue.unbounded<typeof Response.Type>();
643
- yield* Stream.fromQueue(responses).pipe(
644
- Stream.pipeThroughChannel(
645
- Ndjson.duplexSchema(Socket.toChannel(socket), {
646
- inputSchema: Response, // we write responses
647
- outputSchema: Request // we read requests
648
- })
649
- ),
650
- Stream.runForEach((request) =>
651
- Queue.offer(responses, { id: request.id, result: request.op.toUpperCase() })
652
- )
653
- );
654
- }, Effect.scoped)
655
- ).pipe(Effect.forkScoped);
656
- });
657
- ```
658
-
659
- ### Reconnecting WebSocket consumer
660
-
661
- ```ts
662
- const tradeFeed = Effect.gen(function*() {
663
- const socket = yield* Socket.makeWebSocket(
664
- Effect.gen(function*() {
665
- const token = yield* freshAuthToken; // re-evaluated per reconnect
666
- return `wss://example.com/feed?token=${token}`;
667
- }),
668
- { closeCodeIsError: (code) => code !== 1000, openTimeout: '5 seconds' }
669
- );
670
- const write = yield* socket.writer;
671
-
672
- yield* socket.runString((text) => handleTrade(JSON.parse(text)), {
673
- onOpen: Effect.orDie(write(JSON.stringify({ type: 'subscribe', channel: 'trades' })))
674
- }).pipe(
675
- Effect.retry({
676
- schedule: Schedule.min([
677
- Schedule.exponential('1 second'),
678
- Schedule.spaced('30 seconds')
679
- ])
680
- })
681
- );
682
- }).pipe(
683
- Effect.scoped,
684
- Effect.provide(NodeSocket.layerWebSocketConstructor)
142
+ const reconnect = (socket: Socket.Socket) => consume(socket).pipe(
143
+ Effect.retry({
144
+ schedule: Schedule.exponential(Duration.millis(500)).pipe(Schedule.upTo({ times: 5 })),
145
+ while: (error) => error.reason._tag === 'SocketCloseError' && error.reason.code !== 1000
146
+ })
685
147
  );
686
148
  ```
687
149
 
688
- ## Common Mistakes
689
-
690
- 1. **Importing from `@effect/platform/Socket`** — gone in v4. Use `import { Socket, SocketServer } from 'effect/unstable/socket'`; platform pieces come from `@effect/platform-node` (`NodeSocket`, `NodeSocketServer`) or `@effect/platform-bun`.
691
- 2. **Assuming a clean WebSocket close (code 1000) completes `run` successfully** — by default *every* close code fails the run with `SocketError`/`SocketCloseError` (`defaultCloseCodeIsError` is `() => true`). Pass `closeCodeIsError: (code) => code !== 1000`, or catch with `SocketCloseError.filterClean`.
692
- 3. **Calling `write` without a concurrent `run`** — writes latch until a run loop has opened the connection. A lone `socket.writer` + `write(...)` suspends forever; fork `socket.run(...)` first (or alongside).
693
- 4. **Expecting effectful handlers to run in frame order** — handler effects are forked into a `FiberSet` and run concurrently. For ordered processing return `void` and `Queue.offerUnsafe` synchronously, or use the channel/stream adapters which do this for you.
694
- 5. **Forgetting `Effect.scoped` around `socket.writer`** — `writer` requires `Scope`. In `SocketServer.run` handlers this is fatal at runtime, because the server strips `Scope` from the captured handler context: wrap the handler with `Effect.fnUntraced(function*(socket) {...}, Effect.scoped)`.
695
- 6. **Closing the writer scope too early on TCP** — releasing the writer's scope calls `socket.end()` (half-close, peer sees EOF). Keep the writer scope open for the connection's lifetime unless you intend to signal end-of-output.
696
- 7. **Not forking `server.run(handler)`** — it returns `Effect<never, SocketServerError, R>` and never completes. `yield*`-ing it inline blocks the rest of your setup; use `Effect.forkScoped`.
697
- 8. **Expecting connection-handler failures to fail the server** — `SocketServer` handler errors are logged ("Unhandled error in SocketServer") and dropped. Handle/report errors inside the handler if you need them.
698
- 9. **Decoding TCP bytes per-frame with `runString`/`toChannelString`** — these `TextDecoder.decode` each chunk independently, corrupting multi-byte UTF-8 characters split across packets. For byte streams use `Socket.toChannel` + `Stream.decodeText` (streaming decode), and `Ndjson.duplexSchema` rather than `duplexSchemaString`. String variants are fine for WebSocket text frames, which arrive whole.
699
- 10. **Treating a `Socket` as a live connection** — it's a recipe. Each `run` opens a fresh transport; the connection closes when the run ends. Never run two run loops concurrently on one `Socket` value (unenforced — each opens its own transport and writes silently target the last-opened one). Reconnecting = `Effect.retry` on the run loop, not on the constructor.
700
- 11. **Forgetting the `WebSocketConstructor` layer** — `Socket.makeWebSocket`/`layerWebSocket`/`makeWebSocketChannel` require it. Provide `Socket.layerWebSocketConstructorGlobal` (browser), `NodeSocket.layerWebSocketConstructor` (Node), or `BunSocket.layerWebSocketConstructor` — or use the platform `layerWebSocket` convenience which bundles it.
701
- 12. **Relying on `openTimeout` for TCP without setting it** — `makeNet`/`fromDuplex` have *no* default open timeout (only WebSockets default to 10s). Pass `openTimeout` or a hung connect waits forever. Zero and `Duration.zero` are real immediate timeouts, not aliases for "disabled."
702
- 13. **Writing a `Socket.CloseEvent` to a TCP socket expecting a graceful close** — *any* CloseEvent destroys the connection (discarding queued writes) and fails the *local* run: `SocketCloseError` code 1000 for code 1000, `SocketReadError` for codes above 1000. The peer sees an ordinary EOF or ECONNRESET — close codes never cross the wire on raw TCP. Releasing the writer scope (`socket.end()`, half-close FIN) is the only graceful close.
703
- 14. **Reading `server.address` before the server exists** — the address is captured when `NodeSocketServer.make` completes (after `listen`). With `{ port: 0 }`, read the real port from `server.address as SocketServer.TcpAddress`; there is no separate "address ready" event to await.
150
+ `new Socket.CloseEvent(1000, 'done')` requests a close through `writer.write`.
151
+ WebSocket close codes are protocol data; TCP does not transmit them. Prefer
152
+ writer-scope release for a graceful TCP half-close rather than a close event.
153
+
154
+ ## Streams, channels, and framing
155
+
156
+ `Socket.toStream(socket)` provides read-only byte consumption.
157
+ `Socket.toChannel(socket)` connects outgoing upstream frames to incoming byte
158
+ batches; `toChannelString` converts incoming frames to strings. These adapters
159
+ fail on close. A finite input alone does not imply successful completion of a
160
+ bidirectional protocol: bound expected responses or explicitly handle closure.
161
+
162
+ For TCP message boundaries use `Ndjson.duplexSchema(Socket.toChannel(socket),
163
+ { inputSchema, outputSchema })`, or `SchemaBinary.duplex` with compatible schemas.
164
+ WebSocket text frames may use `Ndjson.duplexSchemaString` when NDJSON is the
165
+ agreed protocol. Decode unknown frames with schemas before domain processing.
166
+ MessagePack adapters are removed; SchemaBinary is a different wire format and
167
+ requires coordinated peer and persisted-data migration.
168
+
169
+ `Socket.make({ reader, writer })` is the custom-adapter boundary. Its reader must
170
+ fail suspended pulls when the acquisition scope closes; otherwise downstream
171
+ channel shutdown hangs. Prefer built-in adapters for platform transports.
172
+
173
+ ## Servers and bound addresses
174
+
175
+ `NodeSocketServer.make({ port: 0 })` / `.layer(...)` bind TCP or Unix listeners.
176
+ Use `makeTls` / `layerTls` for TLS and `makeWebSocket` / `layerWebSocket` for
177
+ WebSockets. Bun exposes the shared API under `BunSocketServer`.
178
+
179
+ Fork `server.run(handler)` into the server scope; it does not complete normally.
180
+ Each accepted socket pauses until its reader takes ownership. Scope each handler's
181
+ reader/writer with `Effect.scoped`. Handler errors are reported by the server
182
+ rather than becoming accept-loop failures; add application-level supervision
183
+ where they must stop the service. HTTP upgrades yield the same Socket interface;
184
+ see `effect-http-server`.
185
+
186
+ Server addresses are `NetAddress.SocketAddress` from `effect/unstable/net`.
187
+ Narrow to `InetAddress` before reading `port`; format its IP with `formatIp`,
188
+ or use `formatHost` when passing a host and port separately (preserves IPv6 scopes).
189
+ Unix path addresses expose `path`. Use `inetAddressFromHostString` to parse numeric
190
+ hosts and `scopeIdsFromInterfaces` with supplied interface data for named zones.
191
+ URL helpers bracket IPv6 and reject scoped IPv6; do not assemble URLs by casting
192
+ an address to the removed `TcpAddress` type.
193
+
194
+ ## Higher-level transports and tests
195
+
196
+ - RPC: `RpcClient.layerProtocolSocket` and `RpcServer.layerProtocolSocketServer`;
197
+ provide matching `RpcSerialization` layers and the socket/server layer.
198
+ - Cluster: `NodeClusterSocket` / `BunClusterSocket`; SchemaBinary is the default,
199
+ NDJSON is explicitly selectable.
200
+ - In-memory tests: `Socket.fromTransformStream` wraps a readable/writable pair;
201
+ end-of-input is still a close failure. Bound the read or handle the close reason.
202
+ - Integration tests: use a loopback listener on port 0, scope both sides, assert
203
+ message order, close/error propagation, and interruption cleanup.
204
+
205
+ Completion: every connection has one reader owner, writes have an active reader,
206
+ frames are decoded, retries reacquire resources, and shutdown releases both sides.