migratingAdapter
migratingAdapter handhabt nicht-additive Migrationen —
Umbenennungen, Restrukturierungen, aus-anderen-Feldern-berechnet —
indem es eine
MigrationChain
ausführt: eine Sequenz typisierter Upcaster. Du baust die
Chain und wickelst sie dann ein:
import { migratingAdapter, MigrationChain, type EventAdapter } from 'actor-ts';
type DepositedV1 = { kind: 'deposited'; amount: number };type DepositedV2 = { kind: 'deposited'; cents: number; currency: string };type DepositedV3 = { kind: 'deposited'; cents: number; currency: string; tenantId: string };
class Account extends PersistentActor<Command, DepositedV3, State> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter(): EventAdapter<DepositedV3> { const chain = MigrationChain.for<DepositedV3>('BankAccount.Deposited', 3) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ kind: v1.kind, cents: v1.amount * 100, currency: 'USD', }) }) .add({ fromVersion: 2, toVersion: 3, upcast: (v2: DepositedV2): DepositedV3 => ({ ...v2, tenantId: this.tenantId, }) }); return migratingAdapter(chain); }}Die Chain:
- v1 → v2 restrukturiert:
amountincentsumbenennen, mal 100,currencyhartkodieren. - v2 → v3 ergänzt:
tenantIdaus der Actor-Instanz ziehen.
Der Adapter wendet an, welche Schritte nötig sind:
- v1-Events: beide Schritte laufen → v3-Form.
- v2-Events: nur der v2 → v3-Schritt läuft → v3-Form.
- v3-Events: keine Schritte → bereits aktuell.
Konfiguration
Abschnitt betitelt „Konfiguration“migratingAdapter ist eine Funktion über einer MigrationChain.
currentVersion und Manifest liegen auf der Chain; die
Optionen des Adapters steuern nur die Schreib-Version:
function migratingAdapter<E>( chain: MigrationChain<E>, options?: { writeVersion?: number }, // default: chain.currentVersion): EventAdapter<E, unknown>;
// Steps added to the chain:interface MigrationStep<From, To> { fromVersion: number; toVersion: number; upcast(from: From): To;}Baue die Chain mit
MigrationChain.for<E>(manifest, currentVersion) und .add(step).
Schritte müssen vorwärts laufen (fromVersion < toVersion) —
konsekutiv ist normal, größere Sprünge sind erlaubt, aber selten.
Wann einsetzen
Abschnitt betitelt „Wann einsetzen“migratingAdapter ist das richtige Werkzeug, wenn:
- Umbenennungen —
amount→cents. - Restrukturierungen — flache Felder → verschachtelte Objekte oder umgekehrt.
- Berechnete Felder — ein neues Feld, abgeleitet aus alten Feldern.
- Mehrstufige Evolution — v1 → v2 → v3 → v4, jeder Schritt baut auf dem letzten auf.
Für reine Ergänzungen ist
defaultsAdapter
einfacher. Für beliebig geformte Migrationen schreib einen eigenen
EventAdapter.
Closure-Kontext
Abschnitt betitelt „Closure-Kontext“class Account extends PersistentActor<Command, EventV3, State> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter(): EventAdapter<EventV3> { // The upcaster closes over `this.tenantId`: const chain = MigrationChain.for<EventV3>('BankAccount.Deposited', 3) .add({ fromVersion: 2, toVersion: 3, upcast: (v2: EventV2): EventV3 => ({ ...v2, tenantId: this.tenantId }) }); return migratingAdapter(chain); }}Upcaster sind einfache Funktionen — sie können über die Konstruktor-Argumente des Actors schließen. Nützlich für Per-Instanz-Migrationskontext (Per-Tenant-Defaults, Per-Region-Währung).
Schritt-für-Schritt-Semantik
Abschnitt betitelt „Schritt-für-Schritt-Semantik“Gegeben ein v1-Event + eine Chain mit den Schritten 1 → 2 und
2 → 3:
storedV1 → upcast₁→₂(storedV1) = intermediateV2 → upcast₂→₃(intermediateV2) = finalV3Die Zwischentypen müssen keiner historischen Event-Form
entsprechen — sie sind nur Trittsteine. Die Chain läuft vom Cursor
der gespeicherten Version vorwärts, bis sie currentVersion
erreicht.
Rolling Deploys mit Downcastern
Abschnitt betitelt „Rolling Deploys mit Downcastern“Während eines Rolling-Deploys müssen v2-Nodes womöglich weiterhin
v1-Events emittieren, solange v1-Reader noch im Cluster sind. Füge
der Chain Downcaster hinzu und setze writeVersion am Adapter:
const chain = MigrationChain.for<DepositedV2>('BankAccount.Deposited', 2) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ ...v1, currency: 'USD' }) }) .addDown({ fromVersion: 2, toVersion: 1, downcast: (v2: DepositedV2): DepositedV1 => { const { currency: _c, ...rest } = v2; void _c; return rest; } });
// Phase 1 — read both, still WRITE v1:const phase1 = migratingAdapter(chain, { writeVersion: 1 });
// Phase 2 — every reader upgraded; write the current version:const phase2 = migratingAdapter(chain); // writeVersion = currentVersion = 2writeVersion muss ≤ currentVersion sein, und die Chain muss
Downcaster für jeden Schritt auf dem Pfad currentVersion → writeVersion haben — sonst wirft toJournal.
Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“.add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => { if (v1.amount < 0) throw new Error('invalid v1 event'); return { kind: v1.kind, cents: v1.amount * 100, currency: 'USD' }; } })Wenn ein Schritt wirft, schlägt die Recovery fehl, und der Fehler
kommt über das onRecoveryFailure des Actors zum Vorschein — und der
Actor stoppt anschließend, sofern dieser Hook nicht in die Supervision
rethrowt (siehe Wenn die Recovery
fehlschlägt).
Eine Chain-Lücke — kein Upcaster für den Cursor registriert, bevor
currentVersion erreicht ist — wirft ein MigrationError, das die
fehlende fromVersion nennt.
Einen Schritt hinzufügen
Abschnitt betitelt „Einen Schritt hinzufügen“// v2 in production; now you want v3. Add a step and bump the chain version:const chain = MigrationChain.for<EventV3>('BankAccount.Deposited', 3) .add({ fromVersion: 1, toVersion: 2, upcast: v1To2 }) .add({ fromVersion: 2, toVersion: 3, upcast: v2To3 }); // NEWFüge den v2 → v3-Schritt hinzu, behalte den bestehenden v1 →
v2-Schritt und bump MigrationChain.for(..., 3). Bestehende
v2-Events werden durch den neuen Schritt hochgecastet; v3-Events
werden direkt geschrieben.
Ändere nie bestehende Schritte, die Events behandelt haben, die noch im Journal liegen — sie sind tragend.
Snapshots
Abschnitt betitelt „Snapshots“Für eine MigrationChain, die einen Snapshot- /
Durable-State-Datensatz regiert, nimm migratingSnapshotAdapter —
dieselbe Chain, gibt einen SnapshotAdapter zurück:
import { migratingSnapshotAdapter } from 'actor-ts';
const adapter = migratingSnapshotAdapter(chain);Fallstricke
Abschnitt betitelt „Fallstricke“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Migration im Überblick — das Gesamtbild.
- Envelope-Format — das zugrunde liegende Wire-Format.
- defaultsAdapter — die additive-only Alternative.
- Legacy wrappen — um Pre-Envelope-Events in die Chain zu bringen.
- Recipes — gängige Muster durchgespielt.
