Skip to content

Actor lifecycle

Every MatrixActor instance moves through a predictable sequence: construct, initialize, bootstrap, run, dispose. Hooks at each stage let your subclass do work without overriding the dispatch machinery.

Construct → initialize → bootstrap

Construction is plain JavaScript. The base class allocates internal state (subscription bag, mailbox slot, child manager slot lazily, history buffers) but does not touch the bus. Until initialize() is called there is no _context.

ts
const actor = new Greeter();      // constructed, no bus presence
await actor.initialize(context, 'greeter');   // now reachable

initialize(context, name) is the entry point (MatrixActor.ts:618-639). It:

  1. Stores the context and the actor name.
  2. Applies the regime cascade (parent's regime overrides own if more restrictive).
  3. Calls _subscribeToInbox() — the single subscription that backs all dispatch.
  4. Calls _bootstrap(), which calls your onBootstrap() hook.

After initialize() returns, the actor is on the bus, will respond to $ping, $introspect, and $inbox messages for declared ops.

ts
class MyActor extends MatrixActor {
  protected async onBootstrap(): Promise<void> {
    // Allocate resources, hydrate cached state, fetch initial data.
    this.cache = await this.fetchInitialData();
  }
}

onBootstrap() is the right place to:

  • Read state from the bus (other actors' state declarations).
  • Hydrate fact-domain values you declared in subscribes.
  • Open ancillary connections (DB, websocket).
  • Start timers — but pair every timer with cleanup in onDispose().

If onBootstrap() throws, the failure propagates to whoever called initialize(). The runtime treats this as "actor failed to start" and applies the supervisor restart policy if the actor was created via createSupervised().

After bootstrap completes, the framework emits lifecycle.available (MatrixActor.ts:2800-2802). Anything subscribed to your $events topic can pick this up to know "the actor is ready to accept ops."

The ready promise

Every actor exposes a readonly ready: Promise<void>. It resolves when _bootstrap() finishes (success or thrown). Wait on it from another actor when you need to be sure a peer is up:

ts
const actor = await runtime.create(Greeter, 'greeter');
await actor.ready;
// guaranteed bootstrapped

MatrixRuntime.waitForReady(id, timeoutMs) is the runtime-level helper (MatrixRuntime.ts:764-776) and races the promise against a timeout.

Re-initialization (remount)

If initialize() is called a second time on the same actor with a different context, the framework treats it as a remount (MatrixActor.ts:619-628):

  1. Unbind previous subscriptions.
  2. Replace the context.
  3. Re-subscribe to $inbox with the new mount.
  4. Call _onContextChanged() (which by default remounts children).

Remount is the only way an existing actor moves between mount paths. New context = new wire prefix, new $inbox subject. State on this._state is preserved across remount; subscriptions are not.

The $join / $join_ack handshake

When a child actor (typically a MatrixActorHtmlElement in the browser) is constructed inside a parent's template, it does not yet have a context. The handshake that wires it up:

  1. Child publishes $join to the parent's $inbox (MatrixActorHtmlElement.ts connectedCallback path).
  2. Parent's on$join() handler (MatrixActor.ts:704-745) registers the child, lazy-initializes child management infrastructure, and replies with $join_ack to the child's $inbox.
  3. Child receives $join_ack, derives its context from the parent, and bootstraps.

The handshake is automatic for components inside a MatrixActorHtmlElement template. Headless actors mounted programmatically through runtime.create() skip the handshake — they receive their context directly.

Disposal

dispose() is the public teardown hook (MatrixActor.ts:2719-2781). It:

  1. Emits lifecycle.removed (best-effort; transport may be gone).
  2. Calls your onDispose() hook.
  3. Disposes all children (recursively).
  4. Stops the actor behavior (mailbox, dispatch).
  5. Disposes the LISP strategy if any.
  6. Clears prompt-trigger subscriptions and debounce timers.
  7. Disposes the subscription bag — every subscription registered through _subs.add() is torn down.
ts
class MyActor extends MatrixActor {
  private heartbeat?: NodeJS.Timeout;

  protected async onBootstrap(): Promise<void> {
    this.heartbeat = setInterval(() => this.emit('heartbeat', {}), 1000);
  }

  protected onDispose(): void {
    clearInterval(this.heartbeat);
  }
}

dispose() is idempotent — calling it twice is safe. After disposal, calls to setState, emit, or sendTo will throw or warn. The _disposing flag prevents the lifecycle event itself from re-entering disposal.

Supervised lifecycle (root actors)

Root actors created via MatrixRuntime.createSupervised(Class, id, props) (MatrixRuntime.ts:353-370) are monitored for $exit signals. When a supervised actor exits with reason: 'error':

  1. Runtime checks the restart budget (default maxRestarts: 5 per windowMs: 10000, MatrixRuntime.ts:115).
  2. If within budget, remove(mount) then re-create() from the saved blueprint.
  3. If outside the budget, the runtime gives up, deletes the blueprint, and stops monitoring (MatrixRuntime.ts:414-425).
ts
const actor = await runtime.createSupervised(MyActor, 'my-actor', { initialUrl: '...' });
// crashes ⇒ auto-restarted up to 5 times in 10s

For non-root actors (children of another actor), supervision policy is governed by the parent's SecurityRealm and _securityRealm.handleExitSignal(signal). The default policy is "restart child up to N times then escalate" (see core/Supervision.ts).

State during lifecycle

Stage_contextstateBus presenceCan setState?Can emit?
Constructedundefined{}noneno (throws)no (throws)
initialize() runningset{}inbox subscribedyesyes
onBootstrap() runningset{}inbox subscribedyesyes
Livesetmutableinbox subscribedyesyes
dispose() runningsetlast valuetorn downnobest-effort (lifecycle.removed)
Disposedsetlast valuegonewarns + no-opwarns + no-op

Common mistakes

  • Calling setState in the constructor. No _context yet — throws. Move it to onBootstrap().
  • Not clearing intervals in onDispose(). They keep firing on a disposed actor and warn into kernel logs.
  • Holding references to the actor outside the runtime. After disposal, the actor is dead; the only legitimate way to find an actor is runtime.hasComponent(mount) and the bus.
  • Throwing from onDispose(). The base class catches and logs but the rest of disposal continues. Still, don't throw — clean up defensively.

See also

  • Define an actor — the static surface and on{Op} handlers.
  • Actor ops — what _subscribeToInbox actually subscribes to.
  • Actor statesetState(), $stateChanged, persistence scopes.
  • Actor testing — testing the lifecycle without the runtime.

Source: projects/matrix-3/packages/core/src/core/MatrixActor.ts:618-639 (initialize), :2719-2781 (dispose), :2787-2806 (_bootstrap), :704-745 (on$join); runtime/MatrixRuntime.ts:353-453 (supervised lifecycle, restart budget).