Skip to content

Guide list | Previous: Java | Next: Python

Node.js Binding Guide (@zlink-systems/zlink)

Contract-owning document for this chapter — the Node.js bindings spec covers it. This chapter shows that contract as working sample code.

Explains how to use zlink in Node.js through working sample code. See the core guide for messaging concepts.


Installation

npm install @zlink-systems/zlink
  • Node.js 22 or later.
  • The native core is bundled as a per-platform prebuild.
const zlink = require('@zlink-systems/zlink');
// or ESM / TypeScript
import * as zlink from '@zlink-systems/zlink';

5-Minute Example

const zlink = require('@zlink-systems/zlink');

// Server
const ctx = zlink.createContext();
const server = zlink.createPairSocket(ctx);
server.bind('tcp://127.0.0.1:5555');

const received = new zlink.Received();
server.recv(received);
console.log(received.parts[0].data().toString()); // PING
received.close();

await server.send().message(Buffer.from('ACK')).submit().admitted;

server.close();
ctx.close();
// Client
const ctx = zlink.createContext();
const client = zlink.createPairSocket(ctx);
client.connect('tcp://127.0.0.1:5555');

await client.send().message(Buffer.from('PING')).submit().admitted;

const received = new zlink.Received();
client.recv(received);
console.log(received.parts[0].data().toString()); // ACK
received.close();

client.close();
ctx.close();

Core Types

Context

const ctx = zlink.createContext();
// always close after use — this interrupts blocking operations on child sockets
ctx.close();

Message

The Node binding uses Buffer directly as a message. message() makes a copy, so you're free to reuse the original Buffer.

await socket.send().message(Buffer.from('hello')).submit().admitted;
await socket.send().message(Buffer.from([0x01, 0x02])).submit().admitted;

// access the payload after receiving
const received = new zlink.Received();
socket.recv(received);
const data = received.parts[0].data();   // Buffer
const text = data.toString('utf8');
received.close();

HWM-managed sends provide asynchronous submit() and synchronous submit_sync() terminals. On Node's event loop, use the result-object-returning submit() by default; it returns a SendSubmission (result: OK|BACKPRESSURED as a synchronous field, admitted: Promise<void>), uses DONTWAIT, and settles admission from the socket completion queue. submit_sync() blocks in Core until local admission.

const send = socket.send().message(Buffer.from('data')).submit(); // result object
if (send.result === SubmitResult.BACKPRESSURED) await send.admitted; // wait only at HWM
socket.send().message(Buffer.from('data')).submit_sync();  // synchronous Core admission

Request provides submit_sync() to block until the reply and submit() to return a RequestSubmission (result, admitted, plus reply: Promise<Message[]>). When result is OK you can await reply directly; the reply is that terminal result, not DATA received separately.

Core owns retry after accepting a pre-admission operation; do not add a caller retry queue or resubmit its payload. The shared native ZLINK_OPT_PENDING_MAX_MSGS/BYTES limits cover pending SEND and REQUEST, with no send-only pending names. Completion confirms local admission, not peer delivery or an application acknowledgement.

Before submit, cancellation means omitting the call. Node exposes no public Core cancel after a successful submit; abandoning a Promise only stops caller observation, while the socket owner drains the late completion. Set stream.options.recvMode to zlink.StreamRecvMode.Raw or .Packet before bind/connect, then use recv or recvPacket respectively.

If a public poller owns zlink.PollEventFlag.PollCompletion for a socket, keep another thread calling wait() while a blocking request or Promise is pending. wait() drains native completions and settles or cleans Node state; calling a blocking terminal between waits on the same thread can stall it.

Received — the receive envelope

const received = new zlink.Received();
socket.recv(received);                  // synchronous, blocking
try {
  const parts = received.parts;         // Message[]
  const rid = received.routingId;       // RoutingId or null
  const token = received.replyToken;    // ReplyToken or null on ROUTER request
} finally {
  received.close();
}

Routing ID

const rid = zlink.RoutingId.from(Buffer.from('server-01'));
socket.setRoutingId(rid);

Ownership And Lifetime

Situation Rule
submit() succeeds the passed Buffer is copied internally, so you can reuse the original
recv() succeeds received.close() is required (prefer a finally block)
submit() (Promise) completes close each reply part with part.close()
ctx.close() interrupts blocking operations on child sockets
const received = new zlink.Received();
socket.recv(received);
try {
  // process parts
} finally {
  received.close();
}

Error Handling

The Node binding throws per-operation error classes.

try {
  await socket.send().message(Buffer.from('data')).submit().admitted;
} catch (error) {
  if (error instanceof zlink.SubmitError) {
    if (error.result === zlink.SubmitResult.Backpressured) {
      // retry
    } else {
      throw error;
    }
  }
}

Error classes: SubmitError, RequestError, RecvError, BindError, ConnectError, ConfigError, CloseError, HandlerError. Each exposes the result code via a .result property.

