agenthub/src/discovery.ts

102 lines
2.5 KiB
TypeScript

import dgram from 'node:dgram';
import { networkInterfaces } from 'node:os';
export const DISCOVERY_PORT = 3378;
export const DISCOVERY_MULTICAST = '239.255.42.99';
export const DISCOVERY_PREFIX = 'AGENTHUB|';
export function getLanIPv4(): string | undefined {
for (const iface of Object.values(networkInterfaces())) {
if (!iface) continue;
for (const entry of iface) {
if (entry.family === 'IPv4' && !entry.internal) {
return entry.address;
}
}
}
return undefined;
}
export function resolveAdvertiseUrl(host: string, port: number): string {
const advertiseHost = host === '0.0.0.0' || host === '::' ? getLanIPv4() ?? '127.0.0.1' : host;
return `http://${advertiseHost}:${port}`;
}
export function startDiscoveryBroadcaster(serverUrl: string, options?: { port?: number; intervalMs?: number }) {
const port = options?.port ?? DISCOVERY_PORT;
const intervalMs = options?.intervalMs ?? 2000;
const socket = dgram.createSocket('udp4');
const message = Buffer.from(`${DISCOVERY_PREFIX}${serverUrl}`);
socket.on('error', () => {
// Discovery is best-effort; ignore network errors.
});
const send = () => {
try {
socket.send(message, 0, message.length, port, DISCOVERY_MULTICAST);
} catch {
try {
socket.setBroadcast(true);
socket.send(message, 0, message.length, port, '255.255.255.255');
} catch {
// Ignore send failures.
}
}
};
send();
const interval = setInterval(send, intervalMs);
return {
socket,
interval,
stop: () => {
clearInterval(interval);
socket.close();
},
};
}
export function discoverServer(timeoutMs = 3000, port = DISCOVERY_PORT): Promise<string | undefined> {
return new Promise((resolve) => {
const socket = dgram.createSocket('udp4');
let timer: NodeJS.Timeout;
const cleanup = () => {
try {
socket.close();
} catch {
// Already closed.
}
clearTimeout(timer);
};
socket.on('message', (msg) => {
const text = msg.toString();
if (text.startsWith(DISCOVERY_PREFIX)) {
cleanup();
resolve(text.slice(DISCOVERY_PREFIX.length));
}
});
socket.on('error', () => {
cleanup();
resolve(undefined);
});
socket.bind(port, () => {
try {
socket.addMembership(DISCOVERY_MULTICAST);
} catch {
// Multicast may be blocked; still try to receive broadcasts.
}
});
timer = setTimeout(() => {
cleanup();
resolve(undefined);
}, timeoutMs);
});
}