MigratingAdapter
MigratingAdapter handhabt nicht-additive Migrationen —
Umbenennungen, Restrukturierungen, aus-anderen-Feldern-berechnet —
über eine Chain von Versions-Transformationen.
import { MigratingAdapter } from 'actor-ts';
type EventV1 = { kind: 'deposited'; amount: number };type EventV2 = { kind: 'deposited'; cents: number; currency: string };type EventV3 = { kind: 'deposited'; cents: number; currency: string; tenantId: string };
class Account extends PersistentActor<Cmd, EventV3, State> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter() { return new MigratingAdapter<EventV3>({ currentVersion: 3, chain: [ { from: 1, to: 2, fn: (v1: EventV1) => ({ kind: v1.kind, cents: v1.amount * 100, currency: 'USD', }) }, { from: 2, to: 3, fn: (v2: EventV2) => ({ ...v2, tenantId: this.tenantId, }) }, ], }); }}Die Chain:
- V1 → V2 restrukturiert:
amountzucentsumbenennen, mal 100 multiplizieren,currencyhardcoden. - V2 → V3 fügt hinzu:
tenantIdaus der Actor-Instanz ziehen.
Der Adapter wendet die nötigen Schritte an:
- V1-Events: beide Schritte ausführen → V3-Form.
- V2-Events: nur den V2→V3-Schritt ausführen → V3-Form.
- V3-Events: keine Schritte → bereits aktuell.
Konfiguration
Abschnitt betitelt „Konfiguration“interface MigratingAdapterSettings<E> { currentVersion: number; chain: Array<MigrationStep<unknown, unknown>>;}
interface MigrationStep<From, To> { from: number; to: number; fn: (from: From) => To;}Schritte müssen zusammenhängend sein — 1 → 2, 2 → 3, etc.
Kein Versions-Überspringen; keine parallelen Pfade.
Wann verwenden
Abschnitt betitelt „Wann verwenden“MigratingAdapter ist das richtige Werkzeug, wenn:
- Umbenennungen —
amount→cents. - Restrukturierungen — flache Felder → verschachtelte Objekte oder umgekehrt.
- Berechnete Felder — neues Feld, abgeleitet aus alten Feldern.
- Multi-Step-Evolution — v1 → v2 → v3 → v4, jeder Schritt baut auf dem letzten auf.
Für reine Additionen ist
DefaultAdapter
einfacher. Für beliebig geformte Migrationen verwende einen
benutzerdefinierten EventAdapter.
Closure-Kontext
Abschnitt betitelt „Closure-Kontext“class Account extends PersistentActor<...> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter() { return new MigratingAdapter<EventV3>({ chain: [ // Die Migrations-Funktion schließt über `this.tenantId`: { from: 2, to: 3, fn: (v2) => ({ ...v2, tenantId: this.tenantId }) }, ], }); }}Schritte sind plain Funktionen — sie können über die Konstruktor-Argumente des Actors schließen. Nützlich für Per-Instanz-Migrations-Kontext (Per-Tenant-Defaults, Per-Region-Currency).
Schritt-für-Schritt-Semantik
Abschnitt betitelt „Schritt-für-Schritt-Semantik“Gegeben V1-Event + Chain [1→2, 2→3]:
storedV1 → fn1→2(storedV1) = intermediateV2 → fn2→3(intermediateV2) = finalV3Die Zwischen-Typen müssen keiner historischen Event-Form entsprechen — sie sind nur Trittsteine. Das ist manchmal hilfreich: der v1→v2-Schritt könnte eine temporäre Form produzieren, die weder v1 noch kanonisches v2 ist, und der v2→v3-Schritt normalisiert.
Fehlerbehandlung
Abschnitt betitelt „Fehlerbehandlung“{ from: 1, to: 2, fn: (v1) => { if (v1.amount < 0) throw new Error('invalid v1 event'); return { kind: v1.kind, cents: v1.amount * 100 };}}Wenn ein Schritt wirft, scheitert die Recovery mit dem Fehler,
in MigrationError eingewickelt. Das onRecoveryFailure des
Actors wird aufgerufen.
Default: das propagiert zum Supervisor des Actors. Überschreibe
onRecoveryFailure, wenn du problematische Events überspringen
willst (selten; normalerweise willst du sie finden, nicht
überspringen).
Neunummerieren vs. neuer Schritt
Abschnitt betitelt „Neunummerieren vs. neuer Schritt“// Szenario: V2 ist in Produktion, alle Events V1 und V2.// Jetzt willst du V3. Einen Schritt hinzufügen:
chain: [ { from: 1, to: 2, fn: v1To2 }, { from: 2, to: 3, fn: v2To3 }, // NEU]Den V2→V3-Schritt hinzufügen; den existierenden V1→V2-Schritt
behalten. currentVersion: 3 setzen. Existierende V2-Events
werden über den neuen Schritt hochgekastet; V3-Events werden
direkt geschrieben.
Modifiziere niemals existierende Schritte, die Events behandelt haben, die noch im Journal sind — sie sind load-bearing.
Schema-Registry-Integration
Abschnitt betitelt „Schema-Registry-Integration“Für Projekte, die die Schema-Registry verwenden, kannst du validieren, dass die Source-/Destination- Versionen der Chain mit den deklarierten Versionen der Registry übereinstimmen. Optional, aber nützlich als Sanity-Check in Tests.
Stolperfallen
Abschnitt betitelt „Stolperfallen“Wie geht’s weiter
Abschnitt betitelt „Wie geht’s weiter“- Migration im Überblick — das größere Bild.
- Envelope-Format — das zugrunde liegende Wire-Format.
- DefaultAdapter — die nur-additive Alternative.
- WrapLegacy — Pre-Envelope-Events in die Chain bringen.
- Rezepte — häufige Muster durchgespielt.