Live Queries
TopGun provides a powerful reactive query system that allows you to subscribe to changes in your data. Unlike traditional databases where you have to poll for updates, TopGun pushes updates to your client in real-time.
Real-time Updates
Changes are pushed to subscribers immediately as they happen, no polling required.
Powerful Filters
Use simple equality matching or complex predicates with logical operators.
React Integration
First-class React hooks for seamless integration with your UI components.
Basic Usage
To create a live query, use the client.query() method.
This returns a QueryHandle that you can subscribe to.
const query = client.query('todos', {
where: { completed: false },
sort: { createdAt: 'desc' }
});
const unsubscribe = query.subscribe((results) => {
console.log('Active todos:', results);
});
// Later, when you're done:
unsubscribe(); Query Filters
The QueryFilter object supports several options:
where: Simple equality matchingpredicate: Complex conditions using logic operatorssort: Sort orderlimit: Max number of results
Complex Predicates
For more complex filtering, use the predicate field:
import { Predicates } from '@topgunbuild/client';
const query = client.query('products', {
predicate: Predicates.and(
Predicates.greaterThan('price', 100),
Predicates.equal('category', 'electronics')
)
}); Available Predicate Methods
| Method | Description | Example |
|---|---|---|
equal(attr, value) | Exact match | Predicates.equal('status', 'active') |
notEqual(attr, value) | Not equal | Predicates.notEqual('type', 'draft') |
greaterThan(attr, value) | Greater than | Predicates.greaterThan('price', 100) |
greaterThanOrEqual(attr, value) | Greater or equal | Predicates.greaterThanOrEqual('stock', 0) |
lessThan(attr, value) | Less than | Predicates.lessThan('age', 18) |
lessThanOrEqual(attr, value) | Less or equal | Predicates.lessThanOrEqual('priority', 5) |
like(attr, pattern) | SQL-like pattern (% = any, _ = single char) | Predicates.like('name', '%john%') |
regex(attr, pattern) | Regular expression | Predicates.regex('email', '^.*@gmail\\.com$') |
between(attr, from, to) | Range (inclusive) | Predicates.between('price', 10, 100) |
and(...predicates) | Logical AND | Predicates.and(p1, p2, p3) |
or(...predicates) | Logical OR | Predicates.or(p1, p2) |
not(predicate) | Logical NOT | Predicates.not(p1) |
Change Tracking
TopGun tracks not just the current state, but what changed. This enables:
- Efficient UI updates (only re-render changed items)
- Add/remove animations (framer-motion, react-spring)
- Notifications (“New item added”)
- Optimistic UI rollback on specific keys
const query = client.query('todos');
// Subscribe to changes
const unsubscribe = query.onChanges((changes) => {
for (const change of changes) {
console.log(`${change.type}: ${change.key}`, change.value);
// change.type: 'add' | 'update' | 'remove'
// change.previousValue: available for update/remove
}
});
// Get pending changes without subscribing
const pending = query.getPendingChanges();
// Consume and clear changes
const consumed = query.consumeChanges();
React Integration
If you are using React, we recommend using the useQuery hook which handles subscription management automatically.
See the React Hooks reference for detailed documentation on useQuery (including change tracking), useMutation, and other hooks.
Distributed Live Queries (Cluster Mode)
In clustered environments, live queries automatically work across all nodes. When you subscribe to a query, you receive real-time updates for matching documents regardless of which node owns the data.
Cluster-Wide Updates
Changes on any cluster node automatically push to all relevant subscribers.
Transparent Coordination
The node you connect to becomes the coordinator, aggregating updates from all nodes.
How It Works
// Live queries work seamlessly in clustered environments
// The client connects to any node - queries see data from ALL nodes
const client = new TopGunClient({
serverUrl: 'ws://node1:8080' // Connect to any cluster node
});
// This subscription receives updates from all cluster nodes
const query = client.query('orders', {
predicate: Predicates.greaterThan('total', 100)
});
// Updates are pushed automatically when:
// - New matching documents are added on ANY node
// - Existing documents are modified to match/unmatch on ANY node
// - Documents are removed from ANY node
query.subscribe((results) => {
console.log('All matching orders across cluster:', results.length);
}); Architecture
When a client subscribes to a live query in a cluster:
- Registration Phase: The connected node (coordinator) broadcasts the subscription to all cluster nodes
- Initial Results: Each node evaluates the query against its local data and sends results
- Result Merging: The coordinator deduplicates and merges results from all nodes
- Live Updates: When data changes on any node, that node evaluates affected subscriptions and sends targeted updates to the coordinator
- Client Delivery: The coordinator forwards updates to the client
Client ──QUERY_SUB──▶ Coordinator ──CLUSTER_SUB_REGISTER──▶ All Nodes
│ │
│◀───────────────────────────────────┘
│ Initial results + ACKs
│
▼
Merge & Send to Client
│
┌─────────────┼─────────────┐
│ │ │
On change: On change: On change:
Node A Node B Node C
│ │ │
└─────────────┼─────────────┘
│
CLUSTER_SUB_UPDATE
│
▼
Forward to Client
Delta Updates
Instead of resending full result sets, nodes send targeted delta updates:
| Update Type | Description |
|---|---|
ENTER | Document now matches the query predicate |
UPDATE | Document still matches but its value changed |
LEAVE | Document no longer matches the query predicate |
This approach minimizes network traffic and enables efficient UI updates.
Field Projection
Use the fields option to receive only the fields you need. This reduces network traffic when records contain many fields but your UI only displays a subset.
const query = client.query('users', {
where: { status: 'active' },
fields: ['name', 'avatar'],
});
query.subscribe((results) => {
// Each result contains only { name, avatar } — other fields are omitted
results.forEach(r => console.log(r.value));
});
Field projection is applied server-side, so only the selected fields traverse the network. The key is always included regardless of the fields list.
Result Limit
Use the limit option to cap the number of results returned in the initial response. This is useful for pagination or when you only need a top-N view.
const query = client.query('orders', {
where: { status: 'pending' },
sort: { createdAt: 'desc' },
limit: 20,
});
query.subscribe((results) => {
// results contains at most 20 entries
// Live updates (ENTER/UPDATE/LEAVE) still arrive for subsequent mutations
console.log('Top orders:', results.length);
});
When the total matching records exceed the limit, the response includes hasMore: true so your application can implement load-more or cursor-based pagination.
Merkle Delta Reconnect
When a client reconnects after going offline, TopGun uses Merkle trees to efficiently synchronize only the changes that occurred while the client was disconnected, instead of retransmitting the full result set.
How it works:
- The server maintains a per-query Merkle tree of record hashes
- On initial
QUERY_RESP, the server includes amerkleRootHash - When the client reconnects, it sends a
QUERY_SYNC_INITmessage with its last knownmerkleRootHash - The server compares trees and sends only the changed records
This is fully automatic — no application code is needed. The client stores the Merkle root hash and sends it on reconnect. If the hashes match, no data is retransmitted.
Automatic failover: If a cluster node disconnects, the coordinator automatically removes its results from the subscription state. When the node rejoins, its data becomes visible again through normal query updates.