Zum Inhalt springen
Deutsch

Quickstart

Fünf Minuten von “was ist ein Actor” zu “ich habe einen laufen, der Nachrichten sendet und empfängt.” Dieser Guide setzt keine Vorerfahrung mit dem Actor-Modell voraus.

Ein kleines Programm, das ein ActorSystem erzeugt, einen einzelnen Greeter-Actor darin spawnt, dem Actor eine Nachricht sendet und herunterfährt. Dieselbe Datei läuft unverändert unter Bun, Node und Deno — das richtige Runtime-Backend wird automatisch gewählt.

Nach diesem Guide weißt Du:

  • Wie Du einen Actor definierst, indem Du Actor<T> erweiterst.
  • Wie Du ihn via system.spawnAnonymous(...) spawnst.
  • Den Unterschied zwischen tell (fire-and-forget) und dem onReceive-Handler des Actors.
  • Wie Du das System herunterfährst, ohne Timer oder Sockets zu lecken.
Terminal-Fenster
bun add actor-ts

Für eine vollständige Installationsanleitung — einschließlich der optionalen Peer-Dependencies (better-sqlite3, @hono/node-server, Broker-Clients) — siehe die Installation-Seite.

Erstelle hello.ts und füge ein:

import { Actor, ActorSystem } from 'actor-ts';
// 1. Definiere einen Actor. Der Typparameter `<string>` schränkt
// ein, welche Nachrichten dieser Actor akzeptiert — das Typsystem
// fängt schlechte Sends zur Compile-Zeit.
class Greeter extends Actor<string> {
override onReceive(name: string): void {
console.log(`hello, ${name}!`);
}
}
// 2. Erzeuge das System. Ein `ActorSystem` pro Prozess ist die
// Norm; alles andere lebt darin.
const system = ActorSystem.create('hello');
// 3. Spawne den Actor. Du übergibst die Klasse, keine Instanz — das
// System konstruiert sie; `'greeter'` ist ihr Name in der Hierarchie.
const ref = system.spawn(Greeter, 'greeter');
// 4. Sende eine Nachricht. `tell` ist fire-and-forget — es kehrt
// sofort zurück, der Actor verarbeitet die Nachricht asynchron
// auf seiner eigenen Mailbox.
ref.tell('world');
// 5. Gib der Mailbox einen Tick zum Leerlaufen, dann fahre herunter.
// In einer echten App würdest Du via `CoordinatedShutdown` auf
// SIGTERM terminieren; für dieses Skript reicht ein kurzes
// Sleep.
await new Promise((resolve) => setTimeout(resolve, 20));
await system.terminate();

Ausführen:

Terminal-Fenster
bun run hello.ts

Du siehst:

hello, world!

Das ist die ganze Schleife: definieren → spawnen → tell → terminieren.

Die fünf Zeilen bilden auf die fünf Kernideen des Actor-Modells ab:

  1. class Greeter extends Actor<string> — ein Actor ist eine Klasse mit einer privaten Mailbox und einem einzigen onReceive-Handler. Der Typparameter sagt “dieser Actor akzeptiert string-Nachrichten” — ihm eine Zahl zu senden, wäre ein TypeScript-Fehler zur Compile-Zeit.

  2. ActorSystem.create('hello') — das ActorSystem ist der Runtime-Container. Es besitzt den Dispatcher (der die Nachrichtenverarbeitung plant), die Supervisor-Hierarchie (die Actor-Fehler auffängt), den Scheduler, den Event-Stream. Es gibt typischerweise einen pro Prozess.

  3. system.spawn(Greeter, 'greeter') — Du übergibst die Klasse, keine Instanz. Das System muss kontrollieren, wann der Actor konstruiert wird (auf seinem Mailbox-Thread, nicht auf Deinem), und es baut den Actor bei jedem Neustart neu — die Konstruktion muss also in seiner Hand bleiben. Nimmt der Konstruktor Argumente, übergibst Du stattdessen eine Factory, die sie liefert: system.spawn(() => new Greeter(config), 'greeter'). Das zurückgegebene ActorRef ist ein Handle, nicht der Actor selbst — Du kannst es über den Cluster weitergeben, speichern, an andere Actors übergeben.

  4. ref.tell('world')tell ist das primäre Actor-Verb. Es queued die Nachricht in die Mailbox des Actors und kehrt sofort zurück. Der Actor verarbeitet seine Mailbox eine Nachricht nach der anderen auf einem einzigen logischen Thread, sodass Du nie über Locks oder Races innerhalb von onReceive nachdenken musst.

  5. system.terminate() — graceful Shutdown. Stoppt den Dispatcher, stoppt alle Actors (via den Supervisor-Tree), schließt die Transports. Gibt ein Promise zurück, das auflöst, wenn der Teardown abgeschlossen ist. Für echte Apps hänge das an einen SIGTERM-Handler — siehe Coordinated Shutdown.

