Core
@topgunbuild/core ships the CRDT primitives, Hybrid Logical Clock, and Merkle tree that the client and server share. This page documents the public surface (the “what / how”). For background on why TopGun picks these primitives over alternatives, see Concepts → Data Structures.
The two primary data structures are LWWMap (last-write-wins) and ORMap (observed-remove). Both are exposed on TopGunClient via getMap() and getORMap() — most application code never instantiates them directly.
LWWMap
import { LWWMap, HLC } from '@topgunbuild/core';
const hlc = new HLC('node-1');
const map = new LWWMap<string, User>(hlc);
Last-write-wins semantics: each key holds exactly one value. Concurrent writes from different nodes are resolved by HLC timestamp (highest wins, ties broken by nodeId).
Constructor
new LWWMap<K, V>(hlc: HLC)
Methods
set(key, value, ttlMs?) — LWWRecord<V>
Write a value. Stamps the record with hlc.now(). Optional ttlMs for time-bounded entries.
const record = map.set('u1', { name: 'Alice', email: '[email protected]' });
// { value: {...}, timestamp: { millis, counter, nodeId } }
// With 1-hour TTL
map.set('session-123', sessionData, 3600000);
get(key) — V | undefined
Read a value. Returns undefined for deleted or expired keys.
getRecord(key) — LWWRecord<V> | undefined
Read the full record (value, timestamp, optional TTL).
remove(key) — LWWRecord<V>
Delete a key by writing a tombstone (value: null). Returns the tombstone record.
merge(key, remoteRecord) — boolean
Merge a remote record. Returns true if local state was updated (i.e., the remote record’s timestamp dominated).
const remote: LWWRecord<User> = {
value: { name: 'Bob' },
timestamp: { millis: Date.now(), counter: 0, nodeId: 'node-2' },
};
const updated = map.merge('u1', remote);
subscribe(callback) — () => void
Subscribe to map-change notifications. The callback receives entries: Array<[K, V]>. Returns an unsubscribe function.
const unsubscribe = map.subscribe((entries) => {
console.log('Map changed,', entries.length, 'live entries');
});
entries() — IterableIterator<[K, V]>
Iterate non-tombstoned entries (drops deleted and expired).
allKeys() — IterableIterator<K>
Iterate every key including tombstones.
size (getter) — number
Count of entries including tombstones.
prune(olderThan) — K[]
Drop tombstones older than the given Timestamp. Returns the removed keys.
clear() — void
Reset to empty state.
getMerkleTree() — MerkleTree
Access the underlying Merkle tree for delta-sync diffing.
LWWRecord shape
interface LWWRecord<V> {
value: V | null; // null indicates tombstone
timestamp: Timestamp; // HLC stamp
ttlMs?: number; // optional TTL
}
ORMap
import { ORMap, HLC } from '@topgunbuild/core';
const hlc = new HLC('node-1');
const map = new ORMap<string, string>(hlc);
Observed-remove semantics: each key holds a set of values. Concurrent adds preserve every value (tagged with unique IDs). A remove operation tombstones only the tags it observed locally, so concurrent add-vs-remove resolves correctly (no spurious deletes).
Use this for tag clouds, comment threads, multi-select fields, presence rosters — anywhere two clients can legitimately contribute different values to the same key.
Constructor
new ORMap<K, V>(hlc: HLC)
Methods
add(key, value, ttlMs?) — ORMapRecord<V>
Add a value under a key. Generates a unique tag. Returns the record.
const tags = new ORMap<string, string>(hlc);
tags.add('post:123', 'javascript');
tags.add('post:123', 'typescript');
// post:123 -> ['javascript', 'typescript']
remove(key, value) — string[]
Remove a specific value under a key. Returns the tombstone tags created.
get(key) — V[]
Read all live values for a key.
const postTags = tags.get('post:123');
// ['javascript', 'typescript']
getRecords(key) — ORMapRecord<V>[]
Read full records (value + tag + timestamp) for a key.
getTombstones() — string[]
Read all current tombstone tags.
apply(key, record) — boolean
Apply a remote ORMapRecord to local state. Returns true if state changed.
applyTombstone(tag) — void
Apply a remote tombstone tag.
merge(other) — void
Merge another ORMap of the same key/value types in bulk.
subscribe(callback) — () => void
Subscribe to change events. The callback receives entries: Array<[K, V[]]>.
allKeys() — K[]
All keys with at least one live or tombstoned value.
size / totalRecords (getters) — number
size counts distinct keys; totalRecords counts every tagged record (including duplicates per key).
prune(olderThan) — string[]
Drop tombstones older than the timestamp. Returns dropped tags.
clear() — void
Reset to empty.
getMerkleTree() — ORMapMerkleTree
Underlying Merkle tree (a different shape than LWWMap’s tree because ORMap stores multi-value-per-key).
getSnapshot() — ORMapSnapshot<K, V>
Serializable snapshot for persistence.
isTombstoned(tag) — boolean
Check whether a tag is in the tombstone set.
ORMapRecord shape
interface ORMapRecord<V> {
value: V;
tag: string; // unique add ID
timestamp: Timestamp; // HLC stamp
ttlMs?: number;
}
HLC (Hybrid Logical Clock)
import { HLC } from '@topgunbuild/core';
const hlc = new HLC('node-1');
const ts = hlc.now();
// { millis: 1748025600000, counter: 0, nodeId: 'node-1' }
The HLC combines physical wall-clock time (millis) with a logical counter (counter) so total ordering is preserved across nodes even when clocks drift.
Constructor
new HLC(nodeId: string, options?: HLCOptions)
HLCOptions fields:
| Field | Default | Description |
|---|---|---|
strictMode | false | Throw on clock drift exceeding maxDriftMs. |
maxDriftMs | 60000 | Permitted drift threshold (ms). |
clockSource | wall clock | Custom ClockSource for deterministic tests. |
Methods
now() — Timestamp
Issue a fresh timestamp. Monotonic per node.
update(remote) — void
Update the local clock against a received remote timestamp. Bumps lastMillis/lastCounter as needed to maintain causal ordering.
getClockSource() — ClockSource
Returns the configured clock source.
getNodeId / getStrictMode / getMaxDriftMs (getters)
Inspect configured fields.
Static helpers
HLC.compare(a, b) — number
Total-order comparator. Returns negative if a < b, zero if equal, positive if a > b. Ordered by millis, then counter, then nodeId.
HLC.toString(ts) — string
Canonical string encoding for serialization or sort keys.
HLC.parse(str) — Timestamp
Inverse of toString.
Timestamp shape
interface Timestamp {
millis: number;
counter: number;
nodeId: string;
}
MerkleTree
import { MerkleTree } from '@topgunbuild/core';
const tree = new MerkleTree();
The Merkle tree powers delta sync: clients and servers compare hash trees and exchange only differing buckets instead of full state. LWWMap and ORMap maintain their own trees internally — use this directly only when implementing custom sync providers or diagnostics.
Constructor
new MerkleTree(records?: Map<string, LWWRecord<any>>, depth = 3)
depth controls fan-out (default 3 levels deep).
Methods
update(key, record) — void
Insert or replace the leaf for key and rehash the path to the root.
remove(key) — void
Drop the leaf and rehash.
getRootHash() — number
Top-level hash. Two trees with identical content produce the same root.
getNode(path) — MerkleNode | undefined
Return the node at a given path string (used during diff walks).
getBuckets(path) — Record<string, number>
Per-bucket hashes for the children of the node at path.
getKeysInBucket(path) — string[]
Leaf keys grouped under the given path.
Re-exports
@topgunbuild/core also re-exports types and helpers consumed across the stack. Highlights:
Predicates— composable filter builders (equal,gt,lt,contains,and,or,not,match,matchPhrase,matchPrefix). Used byclient.query,client.hybridQuery, and the React hooks.SearchOptions— shared FTS options shape used byclient.search/searchSubscribeanduseSearch.MergeRejection— event shape emitted when the built-in CRDT merge logic rejects a remote change. Observed viauseMergeRejections(React) orclient.getConflictResolvers().onRejection().WriteConcern/ConsistencyLevel— durability and consistency knobs for cluster writes.IndexedLWWMap/IndexedORMap— indexed variants used by the server’s query engine.- Full-Text Search —
Tokenizer,InvertedIndex,BM25Scorer,FullTextIndexfor embedding FTS directly. - Deterministic Simulation Testing —
VirtualClock,SeededRNG,VirtualNetwork,InvariantChecker,ScenarioRunnerfor property-based distributed tests.
The complete re-export list lives in packages/core/src/index.ts.