Skip to content

Spec index | Previous: Java | Next: Python

Node / TypeScript Binding Implementation Blueprint

What this chapter defines — the contracts/runtime shape the Node/TypeScript library must have, and the package export boundary.

This document defines the shape the Node/TypeScript library must have. It is not an exhaustive list of every class or type member. The concrete public contract is the package-root export declared by bindings/node/src/index.ts, the package.json exports, and the generated .d.ts surface.

The source package tree, package exports, .d.ts types, tests, samples, perf runners, and runtime project core/include/zlink.h capabilities into a TypeScript-idiomatic API. This document defines where each public contract, runtime implementation, and native bridge helper is owned.

This binding follows This binding follows the shared bindings architecture map with TypeScript naming conventions. It uses lower-case contracts and runtime source folders, and package exports decide what is public. It does not copy capitalized .NET or C++ folder names into the Node package as-is.

Node follows the .NET design shape after alignment. Native-backed resource behavior is described by public contract interfaces and types under src/zlink/contracts; runtime implementations live under src/zlink/runtime and are obtained through package-root factory functions. Concrete value classes, DTO-shaped objects, enums, literal unions, results, and errors stay in the contract source.

The first code a reviewer should read is the public contract under src/zlink/contracts, the same way the .NET binding blueprint starts from Contracts/. Runtime files must implement that contract; they must not be the place where callers discover new behavior.

The binding uses the same architecture map while keeping TypeScript and Node conventions: lower-case folders, camelCase methods, PascalCase public types, structural interfaces where TypeScript makes that clearer, small DTO-shaped results as plain objects where that is clearer, and package-root exports as the consumer surface. It does not copy C# interface prefixes, namespace casing, or file names when a TypeScript idiom is clearer.

Section Covers
Public Contract Source Export projection, contract source location, package boundary
Repository Layout The aligned directory tree and lower-case folder rules
API Change Principles Public mapping and contract/runtime boundary requirements
Library Shape Resources/roles that need interface-first definitions
Contract / Runtime Placement Rules The boundary between public declarations and runtime implementation
Contract Category Map Category-to-folder mapping
Contract File Layout Files by category under contracts/
Runtime File Layout Files by category under runtime/, and alignment-failure examples
Construction Entry Points The list of package-root factory functions
Function Naming Rules camelCase, canonical action names, handler-registration rules
Canonical Interface Rules recv signatures, builders, and submit rules
Public Entry Shape Domain grouping of the package entrypoint
64-bit Byte HWM and Monitoring Contract bigint HWM representation and monitor snapshot fields
Receive flow state The receive-flow state type, setter, and monitor surface
Required Capability Coverage User-facing capabilities that alignment must guarantee
Spot Get-Or-Create The getOrCreateSpot contract
Receive and Subscribe Shape Caller-provided storage and no-data distinction
Error and Validation Policy Validation timing and error structuring
Performance Policy Hot-path constraints
Implementation Checklist Pre-alignment checks and required verification commands
Actor and Spot Route Results Route result types, and Actor-targeted send/request

Public Contract Source

  • Public contract projection: bindings/node/src/index.ts, the generated .d.ts, and package.json exports.
  • Contract source: bindings/node/src/zlink/contracts/.
  • Package projection: symbols exported from the package entrypoint and declared in the published TypeScript definitions.
  • Internal implementation: native addon modules, private source modules, N-API handles, completion-drain owners and provisional registries, converters, and whole-message array helpers.
  • Package boundary: package.json exports expose only documented public entrypoints.
  • Documentation role: this README defines shape and semantic coverage. The package entrypoint and declarations own the exact public member list.

Deep imports into source files or native bridge modules are not public API.

Repository Layout

Use these paths consistently when changing the Node/TypeScript binding.

  • Public entrypoint: bindings/node/src/index.ts.
  • Contract source: bindings/node/src/zlink/contracts/.
  • Runtime implementation: bindings/node/src/zlink/runtime/.
  • Native bridge/artifacts: bindings/node/src/zlink/runtime/native/, bindings/node/native/, bindings/node/prebuilds/, and generated runtime loading code.
  • Generated output: bindings/node/dist/. Not contract source.
  • Codec distribution scope: follow the common raw payload policy.
  • Tests: bindings/node/tests/.
  • Samples: bindings/node/samples/.
  • Perf: bindings/node/perf/.

  • package.json exports and the generated .d.ts file must agree with the public entrypoint. Do not document or test deep source imports as public API.

  • index.ts, the published .d.ts file, and package.json exports are the contract's TypeScript package projection.
  • Do not expose deep source paths as public API unless they are deliberately listed in package.json exports.
  • Use lower-case source directory names. Do not create src/zlink/Contracts or src/zlink/Runtime; those names could be mistaken for a public deep-import surface.
  • src/zlink/contracts owns public TypeScript types, classes, builders, enums, errors, and factory return contracts. The package entrypoint or a runtime factory module owns the factory implementation.
  • src/zlink/runtime owns native-backed runtime implementations, native addon calls, handle owners, completion-drain owners and provisional registries, marshalling, and platform loading.

The following tree is the aligned implementation structure.

File granularity follows the common policy in ../README.md. Keep one file per independent public concept or tight operation/model group. Very small type aliases, callback types, enum-only files, and pass-through helper modules are merged into the nearby contract file to keep the public shape readable.

