Zum Inhalt springen
Deutsch

Chat-Sample

Das Chat-Sample ist eine komplette Demo-App, die zeigt, wie die Teile des Frameworks zusammenspielen:

  • TCP-Cluster aus 3 Bun-Prozessen, per Gossip verbunden.
  • Sharded Chatroom-Actors - ein ChatRoomActor pro Room, über den Cluster als 16 Shards verteilt.
  • DistributedPubSub für Cross-Node-Chatroom-Broadcasts.
  • PersistentActor für die Chatroom-History (SQLite-Journal + Snapshots).
  • DistributedData für Presence, das Laufzeit-Room-Directory und Read-Receipts.
  • ClusterSingleton als HTTP-Eingangstür - ein Node bindet :8080; bei Failover übernimmt ein überlebender Node den Bind.
  • WebSocket-only-Client-Protokoll - ein einziger /ws-Endpoint trägt Login, Rooms, History, Presence und Nachrichten.

Zu finden unter examples/chat/ im Repo.

3-Node-Cluster (Bun)

join / send

persist + publish

deliver

presence / receipts

converged state

:8080

WebSocket-Clients

(6 Frontends)

HTTP-Ingress

ClusterSingleton - bindet :8080

serviert Static + /ws-Upgrade

UserSession-Actors

einer pro WS-Verbindung

hält Socket + State

ChatRoom-Actors (sharded PersistentActor)

einer pro Room, 16 Shards

persistiert Nachrichten-History

DistributedPubSub

Topic pro Room

Rooms publishen; Sessions abonnieren

DistributedData

ORSet Presence + Room-Directory

LWWMap Read-Receipts

Der vollständige Pfad eines “User sendet Nachricht”-Flows:

1. Der WS-Client sendet einen "send"-Frame über /ws an den Ingress-Singleton.
2. Der Ingress routet ihn an den UserSession-Actor dieser Verbindung (einer pro Socket).
3. UserSession informiert die zuständige ChatRoom-Entity (sharded nach Room-Name).
4. ChatRoom persistiert die Nachricht via PersistentActor.persist().
5. ChatRoom publisht auf DistributedPubSub auf das Topic des Rooms.
6. Jede UserSession, die dieses Topic abonniert hat, empfängt die Nachricht.
7. Jede UserSession pusht sie an ihren eigenen WS-Client.

Cluster + Persistenz + PubSub + WebSocket - alle zusammen am Werk.

Terminal-Fenster
# Repo klonen und Dev-Deps installieren:
git clone https://github.com/pathosDev/actor-ts.git
cd actor-ts
bun install
# 3-Node-Cluster starten - drei Terminals, gleicher Befehl, keine Flags:
bun examples/chat/backend/main.ts
bun examples/chat/backend/main.ts
bun examples/chat/backend/main.ts
# Chat-UI öffnen (eine URL, egal welcher Node den Singleton hält):
open http://localhost:8080/

Jeder Node führt dasselbe Binary aus. Sie finden einander, indem sie den Cluster-Port-Bereich ab 2551 scannen, sodass sich die drei Terminals ohne konfigurierte Seeds auf 2551 / 2552 / 2553 einpendeln. Der Cluster wählt dann einen Node, der den HTTP-Ingress-ClusterSingleton betreibt; dieser bindet :8080 und serviert den Frontend-Selector sowie den /ws-Endpoint. Die Persistenz ist ein lokales SQLite-Journal + Snapshot-Store unter ./data/ - keine externe Datenbank. Beende den Node, der den Singleton hält, und ein überlebender Node bindet :8080 innerhalb weniger Sekunden neu; die persistierte History bleibt erhalten.

const chatRoomRegion = cluster.sharding.start('ChatRoom', ChatRoomActor,
StartShardingOptions.create<ChatRoomCommand>()
.withExtractEntityId((message) => message.room)
.withNumShards(16));

