Appearance
NATS transport
NatsTransport is the production-side implementation of ITransportAdapter. It is what every Host Service runtime uses to talk to NATS, and what the browser variant (createBrowserNatsTransport) wraps. This page covers root prefixing, the semantic-to-NATS subject grammar, JSON encoding, and the INatsLikeConnection adapter that lets the same class work with both @nats-io/nats-core (Node) and nats.ws (browser).
Constructor and the INatsLikeConnection boundary
ts
new NatsTransport(connection: INatsLikeConnection, root: string)
new NatsTransport(connection: INatsLikeConnection, options: INatsTransportOptions)INatsLikeConnection (NatsTransport.ts:20-31) is a minimal NATS-shaped interface:
ts
interface INatsLikeConnection {
publish(subject: string, data?: Uint8Array): void;
subscribe(subject: string, options: { callback: (err, msg) => void }): INatsLikeSubscription;
isClosed(): boolean;
drain(): Promise<void>;
close(): Promise<void>;
}This abstraction lets NatsTransport work with whichever NATS client wrapper the environment provides. In Node, the runner constructs a connection via @nats-io/nats-core. In the browser, createBrowserNatsTransport uses nats.ws. Test harnesses can stub the connection to drive the transport without a real broker.
Use createTransport (projects/matrix-3/packages/core/src/transport/createTransport.ts) as the convenience constructor:
ts
import { createTransport } from '@open-matrix/core';
import { connect } from '@nats-io/nats-core';
const connection = await connect({ servers: 'nats://127.0.0.1:4222' });
const transport = createTransport({
natsConnection: connection as unknown as INatsLikeConnection,
root: 'COM.NIMBLETEC.RICHARD-SANTOMAURO',
});Root prefix
Every subject NatsTransport publishes is prefixed with the authority root. Root validation (NatsTransport.ts:107-126):
ts
static readonly ROOT_PATTERN = /^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$/;Roots are DNS-style (alphanumeric, hyphens, dots, underscores). Examples in the tree:
COM.NIMBLETEC.RICHARD-SANTOMAURO— derived from emailrichard.santomauro@nimbletec.com.space.alt.stories.ghost-stories.funny— derived from a public Space path.SPACE.SPC_7F3A9B2C— derived from a Space identity.
Constructing a NatsTransport with a root that fails the pattern throws synchronously.
Semantic to NATS subject grammar
Application-level code uses semantic topics (mount/$inbox, mount/$events, $replies.<cid>). The transport translates them to wire subjects with semanticToNats(topic) (NatsTransport.ts:267-350).
| Semantic topic | Wire subject |
|---|---|
system.llm/$inbox | <root>.system.llm.$inbox |
system.llm/$events | <root>.system.llm.$events |
system.llm/$exit | <root>.system.llm.$exit |
$replies.corr-123 | <root>.$reply.corr-123 |
system.llm/# | <root>.system.llm.> (wildcard) |
# | <root>.> (wildcard everything under our root) |
targetRoot/system.llm/$inbox | targetRoot.system.llm.$inbox (cross-root) |
generic.topic | <root>.generic.topic |
Cross-root subjects use targetRoot/localTopic syntax — the leading slash signals "this is for a different authority root."
The reverse, natsToSemantic(wireTopic) (NatsTransport.ts:362-422), turns a wire subject back into a semantic topic. Used when receiving messages, when reporting wire info, and in the federation adapter.
JSON encoding
By default (jsonMode: true, NatsTransport.ts:133), payloads are encoded with JSON.stringify + TextEncoder and decoded with TextDecoder + JSON.parse. The wire payload is always UTF-8 bytes. Payloads that aren't JSON-serializable (functions, circular refs, BigInt) throw at publish time.
You can disable JSON mode by passing jsonMode: false — useful when bridging non-JSON payloads (binary blobs, protobuf). The handler receives a Uint8Array directly.
Credentials
ts
new NatsTransport(connection, {
root: '<root>',
user: 'me',
pass: 'shh',
});
new NatsTransport(connection, {
root: '<root>',
credentials: { user: 'me', pass: 'shh' },
});Credentials are stored on the transport but the connection itself must already be authenticated with them. The transport does not authenticate; it just remembers the principal. Used by upstream code that needs to know which user this transport is acting as.
Lifecycle
ts
transport.subscribe(topic, handler) → unsub function
transport.publish(topic, payload?)
transport.disconnect() → drains then closes connectiondisconnect() (NatsTransport.ts:235-255):
- Clears the JWT refresh timer if
createBrowserNatsTransportset one. - Unsubscribes every active subscription.
- Calls
connection.drain()(NATS-protocol clean shutdown). - Calls
connection.close().
Failure modes during disconnect are non-fatal — a half-shut connection is fine to leak past the end of a process.
Error and unhandled-message hooks
ts
new NatsTransport(connection, {
root,
onError: (err) => console.error('nats error', err),
onUnhandledMessage: (topic, payload) => console.warn('unhandled', topic, payload),
});onError is called when the underlying connection reports an error. onUnhandledMessage is called when a message arrives on a wire subject for which no local handler exists (multiple transports sharing a connection — uncommon but supported).
getWireTopicInfo
ts
const info = transport.getWireTopicInfo('system.llm/$inbox');
// {
// appTopic: 'system.llm/$inbox',
// wireTopic: 'COM.NIMBLETEC.RICHARD-SANTOMAURO.system.llm.$inbox',
// prefix: 'COM.NIMBLETEC.RICHARD-SANTOMAURO',
// root: 'COM.NIMBLETEC.RICHARD-SANTOMAURO',
// sessionId: undefined,
// }Used for debugging and when displaying actor addresses in tooling. Mirrored on InMemoryTransport for parity (with prefix: '').
Worked example
ts
import { createTransport, MatrixRuntime, MatrixActor } from '@open-matrix/core';
import { connect } from '@nats-io/nats-core';
const connection = await connect({ servers: 'nats://127.0.0.1:4222' });
const transport = createTransport({
natsConnection: connection as unknown as INatsLikeConnection,
root: 'COM.NIMBLETEC.RICHARD-SANTOMAURO',
});
const runtime = new MatrixRuntime({ transport });
class Echo extends MatrixActor {
static accepts = { 'echo': { description: 'Echo', schema: { msg: { type: 'string', description: 'msg' } }, returns: { msg: { type: 'string', description: 'echoed' } } } };
async onEcho(p: { msg: string }) { return { msg: p.msg }; }
}
await runtime.create(Echo, 'echo');
// Now any other Matrix participant on COM.NIMBLETEC.RICHARD-SANTOMAURO
// can reach this actor at <root>.echo.$inbox.Caveats and gotchas
- Wildcard subscriptions span only your root.
subscribe('#', ...)becomes<root>.>— you do not see traffic on other roots unless you publish/subscribe with a cross-root topic. - Reply subjects are root-scoped.
$replies.cidbecomes<root>.$reply.cid. Replies for a request you sent cross-root come back on YOUR root, not the target's. - Wire format is JSON, not the legacy stringified envelope. If you have a Python/Go bridge, decode payloads as JSON.
disconnect()is the only cleanup. Letting a transport go out of scope without draining leaks the NATS connection until the process exits.
See also
- In-memory transport — the in-process variant used in tests.
- Browser transport —
createBrowserNatsTransportover WebSockets. - Transport testing — stubbing
INatsLikeConnection.
Source:
projects/matrix-3/packages/core/src/transport/NatsTransport.ts:33-86(options),:107-160(constructor + validation),:267-350(semanticToNats),:362-422(natsToSemantic),:235-255(disconnect);projects/matrix-3/packages/core/src/transport/createTransport.ts(convenience factory).