bindings/node/
+-- src/
|   +-- index.ts
|   +-- zlink/
|   |   +-- contracts/
|   |   |   +-- core/
|   |   |   |   +-- context.ts
|   |   |   |   +-- zlink.ts
|   |   |   |   +-- routing_id.ts
|   |   |   +-- messaging/
|   |   |   |   +-- message.ts
|   |   |   |   +-- received.ts
|   |   |   |   +-- topic_message.ts
|   |   |   |   +-- subscription_event.ts
|   |   |   +-- sockets/
|   |   |   |   +-- socket.ts
|   |   |   |   +-- pair_socket.ts
|   |   |   |   +-- dealer_socket.ts
|   |   |   |   +-- router_socket.ts
|   |   |   |   +-- pubsub_sockets.ts
|   |   |   |   +-- stream_socket.ts
|   |   |   |   +-- socket_options.ts
|   |   |   |   +-- socket_operations.ts
|   |   |   +-- eventing/
|   |   |   |   +-- monitor.ts
|   |   |   |   +-- poller.ts
|   |   |   |   +-- timer.ts
|   |   |   +-- service/
|   |   |   |   +-- spot/
|   |   |   |   |   +-- spot_node.ts
|   |   |   |   |   +-- spot.ts
|   |   |   |   |   +-- actor.ts
|   |   |   |   |   +-- spot_operations.ts
|   |   |   |   |   +-- spot_models.ts
|   |   |   +-- errors/
|   |   |   |   +-- errors.ts
|   |   |   |   +-- results.ts
|   |   +-- runtime/
|   |   |   +-- core/
|   |   |   |   +-- context.ts
|   |   |   |   +-- context_options.ts
|   |   |   |   +-- runtime_info.ts
|   |   |   +-- handles/
|   |   |   |   +-- native_handle.ts
|   |   |   |   +-- lifetime.ts
|   |   |   +-- messaging/
|   |   |   |   +-- message_materializer.ts
|   |   |   |   +-- request_progress.ts
|   |   |   +-- buffers/
|   |   |   |   +-- message_conversion.ts
|   |   |   |   +-- buffer_policy.ts
|   |   |   +-- sockets/
|   |   |   |   +-- socket_base.ts
|   |   |   |   +-- socket_options.ts
|   |   |   |   +-- socket_operations.ts
|   |   |   |   +-- pair_socket.ts
|   |   |   |   +-- dealer_socket.ts
|   |   |   |   +-- router_socket.ts
|   |   |   |   +-- pub_socket.ts
|   |   |   |   +-- sub_socket.ts
|   |   |   |   +-- xpub_socket.ts
|   |   |   |   +-- xsub_socket.ts
|   |   |   |   +-- stream_socket.ts
|   |   |   +-- eventing/
|   |   |   |   +-- monitor_socket.ts
|   |   |   |   +-- poller.ts
|   |   |   |   +-- poll_events.ts
|   |   |   |   +-- timer.ts
|   |   |   +-- options/
|   |   |   |   +-- option_mapping.ts
|   |   |   |   +-- validation.ts
|   |   |   +-- service/
|   |   |   |   +-- spot/
|   |   |   |   |   +-- spot_node.ts
|   |   |   |   |   +-- spot.ts
|   |   |   |   |   +-- actor.ts
|   |   |   |   |   +-- spot_operations.ts
|   |   |   +-- errors/
|   |   |   |   +-- native_errors.ts
|   |   |   +-- native/
|   |   |   |   +-- native.ts
|   |   |   +-- internal/
|   |   |   |   +-- request_pump.ts
|   |   |   |   +-- service_mapping.ts
+-- native/
+-- tests/
+-- samples/
+-- perf/
+-- prebuilds/
+-- dist/

The package-root export is the consumer entrypoint. Tests, samples, and perf import from that entrypoint or another documented package export, not from src/zlink/runtime, native addon modules, or generated private files. If a symbol appears in the package root or the generated .d.ts, a reviewer must be able to point to its owner under src/zlink/contracts or the package-root entrypoint.

API Change Principles

The public projection of a Core capability follows these principles:

  1. Add the public symbol to the correct contract source category.
  2. Update the package entrypoint, declaration surface, and package.json projection.
  3. Keep native addon calls, N-API handles, and request progress helpers behind private modules.
  4. Choose a class, interface, type alias, literal union, or plain-object shape according to normal TypeScript usage.
  5. Add runtime tests and type-surface tests against the package entrypoint.
  6. Update samples and perf only through public imports.
  7. Confirm the generated dist and .d.ts output do not expose private bridge modules.

The contract/runtime boundary meets these requirements:

  1. Move public behavior declarations to src/zlink/contracts/<category>/.
  2. Move native-backed runtime implementations to src/zlink/runtime/<category>/.
  3. Keep native addon loading and N-API calls under src/zlink/runtime/native/.
  4. Public code constructs resources through package-root factories or contract methods.
  5. Package exports do not expose runtime modules as public API.
  6. Deprecated wrappers, duplicate overload families, and compatibility-only naming aliases are not part of the public surface.
  7. Tests, samples, and perf import from the package root only.
  8. Generated declarations in dist/index.d.ts carry the contract surface, not runtime implementation modules.

The following Node-specific shortcuts are not allowed.

  • src/zlink/contracts does not re-export runtime handle modules.
  • Contract files do not import runtime resource classes to describe public service models.
  • A public runtime aggregate such as runtime/handles/canonical.ts does not remain the source of public resource behavior. Split those declarations into named contract files and resource-named runtime implementation files.
  • src/index.ts exports package contract names and factories, not runtime implementation modules.
  • package.json does not expose runtime, native, generated, or private source subpaths.
  • Generated declarations do not mention runtime implementation module paths as public types.

