Aller au contenu
Français

defaultsAdapter

Ce contenu n’est pas encore disponible dans votre langue.

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.

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.

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)
};
FieldWhat
manifestThe stable discriminator stored with every event; must match the on-disk manifest.
currentVersionThe version newly-written events carry.
defaultsKeyed by from-version: defaults[v] is the set of fields added going from v to v+1.
writeVersionOptional — 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.

// 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.

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' } },
});

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.

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.

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.