Versioning & Resync
This guide explains how Mesh handles record versioning and resynchronization to ensure data consistency across clients.
The Challenge of Distributed State
In a distributed real-time system, maintaining consistent state across multiple clients is challenging:
- Clients may disconnect temporarily
- Network issues can cause messages to be lost
- Updates may arrive out of order
- Clients may join after many updates have occurred
Mesh addresses these challenges with a robust versioning and resynchronization system for records.
How Versioning Works
Every record in Mesh has a version number that increments with each update:
- When a record is first created, it starts at version 1
- Each update increments the version by 1
- Clients track the current version they have
- Updates include the new version number
This versioning system allows clients to detect if they’ve missed any updates.
Version Tracking
When a client subscribes to a record, it receives the current value and version:
const { success, record, version } = await client.subscribeRecord(
"user:123",
(update) => {
console.log(`Received update v${update.version}`);
// Handle update...
}
);
console.log(`Initial record: ${record}, version: ${version}`);
The client stores this version number and expects each subsequent update to have a version that is exactly one higher than its current version.
Detecting Missed Updates
In patch mode, version numbers are critical for detecting missed updates:
let currentVersion = version;
client.subscribeRecord("user:123", (update) => {
// Check if we missed any updates
if (update.version !== currentVersion + 1) {
console.log(`Version mismatch! Expected ${currentVersion + 1}, got ${update.version}`);
// Resync will happen automatically
}
currentVersion = update.version;
// Handle update...
});
If a client detects that update.version !== currentVersion + 1
, it knows it has missed one or more updates.
Automatic Resynchronization
When a version mismatch is detected, Mesh automatically handles resynchronization:
- The client detects the version mismatch
- The client requests a full resync from the server
- The server sends the complete current value and version
- The client updates its local state and continues from the new version
This happens transparently to your application code. In patch mode, your update handler will receive a full
update instead of a patch
:
let productData = {};
let currentVersion = version;
client.subscribeRecord(
"product:456",
(update) => {
if (update.patch) {
// Normal update
applyPatch(productData, update.patch);
} else {
// Resync update (full value)
productData = update.full;
console.log(`Resynced to version ${update.version}`);
}
currentVersion = update.version;
},
{ mode: "patch" }
);
How Resync Works Under the Hood
When a client detects a version mismatch, here’s what happens:
- The client automatically unsubscribes from the record
- The client immediately resubscribes to the record
- The server sends the current full value and version
- The client’s update handler receives a
full
update - Normal operation continues from the new version
This process is handled automatically by the Mesh client SDK.
Versioning in Full Mode
Even in full mode, version numbers are included with each update:
client.subscribeRecord("user:123", (update) => {
console.log(`Received full update v${update.version}`);
// update.full contains the complete record
});
While version checking is less critical in full mode (since you always get the complete state), the version numbers are still useful for tracking the update history.
Server-Side Versioning
On the server, version numbers are stored in Redis alongside the record values. When you call publishRecordUpdate()
, the server:
- Retrieves the current value and version from Redis
- Increments the version
- Stores the new value and version in Redis
- Computes a patch (if needed)
- Broadcasts the update with the new version
// Server-side
await server.publishRecordUpdate("user:123", {
name: "Alice",
email: "alice@example.com",
});
// Later...
await server.publishRecordUpdate("user:123", {
name: "Alice",
email: "alice@updated.com",
status: "active",
});
Handling Reconnection
When a client reconnects after a disconnection, it needs to resubscribe to records:
// Track 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;
};
// 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);
}
});
When resubscribing, the client will receive the current value and version, effectively performing a resync.
Conflict Resolution
Mesh doesn’t include built-in conflict resolution for simultaneous updates. The last update wins. If you need more sophisticated conflict resolution:
- Use server-side logic to validate and merge updates
- Implement optimistic updates with rollback on the client
- Consider using a command-based approach for critical operations
// Server-side command with validation
server.exposeCommand("update-user-field", async (ctx) => {
const { userId, field, value, expectedVersion } = ctx.payload;
// Get current record
const { value: currentValue, version } =
await server.recordManager.getRecord(`user:${userId}`);
// Check version to detect conflicts
if (expectedVersion !== version) {
return { success: false, reason: "conflict" };
}
// Update just one field
const newValue = { ...currentValue, [field]: value };
// Publish update
await server.publishRecordUpdate(`user:${userId}`, newValue);
return { success: true, version: version + 1 };
});
Best Practices
-
Trust the versioning system
- Let Mesh handle version tracking and resync
- Don’t try to implement your own versioning on top of Mesh
-
Design for eventual consistency
- Assume clients may temporarily have different views of the data
- Use UI patterns that gracefully handle state transitions
-
Consider update frequency
- High-frequency updates may lead to more resyncs
- Batch related changes into single updates when possible
-
Handle reconnection properly
- Track active subscriptions
- Resubscribe after reconnection
-
Monitor version jumps
- Large version jumps may indicate network issues
- Consider logging when significant resyncs occur