Ein ChatRoomActor pro Room, über den Cluster als 16 Shards verteilt - beende einen beliebigen Node, und die überlebenden Nodes übernehmen dessen Rooms. Direct-Message-Channels nutzen dasselbe Pattern in einer separaten Region, gekeyt auf die kanonische DM-Pair-ID.

class ChatRoomActor extends PersistentActor<ChatRoomCommand, ChatEvent, ChatState> {
readonly persistenceId = `room-${this.roomName}`;
// ... onCommand persistiert; onEvent aktualisiert State ...
}

Jeder Room-Actor zeichnet jede Nachricht auf; Recovery spielt sie zurück.

// ChatRoom publisht nach dem Persistieren:
ps.mediator.tell(new Publish(`room.${roomId}`, message));
// UserSession abonniert, wenn der User einem Room beitritt:
ps.mediator.tell(new Subscribe(`room.${roomId}`, this.self));

Sessions auf beliebigen Nodes empfangen Room-Nachrichten, unabhängig davon, auf welchem Node der ChatRoom publisht.

class UserSessionActor extends Actor<SessionMessage> {
private ws: WebSocket | null = null;
override onReceive(message: SessionMessage): void {
match(message)
.with({ kind: 'connect-ws' }, (m) => this.onConnectWs(m))
.with({ kind: 'inbound' }, (m) => this.onInbound(m))
.exhaustive();
}
private onConnectWs(message: ConnectWsMessage): void {
this.ws = message.socket;
}
private onInbound(message: InboundMessage): void {
this.ws?.send(JSON.stringify(message.payload));
}
}

Jede Session hält den WebSocket ihres Users; Sends pushen direkt zum Client.

  • Sharded Daemon Processes - das Chat-Sample braucht keine fixen Background-Worker.
  • Replicated Event Sourcing - ein Single-Writer pro Room reicht aus.

Dafür siehe die eigenständigen Snippets oder das Voice-Sample.

examples/chat/
├── README.md
├── application.conf # HOCON: Log-Level, Gossip-Kadenz
├── data/ # SQLite-Journal + Snapshots (gitignored)
├── backend/
│ ├── main.ts # Einstieg: nur Wiring (Cluster, Persistenz, Sharding, Singleton)
│ ├── config.ts # CLI-Argument-Parsing
│ ├── routes.ts # HTTP-DSL-Route (Frontend-Selector)
│ ├── auth/ # scrypt-Passwortprüfung + HMAC-Session-Tokens
│ ├── discovery/ # Same-Host-Port-Scan-Seed-Provider
│ └── actors/
│ ├── ChatRoomActor.ts # sharded PersistentActor (pro Room)
│ ├── ChatRoomDirectoryActor.ts # DistributedData ORSet (Laufzeit-Rooms)
│ ├── DirectMessageChannelActor.ts # sharded PersistentActor (pro DM-Paar)
│ ├── UserSessionActor.ts # Session pro WS-Verbindung
│ ├── OnlineUsersActor.ts # DistributedData ORSet (Presence)
│ ├── ReadReceiptsActor.ts # DistributedData LWWMap (Read-Pointer)
│ ├── HttpIngressActor.ts # ClusterSingleton: hält den :8080-Bind
│ └── WebsocketIngressActor.ts # WS-Plumbing pro Verbindung
├── shared/
│ ├── protocol.ts # geteilte WS-Nachrichten-Typen
│ ├── rooms.ts # Default-Room-Liste
│ ├── users.ts # Test-Credentials
│ └── directMessage.ts # Kanonisierung der DM-Pair-ID
├── static/ # gebaute Frontend-Assets (@fastify/static)
├── frontend-{plain,angular,react,next,svelte,lit}/ # sechs UI-Varianten
├── smoke-test.ts # Single-Node-Messaging-Round-Trip
└── failover-test.ts # HTTP-Singleton-Failover

Jeder Actor lebt in seiner eigenen Datei unter backend/actors/; main.ts ist reines Wiring. Sechs Frontends teilen sich ein WebSocket-Protokoll, sodass du sie direkt nebeneinander vergleichen kannst.