Docs Menu
Docs Home
/ /
Atlas Device SDKs
/
/

Manual Client Reset Data Recovery - Node.js SDK

On this page

  • Track Changes by Object Strategy
  • Example
  • Include Last Updated Time in Your Schema
  • Configure Realm to Use Manual Client Reset
  • Track Synchronization in Separate Realm
  • Create Callback to Handle Client Reset
  • Alternative Strategies

This page explains how to manually recover unsynced realm data after a client reset using the Manual Recovery client reset mode.

Manual recovery requires significant amounts of code, schema concessions, and custom conflict resolution logic. You should only perform a manual recovery of unsynced realm data if you cannot lose unsynced data and the other automatic client reset methods do not meet your use case.

For more information about the other available client reset modes, refer to Reset a Client Realm.

The specifics of manual recovery depend heavily upon your application and your schema. However, there are a few techniques that can help with manual recoveries. The Track Changes by Object Strategy section demonstrates one method of recovering unsynced changes during a client reset.

Warning

Avoid Making Breaking Schema Changes in Production

Do not expect to recover all unsynced data after a breaking schema change. The best way to preserve user data is to never make a breaking schema change at all.

Important

Breaking Schema Changes Require an App Schema Update

After a breaking schema change:

  • All clients must perform a client reset.

  • You must update client models affected by the breaking schema change.

The track changes by object manual client reset data recovery strategy lets you recover data already written to the client realm file but not yet synced to the backend.

In this strategy, you add a "Last Updated Time" to each object model to track when each object last changed. We'll watch the to determine when the realm last uploaded its state to the backend.

When backend invokes a client reset, find objects that were deleted, created, or updated since the last sync with the backend. Then copy that data from the backup realm to the new realm.

The following steps demonstrate implementing the process at a high level:

  1. Client reset error: Your application receives a client reset error code from the backend.

  2. Strategy implementation: The SDK calls your strategy implementation.

  3. Close all instances of the realm: Close all open instances of the realm experiencing the client reset. If your application architecture makes this difficult (for instance, if your app uses many realm instances simultaneously in listeners throughout the application), it may be easier to restart the application. You can do this programmatically or through a direct request to the user in a dialog.

  4. Move the realm to a backup file: Call the Realm.App.Sync.initiateClientReset() static method. This method moves the current copy of the client realm file to a backup file.

  5. Open new instance of the realm: Open a new instance of the realm using your typical sync configuration. If your application uses multiple realms, you can identify the realm experiencing a client reset from the backup file name.

  6. Download all realm data from the backend: Download the entire set of data in the realm before you proceed. This is the default behavior of the SyncConfiguration object.

  7. Open the realm backup: Use the error.config object passed as an argument to the SyncConfiguration.error callback function.

  8. Migrate unsynced changes: Query the backup realm for data to recover. Insert, delete or update data in the new realm accordingly.

This example demonstrates implementing the track changes by object manual client reset data recovery strategy.

Note

Limitations of This Example

  • This example only is for an application with a single realm containing a single Realm object type. For each additional object type, you'd need to add another sync listener as described in the Track Synchronization in Separate Realm section.

  • This example keeps track of the last time each object was updated. As a result, the recovery operation overwrites the entire object in the new realm if any field was updated after the last successful sync of the backup realm. This could overwrite fields updated by other clients with old data from this client. If your realm objects contain multiple fields containing important data, consider keeping track of the last updated time of each field instead, and recovering each field individually.

For more information on other ways to perform a manual client reset with data recovery, refer to the Alternative Strategies section.

1

Add a new property to your Realm object schema to track the last time it was updated. Whenever you create or update a Realm object with the schema, include a timestamp with the update time.

Ordinarily, there is no way to detect when a Realm object was last modified. This makes it difficult to determine which changes were synced to the backend. By adding a timestamp lastUpdated to your Realm object models and updating that timestamp to the current time whenever a change occurs, you can keep track of when objects were changed.

const DogSchema = {
name: "Dog",
properties: {
name: "string",
age: "int?",
lastUpdated: "int",
},
};
2

In the realm's SyncConfiguration, set the clientReset field to manual mode and include an error callback function. You'll define the error callback function in the Create Callback to Handle Client Reset section.

