Offline Data Synchronization

Offline Data Synchronization

At any time, the Backend Services JavaScript SDK keeps the state of each offline item. It knows if the item has been modified, created, or deleted. When the SDK goes online, it can synchronize the changes based on this information.

This article contains these topics:

What is Synchronized

The JavaScript SDK keeps track of each content type that is available offline, the items that it contains, and if the items are in their original state or have been updated, deleted, or created.

The synchronization process is implemented entirely by the Backend Services JavaScript SDK. It is the SDK that synchronizes items with the server on demand.

The SDK synchronizes these items:

  • Items updated on the device
  • Items created on the device
  • Items deleted on the device
  • Items updated on the server but unmodified on the device
  • Items deleted on the server but unmodified on the device

The SDK does not synchronize these items:

  • Items created on the server but not on the device

By default, an item is synchronized if it has been updated on the device. However if the item has been updated on the server but not on the device, it will not be synchronized. You can change the default behavior using a constructor option.

To automatically synchronize the server items, set the offline.syncUnmodified constructor option to true:

var el = new Everlive({
    appId: 'your-app-id',
    offline: {
        syncUnmodified: true
    }
});

Triggering Synchronization

Synchronization is triggered by invoking a function:

el.sync();

If you need the synchronization to happen whenever you go online, simply call sync right after the online function call:

el.online();
el.sync();

Conflict Resolution

When using offline support, the same data items may end up being updated or deleted by offline users and online users (or different offline users). This may cause conflicts when synchronizing changes to the cloud.

To identify conflicts, the SDK compares the items' ModifiedAt date.

Types of Conflicts

The following cases are considered conflicts:

  • An item is modified both on the client and on the server
  • An item is deleted on the client and is modified on the server
  • An item is modified on the client and is deleted on the server

Resolution Strategies

The resolution strategy determines which of the conflicting items is kept and which is discarded. The Backend Services JavaScript SDK provides the following conflict resolution strategies: ClientWins, ServerWins, and Custom.

Note that you cannot use some of the strategies with all data sources. See the following table for details:

Strategy Internal Content Type External Content Type (with ModifiedAt) External Content Type (no ModifiedAt)
ClientWins
ServerWins
Custom

The resolution strategies only apply to conflicting items. Changes that do not cause conflicts are always applied on the server.

Client Wins Strategy

If you choose the Client Wins conflict resolution strategy, all conflicts are resolved in favor of the client—the client copy of the item replaces the server copy; all changes made to the server copy are lost.

This is the only available strategy if your content type does not have a ModifiedAt field (e.g. if it comes from an external data source through Data Connectors).

var el = new Everlive({
    appId: 'your-app-id',
    offline: {
        conflicts: {
            strategy: Everlive.Constants.ConflictResolutionStrategy.ClientWins
        }
    }
});

Server Wins Strategy

If you choose the Server Wins conflict resolution strategy, all conflicts are resolved in favor of the server—the server copy of the item replaces the client copy; all changes made to the client copy are lost.

var el = new Everlive({
    appId: 'your-app-id',
    offline: {
        conflicts: {
            strategy: Everlive.Constants.ConflictResolutionStrategy.ServerWins
        }
    }
});

Custom Strategy

The custom resolution strategy allows you to specify a function to be invoked to resolve conflicts. All collected conflicts are presented to the function at once.

var el = new Everlive({
    appId: 'your-app-id',
    offline: {
        conflicts: {
            strategy: Everlive.Constants.ConflictResolutionStrategy.Custom,
            implementation: function(conflicts) { ... }
        }
    }
});

You can choose to write logic that resolves conflicts in the background or you can leave the resolution to the app user. If you go with the latter, receiving all conflicts at once helps you integrate conflict resolution in your UI better (display progress, implement multiple selection options such as "apply to all conflicts", and so on).

Skipped Items

Conflicting items that you fail to handle in your custom function are treated as skipped. Skipped items are left untouched in the offline storage. They are picked every time you run synchronization until finally resolved.

You can intentionally skip items in your custom resolution function. For example:

