Zum Inhalt springen
Deutsch

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.

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.

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)
};
FeldWas
manifestDer stabile Diskriminator, der mit jedem Event gespeichert wird; muss dem On-Disk-Manifest entsprechen.
currentVersionDie Version, die neu geschriebene Events tragen.
defaultsNach From-Version indexiert: defaults[v] ist die Menge der Felder, die beim Übergang von v nach v+1 hinzukamen.
writeVersionOptional — 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.

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

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' } },
});

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.

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.

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.