Typed — Überblick
actor-ts liefert zwei Schichten für dasselbe Actor-Modell aus:
| API | Stil | Source of Truth |
|---|---|---|
| Untyped (die Fundamentals) | Subklasse Actor<TMessage>, override onReceive, mutiere this.state. | Das ist die niedrigere, flexiblere API. |
| Typed (dieser Bereich) | Komponiere Behavior<T>-Werte via Behaviors.receive(...), gib das nächste Behavior aus Handlern zurück. | Eine funktionale Fassade auf der untyped-Engine. |
Das Framework lässt beide auf demselben Dispatcher, derselben Mailbox, demselben Supervisionsbaum laufen. Die Wahl ist Ausdrucksstil, nicht Fähigkeit.
Ein direkter Vergleich
Abschnitt betitelt „Ein direkter Vergleich“Derselbe Counter, zwei Wege:
Untyped:
import { match } from 'ts-pattern';import { Actor, ActorSystem } from 'actor-ts';
type IncrementCommand = { kind: 'increment' };type DecrementCommand = { kind: 'decrement' };type Command = IncrementCommand | DecrementCommand;
class Counter extends Actor<Command> { private count = 0; override onReceive(command: Command): void { match(command) .with({ kind: 'increment' }, () => this.onIncrement()) .with({ kind: 'decrement' }, () => this.onDecrement()) .exhaustive(); }
private onIncrement(): void { this.count++; } private onDecrement(): void { this.count--; }}
const system = ActorSystem.create('demo');const counter = system.spawnAnonymous(Counter);Typed:
import { match } from 'ts-pattern';import { ActorSystem, Behaviors } from 'actor-ts';
type IncrementCommand = { kind: 'increment' };type DecrementCommand = { kind: 'decrement' };type Command = IncrementCommand | DecrementCommand;
const counter = (n: number) => Behaviors.receive<Command>((_context, command) => match(command) .with({ kind: 'increment' }, () => counter(n + 1)) .with({ kind: 'decrement' }, () => counter(n - 1)) .exhaustive());
const system = ActorSystem.create('demo');const ref = system.spawnTypedAnonymous(counter(0));Gleiche Runtime-Semantik; unterschiedliche Ergonomie:
- Die untyped-Form hält
countin einem Feld, mutiert es, gibtvoidzurück. Klassisches OO. - Die typed-Form übergibt
nals Parameter an das nächste Behavior. Kein mutabler Zustand — das nächste Behavior fängt den neuen Count in seinem Closure ein.
Was dir typed bringt
Abschnitt betitelt „Was dir typed bringt“Vier Dinge, die die typed-API tut und untyped nicht:
- Zustand als Parameter, nicht als Felder. Jedes Behavior
fängt seinen Zustand in einem Closure ein; der “nächste Zustand”
ist das, was du aus dem Handler zurückgibst. Weniger
Footgun-anfällig als
this.x-Mutation, besonders beim Refactoring. - Pure-funktionales Message-Handling. Handler sind
(context, message) => Behavior<T>— keine Seiteneffekte aufthis, keine imperative State-Machine. Einfacher isoliert zu testen; ein Behavior ist nur ein Wert. Behaviors.same/Behaviors.stopped-Sentinels. Der Return-Type des Handlers ist die State-Transition-Entscheidung des Actors, “behalte dasselbe Behavior” und “stoppe den Actor” sind also Werte, die du zurückgibst, statt imperativercontext.stopSelf()-Aufrufe.- Cast-freies Child-Spawning.
context.spawn(behavior)gibt eineActorRef<U>zurück, typisiert auf den Nachrichtentyp des Kindes, abgeleitet vom Behavior. Keineas ActorRef<...>-Casts zur Spawn-Zeit.
Was dir untyped bringt
Abschnitt betitelt „Was dir untyped bringt“Drei Dinge, die die untyped-API sauberer tut:
- Mutabler Zustand über Nachrichten hinweg. Wenn der Actor
ein komplexes Aggregat hat (eine
Map, einen Buffer, eine State-Machine mit fünf Feldern), wird es umständlich, es durch jeden Behavior-Return zu fädeln.thiszu mutieren ist okay. - Lifecycle-Hooks (
preStart,postStop,preRestart). Die typed-API exponiert diese alsSignal-Events auf einemreceiveWithSignal-Handler, was etwas umständlicher ist als das Überschreiben einer Methode. - Direkter Interop mit dem Rest des Frameworks. Die meisten
Beispiele in den Docs verwenden die untyped-Form. Die meisten
Cluster-Extensions (Sharding, Singleton, PubSub) sind ohnehin
in
ActorRef<T>typisiert — aber ihre Internas sind untyped-Klassen, und das leakt gelegentlich an den Nähten.
Wann welches wählen
Abschnitt betitelt „Wann welches wählen“Wähle typed, wenn:
- Der Actor eine State-Machine ist und du willst, dass Phasen verschiedene Behaviors sind (verschiedene Valid-Message-Sets pro Phase). Die typed-Form macht die Übergänge zu expliziten Returns.
- Du von einer typed-Actor-API auf einer anderen Runtime, von Cats Effect oder fp-ts kommst und der funktionale Stil sich natürlich anfühlt.
- Du compiler-geprüfte State-Transition-Logik willst — typed Behaviors komponieren auf Typ-Ebene.
Wähle untyped, wenn:
- Der Actor erheblichen mutablen Zustand hat (einen Cache, einen Buffer, eine Subscriber-Menge) und ihn durch Behaviors zu fädeln mehr Zeremonie als Wert ist.
- Du das Actor-Modell lernst und keine zweite Abstraktions-Schicht im Weg willst.
- Du Eins-zu-eins-Parität mit klassischen untyped-Actor-APIs aus anderen Bibliotheken willst, die du verwendet hast.
Du kannst sie mischen. Ein untyped-Parent kann typed-Kinder
über context.spawnTypedAnonymous(behavior) spawnen, und ein typed-Parent
spawnt Kinder über context.spawn(behavior). Refs sind interoperabel —
beide Formen exponieren ActorRef<T> mit derselben
tell(message)-Signatur, sodass typed- und untyped-Actors einander über
die Grenze hinweg Nachrichten senden können.
Wie die typed-Schnittstelle aussieht
Abschnitt betitelt „Wie die typed-Schnittstelle aussieht“Eine Handvoll Bausteine:
| Kombinator | Was er tut |
|---|---|
Behaviors.setup(context => behavior) | Läuft einmal mit dem Context; gibt das initiale Behavior zurück. Wie ein “Konstruktor”. |
Behaviors.receive((context, message) => behavior) | Standard-Message-Handler. Gibt das nächste Behavior zurück. |
Behaviors.receiveMessage(message => behavior) | Shortcut, wenn du den Context nicht brauchst. |
Behaviors.receiveWithSignal(handler, signalHandler) | Fügt einen Lifecycle-Signal-Handler hinzu (postStop, preRestart, terminated). |
Behaviors.withTimers(timers => behavior) | Fängt den Per-Actor-TimerScheduler in einem Closure ein. |
Behaviors.withStash(capacity, stash => behavior) | Fängt einen StashBuffer<T> mit der gegebenen Kapazität ein. |
Behaviors.supervise(behavior).onFailure(strategy) | Wickelt ein Behavior mit einer Supervisor-Strategie. |
Behaviors.intercept(behavior, interceptor) | Lässt einen Interceptor vor jeder Nachricht laufen — beobachten, transformieren oder verwerfen. |
Behaviors.monitor(observer, behavior) | Leitet jede Nachricht an einen anderen Actor weiter, bevor sie behandelt wird. |
Behaviors.logMessages(behavior, options?) | Loggt jede Nachricht, bevor sie behandelt wird. |
Behaviors.same | Sentinel: behalte das aktuelle Behavior. |
Behaviors.stopped | Sentinel: stoppe den Actor. |
Behaviors.unhandled | Sentinel: diese Nachricht ist unhandled (geht zu Dead Letters). |
Behaviors.empty | Sentinel: No-Op-Handler. |
Behaviors.ignore | Sentinel: verwirf jede Nachricht still. |
Jede Seite in diesem Bereich bohrt in einen Ausschnitt — siehe Behaviors für die Kombinatoren, Typed Actor für die Engine, die ein Behavior hostet, Spawn Typed für die drei Spawn-Helfer.
Wie es weitergeht
Abschnitt betitelt „Wie es weitergeht“- Behaviors — das volle DSL.
- Typed Actor — was ein Behavior zur Laufzeit ausführt.
- Spawn Typed —
system.spawnTyped,context.spawnTyped,typedActor. - Fundamentals-Überblick — die untyped Konzeptkarte; viele Ideen übertragen sich.
