Sockets
Decorator-driven WebSocket, TCP, and UDP for network peers, with a WebCrypto secure channel and first-class text vs binary frames.
Features
Decorator API:
@SocketGateway,@OnConnect,@OnMessage,@OnClose,@OnError— same DI style as@Controllerand@EventBridge.Frame kind: WebSocket text (opcode 1) vs binary (opcode 2) preserved end-to-end; never silently coerced.
Secure channel: Ephemeral ECDH P-256, HKDF-SHA-256, mutual confirmation, AES-256-GCM (kind authenticated in AAD).
Modes:
secure(default) or explicitplain.Node primitives: WebSocket (
node:http+ws), TCP (node:net), UDP (node:dgram) — works on Node and Bun via Node compatibility (no Bun.serve / Bun.listen).Cloudflare:
@di-framework/socket/workersfor Workers and hibernatable Durable Objects.GraphQL:
@di-framework/socket/graphql—graphql-transport-wshelper for subscriptions.
Installation
Decorators need TypeScript 5 and experimentalDecorators. Peer dependency: @di-framework/core.
Not the same as core events
Core events |
|
| |
|---|---|---|---|
What | In-process bus | Bus ↔ Kafka / NATS | Network I/O |
Where bytes go | Same process | Brokers | Peers on the network |
API |
|
|
|
Security | N/A | Broker ACLs | Handshake, AEAD, frame kind |
Optional composition: a socket handler may emit onto the container bus so existing @Subscribers react — same idea as inbound broker routes.
Decorator API (primary)
@SocketGateway options
Option | Meaning |
|---|---|
| Built-in listener via Node primitives ( |
| Custom factory (e.g. Cloudflare Workers) |
|
|
| Listen on resolve (default |
| DI lifecycle |
| Max payload size (adapter-dependent) |
Manual control: await gateway.$startGateway()/$stopGateway(), or startSocketGateways()/stopSocketGateways().
Lifecycle handlers
Decorator | When |
|---|---|
| After accept (and after secure handshake when mode is |
| Every application frame ( |
| Filter by frame kind |
| Filter JSON |
| Connection closed |
| Handler threw |
Text vs binary frames
Frame kind is first-class. Do not TextDecoder binary payloads or force every message through JSON unless that is the protocol.
Send | Wire |
|---|---|
| text (WebSocket opcode 1) |
| binary (WebSocket opcode 2) |
| as |
Secure channel
Handshake: text JSON
Sealed application data: binary frames (kind bound in AEAD AAD)
TCP/UDP: kind byte in length-prefix / UDP envelope header
Security model
Property | Mechanism |
|---|---|
Forward secrecy | Ephemeral ECDH P-256 per session |
Key separation | HKDF-SHA-256 purpose labels |
MITM detection | Mutual key confirmation MACs |
Confidentiality + integrity | AES-256-GCM, 96-bit IV |
Replay (within session) | Monotonic counter in AAD |
Modes |
|
Auth identity ≠ channel keys. Use @di-framework/auth for who (sessions, JWT, WebAuthn). This package owns wire confidentiality. Compose both: authenticate at upgrade / connection_init, re-check long-lived tokens with auth’s assertNotExpired.
TLS is complementary. Prefer wss:/ TLS on the public internet in addition to the app-level channel when using TCP/UDP or multi-hop paths.
Limitations (v0.1): no mid-session rekey; replay protection is per-session; Node/Deno listeners are custom-listen only.
TCP and UDP (Node primitives)
UDP is connectionless: clients send a knock datagram so the server can open a per-peer session. Prefer @SocketGateway({ server: { protocol: 'tcp' | 'udp' } }) when you want DI handlers.
These adapters use node:net and node:dgram only — the same code path on Node and Bun.
GraphQL subscriptions (graphql-transport-ws)
Protocol is text JSON (GraphiQL / graphql-ws clients). Pair with auth via connectionParamsToHeaders or @di-framework/auth ’s authenticateUpgrade/requestFromConnectionParams.
The GraphQL example uses this helper instead of a hand-rolled WebSocket loop.
Cloudflare Workers and Durable Objects
Import @di-framework/socket/workers.
Non-hibernating Worker
Uses WebSocketPair + server.accept() + event listeners. Do not use this path if you need Durable Object hibernation billing.
Hibernatable Durable Object
Policy | Behavior |
|---|---|
| No ECDH; frames only; fine with hibernation |
| Snapshot (key + counters) in attachment; restore on wake |
| On wake, close 4001 — client reconnects and handshakes again |
Attachment secrets: rehydrate stores the AES key in serializeAttachment. Convenient but sensitive; for higher assurance keep snapshots in DO storage and only store an id in the attachment. Never log snapshots.
Frame kind on CF: string → text, ArrayBuffer → binary (cfMessageToFrame/sendFrame).
Do not mix ws.accept() with ctx.acceptWebSocket(ws) on the same socket.
Worker front door:
Helpers
Export | Use |
|---|---|
| Map CF messages to |
| Non-hibernating duplex |
| DO: |
| Portable secure session persistence |
Imperative / low-level API
Useful for tests and custom listen factories:
@di-framework/socket/node—createWebSocketServer(node:http+ws), TCP (node:net), UDP (node:dgram)@di-framework/socket/bun— deprecated alias of/nodeSecureSession,SecureHandshakeProvider/SecureHandshakeConsumer,AeadChannelFraming:
encodeLengthPrefix,LengthPrefixFramer, UDP envelope helpers
Prefer @SocketGateway({ server: … }) for process servers. On Cloudflare, prefer the workers helpers (no long-lived port listener).
Capability matrix
Node | Bun | Deno | Workers / DO | |
|---|---|---|---|---|
Decorators + secure channel | yes | yes (Node compat) | custom | via workers hub |
Built-in listener |
| same Node APIs | — |
|
Hibernatable WebSockets | — | — | — |
|
TCP / UDP |
| same | planned | n/a (no server sockets) |
| yes | yes | yes | yes (WS text) |
Related
Events — broker bridge for the in-process bus
GraphQL — schema and
subscribe; sockets own the subscription transportAuthentication — identity; compose with WebSocket
connection_initHTTP Router — request/response; upgrade path is socket’s job