Library Shape

This binding feels like a TypeScript package with a native backend.

  • Native-backed resource behavior contracts are public TypeScript interfaces under src/zlink/contracts.
  • Native-backed runtime implementations live under src/zlink/runtime. They are not package exports and not construction entrypoints.
  • Public contract files must be readable without opening runtime files. A reviewer can understand callable methods, return values, lifecycle, error behavior, and builder shape from contracts/ alone.
  • Resource contracts expose close() or an equivalent lifecycle method.
  • Values such as message, routing id, received metadata, topic message, snapshots, options, enums, literal unions, and errors stay concrete or structural following normal TypeScript convention.
  • Operation builders use public contract interfaces because they hide staged native request state and multipart accumulation.
  • Native addon handles, raw pointers, callback userdata, request pumps, and whole-message array handling are never exposed.

Do not introduce an interface for a pure DTO/value object only for symmetry. Message, RoutingId, Received, TopicMessage, route results, snapshots, option objects, enums, literal unions, and errors remain concrete or structural public values.

Define a public TypeScript interface for each of the following native-backed resources and roles before writing or exposing a runtime class.

  • Context.
  • Socket roles: common socket behavior, PairSocket, DealerSocket, RouterSocket, PubSocket, SubSocket, XPubSocket, XSubSocket, and StreamSocket.
  • Eventing roles: MonitorSocket, Poller, poll event source, Timer, Stopwatch, and AtomicCounter. Spot, Actor.
  • Operation builders: send, routed send, request, reply, publish, channel send/request, SPOT send/request/reply, actor create, actor join, and actor join reply builders.
  • Application handler roles: SPOT dispatch handler and route handler.

The runtime class that implements a role may have a private or unexported name, but the package-root factory and the generated declaration must use the public contract interface name.

Perf and samples do not rely on undocumented deep import paths to reach native objects faster.

