Zum Inhalt springen
Deutsch

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.

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.

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.

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

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.

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
};