Backbone, local storage and server synchronization

Neil Bevis

Backbone comes with methods for fetching and saving data models to and from the server. However, we want an application that works offline and synchronizes with the server when online. Therefore we require to communicate models both with the server and with  the browser’s local storage.

The good news is that a backbone extension “backbone.localstorage.js” provides the communication with local storage, and by simply dropping its .js file. The bad news is that you then cannot communicate between backbone and the server. Unfortunately we failed to find any extensions which allow for both types of communication and provide synchronization between the two.

We therefore used backbone.localstorage.js such that our models are stored in local storage but wrote our own code to synchronize our local data with the server.

The first step was to keep hold of the underlying backbone.js server communication function Backbone.sync(…) before backbone.localstorage.js replaced it. That was simply to execute the following code between the execution of backbone.js and backbone.localstorage.js:

Backbone.serverSync = Backbone.sync;

We could then save data to the server using:

Backbone.serverSync('update', model, options);

while standard model.fetch() and model.save() used elsewhere in the code would use local storage via the backbone.localstorage.js’s Backbone.sync(…).

Then “all” we needed to do was to write the synchronization code using Backbone.serverSync(…), model.fetch(), and model.save().

A great feature of backbone is that updates coming from the server can be reflected on the page with minimal effort. For example, suppose you save a new questionnaire response. The client will assign it a temporary ID via a little piece of code we wrote to ensure uniqueness. However when it sends it to the server, the server will return the real ID of the response, which should be used from now on. That may actually be a few seconds after the user has submitted the response and by now the browser may already be displaying a list of completed responses, with the associated HTML containing the temporary ID. In our code, when the client receives the final ID from the server, the response model is updated and then the on-screen HTML updates itself automatically since the appropriate event structure is in place. That is, a generic server synchronization code ends up triggering specific HTML changes. Nice!

In conclusion then, it was disappointing that we had to write our own local-to-server synchronization code rather than one coming out-of-the-box with backbone, but the structure of backbone meant that the delayed nature of server responses was no trouble at all and the user experience was slicker as a result.


Update with code examples

By popular request, here is some more detail of the synchronization code.

The solution from our DevCamp, firstly involved giving the backbone model a ‘synchronized’ flag, which was a Boolean describing the client’s opinion of the synchronization status.

When asked to ‘push’ local changes to the server from a given collection, the client sends model items with synchronized=false on a model-by-model basis using Backbone.serverSync(‘update’, model, { success: blah, error: blah}). The server response to this simply two  IDs – the client-side ID of the model and the true server-side ID – which will differ if the item is new, while they will be the same if the push simply updated this model. In the case of a new item, local storage needs to be updated with the true ID, which is most easily done by simply deleting the model in local storage with the old ID via the backbone.js.localStorage version of Backbone.sync() and then saving a new version with simply model.save(). The essence is captured in the following code:

//Check we are on-line and that no other ‘push’ actions are active, etc.

for (var i = 0; i < models.length; i++) {
        var model = models[i];
        if (model.get('synchronized')) { continue; }
        model.change();
        Backbone.serverSync('update', model, {
            success: function (data) {
                var model = collection.get(data.ClientId);
                //if new server will return a different Id
                if (data.ServerId != data.ClientId) {
                    //delete from localStorage with current Id
                    Backbone.sync("delete", model, { success: function () { }, error: function () { } });

                    //save model back into localStorage
                    model.save({ Id: data.ServerId })
                }
                model.save({ synchronized: true });
                collection.localCacheActive = false;
            },
            error: function (jqTHX, textStatus, errorThrown) {
                console.log('Model upload failure:' + textStatus);
                collection.localCacheActive = false;
            }
        });
    }

When asked to ‘pull’ server-side changes to a collection from the server, the client first saves any unpushed client-side changes into local storage using model.save(). It then asks the server for the entire collection via the standard backbone fetch method:

tempCollection.sync = Backbone.serverSync;
tempCollection.fetch( { success: blah, error: blah });

Note the associated inefficiency of pulling the entire collection was not relevant for our DevCamp project, but in practice you could take steps to reduce the associated data download to only items which require really updating. The success function then checks each model it receives back from the server against its own list. If the model is new it adds it to the collection that is updating and also uses model.save() to record it into local storage:

collection.add(tempModel);
tempModel.change();
tempModel.save({ synchronized: true });

If the model already exists and the client believes the model is synchronized, then the success function updates it with the revised content.

model.set(tempModel.toJSON());
model.set({ synchronized: true });
model.save();

The final case is that the model already exists, but there are local modifications to it. In our solution, such models are not updated during the pull, and in fact they are pushed to server to update its own database. However, it is clear that that will not be the appropriate action in all scenarios.

Hope that helps!