Google Drive SDK

Build a Collaborative Data Model

In the Google Drive Realtime API, every document is associated with a collaborative data model. This model is comprised of a hierarchy of collaborative objects under the model root. The Realtime API includes several built-in collaborative object classes, including collaborative versions of lists, maps, and strings. These collaborative objects are created using their respective model create methods. You can declare your own custom data model using the model root create('myCustomType') method.

Some things to keep in mind when building a collaborative model:

  • Anything you store in a collaborative data model is automatically written to permanent storage.
  • Anything you store in a collaborative data model is automatically broadcast to all collaborators on the document, with varying (usually minimal) levels of network latency.
  • Collaborative data models may change at any time as the result of modifications from other editors.

Model structure

The data in a collaborative document is represented by a Model object. The Model always contains at least one top-level map with the special identifier "root". All objects that are part of the model must be accessible from the root.

Under the root object, an app builds a tree structure (technically an object graph, because object cycles are allowed) that makes sense for the files or documents it creates. Every object in this graph must be a JavaScript primitive value, a Realtime collaborative object (CollaborativeString, CollaborativeMap, or CollaborativeList), a custom collaborative object, or non-collaborative immutable JavaScript objects that can be serialized to JSON.

For many purposes, the built-in collaborative object classes for lists, maps, and strings should meet your needs. However, if you need more specialized objects for your application, you can create your own custom collaborative objects.

Creating new object instances

All object instances are created by calling the appropriate create method from the model root. Collaborative objects must not be created by calling their constructors directly.

After creating a collaborative object, you can can add it to the model root, collaborative lists, collaborative maps, or to a custom collaborative object.

Creating pre-defined collaborative objects

You can create pre-defined collaborative objects using the Model methods createString(), createList(), and createMap(). You can also use the registerReference() methods for the Collaborative List and Collaborative String to track a particular index location. The index location shifts with changes to the data model. This makes it useful for tracking things such as user cursor locations.

Registering and creating custom objects

Custom collaborative objects allow you to add realtime collaboration features to an existing JavaScript object type. These custom objects can be added to the collaborative data model just like the built in collaborative object types, and special collaborative fields are automatically synced across users.

To use custom objects, you must first register your custom object type before loading a document. To illustrate how this works, we'll look at an example custom class named Book.

myApp.Book = function() {}

Register this class with the registerType method:

gapi.drive.realtime.custom.registerType(myApp.Book, 'Book');

Now that the class is registered, we can add collaborative fields. These fields support normal read/write operations just like a regular field. However, when you write to the field, the new value is automatically saved in the realtime model and sent to other collaborators as an event.

Since our class is a book, let's add fields that describe books:

myApp.Book.prototype.title = gapi.drive.realtime.custom.collaborativeField('title');
myApp.Book.prototype.author = gapi.drive.realtime.custom.collaborativeField('author');
myApp.Book.prototype.isbn = gapi.drive.realtime.custom.collaborativeField('isbn');
myApp.Book.prototype.isCheckedOut = gapi.drive.realtime.custom.collaborativeField('isCheckedOut');
myApp.Book.prototype.reviews = gapi.drive.realtime.custom.collaborativeField('reviews');

Once the document has been loaded, you can create instances of the custom object by calling create on the model with either the class or the string name used to register the type. For example you could write:

var book = model.create(myApp.Book);

or

var book = model.create('Book');

After creating the Book object, we can now assign it to an object in the hierarchy (in this case, the root) as follows:

model.getRoot().set('book', book);

Like the pre-defined collaborative objects, we can also add an event listener to be informed of changes:

book.addEventListener(gapi.drive.realtime.EventType.VALUE_CHANGED, doValueChanged);

To assign values to the fields in our book object, assign them as you would any other object. For instance, to create the book "Moby Dick", we'd set the fields:

book.title = 'Moby Dick';
book.author = 'Melville, Herman';
book.isbn = '978-1470178192';
book.isCheckedOut = false;

Lifecycle of a custom collaborative data object

There are some important differences between the lifecycle of a standard object and a collaborative object. In a traditional data model object, the object's constructor is called exactly once in the object's lifetime. Initial data values are usually set by passing them into the object constructor.

Collaborative objects work a little differently. A collaborative object exists on multiple computers at once, and is reconstructed each time a document is loaded. A collaborative object's constructor may be called many, many times over the object's lifetime. Because of this, initial collaborative data values for newly-created objects must not be set in the object's constructor.

In fact, initial data values can't be set in the object's constructor, because the collaborative object isn't even wired up to the Realtime API's collaboration technology until after it's constructed. Thus, the constructor of a collaborative object can only be used to set up the non-collaborative object state, like local caches. Most collaborative objects should have empty constructors.

In collaborative objects, initial state is set up via the initializer and the onLoaded hook.

The initializer

A custom object may specify an initializer method using the setInitializer method:

gapi.drive.realtime.custom.setInitializer(myApp.Book, doInitialize);

The initializer is called exactly once in the lifetime of an object, immediately after the object is first created. When that object is reloaded in the future, the initializer is not executed; instead, the object is populated by loading saved data from the server. Initializer methods may take parameters, so the initial object state can be set up at creation time.

