WrapLegacy
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 }After you set one, new events are wrapped:
{ "_v": 1, "_t": "deposited", "_e": { "kind": "deposited", "amount": 100 } }This creates a problem when introducing adapters to an
existing journal: old raw events lack the envelope, but the
adapter expects envelopes. Reading a raw event throws
MigrationError.
wrapLegacy bridges:
import { wrapLegacy, MigratingAdapter } from 'actor-ts';
class Account extends PersistentActor<...> { override eventAdapter() { return wrapLegacy<EventV2>({ legacyVersion: 1, baseAdapter: new MigratingAdapter<EventV2>({ currentVersion: 2, chain: [{ from: 1, to: 2, fn: v1ToV2 }], }), }); }}What this does:
- On a raw event (no
_v/_t/_ekeys): treat it asversion: 1. Pass the raw event as the v1 payload into the inner adapter. - On an envelope event: pass through to the inner adapter unchanged.
So existing raw events flow through the same migration chain as new envelope-wrapped events.
Configuration
Section titled “Configuration”interface WrapLegacySettings<E> { legacyVersion: number; // version to assign to raw events baseAdapter: EventAdapter<E>;}| Field | What |
|---|---|
legacyVersion | The version raw events are treated as. Typically 1. |
baseAdapter | The “real” adapter that handles the rest. |
The legacy version should be the version your code wrote raw
events under — usually 1, but if you’ve been on v1 a while
and skipped envelope adoption until v3, use legacyVersion: 1
and chain 1→2, 2→3.
When to use it
Section titled “When to use it”Three scenarios:
- Adding adapters to an existing journal — the most common case. Wrap once; from this point on, new events are envelopes, old events flow through legacy-wrapping.
- Migrating from a different framework — old events written
by a previous system are now in this journal. Treat them as
legacyVersionand migrate up. - Mixed environments during gradual rollout — some events were written by an old version of your code (raw); some by a new version (envelope). Wrap to handle both.
After all raw events have been read (or replaced by snapshots
that contain the migrated state), you could in principle drop the
wrapLegacy wrapper. In practice, leave it on — the cost is
negligible and the safety net is valuable.
What gets considered “legacy”
Section titled “What gets considered “legacy””The detection is simple: a stored payload without all of _v,
_t, _e is legacy. This is unambiguous in practice — real
events virtually never have all three underscore-prefixed fields
by coincidence.
Multiple legacy versions
Section titled “Multiple legacy versions”If your journal contains raw events from multiple legacy
versions, wrapLegacy alone isn’t enough — it gives one
legacyVersion to all raw events.
For multi-legacy scenarios, you’d write a custom adapter that inspects the raw event’s shape to decide its version:
class MultiLegacyAdapter implements EventAdapter<EventV3> { upcast(stored: unknown, version: number): EventV3 { if (version > 0) { // Envelope event with known version — chain handles it return innerChain.upcast(stored, version); } // Legacy event — sniff shape to determine version const e = stored as any; if ('cents' in e) return innerChain.upcast(e, 2); // looks like v2 shape if ('amount' in e) return innerChain.upcast(e, 1); // looks like v1 shape throw new Error('unknown legacy event shape'); }}This is rare; most projects have a single pre-envelope shape and
wrapLegacy is sufficient.
Snapshot compatibility
Section titled “Snapshot compatibility”Snapshots have their own adapter (stateAdapter()), independent
of eventAdapter(). If you have legacy raw snapshots (pre-
envelope), wrap them similarly:
class Account extends PersistentActor<...> { override snapshotAdapter() { return wrapLegacy<StateV2>({ legacyVersion: 1, baseAdapter: new MigratingAdapter<StateV2>({ ... }), }); }}For most setups, snapshots are written less often than events and were already enveloped when adapters were introduced — but double-check before assuming.
Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- Migration overview — the bigger picture.
- Envelope format — what an envelope looks like.
- DefaultAdapter — for additive migrations.
- MigratingAdapter —
for chained transformations (commonly wrapped by
wrapLegacy). - Recipes — the recipe for “I’m introducing adapters to an existing system.”