Skip to content

Mergeable Structs

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';

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();
}
@State
export class TeamMetricsState {
memberProfiles: UnorderedMap<string, MemberProfile> = createUnorderedMap();
version: bigint = 0n;
}

Here displayName resolves last-writer-wins, while roles and contributions use their CRDT semantics.

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).
  • Root fields re-merge; collection values don’t. A @Mergeable struct that is a direct field of your @State root is re-merged field-aware on every replica during sync (this path also runs your custom handler). The limitations below apply to a @Mergeable struct 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.