@effect/platform-bun 4.0.2 → 4.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,666 @@
1
+ /**
2
+ * Bun UDP adapter for `effect/socket/DatagramSocket`, built on the native
3
+ * `Bun.udpSocket`. Requires Bun 1.4 or later: earlier versions throw `EAGAIN`
4
+ * from `send` instead of returning `false`, and never fire `drain`.
5
+ *
6
+ * `make` opens a new Bun socket for each reader acquisition, `fromUdpSocket`
7
+ * adopts a socket the caller creates, and `layer` provides the
8
+ * `DatagramSocket` service.
9
+ *
10
+ * The reader owns the native socket, so a send-only client must still acquire
11
+ * a reader, or its writes wait forever. Bun sends synchronously: a send error
12
+ * fails the write that caused it, and when the kernel buffer is full the write
13
+ * waits for `drain` and sends what is left.
14
+ *
15
+ * `writeAll` sends the batch with one `sendMany` call and fails with its first
16
+ * error. Which datagrams went out is unspecified, nothing is resent, and the
17
+ * error has no `address`, because Bun reports one error for the whole call.
18
+ *
19
+ * ICMP reports, such as a refused port, arrive after the write has completed,
20
+ * so they go to the `onError` option as a `DatagramSocketReadError` with no
21
+ * address.
22
+ *
23
+ * On macOS and Windows a poll error closes the socket without any event. The
24
+ * adapter notices on the next write, which fails with
25
+ * `DatagramSocketClosedError`, as do the reader and any write waiting for
26
+ * `drain`. Until then those waiting writes and `pull` keep waiting. The same
27
+ * applies when the caller closes a socket adopted with `fromUdpSocket`.
28
+ *
29
+ * **Example** (Echo server)
30
+ *
31
+ * ```ts
32
+ * import { BunDatagramSocket } from "@effect/platform-bun"
33
+ * import { Effect } from "effect"
34
+ *
35
+ * const echo = Effect.gen(function*() {
36
+ * const socket = yield* BunDatagramSocket.make({ bind: { port: 9000 } })
37
+ * const reader = yield* socket.reader
38
+ * const writer = yield* socket.writer
39
+ * while (true) {
40
+ * for (const received of yield* reader.pull) {
41
+ * yield* writer.write({ payload: received.payload, address: received })
42
+ * }
43
+ * }
44
+ * }).pipe(Effect.scoped)
45
+ * ```
46
+ *
47
+ * **Example** (Send-only client with a connected socket)
48
+ *
49
+ * ```ts
50
+ * import { BunDatagramSocket } from "@effect/platform-bun"
51
+ * import { Effect } from "effect"
52
+ *
53
+ * const send = (payload: string) =>
54
+ * Effect.gen(function*() {
55
+ * const socket = yield* BunDatagramSocket.make({
56
+ * connect: { address: "localhost", port: 9000 }
57
+ * })
58
+ * // Required even for send-only clients.
59
+ * yield* socket.reader
60
+ * const writer = yield* socket.writer
61
+ * yield* writer.write({ payload })
62
+ * }).pipe(Effect.scoped)
63
+ * ```
64
+ *
65
+ * **Example** (Adopting a socket)
66
+ *
67
+ * ```ts
68
+ * import { BunDatagramSocket } from "@effect/platform-bun"
69
+ * import { Effect } from "effect"
70
+ *
71
+ * const receive = Effect.gen(function*() {
72
+ * // `fromUdpSocket` replaces the socket's handlers with its own
73
+ * const socket = yield* BunDatagramSocket.fromUdpSocket(
74
+ * Effect.promise(() => Bun.udpSocket({ hostname: "127.0.0.1", port: 9000 }))
75
+ * )
76
+ * const reader = yield* socket.reader
77
+ * return yield* reader.pull
78
+ * }).pipe(Effect.scoped)
79
+ * ```
80
+ *
81
+ * @stability experimental
82
+ * @since 4.0.0
83
+ */
84
+ import * as Effect from "effect/Effect"
85
+ import * as Layer from "effect/Layer"
86
+ import * as NetAddress from "effect/net/NetAddress"
87
+ import * as Predicate from "effect/Predicate"
88
+ import type * as Scope from "effect/Scope"
89
+ import * as DatagramSocket from "effect/socket/DatagramSocket"
90
+ import * as Dns from "node:dns"
91
+ import * as Net from "node:net"
92
+ import * as Os from "node:os"
93
+
94
+ /**
95
+ * An open-time endpoint. Hostnames resolve once per reader acquisition via
96
+ * `node:dns.lookup`, avoiding Bun's synchronous `getaddrinfo`.
97
+ * An `InetAddress` can be passed directly.
98
+ *
99
+ * @stability experimental
100
+ * @category models
101
+ * @since 4.0.0
102
+ */
103
+ export interface Endpoint {
104
+ readonly address: string | NetAddress.IpAddress
105
+ readonly port: number
106
+ }
107
+
108
+ /**
109
+ * The local address and port to bind. Defaults to `0.0.0.0` (or `::` for
110
+ * `family: "ipv6"`) and an ephemeral port.
111
+ *
112
+ * @stability experimental
113
+ * @category models
114
+ * @since 4.0.0
115
+ */
116
+ export interface BindOptions {
117
+ readonly address?: string | NetAddress.IpAddress | undefined
118
+ readonly port?: number | undefined
119
+ }
120
+
121
+ /**
122
+ * Options for `fromUdpSocket`. The caller configures the adopted socket, so
123
+ * only the options that live in JavaScript apply.
124
+ *
125
+ * @stability experimental
126
+ * @category models
127
+ * @since 4.0.0
128
+ */
129
+ export interface AdoptOptions {
130
+ readonly peer?: Endpoint | undefined
131
+ readonly receiveBuffer?: DatagramSocket.ReceiveBufferOptions | undefined
132
+ readonly onError?: ((error: DatagramSocket.DatagramSocketError) => void) | undefined
133
+ }
134
+
135
+ /**
136
+ * Options for `make` and `layer`.
137
+ *
138
+ * **Details**
139
+ *
140
+ * `peer` is a default destination and doesn't filter senders. `connect`
141
+ * connects the socket when Bun creates it, so the kernel filters senders. The
142
+ * two can't be combined.
143
+ *
144
+ * Family selection, in order: explicit `family`, a `bind` IP literal, the
145
+ * `peer` or `connect` address, a `bind` hostname, then `"ipv4"`. Hostname
146
+ * lookups use the family selected so far, or prefer IPv4 if none is set.
147
+ *
148
+ * `reuseAddress` means `SO_REUSEADDR` on Linux and `SO_REUSEPORT` on BSD and
149
+ * macOS.
150
+ *
151
+ * `multicast.interface` is the egress interface for datagrams sent to a group,
152
+ * which is a different socket option from the ingress `interface` of
153
+ * `Reader.joinMulticast`.
154
+ *
155
+ * @stability experimental
156
+ * @category models
157
+ * @since 4.0.0
158
+ */
159
+ export type Options =
160
+ & {
161
+ readonly bind?: BindOptions | undefined
162
+ readonly family?: "ipv4" | "ipv6" | undefined
163
+ readonly reuseAddress?: boolean | undefined
164
+ readonly reusePort?: boolean | undefined
165
+ readonly ipv6Only?: boolean | undefined
166
+ readonly broadcast?: boolean | undefined
167
+ readonly ttl?: number | undefined
168
+ readonly multicast?: {
169
+ readonly interface?: NetAddress.MulticastInterface | undefined
170
+ readonly loopback?: boolean | undefined
171
+ readonly ttl?: number | undefined
172
+ } | undefined
173
+ readonly receiveBuffer?: DatagramSocket.ReceiveBufferOptions | undefined
174
+ readonly onError?: ((error: DatagramSocket.DatagramSocketError) => void) | undefined
175
+ }
176
+ & (
177
+ | { readonly peer?: Endpoint | undefined; readonly connect?: undefined }
178
+ | { readonly connect: Endpoint; readonly peer?: undefined }
179
+ )
180
+
181
+ /**
182
+ * A native Bun UDP socket, connected or not.
183
+ *
184
+ * @stability experimental
185
+ * @category models
186
+ * @since 4.0.0
187
+ */
188
+ export type UdpSocket = Bun.udp.Socket<"buffer"> | Bun.udp.ConnectedSocket<"buffer">
189
+
190
+ /**
191
+ * Creates a `DatagramSocket` that opens and binds a new Bun UDP socket for
192
+ * each reader acquisition.
193
+ *
194
+ * **Details**
195
+ *
196
+ * Creating the socket never fails. Binding, applying options and resolving
197
+ * names happen on reader acquisition and fail it with a `DatagramSocketError`.
198
+ * Hostnames in `bind`, `peer` and `connect` are resolved once per acquisition;
199
+ * sends never wait for DNS.
200
+ *
201
+ * Invalid `receiveBuffer.capacity` values cause a defect; use a positive
202
+ * safe integer.
203
+ *
204
+ * @stability experimental
205
+ * @category constructors
206
+ * @since 4.0.0
207
+ */
208
+ export const make = (options: Options = {}): Effect.Effect<DatagramSocket.DatagramSocket> =>
209
+ DatagramSocket.makeFromBackingSocket((events) => open(options, events), options)
210
+
211
+ /**
212
+ * Adopts a Bun UDP socket.
213
+ *
214
+ * **Details**
215
+ *
216
+ * `acquire` runs once per reader acquisition, inside the reader's scope, and
217
+ * the adapter closes the socket when that scope ends. Whether the socket is
218
+ * connected is checked once at open.
219
+ *
220
+ * The adapter installs its own `data`, `drain` and `error` handlers with
221
+ * `socket.reload`, which replaces the ones the caller passed to
222
+ * `Bun.udpSocket`. Packets that arrive before `acquire` completes still go to
223
+ * the caller's handlers.
224
+ *
225
+ * @stability experimental
226
+ * @category constructors
227
+ * @since 4.0.0
228
+ */
229
+ export const fromUdpSocket = <R>(
230
+ acquire: Effect.Effect<UdpSocket, DatagramSocket.DatagramSocketError, R>,
231
+ options: AdoptOptions = {}
232
+ ): Effect.Effect<DatagramSocket.DatagramSocket, never, Exclude<R, Scope.Scope>> =>
233
+ DatagramSocket.makeFromBackingSocket(
234
+ (events) => Effect.flatMap(acquire, (socket) => adopt(socket, options, events)),
235
+ options
236
+ )
237
+
238
+ /**
239
+ * Provides a `DatagramSocket` built with `make`.
240
+ *
241
+ * @stability experimental
242
+ * @category layers
243
+ * @since 4.0.0
244
+ */
245
+ export const layer = (options: Options = {}): Layer.Layer<DatagramSocket.DatagramSocket> =>
246
+ Layer.effect(DatagramSocket.DatagramSocket, make(options))
247
+
248
+ type Family = "ipv4" | "ipv6"
249
+
250
+ const noScopeIds: ReadonlyMap<string, number> = new Map()
251
+
252
+ // Windows reports numeric zones, which parse and format without a map
253
+ const scopeIdsFor = (family: Family): ReadonlyMap<string, number> => {
254
+ if (family === "ipv4" || process.platform === "win32") return noScopeIds
255
+ try {
256
+ return NetAddress.scopeIdsFromInterfaces(Object.entries(Os.networkInterfaces()))
257
+ } catch {
258
+ return noScopeIds
259
+ }
260
+ }
261
+
262
+ const familyOf = (address: string | NetAddress.IpAddress): Family | undefined => {
263
+ if (typeof address !== "string") return NetAddress.isIpv4Address(address) ? "ipv4" : "ipv6"
264
+ const version = Net.isIP(address)
265
+ return version === 4 ? "ipv4" : version === 6 ? "ipv6" : undefined
266
+ }
267
+
268
+ // An endpoint may be a whole `InetAddress`, whose IPv6 scope must survive
269
+ const formatHost = (
270
+ endpoint: { readonly address?: string | NetAddress.IpAddress | undefined },
271
+ scopeIds: ReadonlyMap<string, number>
272
+ ): string | undefined =>
273
+ NetAddress.isInetAddress(endpoint)
274
+ ? NetAddress.formatNativeHost(endpoint, scopeIds)
275
+ : endpoint.address === undefined || typeof endpoint.address === "string"
276
+ ? endpoint.address
277
+ : NetAddress.formatIp(endpoint.address)
278
+
279
+ interface Resolved {
280
+ readonly host: string
281
+ readonly family: Family
282
+ }
283
+
284
+ // Resolves a hostname with one `lookup`, preferring IPv4 unless `family` is
285
+ // fixed. IP literals and `IpAddress` values need no lookup.
286
+ const resolve = (
287
+ address: string | NetAddress.IpAddress | undefined,
288
+ family: Family | undefined
289
+ ): Effect.Effect<Resolved | undefined, DatagramSocket.DatagramSocketError> => {
290
+ if (typeof address !== "string") return Effect.undefined
291
+ const literal = familyOf(address)
292
+ if (literal !== undefined) return Effect.succeed({ host: address, family: literal })
293
+ return Effect.callback((resume) => {
294
+ const fail = (error: unknown) => resume(Effect.fail(openError(error, "AddressNotAvailable")))
295
+ try {
296
+ Dns.lookup(
297
+ address,
298
+ { all: true, family: family === "ipv4" ? 4 : family === "ipv6" ? 6 : 0 },
299
+ (error, results) => {
300
+ if (error) return fail(error)
301
+ const result = results.find((result) => result.family === 4) ?? results[0]
302
+ if (result === undefined) return fail(new Error(`${address} has no addresses`))
303
+ resume(Effect.succeed({ host: result.address, family: result.family === 6 ? "ipv6" : "ipv4" }))
304
+ }
305
+ )
306
+ } catch (error) {
307
+ fail(error)
308
+ }
309
+ })
310
+ }
311
+
312
+ // Picks the family as documented on `Options` and turns `bind` and the remote
313
+ // endpoint into IP literals
314
+ const resolveOpen = Effect.fnUntraced(function*(options: Options) {
315
+ const bind = options.bind
316
+ const remote = options.connect ?? options.peer
317
+ const fixed = options.family ?? (bind?.address === undefined ? undefined : familyOf(bind.address))
318
+ const resolvedRemote = yield* resolve(remote?.address, fixed)
319
+ const remoteFamily = resolvedRemote?.family ?? (remote === undefined ? undefined : familyOf(remote.address))
320
+ const resolvedBind = yield* resolve(bind?.address, fixed ?? remoteFamily)
321
+ const family = fixed ?? remoteFamily ?? resolvedBind?.family ?? "ipv4"
322
+ const scopeIds = scopeIdsFor(family)
323
+ return {
324
+ scopeIds,
325
+ host: resolvedBind?.host ?? (bind === undefined ? undefined : formatHost(bind, scopeIds)) ??
326
+ (family === "ipv6" ? "::" : "0.0.0.0"),
327
+ remote: remote === undefined ? undefined : {
328
+ host: resolvedRemote?.host ?? formatHost(remote, scopeIds)!,
329
+ port: remote.port
330
+ }
331
+ }
332
+ })
333
+
334
+ const errorCode = (error: unknown): unknown => Predicate.hasProperty(error, "code") ? error.code : undefined
335
+
336
+ const openError = (
337
+ error: unknown,
338
+ kind?: DatagramSocket.DatagramSocketOpenError["kind"]
339
+ ): DatagramSocket.DatagramSocketError => {
340
+ if (kind === undefined) {
341
+ const code = errorCode(error)
342
+ kind = code === "EADDRINUSE"
343
+ ? "AddressInUse"
344
+ : code === "EADDRNOTAVAIL"
345
+ ? "AddressNotAvailable"
346
+ : code === "EACCES" || code === "EPERM"
347
+ ? "PermissionDenied"
348
+ : "Unknown"
349
+ }
350
+ return new DatagramSocket.DatagramSocketError({
351
+ reason: new DatagramSocket.DatagramSocketOpenError({ kind, cause: error })
352
+ })
353
+ }
354
+
355
+ const ioKind = (error: unknown): DatagramSocket.IoErrorKind => {
356
+ switch (errorCode(error)) {
357
+ case "EMSGSIZE":
358
+ return "MessageTooLarge"
359
+ case "EHOSTUNREACH":
360
+ case "ENETUNREACH":
361
+ case "EHOSTDOWN":
362
+ case "ENETDOWN":
363
+ return "Unreachable"
364
+ case "ECONNREFUSED":
365
+ return "ConnectionRefused"
366
+ case "EACCES":
367
+ case "EPERM":
368
+ return "PermissionDenied"
369
+ default:
370
+ return "Unknown"
371
+ }
372
+ }
373
+
374
+ const closedError = (): DatagramSocket.DatagramSocketError =>
375
+ new DatagramSocket.DatagramSocketError({ reason: new DatagramSocket.DatagramSocketClosedError() })
376
+
377
+ const writeError = (error: unknown): DatagramSocket.DatagramSocketError =>
378
+ new DatagramSocket.DatagramSocketError({
379
+ reason: new DatagramSocket.DatagramSocketWriteError({ kind: ioKind(error), cause: error })
380
+ })
381
+
382
+ const readError = (error: unknown): DatagramSocket.DatagramSocketError =>
383
+ new DatagramSocket.DatagramSocketError({
384
+ reason: new DatagramSocket.DatagramSocketReadError({ kind: ioKind(error), cause: error })
385
+ })
386
+
387
+ // Bun throws a plain `Error` with this message when sending on a closed socket
388
+ const isClosedError = (error: unknown): boolean =>
389
+ Predicate.hasProperty(error, "message") && error.message === "Socket is closed"
390
+
391
+ // uSockets `LIBUS_SOCKET_*` flags, not part of Bun's public API. Bun's own
392
+ // `node:dgram` passes them the same way.
393
+ // https://github.com/oven-sh/bun/blob/main/packages/bun-usockets/src/libusockets.h
394
+ const LIBUS_SOCKET_REUSE_PORT = 4
395
+ const LIBUS_SOCKET_IPV6_ONLY = 8
396
+ const LIBUS_SOCKET_REUSE_ADDR = 16
397
+
398
+ const open = Effect.fnUntraced(function*(options: Options, events: DatagramSocket.BackingEvents) {
399
+ const { host, remote, scopeIds } = yield* resolveOpen(options)
400
+ const backing = new BunBackingSocket(events)
401
+ const flags = (options.reusePort ? LIBUS_SOCKET_REUSE_PORT : 0) |
402
+ (options.ipv6Only ? LIBUS_SOCKET_IPV6_ONLY : 0) |
403
+ (options.reuseAddress ? LIBUS_SOCKET_REUSE_ADDR : 0)
404
+ const connect = options.connect === undefined ? undefined : { hostname: remote!.host, port: remote!.port }
405
+ const socket = yield* Effect.tryPromise({
406
+ try: () =>
407
+ Bun.udpSocket({
408
+ hostname: host,
409
+ port: options.bind?.port ?? 0,
410
+ socket: backing.handlers,
411
+ ...(flags === 0 ? undefined : { flags }),
412
+ ...(connect === undefined ? undefined : { connect })
413
+ } as Bun.udp.SocketOptions<"buffer">),
414
+ catch: (error) => openError(error)
415
+ })
416
+ // closes the new socket if `attach` or an option fails
417
+ backing.socket = socket
418
+ yield* Effect.try({
419
+ try: () => {
420
+ backing.attach(socket, scopeIds, remote, connect !== undefined)
421
+ if (options.broadcast !== undefined) socket.setBroadcast(options.broadcast)
422
+ if (options.ttl !== undefined) socket.setTTL(options.ttl)
423
+ const multicast = options.multicast
424
+ if (multicast?.ttl !== undefined) socket.setMulticastTTL(multicast.ttl)
425
+ if (multicast?.loopback !== undefined) socket.setMulticastLoopback(multicast.loopback)
426
+ if (multicast?.interface !== undefined) {
427
+ socket.setMulticastInterface(NetAddress.formatMulticastInterface(multicast.interface, scopeIds))
428
+ }
429
+ },
430
+ catch: (error) => openError(error)
431
+ }).pipe(Effect.onError(() => Effect.sync(() => backing.close())))
432
+ return backing
433
+ })
434
+
435
+ const adopt = (
436
+ socket: UdpSocket,
437
+ options: AdoptOptions,
438
+ events: DatagramSocket.BackingEvents
439
+ ): Effect.Effect<DatagramSocket.BackingSocket, DatagramSocket.DatagramSocketError> => {
440
+ const backing = new BunBackingSocket(events)
441
+ backing.socket = socket
442
+ return Effect.gen(function*() {
443
+ if (socket.closed) return yield* Effect.fail(openError(new Error("The adopted Bun UDP socket is closed")))
444
+ const family: Family = yield* Effect.try({
445
+ try: () => {
446
+ // Bun reads `{ socket }`, although the types declare the handler itself
447
+ socket.reload({ socket: backing.handlers } as any)
448
+ return socket.address.family === "IPv6" ? "ipv6" : "ipv4"
449
+ },
450
+ catch: (error) => openError(error)
451
+ })
452
+ const scopeIds = scopeIdsFor(family)
453
+ // an unconnected socket has the property, set to `undefined`
454
+ const remote = "remoteAddress" in socket ? socket.remoteAddress : undefined
455
+ if (remote !== undefined) {
456
+ return backing.attach(socket, scopeIds, { host: remote.address, port: remote.port }, true)
457
+ }
458
+ const peer = options.peer
459
+ const resolved = yield* resolve(peer?.address, family)
460
+ return backing.attach(
461
+ socket,
462
+ scopeIds,
463
+ peer === undefined ? undefined : { host: resolved?.host ?? formatHost(peer, scopeIds)!, port: peer.port },
464
+ false
465
+ )
466
+ }).pipe(Effect.onError(() => Effect.sync(() => backing.close())))
467
+ }
468
+
469
+ // Sends the kernel refused while its buffer was full, in `sendMany`'s flat
470
+ // form: `[data, port, host]` per datagram, or plain payloads when connected
471
+ interface Waiting {
472
+ entries: ReadonlyArray<Uint8Array | string | number>
473
+ readonly done: (error?: DatagramSocket.DatagramSocketError) => void
474
+ }
475
+
476
+ class BunBackingSocket implements DatagramSocket.BackingSocket {
477
+ readonly events: DatagramSocket.BackingEvents
478
+ // installed when the socket is created, so packets can arrive before `attach`
479
+ readonly handlers: Bun.udp.SocketHandler<"buffer">
480
+ socket: UdpSocket | undefined = undefined
481
+ address: DatagramSocket.BackingAddress = { host: "", port: 0 }
482
+ scopeIds: ReadonlyMap<string, number> = noScopeIds
483
+ peer: DatagramSocket.BackingAddress | undefined = undefined
484
+ connected = false
485
+ // entries per datagram in `sendMany`'s flat list
486
+ stride = 3
487
+ // waiting sends, oldest first; later sends queue behind them to keep order
488
+ waiting: Array<Waiting> = []
489
+ closing = false
490
+
491
+ constructor(events: DatagramSocket.BackingEvents) {
492
+ this.events = events
493
+ this.handlers = {
494
+ data: (_socket, payload, port, host) => events.onPacket(payload, host, port),
495
+ // also fires once right after creation, when nothing is waiting
496
+ drain: () => this.flush(),
497
+ // Bun crashes the process without an `error` handler. It passes the
498
+ // error alone, despite the declared `(socket, error)`.
499
+ error: ((first: unknown, second?: unknown) => this.onNativeError(second === undefined ? first : second)) as any
500
+ }
501
+ }
502
+
503
+ attach(
504
+ socket: UdpSocket,
505
+ scopeIds: ReadonlyMap<string, number>,
506
+ peer: DatagramSocket.BackingAddress | undefined,
507
+ connected: boolean
508
+ ): this {
509
+ this.socket = socket
510
+ this.address = { host: socket.address.address, port: socket.address.port }
511
+ this.scopeIds = scopeIds
512
+ this.peer = peer
513
+ this.connected = connected
514
+ this.stride = connected ? 1 : 3
515
+ return this
516
+ }
517
+
518
+ send(
519
+ payload: Uint8Array,
520
+ destination: DatagramSocket.BackingAddress | undefined,
521
+ done: (error?: DatagramSocket.DatagramSocketError) => void
522
+ ) {
523
+ const entries = destination === undefined ? [payload] : [payload, destination.port, destination.host]
524
+ if (this.waiting.length !== 0) return this.wait(entries, done)
525
+ let sent: boolean
526
+ try {
527
+ sent = destination === undefined
528
+ ? (this.socket as Bun.udp.ConnectedSocket<"buffer">).send(payload)
529
+ : (this.socket as Bun.udp.Socket<"buffer">).send(payload, destination.port, destination.host)
530
+ } catch (error) {
531
+ return done(this.sendError(error))
532
+ }
533
+ if (sent) return done()
534
+ this.wait(entries, done)
535
+ }
536
+
537
+ sendMany(
538
+ payloads: ReadonlyArray<Uint8Array>,
539
+ destinations: ReadonlyArray<DatagramSocket.BackingAddress | undefined>,
540
+ done: (error?: DatagramSocket.DatagramSocketError, index?: number) => void
541
+ ) {
542
+ let entries: ReadonlyArray<Uint8Array | string | number> = payloads
543
+ if (!this.connected) {
544
+ const flat = new Array<Uint8Array | string | number>(payloads.length * 3)
545
+ for (let i = 0; i < payloads.length; i++) {
546
+ flat[i * 3] = payloads[i]
547
+ flat[i * 3 + 1] = destinations[i]!.port
548
+ flat[i * 3 + 2] = destinations[i]!.host
549
+ }
550
+ entries = flat
551
+ }
552
+ if (this.waiting.length !== 0) return this.wait(entries, done)
553
+ let sent: number
554
+ try {
555
+ sent = this.socket!.sendMany(entries as any)
556
+ } catch (error) {
557
+ // Bun throws for the whole call, so no index is known
558
+ return done(this.sendError(error))
559
+ }
560
+ if (sent === payloads.length) return done()
561
+ this.wait(entries.slice(sent * this.stride), done)
562
+ }
563
+
564
+ joinMulticast<A extends NetAddress.IpAddress>({ group, interface: ingress, source }: {
565
+ readonly group: NetAddress.MulticastAddress<A>
566
+ readonly interface?: NetAddress.MulticastInterface<A> | undefined
567
+ readonly source?: A | undefined
568
+ }): Effect.Effect<() => Effect.Effect<void>, DatagramSocket.DatagramSocketError> {
569
+ return Effect.try({
570
+ try: () => {
571
+ const socket = this.socket!
572
+ const groupHost = NetAddress.formatIp(group)
573
+ const interfaceHost = ingress === undefined
574
+ ? undefined
575
+ : NetAddress.formatMulticastInterface(ingress, this.scopeIds)
576
+ if (source === undefined) {
577
+ if (!socket.addMembership(groupHost, interfaceHost)) throw new Error(`Could not join ${groupHost}`)
578
+ return () => Effect.sync(() => socket.dropMembership(groupHost, interfaceHost))
579
+ }
580
+ const sourceHost = NetAddress.formatIp(source)
581
+ if (!socket.addSourceSpecificMembership(sourceHost, groupHost, interfaceHost)) {
582
+ throw new Error(`Could not join ${groupHost} from ${sourceHost}`)
583
+ }
584
+ return () => Effect.sync(() => socket.dropSourceSpecificMembership(sourceHost, groupHost, interfaceHost))
585
+ },
586
+ catch: (error) => openError(error)
587
+ })
588
+ }
589
+
590
+ close() {
591
+ if (this.closing) return
592
+ this.closing = true
593
+ this.failWaiting()
594
+ try {
595
+ this.socket?.close()
596
+ } catch {
597
+ // already closed
598
+ }
599
+ }
600
+
601
+ wait(
602
+ entries: ReadonlyArray<Uint8Array | string | number>,
603
+ done: (error?: DatagramSocket.DatagramSocketError) => void
604
+ ) {
605
+ // a closed socket fires no event and never drains
606
+ if (this.socket?.closed) {
607
+ this.closedUnderneath()
608
+ return done(closedError())
609
+ }
610
+ this.waiting.push({ entries, done })
611
+ }
612
+
613
+ // on `drain`, send what the kernel refused, in order, until it refuses again
614
+ flush() {
615
+ const socket = this.socket
616
+ if (socket === undefined) return
617
+ // re-read `waiting`: a completion can close the socket and replace it
618
+ while (!this.closing && this.waiting.length !== 0) {
619
+ const next = this.waiting[0]
620
+ let sent: number
621
+ try {
622
+ sent = socket.sendMany(next.entries as any)
623
+ } catch (error) {
624
+ this.waiting.shift()
625
+ next.done(this.sendError(error))
626
+ continue
627
+ }
628
+ const rest = sent * this.stride
629
+ if (rest < next.entries.length) {
630
+ next.entries = next.entries.slice(rest)
631
+ return
632
+ }
633
+ this.waiting.shift()
634
+ next.done()
635
+ }
636
+ }
637
+
638
+ sendError(error: unknown): DatagramSocket.DatagramSocketError {
639
+ // on macOS and Windows a poll error closes the socket silently
640
+ if (this.socket?.closed || isClosedError(error)) {
641
+ this.closedUnderneath()
642
+ return closedError()
643
+ }
644
+ return writeError(error)
645
+ }
646
+
647
+ onNativeError(error: unknown) {
648
+ if (this.closing) return
649
+ if (this.socket?.closed) return this.closedUnderneath()
650
+ // an ICMP report, with no write left to fail
651
+ this.events.onError(readError(error))
652
+ }
653
+
654
+ closedUnderneath() {
655
+ if (this.closing) return
656
+ this.closing = true
657
+ this.failWaiting()
658
+ this.events.onClose()
659
+ }
660
+
661
+ failWaiting() {
662
+ const waiting = this.waiting
663
+ this.waiting = []
664
+ for (let i = 0; i < waiting.length; i++) waiting[i].done(closedError())
665
+ }
666
+ }