Serialization im Überblick
Zwei Szenarien erfordern es, JS-Werte in Bytes zu verwandeln:
- Cluster-Wire — ein
tellan einen Remote-Actor braucht die Nachricht serialisiert für die TCP-Übertragung. - Persistenz — Events / Snapshots / Durable State auf der Platte oder im Journal gespeichert.
Das Serializer-Interface des Frameworks ist die Abstraktion.
Zwei eingebaute werden mitgeliefert:
| Serializer | Format | Wann |
|---|---|---|
JsonSerializer | JSON | Default. Menschenlesbar debugbar, weit verbreitet. |
CborSerializer | CBOR (RFC 8949) | Kompakt, binärnativ, schneller. |
Zwei weitere nehmen ein Schema entgegen, das du mitbringst — die
Schema-Bibliothek (avsc, protobufjs, generierter Code) bleibt
eine Abhängigkeit deines Projekts:
| Serializer | Format | Wann |
|---|---|---|
AvroSerializer | Avro | Kleinste Zeilen; Schema-Registry-Interop. |
ProtobufSerializer | Protobuf | Sprachübergreifende Verträge; vorwärtskompatibler Wire. |
Plus ein Extension-Point: implementiere Serializer<T> für
MessagePack, FlatBuffers oder alles andere.
Wie das funktioniert
Abschnitt betitelt „Wie das funktioniert“Für ein Cross-Node-tell verpackt der Cluster die Nachricht in
ein Envelope und sendet sie als JSON.stringify-Frame mit
Längenpräfix — der einklinkbare Serializer wird auf der Leitung
nicht konsultiert:
Sender-Seite: remoteRef.tell(message) → in ein Envelope { to, from, body, tag } verpackt → im body eingebettete ActorRefs → wire-sichere Marker → JSON.stringify(envelope) → Frame mit Längenpräfix → Cluster-Transport sendet über die Leitung
Empfänger-Seite: Cluster-Transport setzt den Frame wieder zusammen → JSON.parse(frame) → Envelope → ActorRef-Marker → lebende Remote-Refs → body an onReceive des Actors ausgeliefertDer body muss JSON-sicher sein. Der einzige Typ-Hinweis
auf der Leitung ist tag — der Konstruktor-Name der Nachricht —
fürs Routing; es gibt kein serializerId / manifest-Framing
auf dem Cluster-Wire.
Das Serializer-Interface
Abschnitt betitelt „Das Serializer-Interface“interface Serializer<T = unknown> { readonly id: number; // pro Serializer eindeutig readonly name: string; // menschenlesbar includesManifest: boolean; manifest(obj: T): string; // Type-Tag toBinary(obj: T): Uint8Array; fromBinary(bytes: Uint8Array, manifest: string): T;}Kleine Oberfläche — encoden, decoden, identifizieren. Die
SerializationExtension des Frameworks ist eine Registry, die
Klassen auf Serializer mappt (über bind); wenn du einen Wert über
die Extension encodest (ext.encode), schlägt sie den an die Klasse
des Werts gebundenen Serializer nach.
Default-Verhalten
Abschnitt betitelt „Default-Verhalten“const system = ActorSystem.create('my-app');// → JsonSerializer als Default registriert// → Jeder Wert wird als JSON serialisiert, solange kein spezifischer Serializer gebunden istWenn du nichts tust, sind die Bytes jedes Wertes JSON. Plain- Objekte, Arrays, Strings, Zahlen — alles funktioniert. Klasseninstanzen werden als Plain-Objekte serialisiert (Methoden gehen verloren).
JSON oder CBOR wählen
Abschnitt betitelt „JSON oder CBOR wählen“| Aspekt | JSON | CBOR |
|---|---|---|
| Wire-Größe | Größer | ~20–40 % kleiner |
| Geschwindigkeit | Langsamer als CBOR für Binärdaten | Schneller |
| Debugging | Trivial (Text) | Braucht ein CBOR-fähiges Tool |
| Binäre Felder | base64-encoded (verschwenderisch) | Native Binärform |
| Library-Support | Universal | Solide im JS-Land |
Für die meisten Apps ist JSON okay — der Perf-Unterschied zählt nicht und die Debugbarkeit ist wertvoll.
Für bandbreitenkritische Fälle (großer Cluster, viele Events, IoT-artige Payloads) gewinnt CBOR spürbar.
Siehe JsonSerializer + CborSerializer für Details.
Schema-Serializer und eigene Formate
Abschnitt betitelt „Schema-Serializer und eigene Formate“Avro und Protobuf werden fertig mitgeliefert — du lieferst das kompilierte Schema, das Framework den Serializer:
import { ProtobufSerializer, ProtobufSerializerOptions, SerializationExtensionId } from 'actor-ts';
const protobufOptions = ProtobufSerializerOptions.create<MyEvent>() .withMessageType(root.lookupType('shop.MyEvent')) .withId(100);const serializer = new ProtobufSerializer(protobufOptions);
const ext = system.extension(SerializationExtensionId);ext.register(serializer); // zur ID-Lookup-Tabelle hinzufügenext.bind(MyEvent, serializer.id); // MyEvent darüber routenbind nimmt eine numerische Serializer-ID, der Serializer muss
also zuerst registert werden. Jetzt löst die Extension jeden
MyEvent-Wert zu Protobuf auf; andere Typen fallen auf JSON zurück.
Für jedes andere Format — MessagePack, FlatBuffers, etwas
Maßgeschneidertes — implementierst du Serializer<T> selbst. Die
IDs 1–99 sind reserviert für die eingebauten; nimm ≥ 100.
Siehe Eigene Serializer für das volle Setup.
Was sich serialisieren lässt
Abschnitt betitelt „Was sich serialisieren lässt“Die JsonSerializer-Klasse behandelt:
- Plain-Objekte (
{ a: 1, b: 'two' }). - Arrays.
- Strings, Zahlen, Booleans,
null. - Verschachtelte Kombinationen davon.
Date,Uint8Array,Map,Setundbigint— über Type-Tags als echte Instanzen round-getrippt.NaN/Infinity/-0,RegExp,URL,Error(Name + Message + Cause, ohne Stack) und jedes Typed Array /DataView/ArrayBuffer(#889).
Sie wirft bei Funktionen, Symbolen, undefined, zirkulären
Referenzen, Promise und WeakMap / WeakSet;
Klasseninstanzen dekodieren zu Plain-Objekten (Methoden +
Identität verloren).
CborSerializer trägt dieselbe Liste — eine gemeinsame
Test-Suite stellt sicher, dass die beiden sich bei jedem Typ einig
sind — nur als kompaktes Binärformat statt als getaggten JSON-Text.
Er unterscheidet sich an einer Stelle: undefined wird nativ
getragen statt abgelehnt, womit er der großzügigere der beiden ist.
Der Cluster-Wire ist anders: Cross-Node-Nachrichten laufen
durch rohes JSON.stringify (nicht JsonSerializer), sodass auf
der Leitung die üblichen JSON-Grenzen gelten — Date → ISO-String,
Map / Set → {}, bigint wirft, undefined / Funktionen
verworfen. Konvertiere vor einem Cross-Node-tell in einfache,
JSON-sichere Formen:
// ✗ref.tell({ when: new Date() });
// ✓ref.tell({ when: Date.now() });Wo Serialisierung passiert
Abschnitt betitelt „Wo Serialisierung passiert“1. Cluster-Wire — jedes Cross-Node-tell (rohes JSON.stringify).2. PersistentActor.persist(event) — Events ins Journal.3. DurableStateActor.persist(state) — State in den Store.4. Snapshot-Writes — Snapshots in den Snapshot-Store.5. DistributedData — replizierter State über den Cluster.Die Persistenz (2-4) schreibt das getaggte JSON-Tree-Format —
denselben Tree, den JsonSerializer nutzt — sodass Date / Map /
Set / bigint / Uint8Array ohne Konfiguration durch jeden
Journal, Snapshot-Store und Durable-State-Store round-trippen.
Jeder Store akzeptiert zusätzlich eine explizite
withSerializer(...)-Option, die einen eigenen Serializer in
seine Rows routet (siehe
eigene Serializer);
die Registry-Bindings der SerializationExtension werden für
Persistenz und Wire weiterhin nicht konsultiert — dieses Wiring
trackt #450.
Der Cluster-Wire nutzt JSON.stringify direkt auf dem Envelope —
nicht den einklinkbaren Serializer (siehe Wie das
funktioniert). In-Process-Tells serialisieren nicht — der
Empfänger bekommt die exakte In-Memory-Referenz. Serialisierung
passiert nur an Prozess- / Disk- / Cluster-Grenzen.
Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- JSON-Serializer — der Default.
- CBOR-Serializer — die binäre Alternative.
- Eigene Serializer — Protobuf, Avro usw.
- Nachrichten — welche Formen Serialisierung überleben.
- Refs zwischen Nodes — der Cluster-Wire, der Serializer konsumiert.
