Mergeable Structs
What it is
Section titled “What it is”Rust services reconcile concurrent updates to the same entity through the
Mergeable trait. The JS SDK’s @Mergeable decorator is the analogue: it marks
a plain data class stored inside a CRDT collection (a map value, a register
payload) so the runtime can reconcile two versions field-by-field instead of
overwriting the whole value.
Import it from the package root:
import { Mergeable, type MergeableOptions } from '@calimero-network/calimero-sdk-js';Default behavior
Section titled “Default behavior”Applied with no options, @Mergeable() records the class name as its type
identifier and derives a per-field strategy automatically:
- CRDT fields (nested
UnorderedMap,Vector,Counter, …) defer to the host CRDT merge already in place. - Scalar fields fall back to last-writer-wins based on the entry’s timestamp.
import { Mergeable, State, createUnorderedMap, createVector, createCounter,} from '@calimero-network/calimero-sdk-js';// Collection classes are used as type annotations below; import them from the// /collections entry point (they are not re-exported from the package root).import { UnorderedMap, Vector, Counter } from '@calimero-network/calimero-sdk-js/collections';
@Mergeable()export class MemberProfile { displayName: string = ''; roles: Vector<string> = createVector(); contributions: Counter = createCounter();}
@Stateexport class TeamMetricsState { memberProfiles: UnorderedMap<string, MemberProfile> = createUnorderedMap(); version: bigint = 0n;}Here displayName resolves last-writer-wins, while roles and contributions
use their CRDT semantics.
Custom merge handler
Section titled “Custom merge handler”Pass a merge function to take full control of reconciliation. It receives the
local and remote values and returns the value to persist:
import { Mergeable } from '@calimero-network/calimero-sdk-js';
function mergeStats(local: Stats, remote: Stats): Stats { return { wins: Math.max(local.wins, remote.wins), losses: Math.min(local.losses, remote.losses), };}
@Mergeable({ merge: mergeStats })export class Stats { wins: number = 0; losses: number = 0;}MergeableOptions accepts:
merge?: (localValue, remoteValue) => value— a custom, pure, deterministic reconciler run inside QuickJS during conflict resolution.type?: string— override the recorded type identifier (defaults to the class name).
Limitations and status
Section titled “Limitations and status”- Root fields re-merge; collection values don’t. A
@Mergeablestruct that is a direct field of your@Stateroot is re-merged field-aware on every replica during sync (this path also runs your custom handler). The limitations below apply to a@Mergeablestruct stored inside a CRDT collection — the case this guide targets — where its value crosses to the host as opaque bytes. - Host is metadata-unaware (collection values). For a struct nested in a collection, the storage layer ignores the merge descriptor, so two deltas for the same entry that arrive concurrently on a follower resolve last-write-wins.
- Custom handlers don’t replay (collection values). There is no host-side registry to re-run a custom handler on followers for a nested value.
- No retrofit. Metadata applies only to services built with the updated SDK; adopt the decorator and redeploy with fresh state.
Treat @Mergeable as a way to make local writes safer and to stage metadata for
future host support, not as a full cross-network conflict-resolution guarantee.
For guaranteed convergence today, model state with the built-in
CRDT collections.