Zum Inhalt springen
Deutsch

Typed — Überblick

actor-ts liefert zwei Schichten für dasselbe Actor-Modell aus:

APIStilSource 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.

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 count in einem Feld, mutiert es, gibt void zurück. Klassisches OO.
  • Die typed-Form übergibt n als Parameter an das nächste Behavior. Kein mutabler Zustand — das nächste Behavior fängt den neuen Count in seinem Closure ein.

Vier Dinge, die die typed-API tut und untyped nicht:

  1. 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.
  2. Pure-funktionales Message-Handling. Handler sind (context, message) => Behavior<T> — keine Seiteneffekte auf this, keine imperative State-Machine. Einfacher isoliert zu testen; ein Behavior ist nur ein Wert.
  3. 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 imperativer context.stopSelf()-Aufrufe.
  4. Cast-freies Child-Spawning. context.spawn(behavior) gibt eine ActorRef<U> zurück, typisiert auf den Nachrichtentyp des Kindes, abgeleitet vom Behavior. Keine as ActorRef<...>-Casts zur Spawn-Zeit.

Drei Dinge, die die untyped-API sauberer tut:

  1. 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. this zu mutieren ist okay.
  2. Lifecycle-Hooks (preStart, postStop, preRestart). Die typed-API exponiert diese als Signal-Events auf einem receiveWithSignal-Handler, was etwas umständlicher ist als das Überschreiben einer Methode.
  3. 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.

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.

Eine Handvoll Bausteine:

KombinatorWas 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.sameSentinel: behalte das aktuelle Behavior.
Behaviors.stoppedSentinel: stoppe den Actor.
Behaviors.unhandledSentinel: diese Nachricht ist unhandled (geht zu Dead Letters).
Behaviors.emptySentinel: No-Op-Handler.
Behaviors.ignoreSentinel: 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.