Skip to content

Routers ​

A router is an actor that owns a pool of identical routees and forwards every message it receives to them according to a routing strategy. Use one to spread work over a pool or to broadcast a message to a group without hand-rolling the bookkeeping: the router spawns the routees as its children, supervises them, keeps the rotation up to date as routees die, and stops the whole pool when it stops.

ts
import { Props } from "@tochemey/nodeakt";

const router = await system.spawnRouter("workers", 8, Props.create(Worker), {
  strategy: "roundRobin",
});

system.noSender().tell(router, new Job(42)); // routed to one routee

spawnRouter(name, poolSize, routees, options?) creates and starts the router. The router is a real actor: it lives under the given unique name like any spawn, it appears in the tree, and system.actorOf(name) resolves it. poolSize is a positive integer; routees is Props, so each routee is constructed fresh from the same recipe. Everything spawn rejects with applies, plus ErrInvalidPoolSize for a bad pool size, ErrInvalidRoutingStrategy for an unknown strategy, and ErrRoutingKeyRequired for consistent hashing without a routing key extractor.

Strategies ​

RouterOptions.strategy is a RoutingStrategy; the default is round robin.

ValueConstantEffect
"roundRobin"RoundRobinRoutingEach message goes to the next routee in rotation, so a steady stream spreads evenly.
"random"RandomRoutingEach message goes to a routee picked at random.
"fanOut"FanOutRoutingEach message goes to every live routee: a broadcast.
"consistentHash"ConsistentHashRoutingEach message goes to the routee owning its routing key, so equal keys always land on the same routee.

Consistent hashing requires a routing key extractor in RouterOptions.routingKey, a RoutingKeyFunc mapping a message to a string or number key:

ts
const router = await system.spawnRouter("carts", 8, Props.create(Cart), {
  strategy: "consistentHash",
  routingKey: (message) => (message as AddItem).cartId,
});

Keys are pinned with a consistent-hash ring, so resizing the pool moves as few keys as possible. An extractor that throws does not fail the router: the message is routed to dead letters with the thrown error as the reason.

Forwarding semantics ​

The router forwards; it never processes a user message itself. The delivery is handed to the routee live, so:

  • The routee sees the original sender as ctx.sender, not the router. Replies bypass the router.
  • An ask sent to the router is answered by whichever routee receives it; ctx.response in the routee settles the asker's promise directly, and the ask timeout applies end to end.
  • An ask (or an in-actor request) through a fan-out router is rejected with the ErrFanOutAsk sentinel: a broadcast has no single answer.

Management messages ​

The router consumes two management messages itself instead of forwarding them. They are exported message classes, not methods on a handle: the router is addressed like any other actor, which keeps the surface uniform and works unchanged once messages cross isolates.

GetRoutees asks for the pool; the router answers with a Routees message whose paths field lists the canonical path strings of the live routees:

ts
import { GetRoutees, Routees } from "@tochemey/nodeakt";

const routees = (await system.noSender().ask(router, new GetRoutees(), 1_000)) as Routees;
console.log(routees.paths); // ["nodeakt://sys@127.0.0.1:0/workers/routee-0", ...]

AdjustRouterPoolSize grows or shrinks the pool in place to the given number of live routees. A grow spawns fresh routees; a shrink stops the newest ones gracefully. Sent with tell it is fire and forget; sent with ask the router answers with the resulting Routees once the adjustment is done. A size that is not a non-negative integer is refused with the ErrInvalidPoolSize sentinel.

ts
import { AdjustRouterPoolSize } from "@tochemey/nodeakt";

await system.noSender().ask(router, new AdjustRouterPoolSize(16), 1_000);

Failing routees ​

A routee that throws while processing escalates the failure to the router, which drops it from the pool. Routees are spawned long-lived, so they never passivate; the only way one leaves the pool is by dying, whether it failed, was stopped, or its host went away.

A dead routee leaves the rotation as soon as its death is observed, and the pool shrinks. A pool that reaches zero routees matches the send-path rules: every subsequent send becomes a dead letter carrying ErrDead as the reason (and an ask rejects with the same sentinel), until AdjustRouterPoolSize restores capacity explicitly.

Lifecycle ​

The routees are children of the router, named routee-0, routee-1, and so on; names are never reused within one router. Stopping the router, with shutdown, a PoisonPill, or the system stopping, stops the pool with it. The router and its routees run on the isolate that spawned the router.

Released under the MIT License.