콘텐츠로 이동
한국어

Wrap legacy

이 콘텐츠는 아직 번역되지 않았습니다.

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