Zum Inhalt springen
Deutsch

Serialization im Überblick

Zwei Szenarien erfordern es, JS-Werte in Bytes zu verwandeln:

  1. Cluster-Wire — ein tell an einen Remote-Actor braucht die Nachricht serialisiert für die TCP-Übertragung.
  2. Persistenz — Events / Snapshots / Durable State auf der Platte oder im Journal gespeichert.

Das Serializer-Interface des Frameworks ist die Abstraktion. Zwei eingebaute werden mitgeliefert:

SerializerFormatWann
JsonSerializerJSONDefault. Menschenlesbar debugbar, weit verbreitet.
CborSerializerCBOR (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:

SerializerFormatWann
AvroSerializerAvroKleinste Zeilen; Schema-Registry-Interop.
ProtobufSerializerProtobufSprachübergreifende Verträge; vorwärtskompatibler Wire.

Plus ein Extension-Point: implementiere Serializer<T> für MessagePack, FlatBuffers oder alles andere.

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 ausgeliefert

Der 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.

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.

const system = ActorSystem.create('my-app');
// → JsonSerializer als Default registriert
// → Jeder Wert wird als JSON serialisiert, solange kein spezifischer Serializer gebunden ist

Wenn 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).

AspektJSONCBOR
Wire-GrößeGrößer~20–40 % kleiner
GeschwindigkeitLangsamer als CBOR für BinärdatenSchneller
DebuggingTrivial (Text)Braucht ein CBOR-fähiges Tool
Binäre Felderbase64-encoded (verschwenderisch)Native Binärform
Library-SupportUniversalSolide 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.

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ügen
ext.bind(MyEvent, serializer.id); // MyEvent darüber routen

bind 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.

Die JsonSerializer-Klasse behandelt:

  • Plain-Objekte ({ a: 1, b: 'two' }).
  • Arrays.
  • Strings, Zahlen, Booleans, null.
  • Verschachtelte Kombinationen davon.
  • Date, Uint8Array, Map, Set und bigint — ü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() });
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.