CRDT Collections
Calimero provides conflict-free replicated data types (CRDTs) for automatic state synchronization. Values are encoded with Calimero’s Borsh encoder, so data written from JavaScript matches the bytes produced by Rust services as long as both sides share the same Borsh schema.
Import the collection classes from the /collections entry point, and the
factory helpers from the package root:
import { UnorderedMap, UnorderedSet, Vector } from '@calimero-network/calimero-sdk-js/collections';import { createUnorderedMap, createCounter } from '@calimero-network/calimero-sdk-js';Common members
Section titled “Common members”Every collection exposes the same identity/serialization members:
id(): string— the collection’s id as hex.idBytes(): Uint8Array— a copy of the id bytes.toJSON()— a{ __calimeroCollection, id }handle (how a collection is stored inside another collection and rehydrated).
Most collections also have a static fromId(id) that reopens an existing
collection at a known id without a fresh host allocation — available on
UnorderedMap, SortedMap, AuthoredMap, AuthoredVector, UserStorage,
FrozenStorage, SharedStorage (and Vector.fromArray / Rga.fromText seed
new collections from data).
UnorderedMap<K, V>
Section titled “UnorderedMap<K, V>”Last-write-wins map for key/value storage.
const map = new UnorderedMap<string, string>();
map.set('key', 'value');const value = map.get('key'); // 'value' | nullconst exists = map.has('key'); // booleanmap.remove('key'); // void
const entries = map.entries(); // Array<[K, V]>const keys = map.keys(); // K[]const values = map.values(); // V[]UnorderedSet<T>
Section titled “UnorderedSet<T>”Last-write-wins set for unique membership.
const set = new UnorderedSet<string>();// or seed values: new UnorderedSet<string>({ initialValues: ['alice', 'bob'] })
set.add('alice'); // true on first insert, false if already presentconst present = set.has('alice'); // booleanset.delete('alice'); // boolean — true if it was presentset.clear(); // voidconst count = set.size(); // numberconst all = set.toArray(); // T[]Vector<T>
Section titled “Vector<T>”Ordered list that maintains insertion order.
const vec = new Vector<string>();
vec.push('first'); // voidvec.push('second');const item = vec.get(0); // 'first' | nullconst len = vec.len(); // numberconst last = vec.pop(); // 'second' | nullconst all = vec.toArray(); // T[]
// For initialization only:const seeded = Vector.fromArray(['a', 'b', 'c']);Counter
Section titled “Counter”Grow-only counter (G-Counter) for distributed counting. Each node tracks its own sub-count; the total is the sum across all nodes, so concurrent increments never conflict.
const counter = new Counter();
counter.increment(); // +1counter.incrementBy(5); // +5 (accepts number | bigint)const total = counter.value(); // bigint
// Optional: read a single node's contribution (hex executor id)const mine = counter.getExecutorCount();PNCounter
Section titled “PNCounter”Positive-negative counter (PN-Counter) — like Counter, but it also supports
decrement. Each node keeps a per-node increment and decrement tally; the value
is the signed sum, so concurrent up/down changes from different nodes never
conflict or lose updates.
const votes = new PNCounter();
votes.increment(); // +1votes.incrementBy(5); // +5votes.decrement(); // −1votes.decrementBy(2); // −2 (accepts number | bigint)const net = votes.value(); // bigint — can be negativeLwwRegister<T>
Section titled “LwwRegister<T>”Last-write-wins register for a single value.
const register = new LwwRegister<string>();// or seed an initial value: new LwwRegister<string>({ initialValue: 'default' })
register.set('hello');const value = register.get(); // 'hello' | nullconst ts = register.timestamp(); // number | null — when it was setregister.clear(); // voidSortedMap<K, V>
Section titled “SortedMap<K, V>”Same last-write-wins semantics and API as UnorderedMap, but iteration is in
sorted key order instead of arbitrary order. Ordering is maintained by a
node-local index, so entries()/keys()/values() are deterministic.
const scores = new SortedMap<string, number>();
scores.set('charlie', 3);scores.set('alice', 1);scores.set('bob', 2);
const ordered = scores.keys(); // ['alice', 'bob', 'charlie'] — always sortedscores.get('bob'); // 2 | nullscores.has('bob'); // booleanscores.remove('bob'); // voidSortedSet<T>
Section titled “SortedSet<T>”Sorted counterpart to UnorderedSet — union (add-wins) membership, but
toArray() returns values in sorted order.
const tags = new SortedSet<string>();
tags.add('c'); // true on first insert, false if already presenttags.add('a');tags.add('b');
tags.toArray(); // ['a', 'b', 'c'] — always sortedtags.has('a'); // booleantags.delete('a'); // booleantags.size(); // numberRga<T>
Section titled “Rga<T>”Replicated Growable Array — a sequence CRDT for collaborative text. Concurrent inserts and deletes from different nodes interleave deterministically by position, so two editors typing at once converge without losing characters.
const doc = new Rga();
doc.insert(0, 'Hello'); // insert text at an index (Unicode codepoint offset)doc.insert(5, ' world'); // → 'Hello world'doc.delete(0); // remove one element at an indexconst text = doc.getText(); // current stringconst length = doc.len(); // number of elements
// Seed a new RGA from an existing string:const seeded = Rga.fromText('Hello world');Attributed collections
Section titled “Attributed collections”Attributed (authored) collections record the owner of each entry — the executor that created it — and enforce that only that owner may update or remove it. Ownership travels with the entry and is verified on every node, so one member cannot overwrite another member’s data.
AuthoredMap<K, V>
Section titled “AuthoredMap<K, V>”Like UnorderedMap, but each key is owned by its first inserter.
const claims = new AuthoredMap<string, string>();
claims.insert('seat-1', 'alice'); // owned by the current executorclaims.update('seat-1', 'alice-2'); // ok only if you own 'seat-1' (throws otherwise)claims.set('seat-1', 'alice-2'); // insert-or-update, owner-checked on updateclaims.get('seat-1'); // V | nullclaims.has('seat-1'); // boolean (contains() is an alias)claims.ownerOf('seat-1'); // Uint8Array (owner public key) | nullclaims.ownedByMe('seat-1'); // booleanclaims.remove('seat-1'); // owner-checkedAuthoredVector<T>
Section titled “AuthoredVector<T>”Like Vector, but each slot is owned by whoever pushed it; updates and removals
are owner-gated and removal is a tombstone (the slot index stays stable).
const log = new AuthoredVector<string>();
const index = log.push('entry'); // returns the slot index; owned by the callerlog.update(index, 'edited'); // owner-onlylog.tombstone(index); // owner-only soft-deletelog.get(index); // T | null (null once tombstoned)log.ownerOf(index); // Uint8Array | nulllog.ownedByMe(index); // booleanlog.len(); // number of slots (including tombstones)log.toArray(); // live valuesConflict resolution
Section titled “Conflict resolution”For last-write-wins types (UnorderedMap, UnorderedSet, LwwRegister,
SortedMap, SortedSet, and SharedStorage’s value) a concurrent write to the
same key is resolved by timestamp — the later write wins:
Node A: map.set('key', 'A') at t=1000Node B: map.set('key', 'B') at t=1001Result: key = 'B' (higher timestamp)The other types resolve differently and never lose concurrent updates:
Counter/PNCounter— per-node tallies are summed (additive).Rga— concurrent inserts/deletes interleave by position (union of edits).AuthoredMap/AuthoredVector— each entry is owner-gated, so only its owner can change it; there is no cross-writer conflict on a single entry.FrozenStorage— first write wins; entries are immutable.
Nested collections
Section titled “Nested collections”Collections can be stored inside other collections, and the SDK tracks changes
to the inner collection automatically — you do not need to re-set the parent
after mutating a child.
const reactions = new UnorderedMap<string, UnorderedMap<string, UnorderedSet<string>>>();
function addReaction(messageId: string, emoji: string, userId: string) { let byEmoji = reactions.get(messageId); if (!byEmoji) { byEmoji = new UnorderedMap<string, UnorderedSet<string>>(); reactions.set(messageId, byEmoji); }
let users = byEmoji.get(emoji); if (!users) { users = new UnorderedSet<string>(); byEmoji.set(emoji, users); }
users.add(userId); // propagates automatically — no manual re-serialization}Supported patterns include any combination such as
UnorderedMap<K, UnorderedSet<V>>, UnorderedMap<K, Vector<V>>, and
Vector<UnorderedMap<K, V>>.
How rehydration works
Section titled “How rehydration works”When a map value is itself a CRDT, the host stores a lightweight handle — a small
JSON wrapper of the form {"__calimeroCollection":"Vector","id":"…hex…"} — not
the full contents. Fetching that value rehydrates a handle that carries the
CRDT ID; mutating the handle issues an incremental host call against that ID
rather than replaying the whole structure. The full structure is only
materialized in JS when you explicitly read it (toArray(), or returning the
entire map from a view).
Specialized storage collections
Section titled “Specialized storage collections”UserStorage<V>
Section titled “UserStorage<V>”User-owned, signed storage keyed by the owner’s 32-byte Ed25519 public key. Writes are signed by the executor and verified (with replay protection via a strictly-increasing nonce) on other nodes.
import { createUserStorage } from '@calimero-network/calimero-sdk-js';
interface UserProfile { displayName: string; score: number; }
const profiles = createUserStorage<UserProfile>();
profiles.insert({ displayName: 'Alice', score: 100 }); // key = current executor's PublicKey; returns previous | nullconst mine = profiles.get(); // current user's value | nullconst other = profiles.getForUser(somePublicKey); // another user's value | nullprofiles.setForUser(somePublicKey, { displayName: 'Bob', score: 5 }); // write for a specific user; returns previous | nullconst hasMine = profiles.containsCurrentUser(); // booleanconst hasOther = profiles.containsUser(somePublicKey); // booleanprofiles.remove(); // remove current user's value; returns previous | null
const all = profiles.entries(); // Array<[PublicKey, V]>const users = profiles.keys(); // PublicKey[]const vals = profiles.values(); // V[]const count = profiles.size(); // numberUse it for per-user settings, user-owned game data, or any value that must be verifiably owned by a specific user.
FrozenStorage<T>
Section titled “FrozenStorage<T>”Immutable, content-addressable storage. Each value is keyed by the SHA-256 hash of its serialized bytes; once inserted, values cannot be updated or deleted.
import { FrozenStorage } from '@calimero-network/calimero-sdk-js/collections';import { createFrozenStorage } from '@calimero-network/calimero-sdk-js';
const documents = createFrozenStorage<Document>();
const hash = documents.add({ title: 'Report', body: '…' }); // returns the Hashconst doc = documents.get(hash); // T | nullconst exists = documents.has(hash); // boolean
const all = documents.entries(); // Array<[Hash, T]>const hashes = documents.hashes(); // Hash[]
// Compute a hash without storing (useful for deduplication)const wouldBe = FrozenStorage.computeHash(myValue);
// documents.remove(hash); // throws — FrozenStorage is append-onlyValues are wrapped in FrozenValue<T> (new FrozenValue(value), .value),
whose merge is a no-op so frozen entries never change. Use it for audit logs,
document versioning, certificates, and content-addressable sharing.
SharedStorage<V>
Section titled “SharedStorage<V>”A single value that any member of a rotatable writer set may overwrite.
Writes from different writers converge last-write-wins; the writer set is managed
at runtime and enforced by the host — a non-writer’s set/rotateWriters is
rejected. Writers are 32-byte public keys (raw or hex).
import { SharedStorage } from '@calimero-network/calimero-sdk-js/collections';import { executorId } from '@calimero-network/calimero-sdk-js/env';
// The creator is the sole initial writer.const config = new SharedStorage<string>({ writers: [executorId()] });// Pass `frozen: true` to create an immutable cell: new SharedStorage({ writers, frozen: true })
config.set('hello'); // ok — caller is a writer (throws for a non-writer)config.get(); // 'hello' | nullconfig.writers(); // Uint8Array[] — current writer setconfig.writableByMe(); // booleanconfig.isFrozen(); // boolean — a frozen cell is immutable
// Grant another member write access (writer-gated):config.rotateWriters([...config.writers(), otherKey]);Use it for shared configuration, group-editable records, or any value a defined
set of members may change while everyone else reads. It differs from
AuthoredMap/UserStorage (which gate per entry by the entry’s own owner):
SharedStorage gates the whole cell by a single, rotatable writer set.
Best practices
Section titled “Best practices”- Pick the right type:
UnorderedMap/SortedMapfor key/value data (sorted when you need ordered iteration),Vectorfor ordered lists,Counterfor grow-only totals andPNCounterwhen it also decreases,LwwRegisterfor a single value,Rgafor collaboratively-edited text,AuthoredMap/AuthoredVectorwhen each entry has a single owner,UserStoragefor per-user ownership,FrozenStoragefor immutable records, andSharedStoragefor one value a rotatable set of writers may change. - Mutate the handle, don’t replace it. Fetch the existing entry
(
const v = map.get(key) ?? new …) and mutate it. Assigning a brand-new CRDT instance replaces the stored ID and falls back to whole-value last-write-wins. - Mark selectors
@View(). Read-only methods (get,list,len) tagged with@View()skip persistence, keeping the storage DAG compact. - Avoid plain fields for shared state. A plain
count: number = 0is not a CRDT and loses concurrent updates — useCounterinstead.
See also
Section titled “See also”- Mergeable structs — deterministic conflict resolution for custom structs stored inside collections.
- Architecture — the QuickJS↔host data flow in detail.
- API Reference — full method lists.