Imagine that we extend our book example so that each book contains a collaborative list of reviews (it's a collaborative book, after all). We've already created a collaborative field for the list of reviews, but that collaborative field doesn't have an initial value specified yet. The list of reviews needs to be available in every book object, as soon as it's created, so we need to create that list in the object initializer.

function doInitialize(opt_title) {
  var model = gapi.drive.realtime.custom.getModel(this);
  if (opt_title) {
    this.title = opt_title;
  }
  this.reviews = model.createList();
}

Notice that the initializer can have parameters. We can pass parameters into the initializer via model.create():

// Create a book with a default title.
model.create(myApp.Book, 'Paradise Lost');

Now we can add accessor methods for our list of reviews:

myApp.Book.prototype.addReview = function(review) {
  this.reviews.push(review);
  console.log('Review added locally. Current review count: ' +
      this.reviews.length);
};

myApp.Book.prototype.getReviews = function() {
  return this.reviews.toArray();
};

The onLoaded hook

To run code every time that an object's initial data becomes available, we need an onLoaded hook.

Sometimes, it's necessary to take some action once an object's initial data is populated. In a traditional data model object, this kind of thing is often done in the object constructor, but that won't work for a collaborative object. This setup also can't be done in the initializer, because the initializer only gets called once. Collaborative data model objects do this with an onLoaded hook.

For example, maybe we want to write a local logging message every time a book's review list is modified. This can be done with an onLoaded hook:

gapi.drive.realtime.custom.setOnLoaded(myApp.Book, doOnLoaded);

Now, we can write our logging code, secure in the knowledge that any collaborative fields that were set in the object's initializer will be populated with data:

doOnLoaded() {
  // Note that "this" is the newly created object, even though
  // this method is statically defined. The onLoaded event is
  // always called in the context of the loaded object.
  this.addEventListener(gapi.drive.realtime.EventType.OBJECT_CHANGED,
      logReviewChange);
}

Grouping changes together

In many applications, it is important that a group of changes occur together. However 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 will be changed so that the final state of the document will be the same when all changes are delivered).

However, it is possible to ensure that a batch of changes is delivered to collaborators at the same time, using compound operations.

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 will not be any gap in time between the arrival of the two changes.

However, 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 will never be 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
}

Undoing and redoing changes

Your application can call the Model undo() and redo() methods to undo and redo changes that the local user has made to the collaborative model. Changes can only be undone if there are local changes to the model. Your application must test if changes can be undone/redone by testing the canUndo and canRedo properties. For example, to undo the most recent local change, you can use the following code:

if (model.canUndo) {
  model.undo();
}
else {
  console.log("No events to undo.");
}

Your application can also listen for the UndoRedoStateChangedEvent to be notified when the state of canUndo or canRedo change. For more information on using this event, see Undo and Redo State Events.

Lifecycle of a collaborative model

The collaborative data model itself has a unique lifecycle. A collaborative model cannot be instantiated directly. Instead, a collaborative model is created automatically when a document is loaded for the first time.

Like custom collaborative objects, collaborative data models provide an initializer hook to allow you to populate a model with initial data when it is first created. This initializer function is specified in the gapi.drive.realtime.load function.

Let's say that our example app requires that every data model contains a list of books, and that list should always contain an example book. Building on our example code above, we could write the code like this:

gapi.drive.realtime.load(docId, onLoaded, function(model) {
  var books = model.createList();
  var defaultBook = model.create(myApp.Book, 'Paradise Lost');
  books.push(defaultBook);
  model.getRoot().set('books', books);
});

Best practices

Use initializer hooks to set up initial data on custom objects

Initializer hooks always run in their entirety before data is transmitted to collaborators. If an object must have some initial state to be valid, initializers are the simplest way to ensure that custom objects are fully populated when they are seen by collaborators.

Use initializer hooks to set up initial model data

Initializer hooks are the only safe way to get initial data into a newly-created model.

Use compound operations to ensure that changes are delivered together

Changes may be sent to collaborators at any time, in batches of any size. To ensure that a group of changes is received all at once, bundle those changes together using a compound operation.

Update UIs from change listeners

As noted throughout this documentation, collaborative data models may change without warning as a result of edits from other collaborators. A well-written collaborative app must attach listeners to its data model to update the UI when collaborative edits are received. Whenever possible, all UI updates (even UI updates caused by changes from the current, local user) should be done from data model change event listeners, because then there is a single code path for UI updates.

There are some circumstances where it is necessary to detect the difference between a locally-initiated data model change and a remotely-initiated data model change. In those cases UI updates should still be done from change listeners, but listener code should check the isLocal property of change events so that local changes can be ignored. See Handle Events for more detailed information on event handling.

Limit Realtime document and mutation sizes

The Realtime API limits the maximum sizes of Realtime documents and mutations. These limits are:

  • Total Realtime document size: 10 MB (10*2^10 bytes)
  • Maximum mutation size: 500 KB

A mutation that exceeds either of these limits results in an exception. To check the size of the Realtime document, check the Model.bytesUsed property.

Authentication required

You need to be signed in with Google+ to do that.

Signing you in...

Google Developers needs your permission to do that.