@akala/core
Version:
197 lines (179 loc) • 7.31 kB
text/typescript
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}`);
}
}
}