Unix domain socket (UDS) IPC for Bun and Node.js 20+: framed messages, Server / Client, correlation-based request/response, and lifecycle helpers. Linux and macOS only.
bun add libunix
# or
npm install libunixTyped event maps mirror the integration tests: define the events your app exchanges, then wire handlers on each connected RemotePeer.
import { Client, Server } from 'libunix';
type EchoEvents = {
echo: { msg: string };
[key: string]: unknown;
};
const server = await Server.create<EchoEvents>({ id: 'my-app', adapter: 'bun' });
const ready = Promise.withResolvers<void>();
server.on('connection', (peer) => {
peer.onRequest('echo', (data) => ({ echo: data.msg }));
ready.resolve();
});
const client = await Client.connect<EchoEvents>({ id: 'my-app', adapter: 'bun' });
await ready.promise;
const result = await client.request('echo', { msg: 'hello' });
// { echo: 'hello' }
await client.disconnect();
await server.close();- Logical id (e.g.
'my-app'): resolved to$TMPDIR/my-app.sock(letters, digits,.,_,-only). - Filesystem path: pass a path containing
/or ending in.sock(e.g./run/myapp.sock).
On Bun, omit adapter (defaults to Bun.listen / Bun.connect). On Node, omit adapter (auto-detects node:net) or pass adapter: 'node' explicitly.
Same API; use the node:net transport (no extra dependencies):
import { Client, Server } from 'libunix';
const server = await Server.create({ id: 'my-app', adapter: 'node' });
// ... same handler wiring as above
const client = await Client.connect({ id: 'my-app', adapter: 'node' });Verify on Node after build: bun run test:node.
type LoopEvents = {
ping: null;
tick: { n: number };
[key: string]: unknown;
};
const server = await Server.create<LoopEvents>({ id: 'my-app', adapter: 'bun' });
const ready = Promise.withResolvers<void>();
server.on('connection', (peer) => {
peer.onRequest('ping', () => 'pong');
peer.on('tick', () => {});
ready.resolve();
});
const client = await Client.connect<LoopEvents>({ id: 'my-app', adapter: 'bun' });
await ready.promise;
await client.emit('tick', { n: 0 });
const pong = await client.request('ping', null);
await client.disconnect();
await server.close();import { LibunixError, isLibunixError } from 'libunix';
try {
await Server.create({ id: socketPath, adapter: 'bun' });
} catch (err) {
if (isLibunixError(err) && err.code === 'EADDRINUSE') {
// socket already in use
}
}bun install
bun test
bun run build # tsdown → dist/Benchmarks (hyperfine, requires Nix dev shell): bun run bench. Tracked baselines live in docs/benchmarks/.
libunix targets local Unix domain socket IPC between processes on the same machine. Any process that can connect to the socket path is treated as a trusted peer (similar to any local service bound to a world-readable socket file).
- Inbound envelopes always use safe JSON parsing (
__proto__/constructor/prototypekeys are dropped). - Optional
strictEnvelope: trueonServer.create/Client.connectrejects oversized or deeply nested JSON (maxEnvelopeBytes,maxEnvelopeDepth). - This is not authentication or encryption; do not expose UDS endpoints to untrusted networks.
Publish tarball is built with tsdown; prepublishOnly runs format check, tests, and build.