Ir al contenido
Español

Wrap legacy

Esta página aún no está disponible en tu idioma.

The framework’s envelope format only kicks in when an adapter is configured. Before you set an adapter, events are stored raw:

{ "kind": "deposited", "amount": 100 }

Once an adapter is in place, new events are wrapped in an envelope:

{ "_v": 1, "_t": "BankAccount.deposited", "_e": { "kind": "deposited", "amount": 100 } }

That creates a problem when you add an adapter to an existing journal: the old raw events have no envelope, but the adapter expects one. Reading the first raw event throws MigrationError.

The fix is a one-shot bulk migration: walk the journal once, wrap every raw event as a version-1 envelope, write it back. From then on your normal defaultsAdapter / migratingAdapter replays them like any other v1 event. This is a real rewrite of the stored data, not a read-time shim.

For the in-memory journal, migrateInMemoryJournal does the whole walk-and-rewrite. You supply a manifestFor that derives the stable _t discriminator from each event:

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"

Sequence numbers, timestamps, and tags are preserved — only the event payload is wrapped. Run this before the actor with the new adapter recovers.

migrateInMemoryJournal relies on an internal _remapForMigration hook that only the in-memory journal exposes. Cassandra / SQLite / Postgres / S3 journals each need a backend-specific rewrite path (SQL UPDATE, CQL UPDATE, S3 PUT). Use the pure per-row primitive wrapEventAsEnvelope as the building block:

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?) is pure and idempotent — an event that already looks like an envelope is returned unchanged, so re-running the migration is safe.

Legacy raw snapshots have the same problem. migrateSnapshotStore wraps the latest snapshot per persistence id (sourced from the journal), using wrapStateAsEnvelope under the hood:

import { migrateSnapshotStore } from 'actor-ts';
const persistenceIds = await journal.persistenceIds();
const result = await migrateSnapshotStore(store, persistenceIds, (s) => 'BankAccount.State');

Once the journal is migrated, the raw events are v1 envelopes. The actor uses a normal adapter — there’s no special “legacy-wrapping” adapter at 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' } },
});
}
}

The manifest here must match the _t your manifestFor produced during the migration.

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