コンテンツにスキップ
日本語

Transports

このコンテンツはまだ日本語訳がありません。

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:

TransportUse
TcpTransportProduction. Real TCP sockets, optional TLS.
InMemoryTransportTests. 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.

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.

import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts';
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:port for incoming connections.
  • 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.

TcpTransport doesn’t talk directly to the OS — it goes through a TcpBackend interface, with one implementation per runtime:

RuntimeBackendUnderlying API
BunbunTcpBackendBun.listen / Bun.connect
NodenodeTcpBackendnode:net
DenodenoTcpBackendDeno.listen / Deno.connect

Auto-detected via getTcpBackend(). You usually don’t think about this — same TcpTransport works on every runtime.

import { Cluster, ClusterOptions, TcpTransport } from 'actor-ts';
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.

new TcpTransport(self, log, null, 64 * 1024 * 1024); // 64 MiB max frame

Override 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.

import { InMemoryTransport, NodeAddress, Cluster, ClusterOptions } from 'actor-ts';
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.

  • Network failures. By default, the registry delivers reliably. For fault injection, you’d implement a custom Transport with drop / delay / reorder logic.
  • Latency. Delivery is synchronous within an event-loop turn.
  • Serialization. Messages are passed by reference, not bytes. If your test actually needs to exercise serialization (e.g., testing CBOR codec), use TcpTransport over loopback instead.

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.

const peers = transport.peers(); // currently-connected addresses

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 lines

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.