Zum Inhalt springen
Deutsch

Envelope-Format

Wenn ein Actor einen eventAdapter() (oder stateAdapter()) hat, wird jede persistierte Payload vor dem Write in einen versionierten Envelope verpackt:

{
"_v": 2, // Version
"_t": "deposited", // Typ-Tag
"_e": { "kind": "deposited", "amount": 100, "currency": "USD" }
}

Beim Lesen entpackt das Framework das in einen Stored Frame { manifest, version, payload } und routet ihn durch das fromJournal(stored) des Adapters. Der Adapter sieht das Manifest, die Version und die Payload; er gibt das Event in aktueller Form zurück.

FeldTypZweck
_vnumberDie Version, unter der diese Payload geschrieben wurde.
_tstringDer Typ-Tag — typischerweise der Event-/State-Name. Optional, aber nützlich für Tooling.
_eobjectDie tatsächliche Payload.

Unterstriche, damit sie nicht mit User-Feldern kollidieren. Das Framework betrachtet jedes Objekt mit diesen drei Keys als Envelope.

class Account extends PersistentActor<...> {
override eventAdapter() {
return new SomeAdapter();
}
}

Einen Adapter zu setzen, lässt das Framework Events beim Write einwickeln + beim Read auspacken. Ohne Adapter werden Events roh geschrieben — kein Envelope.

Das ist strikt beim Lesen: sobald du einen Adapter setzt, wirft das Lesen eines Nicht-Envelope-Events MigrationError. Du kannst eingewickelte + rohe Events nicht für dieselbe pid mischen ohne WrapLegacy (siehe unten).

Für Erst-Deployments (frisches Journal) kannst du entweder von Anfang an mit Adaptern starten (jedes Event hat _v: 1) oder die Adapter-Einführung verzögern, bis du Migration wirklich brauchst. Die meisten Teams fügen Adapter erst hinzu, wenn die erste Breaking Change kommt.

interface EventAdapter<DomainEvent, JournalShape = DomainEvent> {
manifest(event: DomainEvent): string;
toJournal(event: DomainEvent): OutboundFrame<JournalShape>;
fromJournal(stored: StoredFrame): DomainEvent;
}
// was der Adapter auf dem Read-Pfad sieht
type StoredFrame = {
manifest: string;
version: number;
payload: unknown;
};
// was der Adapter auf dem Write-Pfad ausgibt (in einen Envelope verpackt)
type OutboundFrame<JournalShape = unknown> = {
manifest: string;
version: number;
payload: JournalShape;
};

Beim Lesen entpackt das Framework den Envelope in einen StoredFrame und ruft das fromJournal(stored) des Adapters auf. stored.payload ist die _e-Payload — ohne den Envelope; stored.version ist der _v-Wert; stored.manifest ist der _t-Tag. Der Read-Pfad-Job des Adapters:

  • stored.version inspizieren.
  • Wenn es die aktuelle Version ist, stored.payload zu DomainEvent (die aktuelle Form) casten und zurückgeben.
  • Wenn es älter ist, stored.payload in die aktuelle Form transformieren, dann zurückgeben.

manifest und toJournal decken den Write-Pfad ab — sie taggen ein ausgehendes Event und verpacken es als das Triple, das das Framework in einen Envelope einwickelt.

Die eingebauten Adapter des Frameworks (defaultsAdapter, migratingAdapter) kapseln dieses Pattern. Benutzerdefinierte Adapter implementieren dasselbe.

Wenn du die Version hochziehen willst:

class Account extends PersistentActor<...> {
override eventAdapter() {
const chain = MigrationChain.for<EventV2>('deposited', 2)
.add({ fromVersion: 1, toVersion: 2, upcast: v1ToV2 });
return migratingAdapter(chain);
}
}

Der Adapter deklariert die aktuelle Version. Neue Events, die von onCommand geschrieben werden, bekommen jetzt _v: 2-Envelopes. Alte _v: 1-Events werden über die Chain hochgekastet.

Das funktioniert unabhängig davon, unter welcher Version ein gegebenes Event geschrieben wurde — die Migrations-Chain handhabt jedes.

Wenn du einen Adapter zu einem existierenden Journal hinzufügst, das bereits persistierte rohe Events hat:

import { migrateInMemoryJournal } from 'actor-ts';
// One-shot: wrap every raw event as a v1 envelope before recovery.
await migrateInMemoryJournal(journal, (e) => `${e.kind}`);

Das schreibt die rohen Events in v1-Envelopes um; von da an upcasted dein normaler Adapter sie wie jedes andere v1-Event. Siehe Legacy wrappen für Details (und das Per-Row-Primitiv wrapEventAsEnvelope für Nicht-In-Memory-Backends).

Der Envelope ist ein reguläres JSON-Objekt — gespeichert über das getaggte JSON-Tree-Format, das jeder Store schreibt (oder über einen konfigurierten Per-Store-Serializer).

Das bedeutet:

  • Reiche Typen round-trippen innerhalb von _eDate, Map, Set, bigint und Uint8Array überleben über Type-Tags; Funktionen, Symbols und zirkuläre Referenzen werfen zur Persist-Zeit. Siehe was Events und State enthalten dürfen.
  • Tief verschachtelte Objekte funktionieren — der Envelope ist eine Ebene tief; _e selbst kann beliebig komplex sein.
  • Binary-freundlich über einen eigenen Serializer — ein Store mit konfiguriertem withSerializer(...) framt den ganzen Envelope durch diesen Serializer (kompakter, binary-freundlich).