Legacy wrappen
Das Envelope-Format des Frameworks tritt nur in Kraft, wenn ein Adapter konfiguriert ist. Bevor du einen Adapter setzt, werden Events roh gespeichert:
{ "kind": "deposited", "amount": 100 }Sobald ein Adapter da ist, werden neue Events in einen Envelope gewickelt:
{ "_v": 1, "_t": "BankAccount.deposited", "_e": { "kind": "deposited", "amount": 100 } }Das erzeugt ein Problem, wenn du einen Adapter zu einem
existierenden Journal hinzufügst: die alten rohen Events haben
keinen Envelope, aber der Adapter erwartet einen. Das Lesen des
ersten rohen Events wirft MigrationError.
Der Fix ist eine Einmal-Bulk-Migration: einmal durch das Journal laufen, jedes rohe Event als Version-1-Envelope wickeln, zurückschreiben. Von da an replayt dein normaler defaultsAdapter / migratingAdapter sie wie jedes andere v1-Event. Das ist ein echtes Umschreiben der gespeicherten Daten, kein Read-Time-Shim.
In-Memory-Journal
Abschnitt betitelt „In-Memory-Journal“Für das In-Memory-Journal erledigt migrateInMemoryJournal den
gesamten Lauf-und-Umschreib-Vorgang. Du lieferst ein
manifestFor, das den stabilen _t-Diskriminator aus jedem Event
ableitet:
import { migrateInMemoryJournal, formatMigrationResult } from 'actor-ts';
const result = await migrateInMemoryJournal( journal, (e: { kind: string }) => `BankAccount.${e.kind}`,);
console.log(formatMigrationResult('events', result));// → "events: 3 wrapped, 0 already enveloped, 3 inspected"Sequenznummern, Timestamps und Tags bleiben erhalten — nur das Event-Payload wird gewickelt. Führe das aus, bevor der Actor mit dem neuen Adapter recovert.
Andere Backends
Abschnitt betitelt „Andere Backends“migrateInMemoryJournal verlässt sich auf einen internen
_remapForMigration-Hook, den nur das In-Memory-Journal
exponiert. Cassandra- / SQLite- / Postgres- / S3-Journale
brauchen je einen backend-spezifischen Umschreib-Pfad (SQL
UPDATE, CQL UPDATE, S3 PUT). Nutze das reine Per-Row-
Primitiv wrapEventAsEnvelope als Baustein:
import { wrapEventAsEnvelope } from 'actor-ts';
// Inside your own per-row rewrite loop:for (const row of rows) { const enveloped = wrapEventAsEnvelope( row.event, (e) => `BankAccount.${e.kind}`, ); await backend.rewrite(row.id, enveloped); // enveloped === { _v: 1, _t: 'BankAccount.deposited', _e: row.event }}wrapEventAsEnvelope(event, manifestFor, version?) ist pur und
idempotent — ein Event, das schon wie ein Envelope aussieht,
wird unverändert zurückgegeben, ein erneuter Lauf der Migration ist
also sicher.
Snapshots
Abschnitt betitelt „Snapshots“Rohe Legacy-Snapshots haben dasselbe Problem.
migrateSnapshotStore wickelt den neuesten Snapshot pro
Persistence-ID (aus dem Journal bezogen) und nutzt intern
wrapStateAsEnvelope:
import { migrateSnapshotStore } from 'actor-ts';
const persistenceIds = await journal.persistenceIds();const result = await migrateSnapshotStore(store, persistenceIds, (s) => 'BankAccount.State');Nach der Migration
Abschnitt betitelt „Nach der Migration“Sobald das Journal migriert ist, sind die rohen Events v1-Envelopes. Der Actor nutzt einen normalen Adapter — es gibt keinen speziellen „Legacy-Wrapping”-Adapter zur Read-Time:
class Account extends PersistentActor<Command, EventV2, State> { override eventAdapter(): EventAdapter<EventV2> { return defaultsAdapter<EventV2>({ manifest: 'BankAccount.deposited', currentVersion: 2, defaults: { 1: { currency: 'USD' } }, }); }}Das manifest hier muss dem _t entsprechen, das dein
manifestFor während der Migration erzeugt hat.
Signaturen
Abschnitt betitelt „Signaturen“function wrapEventAsEnvelope<E>( event: E, manifestFor: (e: E) => string, version?: number, // version default 1): JournalEnvelope<E>;
function wrapStateAsEnvelope<S>( state: S, manifestFor: (s: S) => string, version?: number,): JournalEnvelope<S>;
function migrateInMemoryJournal<E>( journal: Journal, manifestFor: (e: E) => string, options?: { version?: number },): Promise<MigrationResult>;
function migrateSnapshotStore<S>( store: SnapshotStore, persistenceIds: ReadonlyArray<string>, manifestFor: (s: S) => string, options?: { version?: number },): Promise<MigrationResult>;
type MigrationResult = { inspected: number; // entries examined wrapped: number; // raw → envelope skipped: number; // already enveloped, left untouched};Fallstricke
Abschnitt betitelt „Fallstricke“Wohin als Nächstes
Abschnitt betitelt „Wohin als Nächstes“- Migration im Überblick — das Gesamtbild.
- Envelope-Format — wie ein Envelope aussieht.
- defaultsAdapter — für additive Migrationen, sobald Events envelopet sind.
- migratingAdapter — für verkettete Transformationen, sobald Events envelopet sind.
- Recipes — das Rezept für „Ich führe Adapter in ein existierendes System ein”.
