defaultsAdapter
defaultsAdapter handhabt die einfachste Migrations-Form:
jede Version hat Felder hinzugefügt, die alle sinnvolle konstante
Defaults haben. Es ist eine Funktion, die einen EventAdapter
zurückgibt — stecke sie in das eventAdapter() eines Actors:
import { defaultsAdapter, type EventAdapter } from 'actor-ts';
type DepositedV1 = { kind: 'deposited'; amount: number };type DepositedV2 = { kind: 'deposited'; amount: number; currency: 'USD' | 'EUR' };
class Account extends PersistentActor<Command, DepositedV2, State> { override eventAdapter(): EventAdapter<DepositedV2> { return defaultsAdapter<DepositedV2>({ manifest: 'BankAccount.Deposited', currentVersion: 2, defaults: { 1: { currency: 'USD' } }, // fields added going v1 → v2 }); }}Ein gespeichertes v1-Event { kind: 'deposited', amount: 100 }
liest sich zurück als
{ kind: 'deposited', amount: 100, currency: 'USD' } — der Default
wird eingemergt. V2-Events lesen sich unverändert.
Wann das ausreicht
Abschnitt betitelt „Wann das ausreicht“defaultsAdapter ist das richtige Werkzeug, wenn:
- Reine Ergänzung — neue Felder kommen hinzu; alte bleiben unverändert.
- Defaults sind konstant — der Default-Wert ist für jedes Event dieser Version gleich.
- Keine Umbenennungen oder Typänderungen — es ergänzt nur fehlende Felder.
Das deckt einen großen Teil realer Migrationen ab. Wenn es nicht passt, greif zu migratingAdapter.
Konfiguration
Abschnitt betitelt „Konfiguration“Das Argument ist ein DefaultsAdapterSpec<E>:
type DefaultsAdapterSpec<E> = { manifest: string; // stable type identity, e.g. 'BankAccount.Deposited' currentVersion: number; // version this code revision emits defaults: { [fromVersion: number]: Partial<E> }; // fields added at each step writeVersion?: number; // emit an older version (rolling deploys)};| Feld | Was |
|---|---|
manifest | Der stabile Diskriminator, der mit jedem Event gespeichert wird; muss dem On-Disk-Manifest entsprechen. |
currentVersion | Die Version, die neu geschriebene Events tragen. |
defaults | Nach From-Version indexiert: defaults[v] ist die Menge der Felder, die beim Übergang von v nach v+1 hinzukamen. |
writeVersion | Optional — Events während eines Rolling-Deploys in einer älteren Version emittieren (später hinzugekommene Felder werden beim Schreiben entfernt). |
Beim Lesen eines gespeicherten Payloads läuft ein Merge pro Schritt
von seiner Version bis currentVersion; jeder Schritt spreadet
zuerst die Defaults dieses Schritts, dann den Payload:
{ ...defaults[v], // fields added at step v ...storedPayload, // actual values win}Bereits gesetzte Felder im gespeicherten Payload gewinnen — Defaults füllen nur Lücken.
Mehrere Versionssprünge
Abschnitt betitelt „Mehrere Versionssprünge“// v1 → v2 added `currency`; v2 → v3 added `metadata`class Account extends PersistentActor<Command, EventV3, State> { override eventAdapter(): EventAdapter<EventV3> { return defaultsAdapter<EventV3>({ manifest: 'BankAccount.Deposited', currentVersion: 3, defaults: { 1: { currency: 'USD' }, // merged when reading a v1 payload 2: { metadata: {} }, // merged when reading a v1 or v2 payload }, }); }}Jeder defaults[v]-Key muss strikt kleiner als
currentVersion sein — der Adapter validiert das und wirft sonst.
Das Lesen eines v1-Payloads wendet die Schritte 1 und 2 an; ein
v2-Payload nur Schritt 2.
Snapshots und Durable State
Abschnitt betitelt „Snapshots und Durable State“Für dieselbe additive Form auf einem Snapshot- /
Durable-State-Datensatz nimm defaultsSnapshotAdapter — identische
Spec, gibt einen SnapshotAdapter zurück:
import { defaultsSnapshotAdapter } from 'actor-ts';
const adapter = defaultsSnapshotAdapter<StateV2>({ manifest: 'BankAccount.State', currentVersion: 2, defaults: { 1: { currency: 'USD' } },});Wenn der Default nicht stimmt
Abschnitt betitelt „Wenn der Default nicht stimmt“Betrachte currency: 'USD' für ein in Deutschland eröffnetes
Konto — der konstante Default ist falsch (sollte EUR sein). Zwei
Optionen:
migratingAdapter für kontextbewusste Migration verwenden
Abschnitt betitelt „migratingAdapter für kontextbewusste Migration verwenden“import { migratingAdapter, MigrationChain } from 'actor-ts';
const chain = MigrationChain.for<DepositedV2>('BankAccount.Deposited', 2) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ ...v1, currency: lookupCurrencyByTimestamp(v1.ts), }) });const adapter = migratingAdapter(chain);Der Upcaster hat Zugriff auf das vollständige v1-Event und kann so einen Per-Event-Wert aus dem Kontext ableiten.
Backfill statt Default
Abschnitt betitelt „Backfill statt Default“Wenn die historischen Events auf die korrekte Währung umgeschrieben werden sollen, lass ein Einmal-Skript laufen, das jedes Event liest, umschreibt und zurückschreibt. Das ist selten — meist ist es in Ordnung, alte Events unverändert zu lassen und einen kontextbewussten Adapter zu nutzen.
Typsicherheit
Abschnitt betitelt „Typsicherheit“defaultsAdapter<EventV2>({ manifest: 'BankAccount.Deposited', currentVersion: 2, defaults: { 1: { currency: 'USD', nonExistentField: 'oops', // ✓ compiles — but never used }, },});Jedes defaults[v] ist ein Partial<E>, TypeScript erlaubt also
Extra-Felder — sie tun nur nichts, wenn der Event-Typ sie nicht
enthält. Achte auf Tippfehler in Feldnamen; sie schlagen still
fehl.
Fallstricke
Abschnitt betitelt „Fallstricke“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Migration im Überblick — das Gesamtbild.
- Envelope-Format — wie Versionierung auf der Disk funktioniert.
- migratingAdapter — für nicht-additive Transformationen.
- Recipes — das Kochbuch pro Muster.
