Spec index | Previous: Java | Next: Python
Node / TypeScript Binding Implementation Blueprint¶
What this chapter defines — the
contracts/runtimeshape 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, andpackage.jsonexports. - 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.jsonexports 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.jsonexports and the generated.d.tsfile must agree with the public entrypoint. Do not document or test deep source imports as public API. index.ts, the published.d.tsfile, andpackage.jsonexports are the contract's TypeScript package projection.- Do not expose deep source paths as public API unless they are deliberately listed in
package.jsonexports. - Use lower-case source directory names. Do not create
src/zlink/Contractsorsrc/zlink/Runtime; those names could be mistaken for a public deep-import surface. src/zlink/contractsowns 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/runtimeowns 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:
- Add the public symbol to the correct contract source category.
- Update the package entrypoint, declaration surface, and
package.jsonprojection. - Keep native addon calls, N-API handles, and request progress helpers behind private modules.
- Choose a class, interface, type alias, literal union, or plain-object shape according to normal TypeScript usage.
- Add runtime tests and type-surface tests against the package entrypoint.
- Update samples and perf only through public imports.
- Confirm the generated
distand.d.tsoutput do not expose private bridge modules.
The contract/runtime boundary meets these requirements:
- Move public behavior declarations to
src/zlink/contracts/<category>/. - Move native-backed runtime implementations to
src/zlink/runtime/<category>/. - Keep native addon loading and N-API calls under
src/zlink/runtime/native/. - Public code constructs resources through package-root factories or contract methods.
- Package exports do not expose runtime modules as public API.
- Deprecated wrappers, duplicate overload families, and compatibility-only naming aliases are not part of the public surface.
- Tests, samples, and perf import from the package root only.
- Generated declarations in
dist/index.d.tscarry the contract surface, not runtime implementation modules.
The following Node-specific shortcuts are not allowed.
src/zlink/contractsdoes 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.tsdoes not remain the source of public resource behavior. Split those declarations into named contract files and resource-named runtime implementation files. src/index.tsexports package contract names and factories, not runtime implementation modules.package.jsondoes 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, andStreamSocket. - Eventing roles:
MonitorSocket,Poller, poll event source,Timer,Stopwatch, andAtomicCounter.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/contractsor 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.tsfile 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/*fromsrc/zlink/contractsorsrc/index.ts.src/index.tsmay 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(): Contextreturns 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
enumsfolder 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/: aspot/subfolder holding SPOT node, Spot, Actor, topology model, and service operation builders. Use named files such asspot_node.ts,spot.ts,actor.ts, andspot_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, andstream_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 aspot/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.tsholds theSpotNode,Spot, andActorimplementations in one file.runtime/eventing/eventing.tsholds the monitor socket, poll events, poller, timer, stopwatch, and counter implementations in one file.runtime/core/context.tsholds context, context options, and unrelated runtime helper implementation in one file.runtime/core/runtime_info.tsholds a copied implementation prelude, or socket/service behavior, just to reach its helper functions.runtime/sockets/socket_common.tsholds 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(), andcreateStreamSocket()create runtime socket implementations. Service-layer implementations are created accordingly.- A
Spothandle is obtained throughSpotNode.createSpot(),entrySpot(),getOrCreateSpot(...), orspotLookup(...). DirectSpotconstruction is not public. - An Actor handle is created through
SpotNode.createActor(...). Direct Actor construction is not public. createPoller(),createTimer(), andcreateTimer(spot)create eventing resources.PollableisBaseSocket | SocketMonitor | Timer | number, andPoller.add/modify/removeprovideSocketMonitoroverloads (common spec "Monitor sources inPoller"). OnlyPollEventFlag.PollInis valid for a socket monitor; any other readiness mask is rejected with a typedConfigResult.InvalidArgument. Drain withmonitor.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, soPoller.waitblocks 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
nullis 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 remainsPoller's job, and thenumber(raw fd) member ofPollablestays.
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
camelCasefor 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, andrequestToSpot. - 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, orrequestAsync. Keep a single operation name. Send usessubmit_sync()andsubmit(). Request usessubmit_sync()andsubmit(), with the request timeout on the builder.
Canonical Interface Rules¶
- Data-plane
recv, routed recv,subscribe, and subscription-event receive fill a caller-providedReceived,TopicMessage, orSubscriptionEventobject and returnboolean. - 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(...)andrequestToChannel(...). SPOT topic publish stayspublish(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), andsendToSpot(..., message)are not public contract members. Callers usesend(...).message(message).submit(). - Multipart payload is accumulated by repeated
message(...)calls. Amessages(...)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(...)orreply(requestToken, parts). A dealer can start a request throughrequest(), but it has no API-level peer routing id, so it cannot reply to an arbitrary token. - Node
Buffer/Uint8Arraypayload 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 assocketSendBorrowedNoWaitResult. - The message payload factory is
Message.from(...). The public TypeScript contract does not require the caller to usenew 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(). TheMessagereference is invalid when cleanup returns. The caller must not use it again, including repeatedclose(), payload access, or identity-basedMap/WeakMaplookup. 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 separatesubmitAsyncterminal name. - PAIR, DEALER, ROUTER, and STREAM send use one
SendOperationfamily that captures the target.submit_sync()uses CoreNONE;submit()uses CoreDONTWAITcompletion. - 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[]andsubmit(): RequestSubmission(result,admitted, plusreply: Promise<Message[]>) and retains the builder's reply timeout. - The terminal for a raw ROUTER/
Receivedreply is the synchronous one-shotReplySubmitOperation.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, andSNDTIMEO, so the result can beBACKPRESSURED; a ROUTER peer uses the HWM-free Completion connection.NOT_CONNECTED,TERMINATED,INVALID_ARGUMENT, and other submit failures are thrown immediately asSubmitError. - PUB/XPUB
publish(topic).message(...).submit()is a synchronousvoidterminal. The default PUB path is lossy and returns immediately on success;NODROPthrowsSubmitErrorimmediately when it cannot admit the copy. Publish adds nopublishAsync, 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), andclone(): Message(independent deep copy). Since the existingcopy()was a deep copy, that behavior moves toclone();copyis 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 (copy→clone), 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 exposedBufferview is alive, the native frame is released when that view is GC'd/finalized (this has always been the safe behavior). So aftercopy()shares two handles and one isclose()d, if a Buffer view was exposed therefCount()observation does not drop to 1 immediately — it reflects after the buffer is GC'd. This affects only the diagnosticrefCount()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
Messagewhose payload is a JavaScript-ownedBuffercreated by the addon. Reading the payload does not require another native call, and Node manages theBufferlifetime after theMessageis closed. - Result objects manage the JavaScript lifetime of
Message,Buffer, and metadata through reuse orclose(). Ordinary receive preserves multipart framing, the Router sourceRoutingIdand nullableReplyToken, and the Sub/XSub topic and Core-provided sourceRoutingId. 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
Buffercopies, 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.jsonexports do not expose a private module.- The published
.d.tsfile 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/contractshas no import or export dependency onsrc/zlink/runtime.src/index.tsimports 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 samplesif a public example or construction path changed. - Run
npm run perf:singleandnpm run perf:multias 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, andperffor an import fromsrc/zlink/runtime, a runtime handle aggregate, a native addon module, or a generated private file. Checksrc/index.tsseparately 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.
ActorRoutepreserves the resolved Actor reference, Actor node RID, current Spot RID, and current Spot kind.SpotRoutepreserves Spot RID, owner node RID, and Spot kind.SpotKinddistinguishes 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)andSpotNode.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
SendOperationfamily. - 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/
PAUSEDwaiting expires for a raw reply submitted to a DEALER peer,SubmitErrorreportsBACKPRESSURED; 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.
StreamPacketholds a payload after recv success, is empty on no-data or error, and can be reused afterclose().
Pull eventing
- Monitor and timer recv return no-data as
nulland expose events and fire counts without callbacks. - TypeScript declarations and generated JavaScript provide the same public names and terminals.