Skip to content
Effect 4.0 is here. One ecosystem. Zero dependencies.

BunDatagramSocket

Bun UDP adapter for effect/socket/DatagramSocket, built on the native Bun.udpSocket. Requires Bun 1.4 or later: earlier versions throw EAGAIN from send instead of returning false, and never fire drain.

make opens a new Bun socket for each reader acquisition, fromUdpSocket adopts a socket the caller creates, and layer provides the DatagramSocket service.

The reader owns the native socket, so a send-only client must still acquire a reader, or its writes wait forever. Bun sends synchronously: a send error fails the write that caused it, and when the kernel buffer is full the write waits for drain and sends what is left.

writeAll sends the batch with one sendMany call and fails with its first error. Which datagrams went out is unspecified, nothing is resent, and the error has no address, because Bun reports one error for the whole call.

ICMP reports, such as a refused port, arrive after the write has completed, so they go to the onError option as a DatagramSocketReadError with no address.

On macOS and Windows a poll error closes the socket without any event. The adapter notices on the next write, which fails with DatagramSocketClosedError, as do the reader and any write waiting for drain. Until then those waiting writes and pull keep waiting. The same applies when the caller closes a socket adopted with fromUdpSocket.

Example Echo server

import { BunDatagramSocket } from "@effect/platform-bun"
import { Effect } from "effect"
const echo = Effect.gen(function*() {
const socket = yield* BunDatagramSocket.make({ bind: { port: 9000 } })
const reader = yield* socket.reader
const writer = yield* socket.writer
while (true) {
for (const received of yield* reader.pull) {
yield* writer.write({ payload: received.payload, address: received })
}
}
}).pipe(Effect.scoped)

Example Send-only client with a connected socket

import { BunDatagramSocket } from "@effect/platform-bun"
import { Effect } from "effect"
const send = (payload: string) =>
Effect.gen(function*() {
const socket = yield* BunDatagramSocket.make({
connect: { address: "localhost", port: 9000 }
})
// Required even for send-only clients.
yield* socket.reader
const writer = yield* socket.writer
yield* writer.write({ payload })
}).pipe(Effect.scoped)

Example Adopting a socket

import { BunDatagramSocket } from "@effect/platform-bun"
import { Effect } from "effect"
const receive = Effect.gen(function*() {
// `fromUdpSocket` replaces the socket's handlers with its own
const socket = yield* BunDatagramSocket.fromUdpSocket(
Effect.promise(() => Bun.udpSocket({ hostname: "127.0.0.1", port: 9000 }))
)
const reader = yield* socket.reader
return yield* reader.pull
}).pipe(Effect.scoped)
8 exports Since v4.0.0 Source

Constructors

Adopts a Bun UDP socket.

Details

acquire runs once per reader acquisition, inside the reader's scope, and the adapter closes the socket when that scope ends. Whether the socket is connected is checked once at open.

The adapter installs its own data, drain and error handlers with socket.reload, which replaces the ones the caller passed to Bun.udpSocket. Packets that arrive before acquire completes still go to the caller's handlers.

Signature

declare function fromUdpSocket<R>(acquire: Effect<any, DatagramSocketError, R>, options: AdoptOptions): Effect<DatagramSocket, never, Exclude<R, Scope>>
Since v4.0.0

Creates a DatagramSocket that opens and binds a new Bun UDP socket for each reader acquisition.

Details

Creating the socket never fails. Binding, applying options and resolving names happen on reader acquisition and fail it with a DatagramSocketError. Hostnames in bind, peer and connect are resolved once per acquisition; sends never wait for DNS.

Invalid receiveBuffer.capacity values cause a defect; use a positive safe integer.

Signature

declare function make(options: Options): Effect<DatagramSocket>

Layers

Since v4.0.0

Provides a DatagramSocket built with make.

Signature

declare function layer(options: Options): Layer<DatagramSocket>

Models

AdoptOptions

interface
Since v4.0.0

Options for fromUdpSocket. The caller configures the adopted socket, so only the options that live in JavaScript apply.

Signature

interface AdoptOptions {
readonly onError?: (error: DatagramSocketError) => void;
readonly peer?: Endpoint;
readonly receiveBuffer?: any;
}

BindOptions

interface
Since v4.0.0

The local address and port to bind. Defaults to 0.0.0.0 (or :: for family: "ipv6") and an ephemeral port.

Signature

interface BindOptions {
readonly address?: string | IpAddress;
readonly port?: number;
}

Endpoint

interface
Since v4.0.0

An open-time endpoint. Hostnames resolve once per reader acquisition via node:dns.lookup, avoiding Bun's synchronous getaddrinfo. An InetAddress can be passed directly.

Signature

interface Endpoint {
readonly address: string | IpAddress;
readonly port: number;
}
Since v4.0.0

Options for make and layer.

Details

peer is a default destination and doesn't filter senders. connect connects the socket when Bun creates it, so the kernel filters senders. The two can't be combined.

Family selection, in order: explicit family, a bind IP literal, the peer or connect address, a bind hostname, then "ipv4". Hostname lookups use the family selected so far, or prefer IPv4 if none is set.

reuseAddress means SO_REUSEADDR on Linux and SO_REUSEPORT on BSD and macOS.

multicast.interface is the egress interface for datagrams sent to a group, which is a different socket option from the ingress interface of Reader.joinMulticast.

Signature

type Options = {
readonly bind?: BindOptions;
readonly broadcast?: boolean;
readonly family?: "ipv4" | "ipv6";
readonly ipv6Only?: boolean;
readonly multicast?: {
readonly interface?: NetAddress.MulticastInterface;
readonly loopback?: boolean;
readonly ttl?: number;
};
readonly onError?: (error: DatagramSocket.DatagramSocketError) => void;
readonly receiveBuffer?: DatagramSocket.ReceiveBufferOptions;
readonly reuseAddress?: boolean;
readonly reusePort?: boolean;
readonly ttl?: number;
} & {
readonly connect?: undefined;
readonly peer?: Endpoint;
} | {
readonly connect: Endpoint;
readonly peer?: undefined;
}
Since v4.0.0

A native Bun UDP socket, connected or not.

Signature

type UdpSocket = Bun.udp.Socket<"buffer"> | Bun.udp.ConnectedSocket<"buffer">