Zum Inhalt springen
Deutsch

Behaviors

Der Behaviors-Namespace ist eine Sammlung von Kombinatoren, die Behavior<T>-Werte bauen. Ein Behavior beschreibt, was der Actor tut, wenn die nächste Nachricht ankommt — und welches Behavior er danach annimmt. Die Lebenszeit eines Actors ist nur eine Sequenz von Behaviors: jeder Handler gibt das nächste zurück.

import { ActorSystem, Behaviors } from 'actor-ts';
type Msg = { kind: 'tick' };
const ticker = Behaviors.receive<Msg>((ctx, msg) => {
ctx.log.info(`tick at ${Date.now()}`);
return Behaviors.same; // bleibe ein Ticker
});
const system = ActorSystem.create('demo');
const ref = system.spawnTypedAnonymous(ticker);

Behaviors.receive baut das häufigste Behavior: einen Handler, der auf jeder Nachricht läuft und das nächste Behavior zurückgibt. Der same-Sentinel sagt “bleibe wie ich bin” — derselbe Closure behandelt die nächste Nachricht.

const counter = (n: number): Behavior<Msg> => Behaviors.receive<Msg>((ctx, msg) => {
switch (msg.kind) {
case 'inc': return counter(n + 1);
case 'dec': return counter(n - 1);
case 'get': msg.replyTo.tell(n); return Behaviors.same;
}
});

Der Handler empfängt sowohl einen TypedActorContext<T> (zum Spawnen von Kindern, Loggen, Beobachten) als auch die Nachricht. Gibt das nächste Behavior zurück — Behaviors.same behält den Closure; ein frisches counter(n + 1) nimmt einen neuen Closure mit aktualisiertem Zustand an.

const counter = (n: number): Behavior<Msg> => Behaviors.receiveMessage<Msg>((msg) => {
if (msg.kind === 'inc') return counter(n + 1);
return Behaviors.same;
});

Shortcut für den häufigen Fall, dass du den Context nicht brauchst. Äquivalent zu Behaviors.receive((_ctx, msg) => ...).

const watcher = Behaviors.receiveWithSignal<Msg>(
(ctx, msg) => {
// User-Nachricht behandeln...
return Behaviors.same;
},
(ctx, signal) => {
if (signal.kind === 'terminated') {
ctx.log.info(`watched actor ${signal.ref.path} stopped`);
}
return Behaviors.same;
},
);

Der Signal-Handler feuert für Lifecycle-Events:

Signal-KindWann
'post-stop'Der Actor stoppt. Verwende für Cleanup.
'pre-restart'Der Supervisor wird den Actor gleich neu starten. signal.reason ist der Fehler.
'terminated'Ein beobachteter Actor hat gestoppt. signal.ref ist seine Ref.

Das ist das typed-DSL-Äquivalent zum Überschreiben von postStop, preRestart und dem Behandeln von Terminated-Nachrichten in der untyped-Form.

const myActor = Behaviors.setup<Msg>((ctx) => {
ctx.log.info(`I'm starting at ${ctx.path}`);
const helper = ctx.spawn(helperBehavior, 'helper');
return Behaviors.receive((_ctx, msg) => {
helper.tell(msg); // helper im Closure eingefangen
return Behaviors.same;
});
});

setup läuft einmal beim Start des Actors. Verwende es für einmalige Initialisierung, die der Receive-Handler einschließen soll: Kinder spawnen, ctx.self für die Kinder zum Wissen einfangen, externe Verbindungen öffnen.

Drei Kombinatoren wickeln ein anderes Behavior mit zusätzlichen Fähigkeiten:

import { Behaviors, type TimerScheduler } from 'actor-ts';
const heartbeat = Behaviors.withTimers<Msg>((timers) => {
timers.startTimerWithFixedDelay('hb', { kind: 'tick' }, 5_000);
return Behaviors.receiveMessage((msg) => {
if (msg.kind === 'tick') console.log('heartbeat');
return Behaviors.same;
});
});

Die TimerScheduler-API ist dieselbe, die context.timers in der untyped-Form bietet. withTimers fängt sie in einem Closure ein, sodass der Receive-Handler Zugriff hat, ohne bei jeder Nachricht über ctx.timers gehen zu müssen.

