Handlers
A handler is a plain JavaScript module. The broker loads it once, opens its port, and calls its exports as traffic arrives. What a module must export is fixed by its transport — TCP or UDP — read from the filename. Proposed the signatures below.
The filename grammar
A module is an entry point only if it is a top-level file whose name matches one of these patterns:
<name>.tcp.<port>.port.js a TCP handler on <port>
<name>.udp.<port>.port.js a UDP handler on <port>
| Token | Rule |
|---|---|
<name> | A label. Surfaces in logs and as ctx.name;
has no effect on what is opened. Must be a valid path segment (see
Files). |
tcp / udp | The transport. Selects the contract the module must satisfy. |
<port> | Integer 1–65535.
Authoritative — exactly the port opened on the OS firewall. |
| One handler per protocol+port. If two files claim the same pair, the later one is refused and logged; the first keeps the port. | |
TCP — handle(conn, ctx)
Exported by a .tcp. module. Called once per accepted connection, after the broker has
screened it against the access rules. conn is a Node
socket-like duplex stream — read and write it, listen for data, end,
error, close; end it with conn.end() or drop it with
conn.destroy(). You never call listen() and never set up TLS.
// echo.tcp.7000.port.js
export function handle(conn, ctx) {
conn.setEncoding("utf8");
conn.write(`hello from :${ctx.port}\n`);
conn.on("data", (chunk) => conn.write(chunk));
conn.on("error", () => conn.destroy());
}
handle may be async — the broker does not wait on the returned promise;
it is your handle on the connection's lifetime.
UDP — message(msg, rinfo, ctx)
Exported by a .udp. module. UDP is connectionless — there is nothing to hand off — so
the broker binds the socket, checks each datagram's source, and calls message per
datagram. msg is a Buffer; rinfo is the sender
({ address, port, family, size }). Reply with the broker-provided
ctx.reply(buf, rinfo), which uses the already-bound listening socket.
// ping.udp.9000.port.js
export async function message(msg, rinfo, ctx) {
const n = (await ctx.shared.get("count")) ?? 0;
await ctx.shared.set("count", n + 1);
ctx.reply(Buffer.from(`pong #${n + 1}\n`), rinfo);
}
Lifecycle — open(ctx) / close(ctx)
Both are optional and apply to either transport.
| Export | When | Use for |
|---|---|---|
open(ctx) | Once, when the module is loaded — before the first connection or datagram. | Seeding ctx.shared, warming a cache, subscribing to
a channel. May be async; the broker awaits it before delivering traffic. |
close(ctx) | Once, when the module is being unloaded — on shutdown or a reload that replaces it. | Releasing whatever open acquired. Also observe
ctx.signal to stop in-flight work. |
The ctx object
Every export receives the same shape. Proposed its fields:
| Field | Type | Meaning |
|---|---|---|
ctx.name | string | The <name> from the
filename. |
ctx.port | number | The port this handler serves. |
ctx.protocol | string | "tcp" or
"udp". |
ctx.shared | object | The broker's in-memory shared store — get/set/delete plus publish/subscribe, visible across connections and handlers. Full API on Shared. |
ctx.signal | AbortSignal | Fires when the handler is shutting
down or being reloaded. Pass it to timers, loops and fetch so in-flight work aborts
cleanly. |
ctx.reply | function | UDP only — reply(buf, rinfo)
sends a datagram back over the listening socket. |
Concurrency
The module is loaded once and that instance serves many concurrent
connections or datagrams — the broker simply calls the export again for each. There is no
per-connection module instance. Anything that must be shared between calls belongs in
ctx.shared (durable to the process) or, for durability beyond the process, in
datahoster — not in a module-level variable you expect the
broker to isolate.