migratingAdapter
Это содержимое пока не доступно на вашем языке.
migratingAdapter handles non-additive migrations — renames,
restructures, computed-from-other-fields — by running a
MigrationChain: a
sequence of typed upcasters. You build the chain, then wrap it:
import { migratingAdapter, MigrationChain, type EventAdapter } from 'actor-ts';
type DepositedV1 = { kind: 'deposited'; amount: number };type DepositedV2 = { kind: 'deposited'; cents: number; currency: string };type DepositedV3 = { kind: 'deposited'; cents: number; currency: string; tenantId: string };
class Account extends PersistentActor<Command, DepositedV3, State> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter(): EventAdapter<DepositedV3> { const chain = MigrationChain.for<DepositedV3>('BankAccount.Deposited', 3) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ kind: v1.kind, cents: v1.amount * 100, currency: 'USD', }) }) .add({ fromVersion: 2, toVersion: 3, upcast: (v2: DepositedV2): DepositedV3 => ({ ...v2, tenantId: this.tenantId, }) }); return migratingAdapter(chain); }}The chain:
- v1 → v2 restructures: rename
amounttocents, multiply by 100, hardcodecurrency. - v2 → v3 adds: pull
tenantIdfrom the actor instance.
The adapter applies whichever steps are needed:
- v1 events: run both steps → v3 shape.
- v2 events: run only the v2 → v3 step → v3 shape.
- v3 events: no steps → already current.
Configuration
Section titled “Configuration”migratingAdapter is a function over a MigrationChain. The
currentVersion and manifest live on the chain; the adapter’s
options control only the write version:
function migratingAdapter<E>( chain: MigrationChain<E>, options?: { writeVersion?: number }, // default: chain.currentVersion): EventAdapter<E, unknown>;
// Steps added to the chain:interface MigrationStep<From, To> { fromVersion: number; toVersion: number; upcast(from: From): To;}Build the chain with MigrationChain.for<E>(manifest, currentVersion)
and .add(step). Steps must move forward (fromVersion < toVersion) — consecutive is normal, larger jumps allowed but rare.
When to use it
Section titled “When to use it”migratingAdapter is the right tool when:
- Renames —
amount→cents. - Restructures — flat fields → nested objects, or vice versa.
- Computed fields — a new field derived from old fields.
- Multi-step evolution — v1 → v2 → v3 → v4, each step building on the last.
For pure additions,
defaultsAdapter is
simpler. For arbitrarily-shaped migrations, write a custom
EventAdapter.
Closure context
Section titled “Closure context”class Account extends PersistentActor<Command, EventV3, State> { constructor(public readonly tenantId: string) { super(); }
override eventAdapter(): EventAdapter<EventV3> { // The upcaster closes over `this.tenantId`: const chain = MigrationChain.for<EventV3>('BankAccount.Deposited', 3) .add({ fromVersion: 2, toVersion: 3, upcast: (v2: EventV2): EventV3 => ({ ...v2, tenantId: this.tenantId }) }); return migratingAdapter(chain); }}Upcasters are plain functions — they can close over the actor’s constructor arguments. Useful for per-instance migration context (per-tenant defaults, per-region currency).
Step-by-step semantics
Section titled “Step-by-step semantics”Given a v1 event + a chain with steps 1 → 2 and 2 → 3:
storedV1 → upcast₁→₂(storedV1) = intermediateV2 → upcast₂→₃(intermediateV2) = finalV3The intermediate types don’t need to match any historical event
shape — they’re just stepping stones. The chain walks from the
stored version’s cursor forward until it reaches currentVersion.
Rolling deploys with downcasters
Section titled “Rolling deploys with downcasters”During a rolling deploy, v2 nodes may need to keep emitting v1
events while v1 readers are still in the cluster. Add
downcasters to the chain and set writeVersion on the adapter:
const chain = MigrationChain.for<DepositedV2>('BankAccount.Deposited', 2) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ ...v1, currency: 'USD' }) }) .addDown({ fromVersion: 2, toVersion: 1, downcast: (v2: DepositedV2): DepositedV1 => { const { currency: _c, ...rest } = v2; void _c; return rest; } });
// Phase 1 — read both, still WRITE v1:const phase1 = migratingAdapter(chain, { writeVersion: 1 });
// Phase 2 — every reader upgraded; write the current version:const phase2 = migratingAdapter(chain); // writeVersion = currentVersion = 2writeVersion must be ≤ currentVersion, and the chain must have
downcasters covering every step on the path currentVersion → writeVersion — otherwise toJournal throws.
Error handling
Section titled “Error handling”.add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => { if (v1.amount < 0) throw new Error('invalid v1 event'); return { kind: v1.kind, cents: v1.amount * 100, currency: 'USD' }; } })If a step throws, recovery fails with the error surfaced through
the actor’s onRecoveryFailure — and the actor then stops unless
that hook rethrows into supervision (see When recovery
fails). A
chain gap — no upcaster registered for the cursor before
currentVersion is reached — throws a MigrationError naming the
missing fromVersion.
Adding a step
Section titled “Adding a step”// v2 in production; now you want v3. Add a step and bump the chain version:const chain = MigrationChain.for<EventV3>('BankAccount.Deposited', 3) .add({ fromVersion: 1, toVersion: 2, upcast: v1To2 }) .add({ fromVersion: 2, toVersion: 3, upcast: v2To3 }); // NEWAdd the v2 → v3 step, keep the existing v1 → v2 step, and bump
MigrationChain.for(..., 3). Existing v2 events get upcast through
the new step; v3 events get written directly.
Never modify existing steps that handled events still in the journal — they’re load-bearing.
Snapshots
Section titled “Snapshots”For a MigrationChain that governs a snapshot / durable-state
record, use migratingSnapshotAdapter — same chain, returns a
SnapshotAdapter:
import { migratingSnapshotAdapter } from 'actor-ts';
const adapter = migratingSnapshotAdapter(chain);Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- Migration overview — the bigger picture.
- Envelope format — the underlying wire format.
- defaultsAdapter — the additive-only alternative.
- Wrap legacy — for bringing pre-envelope events into the chain.
- Recipes — common patterns walked through.