var el = new Everlive({
    appId: 'your-app-id',
    offline: {
        conflicts: {
            strategy: Everlive.Constants.ConflictResolutionStrategy.Custom,
            implementation: function (conflicts, cb) {
                var isWifi = (navigator.connection.type === Connection.WIFI);
                _.each(conflicts, function (conflict) {
                    _.each(conflict.conflictingItems, function (conflictingItem) {
                        if(!isWifi && conflictingItem.clientItem.File){
                            conflictingItem.result.resolutionType = Everlive.Constants.ConflictResolution.Skip;
                        }
                    });
                });
                cb();
            }
        }
    }
};

Synchronization Events and UI Integration

While the synchronization can run in the background, you often need to integrate it in your app's UI. To enable this, the SDK provides several synchronization events you can subscribe to.

syncStart

The syncStart event is fired whenever a synchronization begins. You can use syncStart in conjunction with syncEnd to show the user that you are synchronizing data.

This is how you subscribe to the event:

el.on('syncStart', function() {...});

syncEnd

The syncEnd event is fired when the synchronization process completes. It contains information about the completed sync operation that you can use to find out how many items were synchronized and let the user know. You can use syncEnd in conjunction with syncStart to show the user that you are synchronizing data.

This is how you subscribe to the event:

el.on('syncEnd', function(syncInfo) {...});

where syncEnd is an object described in Object Reference.

itemProcessed

The itemProcessed event is fired when a single content type item has finished synchronizing—successfully or unsuccessfully. You can use it to display progress more precisely if you combine it with the getItemsForSync() method to first get the number of items for synchronization.

This is how you subscribe to the event:

el.on('itemProcessed', function(evArgs) {...});

where evArgs is an object described in Object Reference.

Object Reference

This section contains reference information about objects that are returned by synchronization events.

evArgs

The evArgs object contains these fields:

Field Description
itemId The ID of the item.
type Operation type. Can be create, update, or delete.
storage Operation direction. Can be "server" when the item is synchronized from the client to the server or "client" when the item is synchronized from the server to the client.
contentType The name of the content type being synchronized.
error An error message explaining why the synchronization failed. Appears only on unsuccessful synchronization.

syncInfo

The syncInfo object contains these fields:

Field Description
syncedToServer The number of items that were synchronized from the client to the server.
syncedToClient The number of items that were synchronized from the server to the client.
syncedItems An array of items that were synchronized successfully. Each item is an evArgs object.
failedItems An array of items that failed to synchronize. Each item is an evArgs object.

Error Handling and Synchronization Information

As soon as synchronization finishes the syncEnd event is emitted. If you have subscribed to this event your callback is called with a single argument containing the result of the synchronization. You can use the result data to handle errors or to display information about the synchronization to the user.

The synchronization result object has the following structure:

{
    syncedItems: { // holds all successfully synced items
        "your-content-type-name": {
            id: "...", // the id of the synced item
            type: "...", // can be "create", "update", or "delete" depending on the type of request that was made
            storage: "..." // can be "server" or "client". Shows if the item was synced to the server or to the client
        },
    },
    failedItems: { // holds any errors that have occurred during synchronization
        "your-content-type-name": {
            id: "...", // the id of the failed item
            type: "...", // can be "create", "update", or "delete" depending on the type of request that was made
            storage: "...", // can be "server" or "client". Shows where the error occurred--whether it was on the client or on the server
            error: {} // the original error received when trying to synchronize the item
        },
        ...
    },
    syncedToServer: Number, // the number of items that were successfully synced to the server
    syncedToClient: Number, // the number of items that were successfully synced to the client
    error: {} // A general error that may have prevented the synchronization procedure from finishing successfully
}

While synchronizing data errors may occur. Various factors can cause these errors such as loss of connectivity, permissions errors coming from the server and so on. After synchronization completes, the failedItems object contains the IDs of all items that failed to synchronize along with the original error as received by the server.

Synchronization and Cloud Code for Data

As explained in Limitations and Design Considerations, Cloud Code for Data is executed on synchronization as if the data was newly created. If you need control on what Cloud Code for Data logic is executed on synchronization, you can use the request object's isSync property.

Assume that you don't want to execute certain beforeUpdate logic then the update request is caused by synchronization. You can place the logic in a conditional statement that evaluates the isSync property:

Everlive.Events.beforeUpdate(function(request, context, done) {
    if(!request.isSync) {
        doSomeProcessing();
    }
    done();
});

See Also

Start a free trial Request a demo
Contact us: +1-888-365-2779
sales@telerik.com
Copyright © 2016-2017, Progress Software Corporation and/or its subsidiaries or affiliates. All rights reserved.