Zum Inhalt springen
Deutsch

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: amount zu cents umbenennen, mal 100 multiplizieren, currency hardcoden.
  • V2 → V3 fügt hinzu: tenantId aus 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.
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 sein1 → 2, 2 → 3, etc. Kein Versions-Überspringen; keine parallelen Pfade.

MigratingAdapter ist das richtige Werkzeug, wenn:

  • Umbenennungenamountcents.
  • 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.

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).

Gegeben V1-Event + Chain [1→2, 2→3]:

storedV1 → fn1→2(storedV1) = intermediateV2 → fn2→3(intermediateV2) = finalV3

Die 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.

{ 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).

// 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.

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.