Actor paths
Ce contenu n’est pas encore disponible dans votre langue.
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-42Reading left to right:
| Segment | What it is |
|---|---|
actor-ts:// | The scheme — same for every actor-ts path. |
my-app | The system name from ActorSystem.create('my-app'). |
user | The user-guardian — every actor you spawn lives under /user. |
api | A great-grandparent (e.g. an Api manager). |
sessions | The immediate parent (e.g. a SessionManager). |
user-42 | The 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
logis 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.
The ActorPath value
Section titled “The ActorPath value”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:
| Member | What it gives you |
|---|---|
path.name | The leaf segment ('logger' above). |
path.parent | The parent’s ActorPath, or null for the root. |
path.systemName | The 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.
How paths are assigned
Section titled “How paths are assigned”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:
-
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. -
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, sospawn(Bar, 'a/b')would otherwise produce a path indistinguishable from a childbof an actora— 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. -
The name must not start with
$. That prefix is reserved for the names the framework generates itself —$anonymous-<n>-<random>fromspawnAnonymous. 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
spawncall rather than onActorPath— 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.
ActorSelection — lookup by path
Section titled “ActorSelection — lookup by path”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.
Accepted path formats
Section titled “Accepted path formats”actorSelection parses three input shapes:
system.actorSelection('actor-ts://my-app/user/api'); // absolute URIsystem.actorSelection('/user/api'); // absolute pathsystem.actorSelection('user/api'); // absolute path, no leading slashURI 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).
When to use a path, when to use a ref
Section titled “When to use a path, when to use a ref”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:
- 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. - Spawn-race resolution — caller doesn’t know exactly when
the target spawns;
resolveOne(timeout)waits. - 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:
// ✗ awkwardclass Worker extends Actor<...> { override onReceive(message) { const cache = await this.context.actorSelection('/user/cache').resolveOne(); cache.tell({ kind: 'put', ... }); }}
// ✓ directclass 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.
Special top-level paths
Section titled “Special top-level paths”Four top-level paths exist in every system:
| Path | What lives there |
|---|---|
/user | Your application’s top-level actors — everything system.spawn(...) creates. |
/system | Framework internals, grouped by subsystem. |
/temp | The one-shot reply refs ask creates, one per outstanding call. |
/deadLetters | The 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.
Inside /system
Section titled “Inside /system”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:
| Path | Actor |
|---|---|
/system/cluster/receptionist | The receptionist. |
/system/cluster/pubsub/mediator | The distributed pub-sub mediator. |
/system/cluster/crdt/data | The 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/hub | The 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/shardingis 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?”
Cluster paths
Section titled “Cluster paths”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| Segment | What it is |
|---|---|
actor-ts:// | The scheme — same as local paths. |
my-app | The cluster’s system name. |
@10.0.0.5:2552 | The node’s host:port, assigned at Cluster.join time. |
/user/api/sessions/user-42 | The 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.
Where to next
Section titled “Where to next”- Spawning actors — the configuration
bundle that includes the optional
namefor 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.