const config = {
schema: [DogSchema],
sync: {
user: app.currentUser,
partitionValue: "MyPartitionValue",
clientReset: {
mode: "manual",
},
error: handleSyncError, // callback function defined later
},
};
3

Just knowing when objects were changed isn't enough to recover data during a client reset. You also need to know when the realm last completed a sync successfully. This example implementation uses a singleton object in a separate realm called LastSynced paired with a change listener to record when a realm finishes syncing successfully.

Define your LastSynced Realm to track the latest time your realm synchronizes.

const LastSyncedSchema = {
name: "LastSynced",
properties: {
realmTracked: "string",
timestamp: "int?",
},
primaryKey: "realmTracked",
};
const lastSyncedConfig = { schema: [LastSyncedSchema] };
const lastSyncedRealm = await Realm.open(lastSyncedConfig);
lastSyncedRealm.write(() => {
lastSyncedRealm.create("LastSynced", {
realmTracked: "Dog",
});
});

Register a change listener to subscribe to changes to the Dog collection. Only update the LastSynced object if the sync session is connected and all local changes have been synced with the server.

// Listens for changes to the Dogs collection
realm.objects("Dog").addListener(async () => {
// only update LastSynced if sync session is connected
// and all local changes are synced
if (realm.syncSession.isConnected()) {
await realm.syncSession.uploadAllLocalChanges();
lastSyncedRealm.write(() => {
lastSyncedRealm.create("LastSynced", {
realmTracked: "Dog",
timestamp: Date.now(),
});
});
}
});
4

Now that you've recorded update times for all objects in your application as well as the last time your application completed a sync, it's time to implement the manual recovery process. This example handles two main recovery operations:

  • Restore unsynced inserts and updates from the backup realm

  • Delete objects from the new realm that were previously deleted from the backup realm

You can follow along with the implementation of these operations in the code samples below.

async function handleSyncError(_session, error) {
if (error.name === "ClientReset") {
const realmPath = realm.path; // realm.path will not be accessible after realm.close()
realm.close(); // you must close all realms before proceeding
// pass your realm app instance and realm path to initiateClientReset()
Realm.App.Sync.initiateClientReset(app, realmPath);
// Redownload the realm
realm = await Realm.open(config);
const oldRealm = await Realm.open(error.config);
const lastSyncedTime = lastSyncedRealm.objectForPrimaryKey(
"LastSynced",
"Dog"
).timestamp;
const unsyncedDogs = oldRealm
.objects("Dog")
.filtered(`lastUpdated > ${lastSyncedTime}`);
// add unsynced dogs to synced realm
realm.write(() => {
unsyncedDogs.forEach((dog) => {
realm.create("Dog", dog, "modified");
});
});
// delete dogs from synced realm that were deleted locally
const syncedDogs = realm
.objects("Dog")
.filtered(`lastUpdated <= ${lastSyncedTime}`);
realm.write(() => {
syncedDogs.forEach((dog) => {
if (!oldRealm.objectForPrimaryKey("Dog", dog._id)) {
realm.delete(dog);
}
});
});
// make sure everything syncs and close old realm
await realm.syncSession.uploadAllLocalChanges();
oldRealm.close();
} else {
console.log(`Received error ${error.message}`);
}
}

Possible alternate implementations include:

  • Overwrite the entire backend with the backup state: With no "last updated time" or "last synced time", upsert all objects from the backup realm into the new realm. There is no way to recovered unsynced deletions with this approach. This approach overwrites all data written to the backend by other clients since the last sync. Recommended for applications where only one user writes to each realm.

  • Track changes by field: Instead of tracking a "last updated time" for every object, track the "last updated time" for every field. Update fields individually using this logic to avoid overwriting field writes from other clients with old data. Recommended for applications with many fields per-object where conflicts must be resolved at the field level.

  • Track updates separately from objects: Instead of tracking a "last updated time" in the schema of each object, create another model in your schema called Updates. Every time any field in any object (besides Updates) updates, record the primary key, field, and time of the update. During a client reset, "re-write" all of the Update events that occurred after the "last synced time" using the latest value of that field in the backup realm. This approach should replicate all unsynced local changes in the new realm without overwriting any fields with stale data. However, storing the collection of updates could become expensive if your application writes frequently. Recommended for applications where adding "lastUpdated" fields to object models is undesirable.

← 
 →