Yesterday morning, while lying in bed consider whether or not to face the snow outside, I saw a long anticipated entry in gReaderPro (a Google Reader app). I think the release of Google Drive Realtime Api is extremely exciting. The way I see it realtime collaboration is one of the few features that makes browser-based productivity applications preferable to conventional desktop applications.
At University I’ve been using Gobby with other students for years, with a local server the latency is zero, and whether we’re writing a paper in LaTeX or prototyping an algorithm, we’re always doing it in Gobby. It’s simply the best way to do pair programming, or to write a text together, even if you’re not always working on the same section. Futhermore, there’s never a merge conflict 🙂
Needless to say that I started reading the documentation over breakfast. And as luck would have it, Lars, who I’m writing my master with, decided that he’d rather work from home than go through the snow, saving me from having to rush out the door.
Anyways, I found sometime last night to play around with CodeMirror and the new Google Drive Realtime Api. I’ve previously had a look at Firebase, which does something similar, but Firebase doesn’t support operational transformations on strings. In Google Drive Realtime Api this is supported through the CollaborativeString object, which has events and methods for inserting text and removing ranges.
So I extended the Quickstart example to use CodeMirror for editing, after a bit of fiddling around it turned out to be quite easy to adapt the beforeChange
event, such that I can all changes on the collaborativeString
using insertText
and removeRange
methods. The following CoffeeScript snippet show how to synchronize an editor and a collaborativeString
.
synchronize = (editor, coString) ->
# Assign initial value
editor.setValue coString.getText()
# Mutex to avoid recursion
ignore_change = false
# Handle local changes
editor.on 'beforeChange', (editor, changeObj) ->
return if ignore_change
from = editor.indexFromPos(changeObj.from)
to = editor.indexFromPos(changeObj.to)
text = changeObj.text.join('\n')
if to - from > 0
coString.removeRange(from, to)
if text.length > 0
coString.insertString(from, text)
# Handle remote text insertion
coString.addEventListener gapi.drive.realtime.EventType.TEXT_INSERTED, (e) ->
from = editor.posFromIndex(e.index)
ignore_change = true
editor.replaceRange(e.text, from, from)
ignore_change = false
# Handle remote range removal
coString.addEventListener gapi.drive.realtime.EventType.TEXT_DELETED, (e) ->
from = editor.posFromIndex(e.index)
to = editor.posFromIndex(e.index + e.text.length)
ignore_change = true
editor.replaceRange("", from, to)
ignore_change = false
I’ve pushed the code to github.com/jonasfj/google-drive-realtime-examples, you can also try the demo here. The demo is a markdown editor, it’ll save the file as a shortcut in Google Drive, but it’s won’t save the file as text document, just store it as a realtime document attached to the shortcut.
So a few things I learned from this exercise, use or at least study the code samples, much of it isn’t documented and the documentation can be a bit fragmented. In particular realtime-client-utils.js was really helpful to get this off the ground.
Hi,
Nice try.
However it’s slow, and I suppose heavy load on the servers is the reason, have you ever tried using sharejs for this purpose?
Comment by Soroush — April 1, 2013 @ 9:15 am
Hmm… I don’t experience the latency as any worse than Google Docs.
I suspect latency has something to do with distance.
It would be interesting to look at where the servers are located.
I imagine that a sharejs/nodejs could suffer from the same latency issues, if deployed in a cloud far away.
Though, a custom server would add much flexibility, in terms of what your data model can look like.
I think it would get fairly complicated when you need to scale instances and host them on different continents for lower latency.
So leaving hosting and scaling to Google is a big plus IMO.
Comment by jonasfj — April 1, 2013 @ 11:45 am