@hile/message-modem
Implement request/response, push, abort, and stream protocol behavior independent of transport.Choose This Package When
Use@hile/message-modem for: Implement request/response, push, abort, and stream protocol behavior independent of transport.
Use When
Use the message packages for request/response messaging over WebSocket, process IPC, worker threads, file-system message handlers, service discovery, streaming RPC, and registry-backed pub/sub.Do Not Use When
- Do not use
stream()for normal single-result calls. - Do not enable retries for a streamed request input. Input streams are consumed once and cannot be replayed safely.
- Do not rely on message IDs for business idempotency. They are transport IDs.
- Use
defineMicroMessage()for Micro business handlers; reserve genericdefineMessage()for transport-neutral loaders. - Do not pass zero, fractional, non-finite, or oversized message timeouts. Explicit timeout values must be safe integers from
1through2_147_483_647milliseconds.
Install
Imports
Copy-Paste Example
Message handler file:More Examples
Typed fixed-path operations
UsedefineMicroContract() and createMicroClient() from @hile/micro-contract to share stable JSON schemas and typed remote calls. Implement each operation in its matching message file with defineMicroMessage(operation, handler), then call loadMicroContract(app, contract, messagesDirectory) from @hile/micro to obtain { local, activate, close }.
The binding validates filesystem paths and complete implementation coverage before activation. Explicit local calls are same-service unary calls through the provider executor, not a namespace-based optimization; normal calls still use RPC. Typed remote calls default to zero retries. Native dynamic and streaming APIs remain supported. See Typed Micro contracts for the full example, schema constraints, and shutdown ordering.
Generic loader protocol registration
protocol on both sides. Protocol tags are nonempty printable ASCII strings, at most 128 characters; registration snapshots the value. Neither payload fields nor Context/extras can select the dispatch protocol.
After one route match, a different protocol throws MessageProtocolMismatchError (HILE_MESSAGE_PROTOCOL_MISMATCH) before the handler runs, with no fallback to another matching route. A tag prevents accidental protocol misdelivery, not unauthorized invocation by a caller able to set the tag.
Native streaming and custom transports
Streaming handler:AsyncIterable, Uint8Array, or
ArrayBuffer directly as data; Hile sends it as the request input stream and
the handler receives data === undefined. app.stream() accepts the same
request input forms when both request and response need to stream.
Custom WebSocket modem:
request() returns a Promise<T>. Await it directly.
Runtime And Lifecycle Notes
MessageLoadermaps*.msg.*files to routes using@hile/loader.- Message files use the same portable DSL as controllers:
[id].msg.tsdeclares one segment and[...paths].msg.tsdeclares one or more final segments. A catch-all handler reads the slash-joined value fromparams.paths; Hile compiles it to rou3’s named catch-all syntax. - Optional catch-all, mixed or non-terminal dynamic segments, multiple catch-alls, duplicate dynamic names, unsafe or object-meta names, and router-native syntax in message file names fail during loading. The configured
prefixmay use rou3-native or bracket-style parameters, is compiled separately from file segments, and cannot duplicate a file parameter name; manualregister()calls continue to accept rou3-native paths. defineMessage(fn, options?)andMessageLoader.register(path, fn, options?)acceptMessageProtocolOptions;MessageLoader.dispatch(path, data, extras?, options?)checks that protocol before invoking the matched handler.- Raw registrations and file-loaded messages share route ownership. Duplicate normalized paths and equal-specificity overlaps fail with
MessageRouteConflictError(HILE_MESSAGE_ROUTE_CONFLICT). This includes/a/:xversus/:x/b; regex parameters are conservatively treated as unrestricted, so disjoint regexes with the same route shape also conflict. Distinct static branches and different-specificity static/parameter/catch-all precedence remain supported by rou3. - A
MessageLoader.load()batch is invisible until it commits. Import, bind, or final commit failure rolls back only that batch. Unload is idempotent and owner-specific; an old release cannot remove a replacement. Concurrent loads or raw registration during a load fail withMessageLoadInProgressError(HILE_MESSAGE_LOAD_IN_PROGRESS); releasing existing owners remains allowed and cannot resurrect them at commit. protected bind(file, metadata)remains the extension point. An override that callssuper.bindmust return/compose its cleanup and unwind its own side effects on failure. The generic loader does not parse business payloads or inspect Context.MessageModem._send()returns aPromise._send()and_stream()acceptoptions.inputas anAsyncIterable,Uint8Array, orArrayBuffer. Passing one of those values directly asdataautomatically selects request streaming and leaves the handler’s structureddataundefined.MessageModem._send()and_push()use a30_000ms timeout when none is provided. An explicit timeout must be a safe integer from1through2_147_483_647; invalid values throwTypeErrorbefore a message is sent.MessageModem._stream()returns a NodeReadablein object mode.- Stream
timeoutandidleTimeoutvalues use the same1through2_147_483_647ms range. Idle timeout is refreshed by valid input or output stream activity. The response streamwindowmust be a safe integer from1through64and defaults to1; request input uses receiver-issued credit with a window of1. - Each modem schedules request, total-stream, and idle-stream deadlines through one internal deadline scheduler. This reduces active Node.js timers without changing timeout, cancellation, ordering, or error semantics.
- Request and response chunks share
STREAM_DATAframes and carry an explicitdirection: 'input' | 'output'; each direction has independent sequence and credit state.STREAM_CANCELstops one direction. A cancel carryingstatusfails the owning request immediately;ABORTcancels the whole invocation. - This frame model replaces the former response-only
stream,streamVersion, andstreamWindowfields. All peers on one transport connection must use compatible@hile/message-modemand transport package versions; do not mix old and new peers during a rolling deployment. @hile/message-wssendsUint8ArrayandArrayBufferrequest and response chunks as native binary frames rather than JSON/base64. PublicdecodeMessageFrame()payloads remain isolated from caller-owned input by default; the owned WebSocketRawDatapath uses a zero-copy binary view internally.@hile/message-ipctransparently Base64-wraps only binarySTREAM_DATApayloads so request and response streams survive Node child-process IPC’s default JSON serialization. Other IPC frames keep their ordinary object representation.- A stream request requires
exec()to return an async iterable. MessageModem.exec(data, signal?, input?, execution?)has an optional fourthMessageExecutionOptionsargument. ItsresponseStreamflag comes from the decoded request frame, not payload metadata or the presence of an input stream. Existing three-argument transport subclasses remain compatible; custom subclasses that forward execution to another dispatcher must preserve this argument when the receiver needs to enforce unary-only operations.- An execution whose response cannot be delivered (including a unary request returning an async iterable, or a stream failing during iteration) aborts its request scope before sending the original error. Cleanup is cooperative and does not delay the failure frame; provider shutdown still owns its bounded drain deadline.
defineMicroMessage()handlers receive request streams asinput: Readable | undefined, separately from structureddataandinvocation.Application.call(namespace, url, data, options)requiresoptions.contextand returns a promise. It acceptsoptions.inputfor a streamed request with a normal response.Application.stream(namespace, url, data, options)requiresoptions.contextand returns a readable response stream; it can carry a request input stream at the same time.Application.call()andApplication.stream()may target the application’s own namespace. These self RPCs use normal Registry-discovered WebSocket/modem transport, not an automatic local branch; replica selection and network behavior remain unchanged. The separateloadMicroContract()binding provides explicit local execution only for its typed fixed-path unary operations, through the same provider executor. See Typed Micro contracts for its distinct failure and lifecycle semantics.- A peer address (
host:port) is the routing and reuse identity, not an individual WebSocket identity. If both peers dial each other concurrently, including an application dialing itself, the server preserves both physical connections while they are active instead of replacing a connection that may carry an in-flight request. Later calls still reuse the cached peer connection. - This connection bookkeeping does not add or change wire frames; request, response, stream, credit, cancel, and abort protocol fields remain unchanged.
Application.call()andApplication.stream()default retries to0when request input is streamed. Explicit nonzero retries fail before service discovery because streamed input is non-replayable.- Native nonstream
Application.call()/stream()retain the existing default of one transport retry.createMicroClient()andcallHttpOverMicro()explicitly default to zero for both reads and writes. Opt-in retries remain subject to the underlying eligible-error policy and the caller’s end-to-end attempt/idempotency budget; provider contract/protocol rejection,HILE_MICRO_EXECUTION_FAILED, and unavailable admission do not trigger automatic retries. Safe typed execution errors must not replay a whole orchestration after a downstream validation failure; native unknown-error handling is unchanged. - Micro adapters can pass
options.protocol; it travels in existing requestmetadata.protocoland is checked by the generic message loader. There is no new modem frame or operation dispatcher. All selectable receivers must support this check before relying on protocol isolation: older receivers may ignore the marker. - A caller-owned request input source failure is surfaced as
MessageInputError, aborts the peer invocation, and is not counted against that peer’s circuit-breaker health. Application.publish(topic, payload)returns an object withupdate()andunpublish().Application.subscribe(topic, callback)returns an unsubscribe function.Registrystores service addresses and retained config/topic state under~/.registry.
Anti-Patterns
- Appending a secondary response getter to
client.request('/x', data) - Returning a plain object from a handler called through
stream(). - Retrying a consumed request stream or hiding it inside a replay-unsafe factory.
- Branching business code on whether a Micro target namespace is local or remote.
- Using pub/sub as a durable queue.
- Forgetting to register
shutdown(await app.listen(...)).
Verification Checklist
- Micro message files default-export
defineMicroMessage(...)and receiveinvocation.context. - Native untyped Micro message files may use
[...name].msg.ts; typed Micro Contract implementations remain fixed-path only and reject all parameterized file routes. - RPC callers use
await app.call(..., { context }). - Same-namespace
call()andstream()use normal Registry discovery, whilestreamPeer()keeps its exact-address selection; all three use the normal WebSocket/modem transport path and callers do not branch on locality. - Streaming handlers are async generators.
- Request-stream handlers consume
inputand callers either passoptions.inputalongside metadata or pass a stream directly asdata. - Streamed request calls do not configure nonzero retries.
- Custom modem timeout values use the documented safe-integer range.
- File and raw owner conflicts, atomic batch failure, concurrent load rejection, and protocol misdelivery are tested before any handler side effect.
- Typed fixed-path services validate both actual source and compiled route trees; explicit local and real remote calls test the same provider executor, admission, cancellation, and bounded shutdown.
- Registry is started before application nodes need discovery.
- Micro apps use stable namespaces and advertise reachable hosts.
Related Recipes
Micro RPC With Message Loader
Complete Example
Provider handler:operation.path values stay fixed and cannot use [id] or [...paths].
Provider boot:
Package-Local AI Guide
This package also shipsAI.md in npm so agents can read accurate examples after installation.