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(Props.create(...)) 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, Props } 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. `Props.create(() => new Greeter())` ist die
// Factory; `'greeter'` ist der Name des Actors in der Hierarchie.
const ref = system.spawn(Props.create(() => new 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.spawnAnonymous(Props.create(() => new Greeter()))Props ist actor-ts’ Art, die Konstruktion aufzuschieben. Das System muss kontrollieren, wann die Actor-Instanz erstellt wird (auf seinem Mailbox-Thread, nicht auf Deinem), also gibst Du ihm eine Factory statt einer vorgefertigten Instanz. 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(cmd).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, Props, type ActorRef } from 'actor-ts';
import { match } from 'ts-pattern';
type Cmd =
| { kind: 'inc' }
| { kind: 'dec' }
| { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> {
private count = 0;
override onReceive(cmd: Cmd): void {
match(cmd)
.with({ kind: 'inc' }, () => { this.count++; })
.with({ kind: 'dec' }, () => { this.count--; })
.with({ kind: 'get' }, (m) => m.replyTo.tell(this.count))
.exhaustive();
}
}
const system = ActorSystem.create('counters');
const counter = system.spawnAnonymous(Props.create(() => new Counter()));
counter.tell({ kind: 'inc' });
counter.tell({ kind: 'inc' });
counter.tell({ kind: 'dec' });

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 zwei äquivalente Schreibweisen:

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

Oder mit der freien Funktion für denselben Effekt:

import { ask } from 'actor-ts';
const value = await ask<Cmd, number>(counter, { kind: 'get' }, 5_000);

In beiden Fällen spawnt das Framework 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, Props } from 'actor-ts';
    const { system } = await Cluster.bootstrap({ name: 'hello' });
    const ref = system.spawn(Props.create(() => new 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.