Contract / Runtime Placement Rules

  • Exported TypeScript classes, interfaces, type aliases, error types, and builder contracts belong in src/zlink/contracts or the package entrypoint.
  • Exported package functions, static helper types, convenience method contracts, and builder helper contracts belong in contract source when a caller can use them directly.
  • Factory return types and callable factory signatures belong to the public contract. Factory implementation goes in the package entrypoint or a runtime factory module so contract files do not import runtime implementations.
  • JavaScript runtime implementations, native handle owners, request pumps, callback adapters, and whole-message array helpers belong in src/zlink/runtime.
  • N-API bindings, native addon handles, marshalling helpers, and platform loading code belong in src/zlink/runtime/native.
  • Package exports and the published .d.ts file must project contract source; they do not expose runtime modules.
  • Runtime concrete classes are construction targets behind package-root factories. Callers do not import runtime modules directly.
  • Do not export src/zlink/runtime/* from src/zlink/contracts or src/index.ts. src/index.ts may import runtime modules only to wire package-root factories. A runtime implementation type may satisfy a public contract, but the exported type name comes from contract source.
  • Package-root factories declare contract return types explicitly. For example, createContext(): Context returns the public contract type even though it instantiates a runtime implementation.

Contract Category Map

src/zlink/contracts is the source-ownership map for the package entrypoint and the published TypeScript declarations.

  • core/: context, context options, routing id, version/capability lookup helpers, and utility contracts.
  • messaging/: Message, received metadata, topic messages, subscription events, stream packet data, and builder payload helpers.
  • sockets/: socket behavior, socket families, typed options, and request/reply and publish/subscribe surfaces.
  • eventing/: monitor, monitor snapshot/event, poller, poll events, timer, and public poll helpers.
  • service/: SPOT node, SPOT handle, topology model, Actor reference, Actor lifecycle, and operation builders.
  • errors/: typed error classes or tagged error domains.
  • Enum, flag, result, and literal-union types live in the category that defines their meaning. Do not create an enums folder just to group them syntactically.

Contract File Layout

Contract source keeps the same classification as the .NET binding blueprint, with TypeScript naming. It keeps the same conceptual file grouping so a developer who knows the .NET binding can find the same public concept in Node quickly. The folder map is shared with .NET, but the names inside it stay idiomatic TypeScript.

  • core/: context.ts, zlink.ts, routing_id.ts, and core option/value files.
  • messaging/: message.ts, received.ts, topic_message.ts, subscription_event.ts, and common operation payload types.
  • sockets/: socket interfaces, socket option types, send/request/reply builder contracts, stream packet values, and socket flags.
  • eventing/: monitor, monitor event/status, poller, poll events, and timer contracts.
  • service/: a spot/ subfolder holding SPOT node, Spot, Actor, topology model, and service operation builders. Use named files such as spot_node.ts, spot.ts, actor.ts, and spot_operations.ts, and group model files with their service domain.
  • errors/: public error classes, result domains, and error-code mapping.

Do not collect public resource behavior into one aggregate models.ts or a runtime-export barrel. Small DTO-shaped objects and literal unions can be grouped with the contract that gives them meaning, but native-backed resources and operation builders need named contract files.

Runtime File Layout

Runtime source follows the runtime classification in the .NET binding blueprint, but holds only implementation. Node runtime file names use the same lower-case TypeScript concept names as the contract tree. Do not use a default_ filename prefix such as default_context.ts or default_pair_socket.ts. In this package, every file under src/zlink/runtime is already the native-backed implementation side of the contract/runtime split, so a file name should describe the resource or operation it implements, not the fact that it is an implementation.

  • core/: context.ts, context_options.ts, and runtime helper functions such as version/capability lookup wrappers.
  • handles/: native handle owners, lifetime checks, close/dispose state, and reference tracking.
  • messaging/: message materialization, request progress, request execution, and multipart progress helpers.
  • buffers/: message conversion, buffer ownership, copy/borrow policy, and pooled/pinned storage helpers.
  • sockets/: socket_base.ts, socket_options.ts, socket_operations.ts, and one implementation file per socket family — pair_socket.ts, dealer_socket.ts, router_socket.ts, pub_socket.ts, sub_socket.ts, xpub_socket.ts, xsub_socket.ts, and stream_socket.ts.
  • eventing/: monitor_socket.ts, poller.ts, poll_events.ts, timer.ts, and related event materialization helpers.
  • options/: option validation and native option id/value mapping shared by context, sockets, and services.
  • service/: SPOT node, Spot, Actor, topology, and service operation implementations. Use a spot/ subfolder once the implementation grows large enough.
  • errors/: native error translation and validation helpers.
  • native/: native addon loading, platform lookup, and N-API binding surface.
  • internal/: only small private glue that does not fit a standard .NET runtime classification. Do not put handle ownership, buffer policy, option mapping, native declarations, or public resource behavior here when a standard classification already exists.

Runtime files may import contract types, but contract files do not import runtime files. The package root may instantiate native-backed runtime implementations in factories, but it exports contract names, not runtime implementation modules.

Category files such as runtime/sockets/sockets.ts, runtime/service/service.ts, runtime/eventing/eventing.ts, and runtime/core/index.ts are allowed only as small barrels. They may re-export nearby implementation files or define factory wiring that stays internal to runtime, but they do not hold native-backed resource class bodies, operation builders, or marshalling logic. If a reviewer must read a category aggregate to understand how RouterSocket, SpotNode, or Poller behaves, the file split is not aligned.

Runtime implementation file names describe the resource or operation they implement, not the fact that they are native-backed. Use router_socket.ts, spot_node.ts, poller.ts, and timer.ts, not default_router_socket.ts, default_spot_node.ts, or default_poller.ts.

A shared helper must not become a second public implementation aggregate. runtime/internal/* may own narrow private glue that crosses several runtime categories, but it does not own public resource behavior and does not hide a standard .NET runtime category. Native handle ownership belongs in runtime/handles, buffer conversion in runtime/buffers, option mapping in runtime/options, native addon declarations in runtime/native, and public resource behavior in a resource runtime file such as sockets/router_socket.ts or service/spot/spot_node.ts.

Shared helper files under a category follow the same rule. A file such as runtime/sockets/socket_common.ts may hold narrow socket helper types or a private base utility, but it does not hold several unrelated concerns at once. If operation builders, monitor socket behavior, routing helpers, marshalling helpers, and concrete resource behavior all sit in one file, that file has become a hidden aggregate and must be split into socket_base.ts, socket_options.ts, socket_operations.ts, and smaller internal helpers.

The following shapes are explicit alignment failures.

  • runtime/service/service.ts holds the SpotNode, Spot, and Actor implementations in one file.
  • runtime/eventing/eventing.ts holds the monitor socket, poll events, poller, timer, stopwatch, and counter implementations in one file.
  • runtime/core/context.ts holds context, context options, and unrelated runtime helper implementation in one file.
  • runtime/core/runtime_info.ts holds a copied implementation prelude, or socket/service behavior, just to reach its helper functions.
  • runtime/sockets/socket_common.ts holds operation builders, monitor socket behavior, route helpers, message conversion, and base socket behavior all in one large file.
  • runtime/internal/* owns public resource behavior instead of a private helper mechanism.
  • runtime/internal/* owns handle lifetime, buffer conversion, option mapping, native addon declarations, or error mapping that belongs to a standard .NET runtime classification.

Construction Entry Points

Interfaces define behavior; construction is provided by package-root factories and public contract methods.

  • createContext() creates a runtime context implementation.
  • Context.createPairSocket(), createDealerSocket(), createRouterSocket(), createPubSocket(), createSubSocket(), createXPubSocket(), createXSubSocket(), and createStreamSocket() create runtime socket implementations. Service-layer implementations are created accordingly.
  • A Spot handle is obtained through SpotNode.createSpot(), entrySpot(), getOrCreateSpot(...), or spotLookup(...). Direct Spot construction is not public.
  • An Actor handle is created through SpotNode.createActor(...). Direct Actor construction is not public.
  • createPoller(), createTimer(), and createTimer(spot) create eventing resources.
  • Pollable is BaseSocket | SocketMonitor | Timer | number, and Poller.add/modify/remove provide SocketMonitor overloads (common spec "Monitor sources in Poller"). Only PollEventFlag.PollIn is valid for a socket monitor; any other readiness mask is rejected with a typed ConfigResult.InvalidArgument. Drain with monitor.recv(RecvFlags.DontWait) after readiness; PollEvents.source(index) returns the registered monitor object.
  • A socket registers a receive readiness notification handler with setReadableHandler(handler). Node runs a single event loop, so Poller.wait blocks it; readiness is delivered through the Node event loop instead (see "Receive Readiness" below).
  • Package-root factory/helper functions such as version, capability lookup, strerror, proxy, sleep, and multipart-cleanup helpers are public contract functions. The native calls behind them stay in runtime modules.

Direct construction of a native-backed runtime class is not part of the aligned contract. Factories are the stable construction surface.

Receive Readiness

Node runs on a single event loop, so it cannot use the blocking readiness wait the other bindings use. Poller.wait is synchronous and blocks that loop, and fixed interval timer polling adds at least a millisecond to every round trip. A socket therefore exposes a readiness notification registered on the Node event loop.

export type ZLinkReadableHandler = () => void;

// BaseSocket
setReadableHandler(handler: ZLinkReadableHandler): void;
  • This is a readiness notification, not a message count. One call does not say how many records arrived. The caller drains with recv(RecvFlags.DontWait) until the no-data representation appears. It is the same axis as "Dispatch readiness semantics" in the common spec, and it must not be described or implemented as an edge-triggered one-shot.
  • The handler takes no argument. The only fact to deliver is "something is readable now." If readiness watching itself fails, the socket's next receive surfaces the typed failure. The handler is not given an error argument.
  • No unregistration surface is added. Clearing a callback by assigning null is forbidden by this document's callback registration rule. A registered handler is released when the socket closes.
  • An active handler keeps the Node event loop alive. The process does not exit while the socket is open and waiting to receive, which is the intended behaviour for a server. Close the socket to stop waiting.
  • Registering twice on the same socket replaces the earlier handler. One readiness handler per socket.
  • This surface does not replace Poller. Waiting on several sources in one place remains Poller's job, and the number (raw fd) member of Pollable stays.

The canonical name is setReadableHandler; other bindings use the same canonical name in their own casing (common spec "Function naming rules").

Function Naming Rules

Function names follow the shared binding meaning rules in ../README.md, using TypeScript spelling.

  • Use camelCase for methods and functions.
  • Use the same canonical action names as the other bindings, differing only in case: send, request, reply, publish, subscribe, unsubscribe, recv, recvRouted, receiveSubscriptionEvent, recvPacket, setDispatchHandler, getOrCreateSpot, sendToChannel, requestToChannel, sendToSpot, and requestToSpot.
  • Do not keep an alias only for compatibility. When another name conflicts with the canonical meaning, expose the canonical TypeScript name.
  • Do not create operation-start variants such as sendNoWait, publishWithFlags, or requestAsync. Keep a single operation name. Send uses submit_sync() and submit(). Request uses submit_sync() and submit(), with the request timeout on the builder.

Canonical Interface Rules

  • Data-plane recv, routed recv, subscribe, and subscription-event receive fill a caller-provided Received, TopicMessage, or SubscriptionEvent object and return boolean.
  • Send, routed send, publish, request, reply, SPOT operations, and Actor location/session operations return a fluent builder.
  • A builder start method takes only the target identity, topic, channel, routing ID, or ReplyToken. Payload and the options supported by that operation are builder steps.
  • SPOT channel-targeted operations use sendToChannel(...) and requestToChannel(...). SPOT topic publish stays publish(topic).
  • Do not add a single-payload shortcut overload sharing a name with an operation start method. send(message), send(routingId, message), publish(topic, message), sendToChannel(channel, message), and sendToSpot(..., message) are not public contract members. Callers use send(...).message(message).submit().
  • Multipart payload is accumulated by repeated message(...) calls. A messages(...) convenience is allowed when it delegates to the same builder contract and is declared in contract source.
  • A Dealer socket does not expose protocol envelope helpers such as requestFrame(...) or reply(requestToken, parts). A dealer can start a request through request(), but it has no API-level peer routing id, so it cannot reply to an arbitrary token.
  • Node Buffer / Uint8Array payload input is copied into message-owned native storage before the native queue could outlive the call. Do not expose or use a borrowed-Buffer send helper such as socketSendBorrowedNoWaitResult.
  • The message payload factory is Message.from(...). The public TypeScript contract does not require the caller to use new Message(...) to create a payload.
  • A receive wrapper may return to a bounded isolate-local pool only after its owner envelope removes part references and completes close(). The Message reference is invalid when cleanup returns. The caller must not use it again, including repeated close(), payload access, or identity-based Map/WeakMap lookup. A wrapper that has not been returned is not reused for another ownership.
  • Operation-start naming follows the Function Naming Rules above. A builder's terminal method keeps using submit(...) even though it now returns a result object. Do not add a separate submitAsync terminal name.
  • PAIR, DEALER, ROUTER, and STREAM send use one SendOperation family that captures the target. submit_sync() uses Core NONE; submit() uses Core DONTWAIT completion.
  • Node has a single JS thread, so while a synchronous terminal runs, that call is the completion consumer. The designated owner (the public poller) cannot progress concurrently on the same thread. Other completions the synchronous call receives are handed to the owner's drain rule (resubmit after NO_DATA) once it returns.
  • DEALER/ROUTER request provides submit_sync(): Message[] and submit(): RequestSubmission (result, admitted, plus reply: Promise<Message[]>) and retains the builder's reply timeout.
  • The terminal for a raw ROUTER/Received reply is the synchronous one-shot ReplySubmitOperation.submit(): void. It returns no Promise and submits a terminal reply or error reply with one native call. A DEALER peer is subject to Application HWM (the queue byte threshold), PAUSED, and SNDTIMEO, so the result can be BACKPRESSURED; a ROUTER peer uses the HWM-free Completion connection. NOT_CONNECTED, TERMINATED, INVALID_ARGUMENT, and other submit failures are thrown immediately as SubmitError.
  • PUB/XPUB publish(topic).message(...).submit() is a synchronous void terminal. The default PUB path is lossy and returns immediately on success; NODROP throws SubmitError immediately when it cannot admit the copy. Publish adds no publishAsync, binding-owned pending queue, worker, retry, or readiness/admission API. A submitted Message is consumed only when Core submit succeeds and remains reusable after synchronous failure.

Public Entry Shape

The package entrypoint groups the API around domain concepts.

  • Core: context, version/capability lookup helpers, options, and utility functions.
  • Messaging: Message, routing id values, received metadata, topic messages, subscription events, and stream packet data.
  • Sockets: pair, dealer, router, pub, sub, xpub, xsub, stream, typed options, request/reply, publish/subscribe, and stream packet APIs.
  • Eventing: monitor, monitor snapshot/event, poller, poll events, and timer.
  • Service: SPOT node, SPOT handle, topology snapshot, Actor reference, Actor lifecycle, and operation builders.
  • Errors: typed error classes, or tagged error objects that preserve the core result domain.

64-bit Byte HWM and Monitoring Contract

Because HWM and the Core HWM memory limit and budget must represent uint64_t byte value losslessly, the public TypeScript type uses bigint. It does not also accept number or change representation based on the safe-integer range. 0n means unlimited for an HWM, and the manual HWM default is 4_096_000n bytes. A negative value or a value above 2n ** 64n - 1n is rejected with RangeError; number and other types are rejected with TypeError.

interface ContextOptions {
  coreHwmMemoryLimitBytes: bigint;
  coreHwmBudgetBytes: bigint;
  coreHwmProfile: CoreHwmProfile;
}

interface Context {
  getCoreHwmBudgetSnapshot(): CoreHwmBudgetSnapshot;
  resetCoreHwmBudgetMetrics(): void;
}

interface CommonSocketOptions {
  sendHwm: bigint; // Directional send-pipe byte HWM; 0n means unlimited.
  recvHwm: bigint; // Directional receive-pipe byte HWM; 0n means unlimited.
}

Input precedence is manual Core budget, explicit memory limit, V8 heap-limit hint, then Core fallback. Setting either of the first two values disables automatic V8-hint detection. The binding does not combine the hint with Core's hard limit. If an explicit input exceeds a finite hard limit Core detected, the binding preserves the existing configuration error corresponding to EINVAL and does not clamp the value.

HWM planning, manual overrides, admission, and value projection follow Core HWM calculation and admission. 0n means unlimited.

monitorOpen(events?, monitorHwmBytes?) accepts only a bigint byte value for the monitor queue. 0n selects the Core monitor default; a positive value is forwarded unchanged. There is no number-valued or message-count alias.

The monitor snapshot projects Core monitoring ABI v4 as-is. Planned, applied, and deferred values, and in-flight HWM values, include Bytes in their name and are provided as bigint. Whether a deferred value is valid is provided as a separate boolean. Pending-message counts are display diagnostics; sndPendingBytes and rcvPendingBytes are separate bigint byte values. Slot, message-unit, size-cap, and connection-bucket properties, including a count-based name such as autoHwmAppliedSndHwm, are not part of the public surface.

CoreHwmBudgetSnapshot projects ABI version/size, configured/runtime/resolved memory limits, configured/effective budgets, planned/applied/manual-reserved HWM, Core-queue/application/current/peak/provisional accounted bytes, completion current/peak/pending and total-messaging values, monitor/instance aggregates, application/completion queue counts, outstandingApplicationLeaseCount, retiredQueueCount, deferredOriginCreditBytes, oversize/blocked/aggregate flags, budgetGeneration, and measurementEpoch as exact bigint/boolean values. applicationAccountedBytes and those three owner-lifecycle fields are ABI-reserved and always 0n. Reset preserves current, pending, and queue-count gauges, rebases both peaks to current, clears epoch counters, and increments measurementEpoch. An ABI version/size mismatch uses the existing unsupported error, not TypeError.

Receive flow state

ReceiveFlowState is a frozen constant object containing RUNNING: 0 and PAUSED: 1, with a value type of the same name. Socket.setReceiveFlowState(state) returns void and reports native config failure as a config-category ZlinkError carrying the native errno. State, result, and monitor projection follow the common receive-flow contract.

Required Capability Coverage

Once aligned to the shared .NET-standard policy, the public entrypoint covers all of the following stable user-facing capabilities.

  • Context lifecycle, options, shutdown, auto-HWM recalculation, version, capability lookup, and strerror.
  • Message ownership, multipart payload, routing id, received metadata, topic message, subscription event, and stream packet value. Payload share/transfer/duplicate follow the common contract's Copy/Move/Clone: copy(): Message (ref-count share, zlink_msg_copy), move(dest: Message): void (ownership transfer, caller left empty, zlink_msg_move), and clone(): Message (independent deep copy). Since the existing copy() was a deep copy, that behavior moves to clone(); copy is now ref-share. JS cannot host the same signature with two return meanings, so a deprecated alias is impossible — this is a major-version breaking change with a documented migration (copyclone), not a silent one. See the common Message ownership contract §"명시적 Copy / Move / Clone".
  • refcount observation timing (Node-specific): Node exposes payload as a JS Buffer; while an exposed Buffer view is alive, the native frame is released when that view is GC'd/finalized (this has always been the safe behavior). So after copy() shares two handles and one is close()d, if a Buffer view was exposed the refCount() observation does not drop to 1 immediately — it reflects after the buffer is GC'd. This affects only the diagnostic refCount() reading, not correctness, safety, or ownership independence (each handle stays valid and is closed independently). close() uses a single release path in all cases and lets GC reclaim exposed buffers — a deliberate choice so close-heavy paths (e.g. REQREP) do not pay extra per-close cost.
  • Every socket family and its typed options.
  • Monitor, poller, timer, and readiness semantics.
  • SPOT node, SPOT handle, topology snapshot, Actor, and stream Actor binding.

The binding may expose a synchronous or asynchronous form where appropriate, but it does not change the meaning of a core operation.

Spot Get-Or-Create

Node exposes SpotNode.getOrCreateSpot(spotRid). This maps directly to zlink_spot_node_spot_get_or_new(...); it is not implemented by combining spotLookup and createSpot.

This method returns { spot, created }. The returned Spot is caller-owned and is closed the normal way. created is true only for the call that created the logical spot.

Receive and Subscribe Shape

  • The data-plane receive and subscribe APIs use a caller-provided result object for reusable storage.
  • Non-blocking no-data returns false, distinct from a thrown error.
  • A SPOT readable dispatch event is a readiness notification. The caller drains the matching receive API until it reaches no-data.
  • Each part received from a general socket, routed socket, subscription, or successful request result becomes a Message whose payload is a JavaScript-owned Buffer created by the addon. Reading the payload does not require another native call, and Node manages the Buffer lifetime after the Message is closed.
  • Result objects manage the JavaScript lifetime of Message, Buffer, and metadata through reuse or close(). Ordinary receive preserves multipart framing, the Router source RoutingId and nullable ReplyToken, and the Sub/XSub topic and Core-provided source RoutingId. The common receive ownership contract defines the boundary with receive accounting. Node's public and internal receive APIs have no per-part release.
  • A service control/admission receive path such as Actor join request receive can use a nullable, undefined, or tagged result-return shape when that is clearer than reusable data-plane storage. It still distinguishes no-data from a thrown hard receive error.

STREAM packet storage

StreamPacket is a reusable output that owns a routing ID, header, and body. On entry, recvPacket() first releases the previous payload. After success, message references remain valid only until the next recv entry or close(); move or copy them to retain them longer. The output is empty on no-data and error.

Error and Validation Policy

  • Validate fixed-size boundary strings and ids before the native addon call.
  • Do not silently truncate a routing id, actor id, endpoint, channel name, or topic.
  • Preserve the submit, request, recv, handler, close, bind, connect, and config error domains.
  • A public error carries enough structured data for a caller to branch without parsing error text.

Performance Policy

  • A hot path does not use reflection-style property walking, dynamic dispatch by string lookup, avoidable allocation, avoidable Buffer copies, hidden sleeps, busy waits, broad locks, or worker-thread joins.
  • Send/request result delivery follows the common completion owner; input retention for resubmission follows the common result projection.
  • Poll result materialization uses a fixed mapping table, not per-event reflective enum scanning.
  • Perf, samples, and tests import only the public package entrypoint.

Implementation Checklist

  • package.json exports do not expose a private module.
  • The published .d.ts file describes the public contract.
  • Native addon detail does not leak through a public type.
  • An exposed helper function or builder convenience method is declared in contract source, not only in a runtime helper.
  • Receive/subscription semantics match the shared binding policy.
  • Wherever a service control/admission receive differs from caller-provided data-plane storage, that exception is documented.
  • Perf semantics match bindings/c/perf.
  • src/zlink/contracts has no import or export dependency on src/zlink/runtime.
  • src/index.ts imports a runtime module only for factory wiring, and does not export a runtime module or runtime implementation type name.
  • Tests, samples, and perf do not use a deep runtime import.
  • A native-backed resource is created through a package-root factory or contract method, and is typed as a contract interface.
  • Every native-backed resource, operation builder, and application handler role listed in Library Shape has its public contract interface first, before the runtime implementation class is wired into a factory.
  • The public surface has no compatibility-only alias, duplicate operation-start name, or deprecated wrapper.

Verify the Node contract with the following commands from bindings/node/.

  • Run npm run build.
  • Run npm run typecheck.
  • Run npm test.
  • Run npm run samples if a public example or construction path changed.
  • Run npm run perf:single and npm run perf:multi as smoke gates if a hot path, receive, send, request, poller, timer, or service behavior changed.
  • Inspect the generated declarations and confirm the package root exposes contract types, not runtime implementation modules.
  • Search the public surface for a private import. At minimum, check src/zlink/contracts, tests, samples, and perf for an import from src/zlink/runtime, a runtime handle aggregate, a native addon module, or a generated private file. Check src/index.ts separately to confirm its runtime import is factory-wiring only and does not appear in an exported declaration.

Actor and Spot Route Results

Node exposes Actor and Spot route lookup results as public JavaScript objects with matching TypeScript declarations.

  • ActorRoute preserves the resolved Actor reference, Actor node RID, current Spot RID, and current Spot kind.
  • SpotRoute preserves Spot RID, owner node RID, and Spot kind.
  • SpotKind distinguishes Entry Spot from a user Spot. An invalid kind is not a successful route result.
  • A SpotNode snapshot entry exposes the same Spot kind/current Spot fields as the core snapshot.

  • Node exposes SpotNode.sendToActor(actorRef) and SpotNode.requestToActor(actorRef), taking a resolved Actor ref as their argument.

  • The send operation hands off ownership of one or more message parts once submit succeeds, and completes once the Actor owner mailbox accepts the handoff.
  • The request operation hands off ownership of the request part once submit succeeds, and delivers the reply part the Actor handler produced.
  • Node does not revive a removed Discovery route table or resolver API as a compatibility helper.

Pull completion public contract

Node package information follows its distribution metadata; the Core ABI version follows Core release metadata.

Node provides blocking submit_sync() and submit() returning a result object (SendSubmission/RequestSubmission: result and admitted, plus reply for a request). Not waiting for the admitted/reply Promise still follows the common completion-lifetime contract below.

Native completion IDs, user_context, and raw drain are not public APIs. Submission results follow the common result projection; completion joins, lifetime, and progress conditions for PollEventFlag.PollCompletion follow the async execution model.

Only module-private makeReplyToken(owner, value), installed by a class static block, creates a ReplyToken; the constructor sentinel is not exported. Equality and hashing use both owner identity and an opaque value. StreamPacket is an empty reusable output. A token provides no raw conversion, ordering, serialization, or close(). Concurrent recv into the same output is invalid-state. Message references remain valid only until the next recv entry or close(). Before the first bind/connect, the recvMode setter accepts only Raw and Packet and rejects Unspecified.

Public interface

export interface SendSubmission {
  readonly result: SubmitResult;        // OK | BACKPRESSURED, submit-time snapshot (synchronous field)
  readonly admitted: Promise<void>;     // completed when result is OK
}

export interface RequestSubmission {
  readonly result: SubmitResult;
  readonly admitted: Promise<void>;
  readonly reply: Promise<Message[]>;   // completes after successful admission
}

export interface SendSubmitOperation {
  message(message: MessageLike): SendSubmitOperation;
  submit(): SendSubmission;
  submit_sync(): void;
}

export class ReplyToken {
  readonly #owner: object;
  readonly #value: bigint;
  private constructor(secret: symbol, owner: object, value: bigint);
  equals(other: ReplyToken): boolean;
  hashCode(): number;
  toString(): "ReplyToken";
}

export interface RequestSubmitOperation {
  message(message: MessageLike): RequestSubmitOperation;
  timeout(timeoutMs: number): RequestSubmitOperation;
  submit(): RequestSubmission;
  submit_sync(): Message[];
}

export interface ReplySubmitOperation
  extends PartBuilder<ReplySubmitOperation> {
  submit(): void;
}

export interface StreamSocket {
  send(routingId: RoutingId): SendOperation;
  recv(out: Received, flags?: RecvFlags): boolean;
  recvPacket(out: StreamPacket, flags?: RecvFlags): boolean;
}

export const StreamRecvMode: Readonly<{
  Unspecified: 0;
  Raw: 1;
  Packet: 2;
}>;
export type StreamRecvMode =
  typeof StreamRecvMode[keyof typeof StreamRecvMode];

export interface StreamSocketOptions {
  recvMode: StreamRecvMode;
}

export class StreamPacket {
  constructor();
  readonly isEmpty: boolean;
  readonly routingId: RoutingId | null;
  readonly header: Message | null;
  readonly body: Message | null;
  close(): void;
}

The operation-start signatures are PAIR send(): SendOperation, DEALER send(): SendOperation and request(): RequestOperation, ROUTER send(routingId): SendOperation, request(routingId): RequestOperation, and reply(routingId, token): ReplyOperation, and STREAM send(routingId): SendOperation. A send factory captures the target in the builder. Received.replyToken is ReplyToken | null. Received.send() returns a SendOperation that captures the source target, and Received.reply() returns a ReplyOperation that captures the source RID and token.

The public Node/TypeScript surface contains no RoutedSendOperation or ImmediateSendOperation, StreamSocket.trySend() or setPacketHandler(), RequestCallback or callback overload, StreamPacketBodyMaterialization, monitor/timer callback, or pair/generation member. ReplySubmitOperation does not extend Flaggable and provides no reply flags.

Monitor provides recv(flags?: RecvFlags): MonitorEvent | null, status(), and close(). Timer provides start(intervalNs: bigint, repeatCount: bigint), stop(), recv(): bigint | null, and close(). Monitor-event connectionId is used only for diagnostics and correlation, not as a send/reply target or reconnect fence. The internal addon enum mirror uses only ZLINK_OPT_PENDING_MAX_MSGS and ZLINK_OPT_PENDING_MAX_BYTES and adds no public option property.

Implementation and contract-test verification requirements

Verify the following using only public TypeScript declarations, JavaScript results and errors, and poller events. Each item maps to one contract test.

Operations and completion

  • PAIR, DEALER, ROUTER, and STREAM send factories return one SendOperation family.
  • Send/request expose only the flag-free Promise and synchronous terminals in the Public interface section and retain request timeout.
  • Common completion, cancellation, and poller observations follow the execution-model verification requirements.
  • When HWM/PAUSED waiting expires for a raw reply submitted to a DEALER peer, SubmitError reports BACKPRESSURED; a reply submitted to a ROUTER peer retains the HWM-free result of the Completion connection.

ReplyToken and STREAM

  • Only ROUTER REQUEST receive returns a token; public construction, raw conversion, and serialization do not succeed.
  • Only tokens with the same owner and value are equal, and reply with a token from another owner fails before the native call.
  • StreamPacket holds a payload after recv success, is empty on no-data or error, and can be reused after close().

Pull eventing

  • Monitor and timer recv return no-data as null and expose events and fire counts without callbacks.
  • TypeScript declarations and generated JavaScript provide the same public names and terminals.