Pular para o conteúdo
Português (BR)

Actor paths

Este conteúdo não está disponível em sua língua ainda.

Every actor in the system has a path — a hierarchical identifier built from its position in the supervisor tree:

actor-ts://my-app/user/api/sessions/user-42

Reading left to right:

SegmentWhat it is
actor-ts://The scheme — same for every actor-ts path.
my-appThe system name from ActorSystem.create('my-app').
userThe user-guardian — every actor you spawn lives under /user.
apiA great-grandparent (e.g. an Api manager).
sessionsThe immediate parent (e.g. a SessionManager).
user-42The leaf actor’s own name.

The path identifies an actor uniquely within its system, the way a filesystem path identifies a file: at any moment, the path either resolves to a live actor or doesn’t.

You’ll see paths in three places:

  • Log lines — every actor’s log is bound to its path, so log messages carry the full hierarchy.
  • Terminated.actor.path — when an actor stops, watchers can inspect the path to dispatch on which actor it was.
  • ActorSelection — looking up actors by path string rather than holding a ref.

context.path and ref.path both return an ActorPath:

import { Actor } from 'actor-ts';
class Logger extends Actor<Message> {
override preStart(): void {
this.log.info(`starting; my path is ${this.context.path}`);
// → "starting; my path is actor-ts://my-app/user/logger"
this.log.info(`depth = ${this.context.path.depth()}`);
// → "depth = 2" (root is 0, /user is 1, /user/logger is 2)
this.log.info(`elements = ${this.context.path.elements().join('')}`);
// → "elements = → user → logger" (leading blank = the empty-named root)
}
}

The full surface:

MemberWhat it gives you
path.nameThe leaf segment ('logger' above).
path.parentThe parent’s ActorPath, or null for the root.
path.systemNameThe system name ('my-app' above).
path.elements()Array of segment names from root to leaf.
path.depth()Distance from root (root = 0).
path.isAncestorOf(other)Does this path enclose other?
path.equals(other)Lexical equality (compares toString() output).
path.toString()Canonical URI form: actor-ts://<sys>/<segments>.

Paths are immutable values — calling path.child('thing') returns a new path; it doesn’t mutate the original.

You can pass a name when spawning:

const ref = system.spawn(Foo, 'my-foo');
// ref.path.toString() === 'actor-ts://my-app/user/my-foo'
const child = this.context.spawn(Bar, 'bar');
// child.path.toString() === 'actor-ts://my-app/user/my-foo/bar'

If you omit the name, the framework synthesizes one — $anonymous-, a per-parent counter and twelve random hex characters, for example '$anonymous-1-3f9c1a0d7b42'. The counter keeps spawn order legible; the random half keeps the path from being guessable from the outside, which matters because a path is an address. Three rules govern naming:

  1. Names must be unique among siblings. context.spawn(Bar, 'bar') followed by another spawn under the same name on the same parent throws — that path is already in use.

  2. The name must be a single path segment. The framework enforces this and throws on a name that would corrupt the path: a / or \ separator, the traversal segments . and .., an empty name, or any control character. A path is rendered by joining segments with / and taken apart again by splitting on it, so spawn(Bar, 'a/b') would otherwise produce a path indistinguishable from a child b of an actor a — colliding with, or impersonating, a different actor, including across the cluster wire where the remote side re-splits the string. Control characters are refused because paths are written to logs and trace spans.

  3. The name must not start with $. That prefix is reserved for the names the framework generates itself — $anonymous-<n>-<random> from spawnAnonymous. Without the rule a hand-picked '$anonymous-1-…' could collide with, or stand in for, a name the framework is entitled to hand out, and which of the two won depended on spawn order. $ anywhere else in the name is fine ('order$42').

    The rule applies to names you choose, so it is enforced at the spawn call rather than on ActorPath — paths also get rebuilt from strings arriving over the cluster wire, and rejecting $ there would make every remote reference to an anonymous actor fail on arrival.

    Anything else is accepted, including spaces, dots inside the name and non-ASCII characters — 'Order.Placed', 'entity#3' and '日本語' are all valid.

The uid field on ActorPath distinguishes successive incarnations of the same path — if my-foo stops and is then re-spawned at the same path, the second one has a different uid. Most code never looks at uid; the framework uses it internally to make sure messages aimed at the old incarnation don’t accidentally land in the new one.

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('my-app');
const selection = system.actorSelection('/user/api/sessions/user-42');
const ref = await selection.resolveOne(5_000);
ref.tell({ kind: 'whatever' });

actorSelection(pathString) builds a description of where to look. resolveOne(timeoutMs) walks the actor tree to find a match; it retries every 10 ms until the deadline, useful when the caller races with the actor’s spawn.

Two ways to send to a selection:

  • selection.tell(message) — resolve once, deliver immediately if found, dead-letter if not. No retry. Useful when you have a clear expectation the actor exists; missing it is an error.
  • (await selection.resolveOne(timeout)).tell(message) — wait until the actor exists (or time out). Useful for spawn-race scenarios.

