Client Records
Mesh supports subscribing to individual records stored in Redis. When a record changes, clients receive either the full value or a JSON patch describing the update—depending on the selected mode. This page explains how to subscribe to and update records on the client side.
Subscribing to Records
Use the subscribeRecord
method to subscribe to a record:
let userProfile = {};
const { success, record, version } = await client.subscribeRecord(
"user:123",
(update) => {
// update contains { recordId, full, version }
userProfile = update.full;
console.log(
`Received full update for ${update.recordId} v${update.version}:`,
update.full
);
}
);
if (success) {
userProfile = record;
console.log("Initial record:", record);
console.log("Initial version:", version);
}
The first parameter is the record ID, and the second parameter is a callback function that will be called whenever the record is updated.
By default, the client receives the entire updated record every time (full
mode). This is simpler to use and ideal for small records or when patching isn’t needed.
Subscribing in Patch Mode
For larger records or when bandwidth is a concern, you can use patch
mode:
import { applyPatch } from "@mesh-kit/core/client";
let productData = {};
const { success, record, version } = await client.subscribeRecord(
"product:456",
(update) => {
// update contains { recordId, patch?, full?, version }
if (update.patch) {
// normally you'll receive `patch`, but if the client falls out of sync,
// the server will send a full update instead to resynchronize.
applyPatch(productData, update.patch);
console.log(`Applied patch for ${update.recordId} v${update.version}`);
} else {
productData = update.full;
console.log(
`Received full (resync) for ${update.recordId} v${update.version}`
);
}
},
{ mode: "patch" }
);
if (success) {
productData = record;
}
In patch
mode, the client receives only changes as JSON patches and must apply them locally. This is especially useful for large records that only change in small ways over time.
Patch mode only works for records that are objects or arrays. Primitive values like strings, numbers, or booleans aren’t representable as JSON patches, so you should use full mode for those cases.
Unsubscribing from Records
When you no longer need to receive updates for a record, you should unsubscribe:
await client.unsubscribeRecord("user:123");
This will stop the client from receiving updates and clean up any resources associated with the subscription.
Updating Records
If a record has been exposed as writable via exposeWritableRecord
on the server, clients can publish updates using the publishRecordUpdate
method:
const userId = "123";
const success = await client.publishRecordUpdate(`cursor:user:${userId}`, {
x: 100,
y: 250,
timestamp: Date.now(),
});
if (success) {
console.log("Cursor position updated successfully.");
} else {
console.error("Failed to update cursor position (maybe permission denied?).");
}
The method takes two parameters:
- The record ID
- The new value for the record
The method returns true
if the server accepted the write, and false
if it was rejected (e.g., due to a failed guard).
Handling Self-Updates
When a client publishes an update to a record using publishRecordUpdate
, it will also receive that update through its subscription callback—just like any other client. This ensures consistency and avoids special-casing updates based on origin.
If your app logic already applies local updates optimistically, you may choose to ignore redundant self-updates in your callback:
// Track the last update we sent
let lastSentUpdate = null;
// Subscribe to the record
await client.subscribeRecord("cursor:user:123", (update) => {
// Check if this is our own update
if (
lastSentUpdate &&
JSON.stringify(update.full) === JSON.stringify(lastSentUpdate)
) {
console.log("Ignoring echo of our own update");
lastSentUpdate = null; // Reset after handling
return;
}
// Handle the update normally
console.log("Received update:", update.full);
});
// When sending an update
const newPosition = { x: 100, y: 250, timestamp: Date.now() };
lastSentUpdate = newPosition;
await client.publishRecordUpdate("cursor:user:123", newPosition);
Versioning and Resync
Every update includes a version
. Clients track the current version and, in patch
mode, expect version === localVersion + 1
. If a gap is detected (missed patch), the client will automatically be sent a full record update to resync.
This happens transparently to your code - the callback will receive a full
update instead of a patch
update, and you should update your local state accordingly.
Handling Reconnection
When a client reconnects after a disconnection, you’ll need to resubscribe to records:
// Store active record subscriptions
const activeRecords = new Map();
// Subscribe to a record and track it
const subscribeToRecord = async (recordId, callback, options) => {
const result = await client.subscribeRecord(recordId, callback, options);
if (result.success) {
activeRecords.set(recordId, { callback, options });
}
return result;
};
// Unsubscribe from a record and stop tracking it
const unsubscribeFromRecord = async (recordId) => {
await client.unsubscribeRecord(recordId);
activeRecords.delete(recordId);
};
// Restore subscriptions after reconnection
client.onReconnect(async () => {
console.log("Reconnected, restoring record subscriptions...");
for (const [recordId, { callback, options }] of activeRecords.entries()) {
await client.subscribeRecord(recordId, callback, options);
}
console.log("Record subscriptions restored");
});
Use Cases
Records are particularly useful for:
-
User profiles
- Store user information that needs to be synchronized across clients
- Update profile fields in real-time
-
Collaborative editing
- Shared cursors and selection ranges
- Document state synchronization
-
Game state
- Player positions
- Game board state
- Inventory and status
-
Dashboard configuration
- Widget layouts
- User preferences
- Real-time metrics
Best Practices
-
Choose the right mode
- Use
full
mode for small records - Use
patch
mode for large records or when bandwidth is a concern
- Use
-
Handle reconnection
- Track active subscriptions
- Resubscribe after reconnection
-
Consider optimistic updates
- Apply updates locally before sending to the server
- Handle conflicts if the server rejects the update
-
Unsubscribe when done
- Always unsubscribe from records when they’re no longer needed
- This reduces unnecessary network traffic and server load
Record Persistence
The server can be configured to persist records to a durable storage backend (like SQLite), which means:
- Automatic restoration - When the server restarts, persisted records are automatically restored from storage
- Historical data preservation - Important record data survives server restarts and Redis failures
- Seamless client experience - Clients don’t need to handle persistence themselves; they’ll receive the restored data automatically upon subscription
This is particularly useful for:
- User profiles that should be preserved between sessions
- Game states that need to survive server restarts
- Document content that requires long-term storage
- Application settings that should persist across deployments
From a client perspective, persistence is completely transparent - you interact with records the same way whether they’re persisted or not. The server handles all the storage and restoration logic automatically.