effect-socket
/effect-socket
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.
- Author
- mpsuesser ↗
- Repository
- opencode-effect-enforcer ↗
- Licence
- MIT
- Last repository change
- 26.10.2026
Assessment and security
- Files in this package
- 1
- Scripts
- 0
- HTML comments
- 5
- URLs in SKILL.md
- 0
No signals detected. Repository changed on 26.10.2026, without references to retired models or APIs.
MIT, open: use and modify while retaining author attribution.
- SKILL.md zawiera komentarze HTML (5). Nie widać ich w podglądzie Markdown, a model je czyta.
What it does and when to use it
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.
Original author 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.
How it works
- 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
SKILL.md preview
---
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.0.6` 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
`NodeSocket.layerWebSocketConstructorWS` on Node 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.
## 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.0.6:
`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.
Versions
| Repository | Score | Licence |
|---|---|---|
| This page · mpsuesser/opencode-effect-enforcer | 100 | MIT |
| mpsuesser/opencode-effect-enforcer | 100 | MIT |
Database snapshot: 13.11.2026 · package version c1a24cf07945 (SHA-256 digest of the entire package)
Downloads in the last 30 days: 0 · ranking
Report a problem with this skill: harmful content, incorrect descriptions or an author’s removal request.
Similar skills
- 100websocket-engineer
Use this agent when implementing real-time bidirectional communication features using WebSockets, Socket.IO, or similar technologies at scale. - 100socket-io
Socket.IO real-time bidirectional communication. Rooms, namespaces, acknowledgments, middleware, scaling with Redis adapter, and TypeScript types. USE WHEN: user mentions "Socket.IO", "socket.io", "real-time chat", "live updates", "bidirectional WebSocket", "rooms", "namespaces"… - 100socketio
Socket.IO is a Node.js library for bidirectional, event-based real-time communication between browsers (or other clients) and a server, with WebSocket transport, HTTP long-polling fallback, automatic reconnection, rooms and namespaces. Use when a user asks to build a chat, live… - 100dotnetty
Guidance for DotNetty event-driven asynchronous network application framework. USE FOR: high-performance TCP/UDP servers and clients, custom binary protocol implementations, Netty-style channel pipelines, event loop groups, codec handlers, TLS/SSL socket connections. DO NOT USE… - 100websocket-engineer
Use when building real-time communication systems with WebSockets or Socket.IO. Invoke for bidirectional messaging, horizontal scaling with Redis, presence tracking, room management. - 100react-websocket
WebSocket integration patterns for React applications. Covers custom hooks, context providers, Socket.IO, and real-time state synchronization with TanStack Query. USE WHEN: user mentions "WebSocket in React", "real-time React", "Socket.IO React", "useWebSocket hook", "live…