Worker mesh
Это содержимое пока не доступно на вашем языке.
JavaScript is single-threaded per actor system. For
parallelism within one OS process, the framework’s
worker mesh runs multiple ActorSystems — one per worker
thread — all participating in the same cluster via a
MessageChannel transport.
Each is a separate cluster node to the cluster’s view — gossip + membership + sharding all apply. Communication between them goes via in-process MessageChannel (no serialization to bytes, no TCP).
When to use it
Section titled “When to use it”Two main scenarios:
- CPU-bound parallelism in one process — actor-ts is single-threaded per system; multi-threading needs multiple systems. Worker mesh distributes them.
- Isolation within one process — a “worker” failing doesn’t take down the main system.
For multi-process parallelism (separate OS processes), use regular cluster + TCP transport. Worker mesh is specifically for the in-process case.
High-level API: WorkerCluster
Section titled “High-level API: WorkerCluster”Wiring the broker, channels, and per-worker handshake by hand (see
Under the hood below) is exactly what
WorkerCluster automates. It spawns a pool of workers from one
entrypoint module, runs the hello/init/ready handshake, registers each
worker with a shared WorkerBroker, and restarts crashed workers per a
restartPolicy. The underlying worker primitive is picked per runtime
— Web Workers on Bun/Deno, node:worker_threads on Node — so the same
code runs everywhere.
// main.ts — main threadimport { WorkerCluster, WorkerClusterOptions } from 'actor-ts';
const workerClusterOptions = WorkerClusterOptions.create() .withWorkers(4) .withBootstrap(new URL('./worker-node.js', import.meta.url)) .withSystemName('multi-core') .withBasePort(2552);const cluster = await WorkerCluster.spawn(workerClusterOptions);
console.log(`Spawned ${cluster.size} workers:`);for (const address of cluster.addresses) console.log(' -', address.toString());
// ...later, on shutdown:await cluster.terminate();Each worker runs the bootstrap module. It calls WorkerNode.join()
to complete the handshake and receive its address, system name,
transport, and initData, then joins the cluster like any other node:
// worker-node.ts — runs inside each workerimport { ActorSystem, Cluster, ClusterOptions, WorkerNode } from 'actor-ts';
async function main(): Promise<void> { const context = await WorkerNode.join<{ seedAddress?: string }>(); const system = ActorSystem.create(context.systemName); const clusterOptions = ClusterOptions.create() .withHost(context.self.host) .withPort(context.self.port) .withSeeds(context.initData.seedAddress ? [context.initData.seedAddress] : []) .withTransport(context.transport); await Cluster.join(system, clusterOptions);
system.spawn(MyActor, 'worker'); context.ready(); // tell the main thread this node is up}
void main();| Option | What |
|---|---|
withBootstrap(url) | Worker entrypoint module. Required. |
withWorkers(n | 'auto') | Pool size; 'auto' uses hardware concurrency. Default 'auto'. |
withSystemName(name) | ActorSystem name each worker hosts. Default 'worker-cluster'. |
withHostname(host) | Hostname component of each worker’s address. Default 'worker'. |
withBasePort(port) | Port of the first worker; each subsequent worker increments. Default 1. |
withInitData(data) | Payload delivered to every worker’s join() context (initData). Default null. |
withRestartPolicy(p) | 'always' / 'on-failure' / 'never'. Default 'on-failure'. |
withReadyTimeoutMs(ms) | Per-worker handshake timeout. Default 10000. |
withBackend(backend) | Spawn through this WorkerBackend instead of the detected one — a runtime auto-detection does not know, or an in-memory fake in a test. Default: detected. |
The returned WorkerCluster exposes size, addresses
(the NodeAddress of each worker), the shared broker, and
terminate(). Misconfigured options (a bad basePort, a
non-positive workers) throw OptionsError at spawn.
Under the hood: manual wiring
Section titled “Under the hood: manual wiring”The WorkerCluster above does this wiring for you. Reach for the
manual path only when you need full control — a custom topology, a
bespoke handshake, or extra per-node setup.
// main.ts — main threadimport { Worker } from 'node:worker_threads';import { ActorSystem, Cluster, ClusterOptions, MessageChannelTransport, NodeAddress } from 'actor-ts';
const channel = new MessageChannel();
const w1 = new Worker('./worker.js', { workerData: { mainPort: channel.port2 }, transferList: [channel.port2],});
const mainAddress = new NodeAddress('main', 'main', 0);const transport = new MessageChannelTransport(mainAddress, channel.port1);
const system = ActorSystem.create('main');const clusterOptions = ClusterOptions.create() .withHost('main') .withPort(0) .withSeeds(['main']) .withTransport(transport);await Cluster.join( system, clusterOptions,);
// worker.js — runs in the worker threadimport { parentPort, workerData } from 'node:worker_threads';import { ActorSystem, Cluster, ClusterOptions, MessageChannelTransport, NodeAddress } from 'actor-ts';
const workerAddress = new NodeAddress('w1', 'w1', 0);const transport = new MessageChannelTransport(workerAddress, workerData.mainPort);
const system = ActorSystem.create('w1');const cluster2Options = ClusterOptions.create() .withHost('w1') .withPort(0) .withSeeds(['main']) .withTransport(transport);await Cluster.join( system, cluster2Options,);
// From here on, w1 is just another cluster nodeThe mesh shape
Section titled “The mesh shape”The transport is a star, not a full mesh. A single
WorkerBroker on the main thread is the hub: it holds one
MessagePort per node and forwards each frame to the node named
by the envelope’s to address. Every node’s
MessageChannelTransport holds a single port — its own end
of one channel to the broker, never an array:
import { MessageChannelTransport, NodeAddress, WorkerBroker } from 'actor-ts';
// Main thread: one broker is the hub for every node.const broker = new WorkerBroker();
// One MessageChannel per worker — the broker keeps one end and// the worker receives the other (transferred via workerData).const workerAddress = new NodeAddress('w1', 'w1', 0);const channel = new MessageChannel();broker.register(workerAddress, channel.port1);
// Inside that worker, its transport holds the single far end:const transport = new MessageChannelTransport(workerAddress, channel.port2);So N workers need N channels (one per worker) — not binomial(N,2). The broker relays every hop, which keeps the wiring linear but makes the main thread the relay for all cross-node traffic.
How it differs from TCP cluster
Section titled “How it differs from TCP cluster”TCP transport: MessageChannelTransport:- Sockets, framing - postMessage between threads- Serialized bytes - Structured cloning (no JSON)- Network latency - Sub-microsecond- Cross-host - Same process onlyMessages between worker systems go through structured clone — faster than JSON.stringify + parse, and preserves more types (Map, Set, Date, etc.).
Use cases
Section titled “Use cases”Sharding across cores
Section titled “Sharding across cores”// 4-worker mesh; sharding distributes entities across them:const startShardingOptions = StartShardingOptions.create() .withTypeName('order') .withEntityActor(...) .withExtractEntityId((message) => message.id) .withNumShards(16);sharding.start( startShardingOptions,);The coordinator (on main) allocates shards to the 4 workers. CPU-bound entity work parallelizes across cores.
Per-worker isolation
Section titled “Per-worker isolation”// Worker that handles GPU-bound jobs:system.spawn(GpuJobActor, 'gpu-jobs');
// Crashes within this worker stay isolated from main + other workersA worker crashing doesn’t take down the main system — separate event loops.
When NOT to use it
Section titled “When NOT to use it”Where to next
Section titled “Where to next”- Cluster overview — the cluster model worker-mesh participates in.
- Transports — the transport interface MessageChannelTransport implements.
- Sharding — the primary consumer of mesh parallelism.
