TypedActor
TypedActor<T> ist der Runtime-Host, der einen Behavior<T>-Wert
nimmt und ausführt. Intern ist es eine Actor<T>-Subklasse —
gleiche Mailbox, gleicher Dispatcher, gleicher Supervisionsbaum —
aber sein onReceive delegiert an das gerade aktive Behavior.
Du konstruierst TypedActor nicht direkt. Die Spawn-Methoden
(system.spawnTyped / context.spawnTyped und
typedActor) wickeln es für dich. Aber
zu wissen, wie es funktioniert, klärt die typed-DSL-Semantik —
besonders, was “die Runtime interpretiert das Behavior” tatsächlich
bedeutet.
Wie ein Behavior zu einem Actor wird
Abschnitt betitelt „Wie ein Behavior zu einem Actor wird“import { ActorSystem, Behaviors } from 'actor-ts';
const myBehavior = Behaviors.receive<Message>((context, message) => { // ... message behandeln return Behaviors.same;});
const system = ActorSystem.create('demo');const ref = system.spawnTypedAnonymous(myBehavior);// Unter der Haube: system.spawnAnonymous(() => new TypedActor(myBehavior))Das Framework:
- Konstruiert ein
TypedActor<T>mitmyBehaviorals initialem Wert. - In
preStartresolved es das initiale Behavior — läuft durch allesetup/withTimers/withStash/supervise-Wrapper, bis es bei einem Blatt landet (einemreceiveoder einem Sentinel). - Bei jeder Nachricht ruft es den resolved Handler auf. Der Return-Wert wird zum neuen “aktuellen Behavior”.
- Wenn der Return
sameist, ändert sich nichts. Wenn es ein frisches Behavior ist, resolved das Framework das und nimmt es als das neue aktuelle an.
Die Resolution-Loop
Abschnitt betitelt „Die Resolution-Loop“Komponierte Behaviors werden Schicht für Schicht ausgepackt. Gegeben:
const b = Behaviors.supervise( Behaviors.withTimers((timers) => Behaviors.setup((context) => Behaviors.receive((context, message) => Behaviors.same)))) .onFailure(strategy);Der Resolver läuft:
supervise(...) ← Supervise-Strategie einfangen, in Kind rekursieren withTimers(...) ← Timer-Scheduler einfangen, in Factory-Return rekursieren setup(...) ← factory(context) laufen lassen, in Return rekursieren receive(...) ← Blatt — als `current` speichernJeder Wrapper hat seinen Seiteneffekt einmal (Supervisor
installieren, Timer einfangen, Setup-Factory ausführen) und
verschwindet. Die finale Form ist ein einfacher receive-Wert.
Das Framework merkt sich die Supervise-Strategie und den
Timer-Scheduler über die Lebenszeit des Actors hinweg; die
setup-Factory läuft nur beim ersten Mal und bei Restart.
“Merkt sich” ist ein Stack, kein einzelner Slot: ein Lauf, der zwei
supervise-Wrapper passiert, hält zwei Geltungsbereiche fest, den
innersten zuletzt. Ein Fehler wird ihnen von innen nach außen
angeboten, und Directive.Escalate rückt einen Bereich nach außen —
ein äußerer Wrapper wird also nicht stillschweigend von einem inneren
verdrängt, siehe
Verschachtelung.
Ein Restart kürzt den Stack, und nur unterhalb des Bereichs, der ihn
entschieden hat, denn das Neuauflösen dessen Kindes baut die Wrapper
darunter neu auf.
Das eine, was der Lauf über einen Übergang hinweg nicht behält, ist
der Signal-Handler: er gehört zum receive-Knoten, der ihn deklariert
hat, ein receive ohne onSignal meldet ihn also ab. Nur die
Sentinels lassen ihn unberührt — genau das erlaubt es einem Behavior,
mit Behaviors.stopped zu antworten und sein post-stop dennoch zu
sehen.
Ein Wrapper-Zyklus (ein setup, das sich selbst zurückgibt, oder
ein supervise, das rekursiert) ist auf 64 Hops gebunden — danach
wirft der Resolver, was die Fehlkonfiguration laut surfacet, statt
ewig zu drehen.
Übergänge
Abschnitt betitelt „Übergänge“Behaviors.receive<Message>((context, message) => match(message) .with({ kind: 'start' }, () => runningBehavior) .with({ kind: 'stop' }, () => Behaviors.stopped) .otherwise(() => Behaviors.same));Vier Ergebnisse:
Behaviors.same→ derselbe Closure handhabt die nächste Nachricht. Die Runtime hältcurrentunverändert.- Ein anderer
Behavior<T>-Wert → die Runtime resolved ihn und tauscht ihn ein. Wrapper führen ihre Seiteneffekte wieder aus (ein frischessetupläuft, frische Timer werden eingefangen, wennwithTimerserneut eingeführt wird — wichtig für “Phase 1 hatte einen Timer, Phase 2 nicht”-Patterns). Behaviors.stopped→ die Runtime stoppt den Actor.Behaviors.unhandled→ die Nachricht geht zu Dead Letters; das aktuelle Behavior bleibt.
Lifecycle-Signale
Abschnitt betitelt „Lifecycle-Signale“Behaviors.receiveWithSignal<Message>( (context, message) => Behaviors.same, (context, signal) => match(signal) .with({ kind: 'post-stop' }, () => { context.log.info('cleaning up'); return Behaviors.same; }) .with({ kind: 'pre-restart' }, (s) => { context.log.warn(`restarting: ${s.reason}`); return Behaviors.same; }) .with({ kind: 'terminated' }, (s) => { context.log.info(`${s.ref.path} stopped`); return Behaviors.same; }) .exhaustive(),);Der Signal-Handler wird aus den postStop /
preRestart-Hooks/Terminated-Message-Delivery-Hooks des Frameworks
aufgerufen. Drei Signale:
| Kind | Ausgelöst durch |
|---|---|
post-stop | Der Actor stoppt (aus irgendeinem Grund: PoisonPill, Behaviors.stopped, Supervisor-Stop). |
pre-restart | Ein Fehler wird gleich neu gestartet. signal.reason ist der Fehler. |
terminated | Ein beobachteter Actor (context.watch(ref)) hat gestoppt. signal.ref ist seine Ref. |
Der Return-Wert funktioniert wie beim Receive-Handler — same, um
das aktuelle Behavior zu behalten, ein neues Behavior, um zu
tauschen.
Der Handler ist an das Behavior gebunden, das ihn deklariert hat. Geh
zu einem Behaviors.receive ohne onSignal über, und die Registrierung
ist weg: post-stop und pre-restart feuern nirgends, und der Tod
eines beobachteten Actors erreicht den Receive-Handler wieder als
gewöhnliche Terminated-Nachricht. Deklariere onSignal in jedem
Zustand neu, der es braucht — siehe
receiveWithSignal.
Wo die Nähte sichtbar werden
Abschnitt betitelt „Wo die Nähte sichtbar werden“TypedActor ist die Brücke — und an der Naht zwischen typed und
untyped tauchen ein paar Details auf:
- Fehler in einem typed-Handler erreichen den typed-Supervisor
zuerst. Wenn der Handler in
Behaviors.supervise(...).onFailure(strategy)gewickelt ist, behandelt diese Strategie den Fehler. Wenn nicht, propagiert er an den Supervisor des Parents — wie bei untyped. Genauso ein Fehler, für den dem Wrapper die Restarts ausgegangen sind: istmaxRetriesinnerhalb vonwithinTimeRangeMsaufgebraucht, hört die typed-Ebene auf zu absorbieren, und der Fehler läuft weiter. Sindsupervise-Wrapper verschachtelt, heißt “läuft weiter”, dass erst der nächste Wrapper nach außen an die Reihe kommt; der Supervisor des Parents ist das, was hinter dem äußersten liegt. context.spawn(behavior)gibt eine voll typisierteActorRef<U>zurück. Die Runtime wickelt das Kind in einen weiterenTypedActor<U>. Das ist das cast-freie Child-Spawning, das die typed-Form bietet.- Watching ist einseitig. Ein typed-Actor kann eine untyped-Ref
context.watch(ref)en oder umgekehrt. Das Terminated-Signal kommt auf der typed-Seite über dasterminated-Signal; auf der untyped-Seite über eineTerminated-Nachricht.
Initial-Behavior-Fallstricke
Abschnitt betitelt „Initial-Behavior-Fallstricke“Das Behavior, das du an system.spawnTypedAnonymous(behavior) übergibst,
wird zum initialen aktuellen. Zwei Grenzfälle:
Behaviors.sameals Initial ist sinnlos — es gibt nichts zu behalten. Die Runtime behandelt es alsBehaviors.empty(stilles No-Op), der Actor existiert also, verwirft aber alle Nachrichten. Das deutet meist auf einen Logik-Bug irgendwo hin.Behaviors.stoppedals Initial stoppt den Actor inpreStart. Die Ref des Actors wird zurückgegeben, aber er terminiert bereits. Nützlich, wenn “soll dieser Actor existieren?” durch eine externe Prüfung zur Spawn-Zeit bestimmt wird.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Behaviors — das DSL, das die
Werte produziert, die
TypedActorinterpretiert. - Spawn Typed — die drei
Helfer, die
TypedActorfür den normalen Gebrauch wickeln. - Actor (untyped) — die
Eltern-Klasse, die
TypedActorerweitert. - Supervision — wohin
die
Behaviors.supervise(...).onFailure(...)-Strategie routet.
