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.
Die Felder
Abschnitt betitelt „Die Felder“| Feld | Typ | Zweck |
|---|---|---|
_v | number | Die Version, unter der diese Payload geschrieben wurde. |
_t | string | Der Typ-Tag — typischerweise der Event-/State-Name. Optional, aber nützlich für Tooling. |
_e | object | Die tatsächliche Payload. |
Unterstriche, damit sie nicht mit User-Feldern kollidieren. Das Framework betrachtet jedes Objekt mit diesen drei Keys als Envelope.
Wann Envelopes angewendet werden
Abschnitt betitelt „Wann Envelopes angewendet werden“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.
Was der Adapter sieht
Abschnitt betitelt „Was der Adapter sieht“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 siehttype 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.versioninspizieren.- Wenn es die aktuelle Version ist,
stored.payloadzuDomainEvent(die aktuelle Form) casten und zurückgeben. - Wenn es älter ist,
stored.payloadin 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.
Versionierung deiner Events
Abschnitt betitelt „Versionierung deiner Events“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.
Mischen mit Legacy-Nicht-Envelope-Events
Abschnitt betitelt „Mischen mit Legacy-Nicht-Envelope-Events“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).
Persistenz-Formate
Abschnitt betitelt „Persistenz-Formate“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
_e—Date,Map,Set,bigintundUint8Arrayü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;
_eselbst 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).
Stolperfallen
Abschnitt betitelt „Stolperfallen“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Migration im Überblick — das größere Bild.
- defaultsAdapter — Zero-Config-Additive-Migrationen.
- migratingAdapter — verkettete Transformationen.
- Legacy wrappen — zum Mischen mit Nicht-Envelope-Legacy-Events.