const init = Behaviors.withStash<Msg>(100, (stash) => {
return Behaviors.receive((ctx, msg) => {
if (msg.kind === 'ready') {
stash.unstashAll(); // alle gepufferten Nachrichten erneut abspielen
return ready;
}
stash.stash(msg); // alles andere für später parken
return Behaviors.same;
});
});
const ready = Behaviors.receive<Msg>((ctx, msg) => {
// Nachrichten normal behandeln
return Behaviors.same;
});

Kapazitäts-gebundener Stash, mit stash / unstashAll / isEmpty / isFull / size. Gleiche Semantik wie das untyped context.stash, exponiert als Wert statt über Context.

import { Behaviors, OneForOneStrategy, Directive } from 'actor-ts';
const supervised = Behaviors
.supervise(myReceiveBehavior)
.onFailure(new OneForOneStrategy(
(err) => Directive.Restart,
{ maxRetries: 5, withinTimeRangeMs: 60_000 },
));

Wickele ein Behavior mit einer Supervisor-Strategie. Vom inneren Handler geworfene Fehler werden durch die Strategie geleitet — Restart re-initialisiert das Behavior (zurück zur initialen Form), Stop terminiert, Resume überspringt die fehlschlagende Nachricht.

Siehe Supervision für die Direktiven-Semantik; sie gelten in der typed-Form identisch.

Werte, die du aus einem Handler zurückgibst, um eine Übergangsentscheidung auszudrücken:

SentinelBedeutung
Behaviors.sameBehalte das aktuelle Behavior. Der Handler-Closure läuft erneut bei der nächsten Nachricht.
Behaviors.stoppedStoppe den Actor. Äquivalent zu context.stopSelf() in der untyped-Form.
Behaviors.unhandledDiese Nachricht wird hier nicht behandelt; route zu Dead Letters.
Behaviors.emptyDas Behavior akzeptiert Nachrichten, tut aber nichts. Nützlich als Platzhalter.
Behaviors.ignoreVerwirf jede Nachricht still (kein Dead-Letter-Routing).

Die ersten drei sind im Alltag am nützlichsten. empty und ignore existieren für Sonderfälle — ein “dieser Actor ist absichtlich erstmal still”-Stub oder eine Senke, die Traffic schlucken soll.

import { ActorSystem, Behaviors, type Behavior } from 'actor-ts';
type Msg =
| { kind: 'configure'; url: string }
| { kind: 'request'; payload: string };
const initializing = Behaviors.withStash<Msg>(100, (stash) =>
Behaviors.receive<Msg>((ctx, msg) => {
if (msg.kind === 'configure') {
const url = msg.url;
stash.unstashAll();
return ready(url);
}
stash.stash(msg);
return Behaviors.same;
}),
);
const ready = (url: string): Behavior<Msg> =>
Behaviors.receive<Msg>((ctx, msg) => {
if (msg.kind === 'request') {
ctx.log.info(`POST ${url}: ${msg.payload}`);
}
return Behaviors.same;
});
const system = ActorSystem.create('demo');
system.spawnTypedAnonymous(initializing);

Zwei Behaviors:

  • initializing stasht alles, bis ein configure ankommt, dann wechselt es nach ready(url) nach dem Wiederabspielen des Stashs.
  • ready behandelt Requests unter Verwendung der eingefangenen url.

Die Übergänge sind explizite Returns; der Zustand lebt in Closure-Parametern; es gibt kein this, um das man sich kümmern muss.

Dekoratoren komponieren von außen nach innen. Behaviors.supervise(Behaviors.withTimers(...)) bedeutet “supervise das Timer-verwendende Behavior”; Behaviors.withTimers(Behaviors.supervise(...)) bedeutet “gib dem supervised Inneren die Timer.” In der Praxis:

const supervised = Behaviors
.supervise(Behaviors.withTimers((timers) =>
Behaviors.receive((ctx, msg) => Behaviors.same)
))
.onFailure(strategy);

supervise ist außen um das withTimers, die Strategie überwacht also die ganze Konstruktion. Das ist fast immer die richtige Verschachtelung.

  • Typed Actor — die Runtime, die ein Behavior interpretiert.
  • Spawn Typedsystem.spawnTyped, ctx.spawnTyped, typedProps.
  • Supervision — was Behaviors.supervise(...).onFailure(...) intern verwendet.
  • Become und Stash (untyped) — die OO-Äquivalente zu Behaviors.withStash + Behavior-Switching via Return-Werte.