Eine string-Mailbox ist das absolute Minimum — das Framework wird erst interessant, sobald Nachrichten Struktur tragen. Zwei kurze Erweiterungen des Hello-Actor-Beispiels, die das konkret machen:

Echte Actors empfangen Commands, keine nackten Werte. Die Konvention ist eine Discriminated Union — jede Nachricht trägt ein kind-Literal, das die Union innerhalb von onReceive verengt. match(command).exhaustive() aus ts-pattern liefert Dir zur Compile-Zeit die Garantie, dass jede Variante behandelt ist: füge ein neues kind ohne passenden with(...)-Arm hinzu, und TypeScript bricht den Build ab.

import { Actor, ActorSystem, type ActorRef } from 'actor-ts';
import { match } from 'ts-pattern';
type IncrementCommand = { kind: 'increment' };
type DecrementCommand = { kind: 'decrement' };
type GetCommand = { kind: 'get'; replyTo: ActorRef<number> };
type Command = IncrementCommand | DecrementCommand | GetCommand;
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())
.with({ kind: 'get' }, (m) => this.onGet(m))
.exhaustive();
}
private onIncrement(): void { this.count++; }
private onDecrement(): void { this.count--; }
private onGet(m: GetCommand): void { m.replyTo.tell(this.count); }
}
const system = ActorSystem.create('counters');
const counter = system.spawnAnonymous(Counter);
counter.tell({ kind: 'increment' });
counter.tell({ kind: 'increment' });
counter.tell({ kind: 'decrement' });

Die Variante { kind: 'get'; replyTo: ActorRef<number> } ist die Standardform für “gib mir eine Antwort”: der Anfragende übergibt seine eigene Ref (oder, häufiger, eine von ask bereitgestellte, siehe unten) und der Counter tellt die Antwort zurück. Für die Tiefe siehe Messages und Pattern Matching.

tell ist Fire-and-Forget. Für Request/Response gibt es die ask-Methode auf der Ref:

import { ActorSystem } from 'actor-ts';
const system = ActorSystem.create('counters');
const counter = system.spawnAnonymous(Counter);
counter.tell({ kind: 'increment' });
counter.tell({ kind: 'increment' });
// ask leitet den Reply-Typ ab:
const value = await counter.ask<number>({ kind: 'get' }, 5_000);
console.log(value); // 2
await system.terminate();

Das Framework spawnt unter der Haube einen One-Shot-Reply-Actor, setzt ihn als replyTo UND als context.sender beim Ziel ein und löst das Promise mit der ersten Antwort auf (oder rejected mit AskTimeoutError). Aufrufer schreiben selbst nie replyTo hin.

Siehe Ask Pattern für Timeout-Verhalten, Error Handling und den Dead-Letter-Fall (Ziel-Actor gestoppt, bevor er geantwortet hat).

Du hast einen laufenden Actor. Von hier aus verzweigt sich die Doku, je nachdem, was Du tun willst:

  • Interessantere Nachrichten senden: siehe Messages für die Konventionen (discriminated unions, Immutability, das kind-Feld) und Pattern Matching für das match().exhaustive()-Dispatch-Idiom, das diese Codebase überall verwendet.

  • Antwort vom Actor zurückbekommen: siehe Ask Pattern — das Request/Response-Äquivalent zu tell.

  • Fehler behandeln: siehe Supervision dafür, wie Eltern-Actors sich von Kind-Abstürzen erholen.

  • Über Maschinen verteilen: siehe die Cluster Overview und Sharding Overview — derselbe Code, den Du oben geschrieben hast, kann über N Nodes mit einer einzigen zusätzlichen Zeile laufen. Cluster.bootstrap baut das ActorSystem, tritt dem Cluster bei, startet den Receptionist und verdrahtet SIGTERM / SIGINT-Shutdown in einem Call:

    import { Cluster } from 'actor-ts';
    const { system } = await Cluster.bootstrap({ name: 'hello' });
    const ref = system.spawn(Greeter, 'greeter');
    ref.tell('world');

    Die Discovery defaultet auf eine env-gesteuerte Kette — CLUSTER_SEEDS → Kubernetes API → DNS — sodass derselbe Code lokal als Single-Node läuft und in Produktion ohne Config-Änderung einem bestehenden Cluster beitritt.

  • State über Neustarts hinweg persistieren: siehe PersistentActor für Event Sourcing und Snapshots für begrenzte Recovery-Zeit.

Wenn Du die ganze Oberfläche des Frameworks auf einmal sehen willst, ist das Chat-Beispiel die umfassendste End-to-End-Demonstration — es verwendet Sharding, Persistenz, Distributed PubSub, Distributed Data, Cluster Singleton, HTTP-Routing, sechs austauschbare Frontends, alles.