Conflict Resolution and Grouping Changes

The Realtime API manages edit conflicts differently than a standard database. Rather than providing ACID guarantees using locking or transactions, conflicts and changes from different collaborators are automatically resolved using a process called operational transformation. For details on how this works within the Realtime API, see The Secrets of the Realtime API from I/O 2014, which provides a deep dive into the implementation of the API.

The data model is guaranteed to be eventually consistent: all users can write to the document at the same time, and any conflicts are automatically resolved such that the model eventually has the exact same state on all clients and the server. However, it is possible to design models that, as a result of collaboration, can end up in a state that your application can no longer understand. You can prevent this situation by properly structuring your data model. This section describes some of the tools available to help you accomplish this.

Atomic Units

Some data models have complex constraints that require a number of values in the data model to change together in lockstep. This can be accomplished by encoding all of these values within a single non-collaborative object. These objects are always written as an atomic unit, so changes between two different collaborators are never merged and one collaborator’s version wins.

For example, consider representing a location on the screen. You need to record both an X and a Y coordinate. It never makes sense to merge the X coordinate from one user’s clicks with the Y coordinate from another. Thus, you want all changes to these two parameters to be made together. To accomplish this, use a 2-element JavaScript array, or a standard JavaScript object. These are serialized to static JSON and treated as an atomic unit by the APIs.

Grouping Changes: Compound Operations

Compound operations provide a mechanism for you to group multiple changes to the data model. Unlike an atomic unit above, the items that were changed could also be changed independently from each other. Changes that are made within a compound operation are executed in a batch with no other users' changes in between.

In the Realtime API, by default, a user's changes can be delivered to collaborators at any time, in batches of any size. It's very possible that two edits that originally occurred 10 milliseconds apart might be delivered with a much larger time gap to some other collaborators. In fact, every collaborator may observe very different timings and orderings of edit events (although if events are reordered, they are changed so that the final state of the document is the same after all changes are delivered).

Using a compound operation ensures that every change inside the compound operation is delivered at the same time with no other changes intermixed.

model.beginCompoundOperation();
myCollaborativeList.push("Hello");
myCollaborativeList.push("World");
model.endCompoundOperation();

This code guarantees that when another collaborator sees that you added "Hello" to the list, she'll also see "World". There is no gap in time between the arrival of the two changes.

Although edits made in a compound operation are delivered together, they are not atomic. It is possible that, because of conflict resolution, some edits in a compound operation are never delivered. Imagine a collaboration scenario where two editors modify the same collaborative map at the same time. Alice runs this code:

model.beginCompoundOperation();
myCollaborativeMap.set('name', 'Alice');
myCollaborativeMap.set('phone', '555-5309');
model.endCompoundOperation();

And Bob runs this code:

model.beginCompoundOperation();
myCollaborativeMap.set('name', 'Bob');
myCollaborativeMap.set('phone', '555-0000');
myCollaborativeMap.set('address', 'Anytown, USA');
model.endCompoundOperation();

If Bob's edits arrive at the server first, the map contents will ultimately resolve to:

{
  'name' : 'Alice',          // Alice's name
  'phone' : '555-5309',      // Alice's number
  'address' : 'Anytown, USA' // Bob's address!
}

If Alice's edits arrive at the server first, the map contents will be:

{
  'name' : 'Bob',            // Bob's name
  'phone' : '555-0000',      // Bob's number
  'address' : 'Anytown, USA' // Bob's address
}

Nested Compound Operations

It is possible for a compound operation to be nested within another:

model.beginCompoundOperation();
myCollaborativeMap.set('name', 'Bob');

model.beginCompoundOperation();
myCollaborativeMap.set('street', 'Main');
myCollaborativeMap.set('number', '901');
model.endCompoundOperation();

model.endCompoundOperation();

All edits within the outer compound operation are delivered together. The inner compound operation is unnecessary, but allowed.

Compound Operation Names

You can name your compound operations as a way to label a group of changes:

model.beginCompoundOperation("New Address");
// do work
model.endCompoundOperation();

This name is provided in any events that are caused by the changes within the compound operation via event.compoundOperationNames.

If a compound operation is nested within another compound operation, both names are included in the event details for changes enclosed in both operations.

Preventing Undo

By default compound operations are undoable. You can specify if an operation is undoable by passing the opt_isUndoable parameter in the call to beginCompoundOperation(). To make Alice's change above a non-undoable compound operation, use the following code:

model.beginCompoundOperation('', false);
myCollaborativeMap.set('name', 'Alice');
myCollaborativeMap.set('phone', '555-5309');
model.endCompoundOperation();

In the case of nested compound operations, the opt_isUndoable flag should be the same on all of the operations.

See Undo and redo for more details.

Send feedback about...

Realtime API
Realtime API