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 Frame mit Längenpräfix im
getaggten JSON-Tree-Format — demselben Tree-Format, das
JsonSerializer und die Persistenz-Stores schreiben. Der
einklinkbare Serializer wird auf der Leitung weiterhin nicht
konsultiert:
Sender-Seite: remoteRef.tell(message) → in ein Envelope { to, from, body, tag } verpackt → im body eingebettete ActorRefs → wire-sichere Marker → encodeJsonTree(envelope) → JSON → Frame mit Längenpräfix → Cluster-Transport sendet über die Leitung
Empfänger-Seite: Cluster-Transport setzt den Frame wieder zusammen → JSON.parse(frame) → decodeJsonTree → Envelope → ActorRef-Marker → lebende Remote-Refs → body an onReceive des Actors ausgeliefertDer body darf also alles enthalten, was unter
Was sich serialisieren lässt steht
— Date, Map, Set, bigint, Uint8Array und der Rest
überstehen ein Cross-Node-tell genau so, wie sie einen
Journal-Round-Trip überstehen (#450). Die Klassenidentität
geht weiterhin verloren: der einzige Typ-Hinweis auf der Leitung
ist tag, der Konstruktor-Name der Nachricht, und 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/serialization';
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 trägt dieselbe Liste: Cross-Node-Nachrichten
laufen durch denselben getaggten Tree (nicht durch JsonSerializer
selbst — der Wire rahmt das ganze Envelope, nicht nur den Body),
sodass ein Date als Date und eine Map als Map ankommt
(#450). Was der Tree ablehnt, lehnt er auch auf der Leitung ab,
und zwar laut: eine Funktion oder ein Symbol im Nachrichten-Body
wurde früher von JSON.stringify still verworfen und ist jetzt ein
verworfener Frame mit einer error-Logzeile.
// ✓ — kommt als Date an, nicht als Stringref.tell({ when: new Date() });
// ✗ — abgelehnt, der Frame verlässt den Node nieref.tell({ onDone: () => {} });Wo Serialisierung passiert
Abschnitt betitelt „Wo Serialisierung passiert“1. Cluster-Wire — jedes Cross-Node-tell (getaggter JSON-Tree).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.Alle fünf schreiben das getaggte JSON-Tree-Format — denselben
Tree, den JsonSerializer nutzt — sodass Date / Map / Set /
bigint / Uint8Array ohne Konfiguration durch jeden Journal,
Snapshot-Store, Durable-State-Store und den Cluster-Wire
round-trippen. Persistenz-Stores akzeptieren zusätzlich eine
explizite withSerializer(...)-Option, die einen eigenen
Serializer in ihre Rows routet (siehe
eigene Serializer);
für den Wire gibt es das noch nicht, und die Registry-Bindings
der SerializationExtension werden weiterhin nirgends
konsultiert — dieses Wiring trackt
#450.
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 — was der Cluster-Wire tatsächlich rahmt.
