defaultsAdapter
此内容尚不支持你的语言。
defaultsAdapter handles the simplest migration shape: each
version added fields, all of which have sensible constant defaults.
It’s a function that returns an EventAdapter — plug it into an
actor’s eventAdapter():
import { defaultsAdapter, type EventAdapter } from 'actor-ts';
type DepositedV1 = { kind: 'deposited'; amount: number };type DepositedV2 = { kind: 'deposited'; amount: number; currency: 'USD' | 'EUR' };
class Account extends PersistentActor<Command, DepositedV2, State> { override eventAdapter(): EventAdapter<DepositedV2> { return defaultsAdapter<DepositedV2>({ manifest: 'BankAccount.Deposited', currentVersion: 2, defaults: { 1: { currency: 'USD' } }, // fields added going v1 → v2 }); }}A stored v1 event { kind: 'deposited', amount: 100 } reads back
as { kind: 'deposited', amount: 100, currency: 'USD' } — the
default is merged in. V2 events read unchanged.
When this is enough
Section titled “When this is enough”defaultsAdapter is the right tool when:
- Pure addition — new fields are added; old ones unchanged.
- Defaults are constant — the default value is the same for every event at that version.
- No renames or type changes — it only adds missing fields.
This covers a large share of real-world migrations. When it doesn’t fit, reach for migratingAdapter.
Configuration
Section titled “Configuration”The argument is a DefaultsAdapterSpec<E>:
type DefaultsAdapterSpec<E> = { manifest: string; // stable type identity, e.g. 'BankAccount.Deposited' currentVersion: number; // version this code revision emits defaults: { [fromVersion: number]: Partial<E> }; // fields added at each step writeVersion?: number; // emit an older version (rolling deploys)};| Field | What |
|---|---|
manifest | The stable discriminator stored with every event; must match the on-disk manifest. |
currentVersion | The version newly-written events carry. |
defaults | Keyed by from-version: defaults[v] is the set of fields added going from v to v+1. |
writeVersion | Optional — emit events at an older version during a rolling deploy (fields added after it are stripped on write). |
Reading a stored payload runs one merge per step from its version
up to currentVersion; each step spreads that step’s defaults
first, then the payload:
{ ...defaults[v], // fields added at step v ...storedPayload, // actual values win}Already-set fields in the stored payload win — defaults only fill gaps.
Multiple version bumps
Section titled “Multiple version bumps”// v1 → v2 added `currency`; v2 → v3 added `metadata`class Account extends PersistentActor<Command, EventV3, State> { override eventAdapter(): EventAdapter<EventV3> { return defaultsAdapter<EventV3>({ manifest: 'BankAccount.Deposited', currentVersion: 3, defaults: { 1: { currency: 'USD' }, // merged when reading a v1 payload 2: { metadata: {} }, // merged when reading a v1 or v2 payload }, }); }}Each defaults[v] key must be strictly less than
currentVersion — the adapter validates this and throws otherwise.
Reading a v1 payload applies steps 1 and 2; a v2 payload applies
step 2 only.
Snapshots and durable state
Section titled “Snapshots and durable state”For the same additive shape on a snapshot / durable-state record,
use defaultsSnapshotAdapter — identical spec, returns a
SnapshotAdapter:
import { defaultsSnapshotAdapter } from 'actor-ts';
const adapter = defaultsSnapshotAdapter<StateV2>({ manifest: 'BankAccount.State', currentVersion: 2, defaults: { 1: { currency: 'USD' } },});When the default isn’t right
Section titled “When the default isn’t right”Consider currency: 'USD' for an account opened in Germany — the
constant default is wrong (should be EUR). Two options:
Use migratingAdapter for context-aware migration
Section titled “Use migratingAdapter for context-aware migration”import { migratingAdapter, MigrationChain } from 'actor-ts';
const chain = MigrationChain.for<DepositedV2>('BankAccount.Deposited', 2) .add({ fromVersion: 1, toVersion: 2, upcast: (v1: DepositedV1): DepositedV2 => ({ ...v1, currency: lookupCurrencyByTimestamp(v1.ts), }) });const adapter = migratingAdapter(chain);The upcaster has access to the full v1 event, so it can derive a per-event value from context.
Backfill instead of default
Section titled “Backfill instead of default”If the historical events should be rewritten to the correct currency, run a one-off script that reads each event, rewrites it, and writes it back. This is rare — usually keeping old events as-is plus a context-aware adapter is fine.
Type safety
Section titled “Type safety”defaultsAdapter<EventV2>({ manifest: 'BankAccount.Deposited', currentVersion: 2, defaults: { 1: { currency: 'USD', nonExistentField: 'oops', // ✓ compiles — but never used }, },});Each defaults[v] is a Partial<E>, so TypeScript allows extra
fields — they just do nothing if the event type doesn’t include
them. Watch for typos in field names; they fail silently.
Pitfalls
Section titled “Pitfalls”Where to next
Section titled “Where to next”- Migration overview — the bigger picture.
- Envelope format — how versioning works on disk.
- migratingAdapter — for non-additive transformations.
- Recipes — the per-pattern cookbook.