Share / Move / Clone (copy / move / clone)

Three explicit Message payload operations, with the same name and meaning across every binding, mapping 1:1 to the Core C API (zlink_msg_copy/zlink_msg_move).

Operation Signature Meaning When
copy() copy(): Message ref-count share — new Message on the same buffer, original stays valid keep the same payload while still using the original
move(dest) move(dest: Message): void ownership transfer — hands off to dest, caller left empty re-send a received message with no copy (relay/echo)
clone() clone(): Message deep copy — independent buffer mutate the duplicate independently
const shared = msg.copy();
await socket.send().message(shared).submit().admitted;  // shared is consumed
// msg is still valid

const out = new zlink.Message();
receivedPart.move(out);                         // receivedPart becomes empty
await socket.send(routingId).message(out).submit().admitted;

const dup = msg.clone();

⚠ Breaking change: the former copy() was a deep copy; now copy() is a ref-count share and the deep copy moved to clone(). JS cannot host the same signature with two return meanings, so no alias is possible — this is a major-version break. Replace deep-copy-intent copy() calls with clone().

Note (refcount timing): Node exposes payload as a Buffer. While an exposed Buffer is alive, the native storage is reclaimed when that Buffer is GC'd. So after copy() shares handles and you close() one, refCount() does not drop to 1 immediately — it reflects after the buffer is GC'd (diagnostic only; no effect on behavior, safety, or ownership independence).


C API Mapping

C API Node API
zlink_ctx_new() zlink.createContext()
zlink_ctx_term() ctx.close()
zlink_socket(ctx, type) zlink.createPairSocket(ctx), etc.
zlink_bind(s, ep) socket.bind(ep)
zlink_connect(s, ep) socket.connect(ep)
zlink_send(..., parts, count, ...) / zlink_send_rid(..., parts, count, ...) + NONE socket.send().message(buf).submit_sync()
DONTWAIT send + completion pull await socket.send().message(buf).submit().admitted
zlink_recv(..., parts_out, capacity, count_out, ...) socket.recv(received)
zlink_msg_data(msg) part.data() (Buffer)
zlink_routing_id_t zlink.RoutingId
zlink_socket_monitor_open(...) socket.monitorOpen([...])
zlink_poller_new() zlink.createPoller()
zlink_timer_new() zlink.createTimer()

Native Library / Deployment

The native core ships in the package as a per-platform prebuild. Works with just npm install, no separate build step.

const [major, minor, patch] = zlink.version(); // [number, number, number]
console.log(`zlink ${major}.${minor}.${patch}`);

Threading notes. Node uses a single-threaded event-loop model.

Item Rule
Context / sockets used on the main event loop
Blocking recv() blocks the event loop, so keep it short or prefer non-blocking + poller
Synchronous submit_sync() stops the event loop while waiting for HWM admission — do not use it on the loop
Async submit() Promise-based — doesn't block the event loop

Do not run a blocking send on Node's event loop. await the asynchronous submit() terminal; use submit_sync() only on a suitable worker thread.


Samples

Verified samples live under bindings/node/samples/.

File Description
pair_recv_sample.ts PAIR send/receive
dealer_router_recv_sample.ts DEALER/ROUTER send/receive
request_reply_sample.ts Request/reply
pubsub_recv_sample.ts PUB/SUB publish/subscribe
stream_recv_sample.ts STREAM raw TCP
stream_packet_sample.ts STREAM PACKET pull
monitor_recv_sample.ts Monitor event receive

SPOT/Actor examples are covered by the framework samples, not the core binding — see the service links under See Also below.

cd bindings/node
npm run build
node dist-tools/samples/pair_recv_sample.js

JavaScript

JavaScript uses the Node binding (@zlink-systems/zlink) as-is, with no separate native binding. The installation, core types, ownership, errors, and mapping table above all apply identically, minus the TypeScript type annotations.

  • Dependency: @zlink-systems/zlink (same as above). No TypeScript build step needed — require it directly as plain .js.
const zlink = require('@zlink-systems/zlink');

const ctx = zlink.createContext();
const socket = zlink.createPairSocket(ctx);
// ... after use: socket.close(); ctx.close();
  • Ownership: clean up with explicit close() calls (same as Node — doesn't rely on GC).
  • Samples: bindings/javascript/samples/ (.js) has the same canonical set as the Node samples. Build the Node binding, then run directly with node.
cd bindings/node && npm run build      # build the shared runtime
cd ../javascript/samples
node pair_recv_sample.js                # or ./run_samples.sh

The core guide's language tabs have a dedicated JavaScript column, so you can see messaging/service usage directly in JavaScript code.


See Also

Socket patterns - Socket pattern overviewPAIR · PUB/SUB · DEALER · ROUTER · STREAM · Proxy

Services - Framework service overview

Operations - Socket options - TLS security - Monitoring - Thread safety - Message API - Routing ID