effect-socket
/effect-socket
Buduje transporty TCP, Unix, TLS, WebSocket i UDP na czytnikach i zapisywaczach Effect Socket, z ramkowaniem i ponownym łączeniem.
- Autor
- mpsuesser ↗
- Repozytorium
- opencode-effect-enforcer ↗
- Licencja
- MIT
- Ostatnia zmiana repozytorium
- 02.11.2026
Ocena i bezpieczeństwo
- Pliki w paczce
- 1
- Skrypty
- 0
- Komentarze HTML
- 5
- Adresy URL w SKILL.md
- 0
Brak sygnałów. Repozytorium zmienione 02.11.2026, bez odwołań do wycofanych modeli i API.
MIT, otwarta: można używać i zmieniać, z zachowaniem informacji o autorze.
- SKILL.md zawiera komentarze HTML (5). Nie widać ich w podglądzie Markdown, a model je czyta.
Co robi i kiedy
Buduje transporty TCP, Unix, TLS, WebSocket i UDP na czytnikach i zapisywaczach Effect Socket, z ramkowaniem i ponownym łączeniem.
Oryginalny opis (EN)
Build pull-based TCP, Unix, TLS, WebSocket, and UDP transports with Effect Socket readers and writers. Use for bidirectional connections, datagrams, framing, reconnects, socket servers, or RPC socket transports.
Jak działa
- Source reference
- Connection ownership
- WebSocket clients
- TCP, Unix, and TLS
- Errors, close, and reconnect
- Streams, channels, and framing
- Servers and bound addresses
- UDP datagrams (experimental)
- Higher-level transports and tests
Podgląd SKILL.md
---
name: effect-socket
description: Build pull-based TCP, Unix, TLS, WebSocket, and UDP transports with Effect Socket readers and writers. Use for bidirectional connections, datagrams, framing, reconnects, socket servers, or RPC socket transports.
---
# Effect Socket
## Source reference
Read `packages/effect/src/socket/{Socket,SocketServer,DatagramSocket}.ts` at
`effect@4.1.0` in the Effect reference. Platform adapters live in
`packages/platform/node-shared/src/{NodeSocket,NodeSocketServer}.ts`; Node and
Bun expose these as `NodeSocket` / `BunSocket` and their server counterparts.
These APIs remain `@stability unstable` and may break in minor releases despite
the stable package version. Keep Effect-family packages on the same release.
Load `effect-scope` and `effect-fiber` for lifecycle ownership,
`effect-stream` for framing, and `effect-scheduling` for reconnect policy.
## Connection ownership
`Socket.Socket` is both the service tag and the interface. A client socket is
a connection recipe. Acquiring `socket.reader` establishes a connection and
returns a `Reader` with `pull` and `upgrade`. Its acquisition scope owns the
connection. A pull yields a non-empty batch of `string | Uint8Array` frames.
Every termination, including clean close, fails with `SocketError`.
`socket.writer` is an infallible scoped acquisition of a `Writer` object:
- `writer.write(frameOrCloseEvent)` sends one frame or a close request.
- `writer.writeAll(nonEmptyBatch)` batches outgoing frames with native backpressure.
- Both write operations may fail with `SocketError`.
- Writes while disconnected wait for a reader to establish the connection.
- Releasing the writer scope half-closes transports that support it.
One owner acquires the reader. Process each batch sequentially by default;
bounded concurrency is a deliberate application decision. Place a handshake
after reader acquisition and before the first pull so it runs per connection.
There are no callback-driven `run`, `runString`, or `runRaw` methods on a socket.
<!-- typecheck -->
```ts
import { Effect } from 'effect';
import { Socket } from 'effect/socket';
const echo = Effect.fn('Socket.echo')(function* (socket: Socket.Socket) {
const { pull } = yield* socket.reader;
const writer = yield* socket.writer;
yield* pull.pipe(Effect.flatMap(writer.writeAll), Effect.forever);
}, Effect.scoped);
```
`Socket.readerBytes(socket)` and `Socket.readerString(socket, encoding?)`
acquire a converted pull directly, rather than returning a Reader object.
The string adapter owns one **streaming decoder per reader acquisition**, so
binary UTF-8 can span frames and pulls. It may emit empty strings while waiting
for the rest of a character. A string frame flushes pending binary text (using a
replacement character for an incomplete sequence) before its own text. A close
with code `1000` flushes any tail once, then the next pull propagates close;
other failures do not flush. `toChannelString` inherits this behavior. For
per-message binary WebSocket decoding, use `readerBytes` and decode frames
separately instead.
## WebSocket clients
<!-- typecheck -->
```ts
import { Duration, Effect } from 'effect';
import { Socket } from 'effect/socket';
const receive = Effect.gen(function* () {
const socket = yield* Socket.makeWebSocket('wss://example.com/feed', {
openTimeout: Duration.seconds(5),
highWaterMark: 64 * 1024,
protocols: ['v1']
});
const pull = yield* Socket.readerString(socket);
const writer = yield* socket.writer;
yield* writer.write('subscribe');
yield* pull.pipe(
Effect.flatMap((batch) => Effect.forEach(batch, Effect.logInfo)),
Effect.forever
);
}).pipe(Effect.scoped, Effect.provide(Socket.layerWebSocketConstructorGlobal));
```
Use `NodeSocket.layerWebSocketConstructor` or
`NodeWS.layerWebSocketConstructor` on Node (import `NodeWS` from
`@effect/platform-node` or `@effect/platform-node-shared`) and
`BunSocket.layerWebSocketConstructor` on Bun. Platform `layerWebSocket(url, options)`
conveniences supply the constructor. Core `Socket.layerWebSocket` requires it.
Constructors accept compatible browser, Bun, and Node implementations without
casts; opening-handshake headers are available where the platform supports them.
Node/Bun WebSocket constructors, layers, and channel helpers accept headers
alongside protocols. The global/browser constructor rejects an options object
containing headers: browsers cannot set arbitrary opening-handshake headers.
An effectful URL is reevaluated on each connection. `fromWebSocket(acquire, options)`
wraps a scoped transport acquisition. Pausable transports pause at `highWaterMark`
(default 64 KiB) and resume when drained. Browser WebSockets cannot pause; their
default buffer is unbounded. Set `highWaterMark` explicitly to fail with
`SocketReadError` when that byte bound is exceeded. Text-frame
buffering counts UTF-8 bytes, not string length.
Node's default constructor now uses global `WebSocket`, including connections
with handshake headers. It cannot pause reads: buffering is unbounded unless a
`highWaterMark` is set, which fails with `SocketReadError` on overflow. Connection
errors are generic `ErrorEvent`s, custom `Host` is ignored, outgoing compression
is disabled, and connections use undici's global dispatcher. Choose
`NodeWS.layerWebSocketConstructor` for the previous `ws` behavior, including
`ws`-specific methods. `NodeSocket.NodeWS` / `BunSocket.NodeWS` were removed;
use `NodeWS.WebSocket` on Node and import `ws` directly on Bun.
Node platform packages now require Node.js 22.19.0 or newer.
Node stream/socket `chunkSize` now bounds chunks per pull (default 4096).
Use the NodeStream `readSize` option for byte read requests; `chunkSize: Infinity`
permits unbounded batching.
## TCP, Unix, and TLS
```ts
import { NodeSocket } from '@effect/platform-node';
const tcp = NodeSocket.makeNet({ host: 'localhost', port: 9000, openTimeout: '3 seconds' });
const unix = NodeSocket.makeNet({ path: '/tmp/app.sock' });
const tls = NodeSocket.makeTls({ host: 'example.com', port: 443, servername: 'example.com' });
```
`makeNet` / `makeTls` return recipes; connection failures occur during reader
acquisition. `layerNet`, `layerTls`, `makeNetChannel`, and `makeTlsChannel` provide
service/channel equivalents. Prefer `makeTls` over hand-wrapping `tls.connect`.
`fromDuplex` remains the adapter for an existing scoped Node duplex source.
Omitted TCP `openTimeout` is unbounded; zero means an immediate timeout.
For STARTTLS acquire the reader, complete the protocol handshake, then call
`reader.upgrade(options)`. TLS keys and passphrases are redacted. Server-role
upgrades require both key and certificate; unsupported transports and invalid
credentials fail with `SocketUpgradeError` inside `SocketError`.
Client-side `reader.upgrade({ servername: 'example.com' })` supports SNI and
certificate-hostname verification; `servername` is ignored for server-role
upgrades. Node duplex readers retain already-consumed bytes across interrupted
pulls and ordinary EOF; closing the acquisition scope discards retained data.
## Errors, close, and reconnect
`SocketError.reason` is one of `SocketOpenError`, `SocketReadError`,
`SocketWriteError`, `SocketCloseError`, or `SocketUpgradeError`. Narrow with
`Effect.catchReason`, schema matching, or the supplied guards. Open failures
distinguish `Unknown` and `Timeout`; close reasons carry `code` and `closeReason`.
Handle a normal close at the owning protocol boundary when completion is intended.
Close-code predicate options and `SocketCloseError.filterClean` are removed.
Reconnect by retrying the **scoped reader acquisition and consumption**, not just
the already-acquired pull. This releases each connection before the next attempt.
Retry only connection failures classified as transient for the protocol; replayed
handshakes and commands must be idempotent. Accepted server connections cannot
reconnect. Use a new socket value for an independent concurrent client connection.
<!-- typecheck -->
```ts
import { Duration, Effect, Schedule } from 'effect';
import { Socket } from 'effect/socket';
const consume = Effect.fn('Socket.consume')(function* (socket: Socket.Socket) {
const pull = yield* Socket.readerString(socket);
const writer = yield* socket.writer;
yield* writer.write('subscribe'); // protocol-specific idempotent handshake
yield* pull.pipe(
Effect.flatMap((batch) => Effect.forEach(batch, Effect.logInfo)),
Effect.forever
);
}, Effect.scoped);
const reconnect = (socket: Socket.Socket) => consume(socket).pipe(
Effect.retry({
schedule: Schedule.exponential(Duration.millis(500)).pipe(Schedule.upTo({ times: 5 })),
while: (error) => error.reason._tag === 'SocketCloseError' && error.reason.code !== 1000
})
);
```
`new Socket.CloseEvent(1000, 'done')` requests a close through `writer.write`.
WebSocket close codes are protocol data; TCP does not transmit them. Prefer
writer-scope release for a graceful TCP half-close rather than a close event.
Node/Bun HTTP upgrade adapters choose server close codes from the owning scope
exit: `1000` for success, `1001` for interruption-only exits, and `1011` for other
failures. An explicit code already sent is preserved. HTTP response conversion
and observation middleware retain the handler failure for request cleanup. This
is server-upgrade behavior, not a configurable clean-close predicate on sockets.
## Streams, channels, and framing
`Socket.toStream(socket)` provides read-only byte consumption.
`Socket.toChannel(socket)` connects outgoing upstream frames to incoming byte
batches; `toChannelString` converts incoming frames to strings. These adapters
fail on close. A finite input alone does not imply successful completion of a
bidirectional protocol: bound expected responses or explicitly handle closure.
For TCP message boundaries use `Ndjson.duplexSchema(Socket.toChannel(socket),
{ inputSchema, outputSchema })`, or `SchemaBinary.duplex` with compatible schemas.
WebSocket text frames may use `Ndjson.duplexSchemaString` when NDJSON is the
agreed protocol. Decode unknown frames with schemas before domain processing.
MessagePack adapters are removed; SchemaBinary is a different wire format and
requires coordinated peer and persisted-data migration.
`Socket.make({ reader, writer })` is the custom-adapter boundary. Its reader must
fail suspended pulls when the acquisition scope closes; otherwise downstream
channel shutdown hangs. Prefer built-in adapters for platform transports.
## Servers and bound addresses
`NodeSocketServer.make({ port: 0 })` / `.layer(...)` bind TCP or Unix listeners.
Use `makeTls` / `layerTls` for TLS and `makeWebSocket` / `layerWebSocket` for
WebSockets. Bun exposes the shared API under `BunSocketServer`.
Fork `server.run(handler)` into the server scope; it does not complete normally.
Each accepted socket pauses until its reader takes ownership. Scope each handler's
reader/writer with `Effect.scoped`. Handler errors are reported by the server
rather than becoming accept-loop failures; add application-level supervision
where they must stop the service. HTTP upgrades yield the same Socket interface;
see `effect-http-server`.
Node HTTP/1.1 clients and servers also support **raw upgrades** in 4.1.0:
`HttpClientRequest.upgrade` requests one and `HttpClientResponse.upgrade` yields
a Socket whose reader scope owns the connection. `HttpApiSchema.Upgrade`
clients return `{ headers, socket }`.
Fetch/XHR reject raw upgrades before sending; Bun/Deno servers support only
WebSocket upgrades. Do not confuse raw upgrades with WebSocket framing or
assume they work through an arbitrary HttpClient layer.
Server addresses are `NetAddress.SocketAddress` from `effect/net`.
Narrow to `InetAddress` before reading `port`; format its IP with `formatIp`,
or use `formatHost` when passing a host and port separately (preserves IPv6 scopes).
Unix path addresses expose `path`. Use `inetAddressFromHostString` to parse numeric
hosts and `scopeIdsFromInterfaces` with supplied interface data for named zones.
URL helpers bracket IPv6 and reject scoped IPv6; do not assemble URLs by casting
an address to the removed `TcpAddress` type.
Treat IP values as opaque: IPv4 now stores one unsigned 32-bit number and IPv6
four words internally, not public `.bytes`. Use `ipv4ToOctets`, `ipv6ToOctets`,
`ipv6ToSegments`, and formatting/construction helpers. Do not persist internal
fields or hash values as a wire representation.
`NetAddress.Family<A>` selects the IPv4/IPv6 address family, `Inet<A>` the matching
port-carrying address, and `MulticastInterface<A>` an IPv4 interface address or
IPv6 numeric interface index (`ipv4Unspecified`/`0` choose the OS default).
Classification predicates such as `isMulticast`, `isUnicast`, `isLoopback`, and
`isLinkLocal` preserve the input family while refining it to the corresponding
branded type. Use the named schemas in `effect/Schema` for boundary validation
rather than asserting these refinements.
<!-- typecheck -->
```ts
import { Effect } from 'effect';
import * as Schema from 'effect/Schema';
import { NetAddress } from 'effect/net';
const parseMulticast = Schema.decodeUnknownEffect(Schema.IpMulticastAddressFromString);
const displayMulticast = Effect.fn('Socket.displayMulticast')(function* (input: unknown) {
const address = yield* parseMulticast(input);
return NetAddress.formatIp(address);
});
```
For native adapters, `formatNativeHost(address, scopeIds, platform?)` and
`formatMulticastInterface(selector, scopeIds, platform?)` use an explicitly
supplied interface-name-to-index map. Non-Windows output prefers a matching
interface name; Windows uses numeric zones. `toCanonical` accepts both IP and
internet addresses: mapped IPv6 becomes IPv4 while retaining its port; other
values retain identity and IPv6 scope metadata.
## UDP datagrams (experimental)
`DatagramSocket` from `effect/socket` is separate from stream `Socket`. Its
experimental Node/Bun/Deno adapters expose `make` and `layer`; Node also exposes
`fromSocket` for an existing scoped native socket acquisition.
Acquire a reader even for a **send-only client**: it opens/binds the native
socket, and writes otherwise wait. Only one reader acquisition is open at a
time; another waits interruptibly. The reader's scope owns the handle.
<!-- typecheck -->
```ts
import { NodeDatagramSocket } from '@effect/platform-node-shared';
import { Effect } from 'effect';
const echoOneBatch = Effect.gen(function* () {
const socket = yield* NodeDatagramSocket.make({ bind: { port: 0 } });
const reader = yield* socket.reader;
const writer = yield* socket.writer;
const packets = yield* reader.pull;
yield* Effect.forEach(packets, (received) =>
writer.write({ payload: received.payload, address: received }),
{ concurrency: 1 }
);
}).pipe(Effect.scoped);
```
- `reader.pull` yields non-empty packet batches. Each packet's `Uint8Array`
payload is safe to retain; sender/local addresses are parsed lazily.
- A platform-received datagram can be the outgoing `address`, as above, without
parsing its sender. Hand-built/custom datagrams must use `received.address`.
- Receive capacity defaults to 1024 **packets**, not bytes (~64 MiB worst case).
`receiveBuffer: { capacity, strategy: 'dropping' | 'sliding' }` drops incoming
packets by default, or drops the oldest with `sliding`. `reader.dropped()`
counts only this buffer overflow, not network/kernel loss or scope-close loss.
- A terminal read error is sticky: drain queued packets, then pulls fail until
scope close. Scope close discards queued packets and fails pending pulls.
- `writer.writeAll` fails on the first send error without resending; partial
delivery is unspecified. A successful send is not delivery acknowledgement.
There is no built-in write timeout; bound writes explicitly when necessary.
- Node's `peer` is a default destination, not sender filtering; `connect`
applies native sender filtering and cannot be combined with `peer`. An
explicit InetAddress destination is rejected on connected sockets.
- Errors wrap Open/Read/Write/Closed/Unsupported reasons in
`DatagramSocketError`. Bun may omit the failed batch packet's address.
`reader.joinMulticast` is scoped; ingress selection is separate from sending
`multicast.interface`. Source-specific multicast is unsupported on Deno.
Do not retry sends blindly: UDP does not provide exactly-once delivery. A new
reader acquisition opens a new socket; scope retry around acquisition and use
protocol-level identifiers/deduplication if a request might be sent again.
## Higher-level transports and tests
- RPC: `RpcClient.layerProtocolSocket` and `RpcServer.layerProtocolSocketServer`;
provide matching `RpcSerialization` layers and the socket/server layer.
Missed pongs are read failures that fail in-flight calls even with transient
open-error retries enabled; reconnect does not replay those calls.
`pingInterval` and `pingTimeout` default to 5 seconds; **any decoded frame**
counts as liveness, not only a pong. Optional `onPingTimeout` runs only when
dropping an open connection, before disconnect notification/in-flight failures;
callback defects are logged and ignored. Delayed ping ticks do not by
themselves disconnect a connection that has received recent traffic.
- Cluster: `NodeClusterSocket` / `BunClusterSocket`; SchemaBinary is the default,
NDJSON is explicitly selectable.
- In-memory tests: `Socket.fromTransformStream` wraps a readable/writable pair;
end-of-input is still a close failure. Bound the read or handle the close reason.
- Integration tests: use a loopback listener on port 0, scope both sides, assert
message order, close/error propagation, and interruption cleanup.
Completion: every connection has one reader owner, writes have an active reader,
frames are decoded, retries reacquire resources, and shutdown releases both sides.
Wersje
| Repozytorium | Wynik | Licencja |
|---|---|---|
| Ta strona · mpsuesser/opencode-effect-enforcer | 100 | MIT |
| mpsuesser/opencode-effect-enforcer | 100 | MIT |
Migawka bazy z 13.11.2026 · wersja paczki 73bb222919e5 (skrót SHA-256 całej paczki)
Pobrania w ostatnich 30 dniach: 0 · ranking
Zgłoś problem z tym skillem: szkodliwa treść, błędny opis albo prośba autora o usunięcie.
Podobne skille
- 100websocket-engineer
Projektuje komunikację dwukierunkową w czasie rzeczywistym przez WebSockety i Socket.IO, także na dużą skalę. - 100socket-io
Opisuje komunikację dwukierunkową w czasie rzeczywistym w Socket.IO: pokoje, przestrzenie nazw, potwierdzenia, middleware, skalowanie z adapterem Redis i typy TypeScript. - 100dotnetty
Wspiera pracę z DotNetty, asynchronicznym frameworkiem sieciowym: serwery i klienci TCP i UDP, własne protokoły binarne, potoki kanałów w stylu Netty, handlery kodeków i połączenia TLS. - 100socketio
Pomaga budować w Socket.IO komunikację w czasie rzeczywistym dla Node.js: czat, powiadomienia na żywo, panele, wskaźniki obecności, współdzielenie edycji i pub/sub, z uwierzytelnianiem połączeń i skalowaniem. - 100react-websocket
Opisuje integrację WebSocket w aplikacjach React: własne hooki, providery kontekstu, Socket.IO i synchronizacja stanu w czasie rzeczywistym z TanStack Query. - 100websocket-engineer
Projektuje systemy czasu rzeczywistego na WebSockets i Socket.IO: dwukierunkowe wiadomości, skalowanie przez Redis, obecność i pokoje.