UNPKG

@akala/core

Version:
197 lines (179 loc) 7.31 kB
import type { AllEventKeys } from "../events/event-bus.js"; import { EventEmitter, type AllEvents } from "../events/event-emitter.js"; import type { EventListener, EventOptions } from "../events/shared.js"; import { IsomorphicBuffer } from "../helpers.js"; import { SocketAdapterAkalaEventMap, SocketAdapter } from "./shared.js"; import { type Subscription, StatefulSubscription } from "../teardown-manager.js"; import { Socket } from 'dgram'; import os from 'os'; export interface RemoteInfo { address: string; port: number } export interface UdpMessage { message: IsomorphicBuffer remote: RemoteInfo } export type UdpSocketAdapterAkalaEventMap = SocketAdapterAkalaEventMap<UdpMessage>; export class UdpSocketAdapter extends EventEmitter<SocketAdapterAkalaEventMap<UdpMessage>> implements SocketAdapter<UdpMessage> { constructor(private readonly socket: Socket) { super(); } pipe(socket: SocketAdapter<UdpMessage>) { this.on('message', (message) => socket.send(message)); this.on('close', () => socket.close()); } /** * Tells the kernel to join a multicast group at the given `multicastAddress` and `multicastInterface` using the `IP_ADD_MEMBERSHIP` socket option. If the `multicastInterface` argument is not * specified, the operating system will choose * one interface and will add membership to it. To add membership to every * available interface, call `addMembership` multiple times, once per interface. * * When called on an unbound socket, this method will implicitly bind to a random * port, listening on all interfaces. * **/ public addMembership(address: string, interfaceAddress?: string) { if (!interfaceAddress) { for (const netifs of Object.values(os.networkInterfaces())) { const netif = netifs.find(netif => netif.family == 'IPv4'); if (!netif) continue; this.socket.addMembership(address, netif.address); } } else this.socket.addMembership(address, interfaceAddress); } public bind(port: number, address?: string) { this.teardown(() => { const wasOpen = this.open; this.socket.close(); return wasOpen }) return new Promise<void>(resolve => this.socket.bind(port, address, resolve)); } private closed: boolean; get open(): boolean { return !this.closed } close(): Promise<void> { return new Promise(resolve => this.socket.close(() => { this.closed = true; resolve(); })); } send(data: UdpMessage): Promise<void> { if (data.remote?.port) if (data.remote?.address) return new Promise((resolve, reject) => this.socket.send(data.message.toArray(), data.remote.port, data.remote.address, err => err ? reject(err) : resolve())); else return new Promise((resolve, reject) => this.socket.send(data.message.toArray(), data.remote.port, err => err ? reject(err) : resolve())); else return new Promise((resolve, reject) => this.socket.send(data.message.toArray(), err => err ? reject(err) : resolve())); } private readonly messageListeners: [(ev: unknown, x) => void, (ev: unknown, x) => void][] = []; public off<const TEvent extends AllEventKeys<UdpSocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]> ): boolean { switch (event) { case 'message': { let listeners = this.messageListeners; if (handler) listeners = listeners.filter(f => f[0] == handler); var result = false; for (const listener of listeners) { this.socket.off('message', listener[1]); result = !!this.messageListeners.splice(this.messageListeners.indexOf(listener), 1)?.length || result; } } break; case 'close': case 'error': case 'open': //eslint-disable-next-line @typescript-eslint/no-explicit-any this.socket.off(event, handler as any); break; default: throw new Error(`Unsupported event ${String(event)}`); } return true; } public on<const TEvent extends AllEventKeys<UdpSocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]>, options?: EventOptions<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': { const x = function (data: Uint8Array, remote: RemoteInfo) { return (handler as EventListener<UdpSocketAdapterAkalaEventMap['message']>).call(this, { message: IsomorphicBuffer.fromBuffer(data), remote }); }; this.messageListeners.push([handler, x]); if (options?.once) this.socket.once('message', x); else this.socket.on('message', x); return new StatefulSubscription(() => { this.messageListeners.splice(this.messageListeners.findIndex(x => x[0] === handler), 1); this.socket.off('message', x); }).unsubscribe; } case 'close': case 'error': case 'open': //eslint-disable-next-line @typescript-eslint/no-explicit-any if (options?.once) this.socket.once(event, handler); else this.socket.on(event, handler); return new StatefulSubscription(() => { this.socket.off(event, handler); }).unsubscribe; case Symbol.dispose: return super.on(event, handler, options); default: throw new Error(`Unsupported event ${String(event)}`); } } public once<const TEvent extends AllEventKeys<UdpSocketAdapterAkalaEventMap>>( event: TEvent, handler: EventListener<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]> ): Subscription { switch (event) { case 'message': return this.on(event, handler, { once: true } as EventOptions<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]>); case 'close': case 'error': case 'open': case Symbol.dispose: return this.on(event, handler, { once: true } as EventOptions<AllEvents<UdpSocketAdapterAkalaEventMap>[TEvent]>); default: let x: never = event; throw new Error(`Unsupported event ${x}`); } } }