- Notifications
You must be signed in to change notification settings - Fork453
Collaborative editing in any app
License
josephg/ShareJS
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
NOTE: ShareJS is nowShareDB. Seehere andhere for more information.
.
.
.
This is a little server & client library to allow concurrent editing of anykind of content via OT. The server runs on NodeJS and the client works in NodeJS or aweb browser.
ShareJS currently supports operational transform on plain-text and arbitrary JSON data.
VisitGoogle groups for discussions and announcements
Immerse yourself inAPI Documentation.
ShareJSshould work with all browsers, down to IE5.5 (although IE supporthasn't been tested with the new version).
That said, I only test regularly with FF, Safari and Chrome, and occasionallywith IE8+.File bug reports if you have issues
# npm install share
Run the example server with:
# coffee node_modules/share/examples/server.coffee
Not all of the sharejs 0.6 examples have been ported across yet. I'd lovesome pull requests!
ShareJS depends onLiveDB for its databasebackend & data model. Read the livedb readme for information on how toconfigure your database.
Run the tests:
# npm install# mocha
To get started with the server API, you need to do 2 things:
- Decide where your data is going to be stored. You can mess around usingthe livedb inmemory store. For more options, see thelivedbapi.
- Decide how your client and server will communicate. The easiest solution isto usebrowserchannel.
To create a ShareJS server instance:
varlivedb=require('livedb');varsharejs=require('share');varbackend=livedb.client(livedb.memory());varshare=require('share').server.createClient({backend:backend});
The method is calledcreateClient
because its sort of a client of thedatabase... its a weird name, just roll with it.
The sharejs server instance has 3 methods you might care about:
- To communicate with a client, create a node stream which can communicate witha client and useshare.listen(stream) to hand control of the stream tosharejs. See the section below on client server communication for an example ofthis.
- share.rest() returns a connect/express router which exposes the sharejsREST API. This code is in the process of moving to its own repo. In themeantime, thedocumentation ishere
- You can intercept requests to the livedb backend to do access control usingsharejs middleware.share.use(method, function(action, callback){...}) willmake your function intercept & potentially rewrite requests. This is notcurrently documented, but when it is, the documentationwill livehere.
ShareJS requiresyou to provide a way for the client to communicate with theserver. As such, its transport agnostic. You can usebrowserchannel,websockets, or whatever you like. ShareJSrequires the transport to:
- Guarantee in-order message delivery. (Danger danger socket.io does not guarantee this)
- Provide a websocket-like API on the client
- Provide a node object stream to the server to talk to a client.
When a client times out, the server will throw away all informationrelated to that client. When the client client reconnects, it will reestablishall its state on the server again.
It is the responsibility of the transport to handle reconnection - the clientshould emit state change events to tell sharejs that it has reconnected.
The server exposes a methodshare.listen(stream)
which you can call with anode stream which can communicate with the client.
Here's an example using browserchannel:
varDuplex=require('stream').Duplex;varbrowserChannel=require('browserchannel').servervarshare=require('share').server.createClient({backend: ...});varapp=require('express')();app.use(browserChannel({webserver:webserver},function(client){varstream=newDuplex({objectMode:true});stream._read=function(){};stream._write=function(chunk,encoding,callback){if(client.state!=='closed'){client.send(chunk);}callback();};client.on('message',function(data){stream.push(data);});client.on('close',function(reason){stream.push(null);stream.emit('close');});stream.on('end',function(){client.close();});// Give the stream to sharejsreturnshare.listen(stream);}));
Andhere is a more complete example using websockets.
The client needs awebsocket-like sessionobject to communicate. You can use a normal websocket if you want:
varws=newWebSocket('ws://'+window.location.host);varshare=newsharejs.Connection(ws);
Sharejs also supports the following changes from the spec:
- The socket can reconnect. Simply call
socket.onopen
again when the socketreconnects and sharejs will reestablish its session state and send anyoutstanding user data. - If your underlying API allows data to be sent while in the CONNECTING state,set
socket.canSendWhileConnecting = true
. - If your API allows JSON messages, set
socket.canSendJSON = true
to avoidextra JSON stringifying.
If you use browserchannel, all of this is done for you. Simply tellbrowserchannel to reconnect and it'll take care of everything:
varsocket=newBCSocket(null,{reconnect:true});varshare=newsharejs.Connection(socket);
The client API can be used either from nodejs or from a browser.
From the server:
varconnection=require('share').client.Connection(socket);
From the browser, you'll need to first include the sharejs library. You can usebrowserify and require('share').client or include the script directly.
The browser library is built to thenode_modules/share/webclient
directorywhen you install sharejs. This path is exposed programatically atrequire('share').scriptsDir
. You can add this to your express app:
varsharejs=require('share');app.use(express.static(sharejs.scriptsDir));
Then in your web app include whichever OT types you need in your app and sharejs:
<scriptsrc="text.js"></script><scriptsrc="json0.js"></script><scriptsrc="share.js"></script>
This will create a globalsharejs
object in the browser.
The client exposes 2 classes you care about:
- TheConnection class wraps a socket and handles the communication to thesharejs server. You use the connection instance to create document referencesin the client.
- All actual data you edit will be wrapped by theDoc class. The documentclass stores an in-memory copy of the document data with your local editsapplied. Create a document instance by calling
connection.get('collection', 'docname')
.
ShareJS also allows you to make queries to your database. Live-bound querieswill return aQuery object. These are not currently documented.
To get started, you first need to create a connection:
varsjs=newsharejs.Connection(socket);
The socket must be a websocket-like object. See the section on client servercommunication for details about how to create a socket.
The most important method of the connection object is .get:
connection.get(collection, docname): Get a document reference to the nameddocument on the server. This function returns the same document reference eachtime you call connection.get().collection anddocname are both strings.
Connections also expose methods for executing queries:
- createFetchQuery(index, query, options, callback): Executes a query against the backend and returns a set of documents matching the query via the callback.
- createSubscribeQuery(index, query, options, callback): Run a query against the backend and keep the result set live. Returns aQuery object via the callback.
The best documentation for these functions is in ablock comment in the code.
For debugging, connections have 2 additional properties:
- Setconnection.debug = true to console.log out all messages sent andrecieved over the wire.
- connection.messageBuffer contains the last 100 messages, for debuggingerror states.
Document objects store your actual data in the client. They can be modifiedsyncronously and they can automatically sync their data with the server.Document objects can be modified offline - they will send data to the serverwhen the client reconnects.
Normally you will create a document object by callingconnection.get(collection, docname). Destroy the document reference usingdoc.destroy().
Documents start in a dumb, inert state. You have three options to get started:
- Normally, you want to calldoc.subscribe(callback). This will fetch thecurrent data from the server and subscribe the document object to a feed ofchanges from other clients. (If you don't want to be subscribed anymore, calldoc.unsubscribe([callback])).
- If you don't want a live feed of changes, calldoc.fetch(callback) to getthe data from the server. Your local document will be updated automaticallyevery time you submit an operation.
- If you know the document doesn't exist on the server (for example the docname is a new GUID), you can immediately calldoc.create(type, data,callback).
There's a secret 4th option - if you're doing server-side rendering, you caninitialize the document object with bundled data by callingdoc.ingestData({type:..., data:...}).
To call a method when a document has the current server data, pair your call tosubscribe withdoc.whenReady(function() { ... }. Your function will becalled immediately if the document already has data.
Both subscribe and fetch take a callback which will be called when theoperation is complete. In ShareJS 0.8 this callback is being removed - most ofthe time you should call whenReady instead. The semantics are a littledifferent in each case - the subscribe / fetch callbacks are called when theoperation has completed (successfully or unsuccessfully). Its possible for asubscription to fail, but succeed when the client reconnects. On the otherhand, whenReady is called once there's data. It will not be called if there wasan error subscribing.
Once you have data, you should calldoc.getSnapshot() to get it. Note thatthis returns the doc's internal doc object. You should never modify thesnapshot directly - instead call doc.submitOp.
Documents follow thesharejs / livedb objectmodel. All documents sort ofimplicitly exist on the server, but they have no data and no type until you'create' them. So you can subscribe to a document before it has been created onthe server, and a document on the server can be deleted and recreated withoutyou needing a new document reference.
To make changes to a document, you can call one of these three methods:
- doc.create(type, [data], [context], [callback]): Create the document onthe server with the given type and initial data. Type will usually be 'text'or 'json0'. Data specifies initial data for the document. For text documents,this should be an initial string. For JSON documents, this should be JSONstringify-able data. If unspecified, initial data is an empty string or nullfor text and JSON, respectively.
- doc.submitOp(op, [context], [callback]): Submit an operation to thedocument. The operation must be valid for the given OT type of the document.See thetext document OTspec and theJSONdocument OTspec. Consider using acontext instead of calling submitOp directly. (Described below)
- doc.del([context], [callback]): Delete the document on the server. Thedocument reference will become null.
In all cases, thecontext
argument is a user data object which is passed toall event emitters related to this operation. This is designed so data bindingscan easily ignore their own events.
The callback for all editing operations is optional and informational. It willbe called when the operation has been acknowledged by the server.
To be notified when edits happen remotely, register for the 'op' event. (See events section below).
If you want to pause sending operations to the server, calldoc.pause().This is useful if a user wants to edit a document without other people seeingtheir changes. Calldoc.resume() to unpause & send any pending changes tothe server.
The other option to edit documents is to use aDocument editing context.Document contexts are thin wrappers around submitOp which provide two benefits:
- An editing context does not get notified about its own operations, but itdoes get notified about the operations performed by other contexts editingthe same document. This solves the problem that multiple parts of your app maybind to the same document.
- Editing contexts mix in API methods for the OT type of the document. Thismakes it easier to edit the document. Note that the JSON API is currently abit broken, so this is currently only useful for text documents.
Create a context usingcontext = doc.createContext(). Contexts have thefollowing methods & properties:
- context.submitOp(op, callback): Wrapper for
doc.submitOp(op, context, callback)
. - context._onOp = function(op) {...} This is a hook for you / the type APIto add your own logic when operations happen. If you're using the text API,bind tocontext.onInsert = ... andcontext.onRemove = ... instead.
- context.destroy(): Destroy the context. The context will stop gettingmessages.
If you're making a text edit binding, bind to a document context instead ofbinding to the document itself.
In the nodejs tradition, documents are event emitters. They emit the following events:
ready: Emitted when the document has data from the server. Consider usingwhenReady(callback) instead of this event so your function is calledimmediately if the documentalready has data from the server.
subscribe: Emitted when the document is subscribed. This will bere-emitted when the document is resubscribed each time the client reconnects.
unsubscribe: Emitted when the document is unsubscribed. This will bere-emitted whenever the document is unsubscribed due to the client beingdisconnected.
nothing pending: Emitted after sending data to the server, when there areno outstanding operations to send. Pair withhasPending to find out whenthere is outstanding data. This is useful for displaying "Are you sure you wantto close your browser window" messages to the user.
create: Emitted when the document has been created. Called with (context).
del: Emitted when the document has been deleted. The del event is triggered with (context, oldSnapshot).
before op: Emitted right before an operation is applied. Called with (op, context).
op: Emitted right after each part of an operation is applied. Called with(op, context). This is usually called just once, but you can specify
doc.incremental = true
to tell the document to break the operation intosmaller parts and emit them one at a time.after op: Emitted after an operation (all of it) is applied. Called with (op, context).
Operations lock the document. For probably bad reasons, it is illegal to callsubmitOp in the event handlers forcreate,del,before op orop events. If youwant to make changes in response to an operation, register for theafter op orunlock events.
Here's some code to get started editing a text document:
<textareaid='pad'autofocus>Connecting...</textarea><scriptsrc="channel/bcsocket.js"></script><scriptsrc="text.js"></script><scriptsrc="share.js"></script><script>var socket = new BCSocket(null, {reconnect: true});var sjs = new sharejs.Connection(socket);var doc = sjs.get('docs', 'hello');// Subscribe to changesdoc.subscribe();// This will be called when we have a live copy of the server's data.doc.whenReady(function() { console.log('doc ready, data: ', doc.getSnapshot()); // Create a JSON document with value x:5 if (!doc.type) doc.create('text'); doc.attachTextarea(document.getElementById('pad'));});
And a JSON document:
varsocket= ...;varsjs=newsharejs.Connection(socket);vardoc=sjs.get('users','seph');// Subscribe to changesdoc.subscribe();// This will be called when we have a live copy of the server's data.doc.whenReady(function(){console.log('doc ready, data: ',doc.getSnapshot());// Create a JSON document with value x:5if(!doc.type)doc.create('json0',{x:5});});// later, add 10 to the doc.snapshot.x propertydoc.submitOp([{p:['x'],na:10}]);
See theexamples directory for more examples.
ShareJS is proudly licensed under theMIT license.
About
Collaborative editing in any app