Add UDP socket support with platform-agnostic interface
Hi there!
I've been learning more about Effect TS and coded this with Claude. I thought it might be useful so I decided to open a PR.
Summary
Add comprehensive UDP socket support to the Effect platform with proper platform-agnostic design and implementations for Node.js and Bun.
Key Changes
-
Platform-agnostic UDP interface (
@effect/platform/UdpSocket)- Message-oriented
UdpMessagewith sender address information - Direct
send(data, address)andrun(handler)methods matching UDP semantics - Non-Effect
addressproperty for symmetry with other platform interfaces
- Message-oriented
-
Node.js implementation (
@effect/platform-node/NodeUdpSocket)- Full UDP socket support using Node.js
dgrammodule - Proper resource management with
acquireReleasepattern - Comprehensive error handling and graceful cleanup
- Full UDP socket support using Node.js
-
Bun implementation (
@effect/platform-bun/BunUdpSocket)- Leverages Bun's Node.js API compatibility for
dgram - Same interface as Node.js implementation
- Leverages Bun's Node.js API compatibility for
-
Comprehensive test coverage
- Socket creation, message sending/receiving
- Error handling for invalid addresses
- Resource cleanup and double-close scenarios
Design Journey & Architecture Decisions
Initial Approach: SocketServer Integration
Initially attempted to make UDP compatible with the existing SocketServer interface by:
- Adding
UdpAddressto theAddressunion type - Creating virtual
Socketinstances for each UDP message - Treating each datagram as a "connection"
Why this felt wrong:
- Semantic mismatch: UDP is connectionless, but SocketServer assumes persistent connections
- Virtual connections: Creating Socket instances per message violated UDP's stateless nature
- RPC incompatibility: Would break RPC protocols expecting persistent, bidirectional streams
- Forced abstraction: Round peg, square hole - UDP semantics don't match TCP patterns
Final Approach: UDP-Specific Interface
Reverted to a dedicated UDP interface that properly represents UDP semantics:
- Message-oriented:
UdpMessagewith discrete datagrams - Addressing explicit: Each
send()specifies destination - Stateless: No connection lifecycle management
- Platform-agnostic: Same interface works across Node.js, Bun, etc.
Platform Support Matrix
| Platform | TCP Socket | UDP Socket | Notes |
|---|---|---|---|
platform |
✅ Interface | ✅ Interface | Platform-agnostic definitions |
platform-node |
✅ NodeSocket | ✅ NodeUdpSocket | Full raw socket support |
platform-bun |
✅ BunSocket | ✅ BunUdpSocket | Node.js compatible dgram |
platform-browser |
✅ BrowserSocket (WebSockets) | ❌ Not supported | Security limitations |
Usage Example
// Platform-agnostic server
const createEchoServer = Effect.gen(function*() {
const socket = yield* UdpSocket.UdpSocket
yield* socket.run((message) => Effect.gen(function*() {
const response = `Echo: ${new TextDecoder().decode(message.data)}`
yield* socket.send(
new TextEncoder().encode(response),
message.remoteAddress
)
}))
})
// Node.js
const nodeProgram = createEchoServer.pipe(
Effect.provide(NodeUdpSocket.layer(address))
)
// Bun
const bunProgram = createEchoServer.pipe(
Effect.provide(BunUdpSocket.layer(address))
)
This implementation demonstrates Effect's platform-agnostic design principles while respecting the fundamental differences between TCP and UDP protocols.
🤖 Generated with Claude Code
Co-Authored-By: Claude [email protected]
🦋 Changeset detected
Latest commit: 2224431a128aed3bdd2b36b68954a6ccc33f83e3
The changes in this PR will be included in the next version bump.
This PR includes changesets to release 30 packages
| Name | Type |
|---|---|
| @effect/platform-node | Major |
| @effect/platform | Minor |
| @effect/cli | Major |
| @effect/cluster | Major |
| @effect/experimental | Major |
| @effect/sql-clickhouse | Major |
| @effect/sql-drizzle | Major |
| @effect/sql-mssql | Major |
| @effect/sql-sqlite-node | Major |
| @effect/ai-amazon-bedrock | Major |
| @effect/ai-anthropic | Major |
| @effect/ai-google | Major |
| @effect/ai-openai | Major |
| @effect/opentelemetry | Major |
| @effect/platform-browser | Major |
| @effect/platform-bun | Major |
| @effect/platform-node-shared | Major |
| @effect/rpc | Major |
| @effect/sql-d1 | Major |
| @effect/sql-libsql | Major |
| @effect/sql-mysql2 | Major |
| @effect/sql-pg | Major |
| @effect/sql-sqlite-bun | Major |
| @effect/sql | Major |
| @effect/workflow | Major |
| @effect/ai | Major |
| @effect/sql-sqlite-do | Major |
| @effect/sql-sqlite-react-native | Major |
| @effect/sql-sqlite-wasm | Major |
| @effect/sql-kysely | Major |
Not sure what this means? Click here to learn what changesets are.
Click here if you're a maintainer who wants to add another changeset to this PR
hey @tim-smart , do you think this adds value?
I think a udp abstraction is worth adding :) I need to set aside some time to look at this and work out if is going in the right direction.