Transports
Esta página aún no está disponible en tu idioma.
The cluster transport is the wire between cluster nodes — it
delivers gossip messages, heartbeats, and application
envelopes (your tells to remote actors). Two implementations
ship with the framework:
| Transport | Use |
|---|---|
TcpTransport | Production. Real TCP sockets, optional TLS. |
InMemoryTransport | Tests. Loops frames through in-process JS structures — no networking. |
Both implement the same Transport interface, so cluster behavior
is identical regardless of which is plugged in.
The interface
Section titled “The interface”interface Transport { readonly self: NodeAddress; start(): Promise<void>; shutdown(): Promise<void>; setHandler(handler: (from: NodeAddress, message: WireMessage) => void): void; send(to: NodeAddress, message: WireMessage): void; disconnect(peer: NodeAddress): void; peers(): NodeAddress[];}Small surface — bootstrap, send, receive, disconnect. The cluster plugs in a handler and gets a stream of inbound wire messages with their sender address.
TcpTransport (default)
Section titled “TcpTransport (default)”import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts/cluster';
const clusterOptions = ClusterOptions.create() .withHost('0.0.0.0') .withPort(2552) .withSeeds(['...']);const cluster = await Cluster.join( system, clusterOptions, // transport defaults to TcpTransport — no need to pass explicitly);What it does:
- Listens on
host:portfor incoming connections —hostis the interface to bind, so a wildcard belongs here. - Announces
advertisedHost:portin its handshake, and keys peers on it. That is the node’s identity, so it is resolved separately and is never a wildcard; unset, it followshostwhenhostis routable. See Cluster overview. - Connects to peers as needed (on first send, or to seeds at join time).
- Per-frame size cap (default 16 MiB) — frames larger than this are rejected to prevent a DoS via fake length-prefix.
- Auto-reconnect — if a connection drops mid-cluster-life, reconnects on the next send.
Runtime backends
Section titled “Runtime backends”TcpTransport doesn’t talk directly to the OS — it goes through a
TcpBackend interface, with one implementation per runtime:
| Runtime | Backend | Underlying API |
|---|---|---|
| Bun | bunTcpBackend | Bun.listen / Bun.connect |
| Node | nodeTcpBackend | node:net |
| Deno | denoTcpBackend | Deno.listen / Deno.connect |
Auto-detected via getTcpBackend(). You usually don’t think about
this — same TcpTransport works on every runtime.
Optional TLS
Section titled “Optional TLS”import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts/cluster';
const transport = new TcpTransport( NodeAddress.parse('actor-ts://my-app@10.0.0.5:2552'), system.log, { cert: '...', // PEM key: '...', // PEM ca: '...', // optional CA bundle rejectUnauthorized: true, },);
const clusterOptions = ClusterOptions.create() .withHost(host) .withPort(port) .withSeeds(seeds) .withTransport(transport);await Cluster.join( system, clusterOptions,);TLS-wrapped TCP, all-or-nothing per cluster. See Cluster security for the production recipe.
Frame size
Section titled “Frame size”new TcpTransport(self, log, null, 64 * 1024 * 1024); // 64 MiB max frameOverride the per-frame size cap. Default 16 MiB is enough for typical cluster traffic (gossip, heartbeats, small envelopes). Larger values don’t improve general throughput — they only matter for individual large messages.
InMemoryTransport (tests)
Section titled “InMemoryTransport (tests)”import { InMemoryTransport, NodeAddress, Cluster, ClusterOptions } from 'actor-ts/cluster';import { TestKit } from 'actor-ts/testkit';
// No bus to wire up — in-memory transports discover each other through a// process-global registry, keyed by each transport's own NodeAddress.const tk1 = TestKit.create('node-1');const tk2 = TestKit.create('node-2');
// Each transport's self address must match its node's system@host:port.const clusterOptions = ClusterOptions.create() .withHost('1') .withPort(0) .withSeeds(['1:0']) .withTransport(new InMemoryTransport(new NodeAddress('node-1', '1', 0)));await Cluster.join( tk1.system, clusterOptions,);
const cluster2Options = ClusterOptions.create() .withHost('2') .withPort(0) .withSeeds(['1:0']) .withTransport(new InMemoryTransport(new NodeAddress('node-2', '2', 0)));await Cluster.join( tk2.system, cluster2Options,);The inMemoryTransport(system, host, port) factory is a shorthand for
the same construction — it derives the NodeAddress from system.name.
How it works:
- A process-global registry routes messages between in-process transports.
- Each transport registers itself in the registry by address on start.
send(to, message)looks up the recipient in the registry and invokes its handler directly — no sockets, no serialization to bytes.
Used by MultiNodeSpec — the multi-node test harness — to spin up multi-node clusters in one process.
What it doesn’t simulate
Section titled “What it doesn’t simulate”- Network failures. By default, the registry delivers reliably.
For fault injection, you’d implement a custom
Transportwith drop / delay / reorder logic. - Latency. Delivery is synchronous within an event-loop turn.
- Serialization. Messages are passed by reference, not bytes —
and
TcpTransportis the only transport that serializes at all:MessageChannelTransporthands the envelope topostMessage, and this one hands the object straight to the recipient’s handler. So to exercise the real framing round-trip (encodeFrame/FrameDecoder, which frames a tagged JSON tree, not bare JSON) you needTcpTransportover loopback. The CBOR codec is not reachable from any cluster transport; test it directly, or through HTTP marshalling.
Custom transport
Section titled “Custom transport”Implementing Transport against a different wire is rare but
possible. Examples:
- WebSocket transport — for browser-side cluster participants (theoretical; not implemented).
- MessageChannel transport — for worker-thread clusters in a single OS process. Used by the “worker mesh” pattern.
The interface is small enough that a competent implementation is ~200 lines of code; the difficulty is in matching the framing + heartbeat semantics the cluster expects.
Diagnostics
Section titled “Diagnostics”const peers = transport.peers(); // currently-connected addressespeers() is also what the framework’s cluster-transport readiness
check reads: a node with members it still expects and no connection
to any of them is cut off, and /ready answers 503 so a load
balancer stops routing to it. See
Health checks. Note
that InMemoryTransport.peers() reports every transport registered
in the process regardless of connection state — a test written
against it cannot observe that check failing.
The transport doesn’t expose per-connection metrics directly — use the cluster’s metrics extension to get connection counts and bytes sent/received per peer.
For lower-level inspection (specific frame contents), enable debug logging on the system:
const actorSystemOptions = ActorSystemOptions.create().withLogLevel(LogLevel.Debug);const system = ActorSystem.create('my-app', actorSystemOptions);// Look for [tcp-transport] log linesMultiplexing
Section titled “Multiplexing”A single TCP connection between two nodes carries:
- Gossip messages — cluster membership exchanges.
- Heartbeat messages — failure-detection.
- Envelope messages — your
tells, encoded with routing information. - Subsystem messages — sharding protocol, pubsub gossip, DistributedData replication.
All multiplexed onto the same TCP stream. There’s no priority-routing — heartbeats and your bulk traffic share the pipe. For most workloads this is fine; for explicit isolation (reserve bandwidth for cluster control), you’d need a custom transport with per-channel framing.
Where to next
Section titled “Where to next”- Cluster overview — what rides on the transport.
- Refs across nodes — how envelopes encode actor refs for cross-node delivery.
- Cluster security — TLS + auth.
- Worker mesh — MessageChannel-based transport for in-process workers.
- MultiNodeSpec —
uses
InMemoryTransportfor multi-node tests.