actorSelection parses three input shapes:

system.actorSelection('actor-ts://my-app/user/api'); // absolute URI
system.actorSelection('/user/api'); // absolute path
system.actorSelection('user/api'); // absolute path, no leading slash

URI form is what ActorPath.toString() produces, so round-tripping a path through string form works. The system-name in URI form is checked — selecting actor-ts://other-app/user/api from a system named my-app returns null (a different system entirely).

Refs are the default way to address actors. Hold the ActorRef you got at spawn time, pass it around, store it in fields. The compiler knows the actor’s message type; selection returns unknown-typed refs that you lose typing on.

Reach for an ActorSelection in three situations:

  1. Cross-tree lookup by convention — the actor you want lives at a well-known path (/user/api/sessions, /system/cluster/pubsub/mediator), and there’s no clean way to thread the ref through.
  2. Spawn-race resolution — caller doesn’t know exactly when the target spawns; resolveOne(timeout) waits.
  3. Looking up actors received as strings — e.g. an HTTP request body contains "/user/sessions/user-42" and you want to forward to that actor. Parse + select; never trust the string without re-checking that the actor exists.

For routine in-actor wiring, prefer passing refs through constructors / messages / parent-child relationships:

// ✗ awkward
class Worker extends Actor<...> {
override onReceive(message) {
const cache = await this.context.actorSelection('/user/cache').resolveOne();
cache.tell({ kind: 'put', ... });
}
}
// ✓ direct
class Worker extends Actor<...> {
constructor(private readonly cache: ActorRef<CacheMessage>) { super(); }
override onReceive(message) {
this.cache.tell({ kind: 'put', ... });
}
}
const cache = system.spawn(Cache, 'cache');
const worker = system.spawnAnonymous(() => new Worker(cache));

The constructor-injection version is type-safe, doesn’t depend on path-naming conventions, and survives renames trivially.

Four top-level paths exist in every system:

PathWhat lives there
/userYour application’s top-level actors — everything system.spawn(...) creates.
/systemFramework internals, grouped by subsystem.
/tempThe one-shot reply refs ask creates, one per outstanding call.
/deadLettersThe synthetic recipient for messages with no live target.

/temp is the odd one out: nothing under it is an actor. ask synthesises a reply ref, gives it a /temp/askResp-<id> path and throws it away as soon as the call settles, so it never appears in a supervisor tree and actorSelection never finds it. It has a path at all because a reply that crosses the cluster wire is addressed by path — the recipient’s node needs something to send back to. You will see these in logs as the sender of an asked message.

Anything the framework spawns for itself goes here, one group per subsystem, so the tree tells you which part of the framework an actor belongs to:

PathActor
/system/cluster/receptionistThe receptionist.
/system/cluster/pubsub/mediatorThe distributed pub-sub mediator.
/system/cluster/crdt/dataThe DistributedData replica.
/system/cluster/sharding/region-<typeName>A shard region.
/system/cluster/sharding/coordinator-<typeName>A shard coordinator.
/system/cluster/singleton/manager-<typeName>A cluster-singleton manager (the singleton itself is its child).
/system/persistence/projection/<name>A projection.
/system/delivery/consumer-<n>-<random>, producer-<n>-<random>Reliable-delivery controllers, when you don’t name them yourself.
/system/devtools/hubThe DevTools WebSocket hub, with its probes alongside it.

Two things follow from this that are worth knowing when you read a tree:

  • The group levels are actors. /system/cluster/sharding is an empty supervisor whose whole job is to hold the grouping and the supervision policy for what is under it. It has children and never handles a message.
  • Groups are created on first use. A system that never starts clustering or DevTools has an empty /system — that is the normal state, not a missing branch.

You rarely address /system paths directly: the extensions (cluster, pubsub, sharding) expose typed APIs that hand you refs. But knowing the layout pays off when reading log output — every actor’s logger is bound to its path — or answering “where did this message go?”

When the cluster extension is active, paths get a host-port fragment:

actor-ts://my-app@10.0.0.5:2552/user/api/sessions/user-42
SegmentWhat it is
actor-ts://The scheme — same as local paths.
my-appThe cluster’s system name.
@10.0.0.5:2552The node’s host:port, assigned at Cluster.join time.
/user/api/sessions/user-42The actor path inside that node, identical in shape to a single-node path.

That fragment tells the runtime which node this actor lives on. A bare path like actor-ts://my-app/user/foo (no host) is local to the resolving system. The cluster transport handles the host-routed delivery transparently — your tell looks the same in both cases.

See Refs across nodes for how the host-aware path is wire-encoded.

  • Spawning actors — the configuration bundle that includes the optional name for the actor’s path.
  • Actor system — the guardian hierarchy (/user, /system, /deadLetters).
  • Refs across nodes — how paths and refs work across cluster nodes.
  • Discovery — for actors whose location isn’t known by path but by service-registry semantics.

The ActorPath and ActorSelection API references cover the full method set.