Data Structures API
TopGun provides two CRDT-based data structures for storing and syncing data. This page documents all available methods for performing CRUD operations.
Which one to use? Use LWWMap for key-value data where each key has one value (profiles, settings).
Use ORMap for collections where a key can have multiple values (todo lists, tags, comments).
See Concepts: Data Structures for detailed comparison.
Timestamp Structure
All records include a Hybrid Logical Clock (HLC) timestamp for ordering and conflict resolution.
// Timestamp structure (Hybrid Logical Clock)
interface Timestamp {
millis: number; // Physical time in milliseconds
counter: number; // Logical counter for same-millisecond ordering
nodeId: string; // Node identifier for tie-breaking
}
// LWWRecord structure
interface LWWRecord<V> {
value: V | null; // null indicates tombstone (deleted)
timestamp: Timestamp;
ttlMs?: number; // Optional TTL in milliseconds
}
// ORMapRecord structure
interface ORMapRecord<V> {
value: V;
timestamp: Timestamp;
tag: string; // Unique identifier for this entry
ttlMs?: number;
} LWWMap (Last-Write-Wins Map)
The LWWMap is a key-value store where conflicts are resolved by accepting the update with the latest timestamp.
Access it via client.getMap<K, V>(name).
Reading Data
LWWMap<K, V>Parameters
get(key: K)V | undefined. Returns the value for the given key, or undefined if not found, deleted, or expired.getRecord(key: K)LWWRecord<V> | undefined. Returns the full record including timestamp and TTL. Useful for sync operations.entries()IterableIterator<[K, V]>. Returns an iterator over all non-deleted, non-expired entries as [key, value] pairs.allKeys()IterableIterator<K>. Returns an iterator over all keys including tombstones. Use getRecord() to check if deleted.sizenumber. The total number of entries in the map (including tombstones).
const users = client.getMap<string, User>('users');
// Read a single value by key
const user = users.get('user-123');
// Returns: User | undefined
// Get the full record with timestamp
const record = users.getRecord('user-123');
// Returns: LWWRecord<User> | undefined
// { value: User, timestamp: { millis, counter, nodeId }, ttlMs? } Creating & Updating Data
set() for both create and update operations.LWWMap<K, V>Parameters
set(key: K, value: V, ttlMs?: number)LWWRecord<V>. Sets the value for the given key. Returns the created record with timestamp. Optional TTL in milliseconds for auto-expiration.
const users = client.getMap<string, User>('users');
// Create a new entry - returns the created record
const id = crypto.randomUUID();
const record = users.set(id, {
id,
name: 'Alice',
email: '[email protected]',
createdAt: Date.now()
});
// Returns: LWWRecord<User>
// { value: {...}, timestamp: { millis, counter, nodeId } }
// Set with TTL (auto-expires after 1 hour)
const tempRecord = users.set('session-123', sessionData, 3600000); Note: LWWMap performs full replacement on set().
To update a single field, read the current value first, merge your changes, then set the entire object.
Deleting Data
LWWMap<K, V>Parameters
remove(key: K)LWWRecord<V>. Removes the entry for the given key. Returns a tombstone record (value: null) for sync.clear()void. Clears all data and tombstones. Resets the internal Merkle tree.
const users = client.getMap<string, User>('users');
// Delete by key - returns the tombstone record
const tombstone = users.remove('user-123');
// Returns: LWWRecord<User>
// { value: null, timestamp: { millis, counter, nodeId } } Iterating Over Data
const users = client.getMap<string, User>('users');
// Iterate over all non-deleted entries
for (const [key, value] of users.entries()) {
console.log(key, value);
}
// Get all keys (including tombstones)
for (const key of users.allKeys()) {
const record = users.getRecord(key);
if (record?.value !== null) {
console.log('Active:', key);
}
}
// Get the size (total entries including tombstones)
console.log('Total entries:', users.size); Observing Changes
LWWMap<K, V>Parameters
onChange(callback: () => void)() => void. Registers a callback that fires whenever the map changes. Returns an unsubscribe function.
const users = client.getMap<string, User>('users');
// Subscribe to any changes in this map
const unsubscribe = users.onChange(() => {
console.log('Map changed!');
// Re-read data as needed
});
// Later: stop listening
unsubscribe(); Synchronization Methods
LWWMap<K, V> - Sync MethodsParameters
merge(key: K, remoteRecord: LWWRecord<V>)boolean. Merges a record from a remote source. Returns true if local state was updated (remote wins).prune(olderThan: Timestamp)K[]. Garbage collection: removes tombstones older than the specified timestamp. Returns array of pruned keys.getMerkleTree()MerkleTree. Returns the internal Merkle tree for efficient sync protocol.
// For synchronization - merge a remote record
const remoteRecord: LWWRecord<User> = {
value: { id: 'u1', name: 'Bob' },
timestamp: { millis: 1678900000, counter: 0, nodeId: 'node-2' }
};
// Returns true if local state was updated
const changed = users.merge('u1', remoteRecord); ORMap (Observed-Remove Map)
The ORMap is designed for collections where a key can have multiple values.
Concurrent additions are preserved, and removals only affect observed items.
Access it via client.getORMap<K, V>(name).
Adding Data
ORMap<K, V>Parameters
add(key: K, value: V, ttlMs?: number)ORMapRecord<V>. Adds a value under the given key. Returns an ORMapRecord with unique tag and timestamp. Multiple adds with same key create separate entries.
const todos = client.getORMap<string, Todo>('todos');
// Add an item to a collection
// Each add() creates a unique entry, even with same key
const record = todos.add('active', { title: 'Write docs', priority: 1 });
// Returns: ORMapRecord
// { value: {...}, timestamp: {...}, tag: "unique-id" }
// Add with TTL (auto-expires)
const tempRecord = todos.add('active', { title: 'Temp task' }, 3600000);
// Add another item with same key (both are preserved)
todos.add('active', { title: 'Fix bugs', priority: 2 }); Reading Data
ORMap<K, V>Parameters
get(key: K)V[]. Returns an array of all active (non-tombstoned, non-expired) values for the key.getRecords(key: K)ORMapRecord<V>[]. Returns full records with metadata for the key. Useful for sync operations.sizenumber. The number of unique keys that have at least one value.totalRecordsnumber. The total count of all active records across all keys.
const todos = client.getORMap<string, Todo>('todos');
// Get all values for a key (returns array)
const activeTodos = todos.get('active');
// Returns: Todo[] (all items added with key 'active')
// Get full records with metadata
const records = todos.getRecords('active');
// Returns: ORMapRecord<Todo>[]
// [{ value: {...}, timestamp: {...}, tag: "..." }, ...]
// Get count of unique keys
console.log('Keys:', todos.size);
// Get total record count across all keys
console.log('Total records:', todos.totalRecords); Removing Data
ORMap<K, V>Parameters
remove(key: K, value: V)string[]. Removes a specific value from the key using strict equality (===). Returns array of removed tags for sync.clear()void. Clears all data and tombstones.
const todos = client.getORMap<string, Todo>('todos');
// Remove specific value from a key
// Returns array of removed tags (for sync)
const removedTags = todos.remove('active', todoToRemove);
// Returns: string[] - tags that were removed
// Note: Uses strict equality (===) for matching
// For objects, you need the exact same reference Note: remove() uses strict equality (===).
For objects, you need the exact same reference that was added. Consider storing a unique ID in your values for easier removal.
Observing Changes
ORMap<K, V>Parameters
onChange(callback: () => void)() => void. Registers a callback that fires whenever the map changes. Returns an unsubscribe function.
const todos = client.getORMap<string, Todo>('todos');
// Subscribe to changes
const unsubscribe = todos.onChange(() => {
console.log('ORMap changed!');
// Re-read data as needed
});
// Later: stop listening
unsubscribe(); Synchronization Methods
ORMap<K, V> - Sync MethodsParameters
apply(key: K, record: ORMapRecord<V>)void. Applies a record from a remote source. Ignored if the tag is already tombstoned.applyTombstone(tag: string)void. Applies a tombstone (deletion) from a remote source. Removes the record with matching tag.getTombstones()string[]. Returns all tombstone tags. Used for sync protocol.merge(other: ORMap<K, V>)void. Merges state from another ORMap instance. Unions items and tombstones.prune(olderThan: Timestamp)string[]. Garbage collection: removes tombstones older than the timestamp. Returns pruned tags.
// For synchronization - apply a remote record
const remoteRecord: ORMapRecord<Todo> = {
value: { title: 'Remote task' },
timestamp: { millis: 1678900000, counter: 0, nodeId: 'node-2' },
tag: 'unique-tag-from-remote'
};
// Apply the record (idempotent - ignores if tag is tombstoned)
todos.apply('active', remoteRecord);
// Apply a tombstone (deletion from remote)
todos.applyTombstone('unique-tag-from-remote');
// Get all tombstones (for sync protocol)
const tombstones = todos.getTombstones();
// Returns: string[] Querying Data
For more advanced data retrieval with filtering, sorting, and live updates, use client.query().
client.query<T>(mapName, options?)Parameters
mapNamestring. The name of the map to query.options.where?Partial<T>. Simple equality filter. Returns entries where all specified fields match.options.predicate?Predicate. Complex filter using Predicates (and, or, gt, lt, contains, etc.).options.sort?{ [field]: 'asc' | 'desc' }. Sort order for results.
Basic Query
// Query with filters
const query = client.query<Todo>('todos', {
where: { completed: false },
sort: { createdAt: 'desc' }
});
// Subscribe to live results
const unsubscribe = query.subscribe((results) => {
console.log('Matching todos:', results);
});
// Get current snapshot (one-time read)
const snapshot = await query.get(); Complex Predicates
import { Predicates } from '@topgunbuild/client';
// Complex filtering with predicates
const query = client.query<Product>('products', {
predicate: Predicates.and(
Predicates.gt('price', 100),
Predicates.lt('price', 500),
Predicates.contains('tags', 'electronics')
)
}); Available Predicates
| Predicate | Description | Example |
|---|---|---|
| eq(field, value) | Equal to | Predicates.eq(‘status’, ‘active’) |
| neq(field, value) | Not equal to | Predicates.neq(‘status’, ‘deleted’) |
| gt(field, value) | Greater than | Predicates.gt(‘price’, 100) |
| gte(field, value) | Greater than or equal | Predicates.gte(‘age’, 18) |
| lt(field, value) | Less than | Predicates.lt(‘stock’, 10) |
| lte(field, value) | Less than or equal | Predicates.lte(‘priority’, 5) |
| contains(field, value) | Array contains value | Predicates.contains(‘tags’, ‘featured’) |
| and(…predicates) | All conditions must match | Predicates.and(p1, p2, p3) |
| or(…predicates) | Any condition must match | Predicates.or(p1, p2) |
| not(predicate) | Negates a condition | Predicates.not(Predicates.eq(‘hidden’, true)) |
CRUD Summary
| Operation | LWWMap | ORMap |
|---|---|---|
| Create | map.set(key, value) | map.add(key, value) |
| Read (single) | map.get(key) | map.get(key) |
| Read (with metadata) | map.getRecord(key) | map.getRecords(key) |
| Read (all) | map.entries() | — |
| Read (filtered) | client.query(name, { where, predicate }) | |
| Update | map.set(key, newValue) | map.remove() + map.add() |
| Delete | map.remove(key) | map.remove(key, value) |
| Subscribe | map.onChange(callback) | |
| Sync (merge) | map.merge(key, record) | map.apply() / map.merge() |
| Garbage collect | map.prune(olderThan) | |