• Stars
    star
    4,929
  • Rank 8,515 (Top 0.2 %)
  • Language
    JavaScript
  • License
    Other
  • Created over 13 years ago
  • Updated about 8 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Collaborative editing in any app

NOTE: ShareJS is now ShareDB. See here and here for more information.

.

.

.

ShareJS

Join the chat at https://gitter.im/share/ShareJS

This is a little server & client library to allow concurrent editing of any kind of content via OT. The server runs on NodeJS and the client works in NodeJS or a web browser.

ShareJS currently supports operational transform on plain-text and arbitrary JSON data.

Visit Google groups for discussions and announcements

Immerse yourself in API Documentation.

Build Status

Browser support

ShareJS should work with all browsers, down to IE5.5 (although IE support hasn't been tested with the new version).

That said, I only test regularly with FF, Safari and Chrome, and occasionally with IE8+. File bug reports if you have issues

Installing and running

# 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 love some pull requests!

ShareJS depends on LiveDB for its database backend & data model. Read the livedb readme for information on how to configure your database.

Run the tests:

# npm install
# mocha

Server API

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 using the livedb inmemory store. For more options, see the livedb api.
  • Decide how your client and server will communicate. The easiest solution is to use browserchannel.

To create a ShareJS server instance:

var livedb = require('livedb');
var sharejs = require('share');

var backend = livedb.client(livedb.memory());
var share = require('share').server.createClient({backend: backend});

The method is called createClient because its sort of a client of the database... 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 with a client and use share.listen(stream) to hand control of the stream to sharejs. See the section below on client server communication for an example of this.
  • share.rest() returns a connect/express router which exposes the sharejs REST API. This code is in the process of moving to its own repo. In the meantime, the documentation is here
  • You can intercept requests to the livedb backend to do access control using sharejs middleware. share.use(method, function(action, callback){...}) will make your function intercept & potentially rewrite requests. This is not currently documented, but when it is, the documentation will live here.

Client server communication

ShareJS requires you to provide a way for the client to communicate with the server. As such, its transport agnostic. You can use browserchannel, websockets, or whatever you like. ShareJS requires 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 information related to that client. When the client client reconnects, it will reestablish all its state on the server again.

It is the responsibility of the transport to handle reconnection - the client should emit state change events to tell sharejs that it has reconnected.

Server communication

The server exposes a method share.listen(stream) which you can call with a node stream which can communicate with the client.

Here's an example using browserchannel:

var Duplex = require('stream').Duplex;
var browserChannel = require('browserchannel').server

var share = require('share').server.createClient({backend: ...});
var app = require('express')();

app.use(browserChannel({webserver: webserver}, function(client) {
  var stream = new Duplex({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 sharejs
  return share.listen(stream);
}));

And here is a more complete example using websockets.

Client communication

The client needs a websocket-like session object to communicate. You can use a normal websocket if you want:

var ws = new WebSocket('ws://' + window.location.host);
var share = new sharejs.Connection(ws);

Sharejs also supports the following changes from the spec:

  • The socket can reconnect. Simply call socket.onopen again when the socket reconnects and sharejs will reestablish its session state and send any outstanding 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 avoid extra JSON stringifying.

If you use browserchannel, all of this is done for you. Simply tell browserchannel to reconnect and it'll take care of everything:

var socket = new BCSocket(null, {reconnect: true});
var share = new sharejs.Connection(socket);

Client API

The client API can be used either from nodejs or from a browser.

From the server:

var connection = require('share').client.Connection(socket);

From the browser, you'll need to first include the sharejs library. You can use browserify and require('share').client or include the script directly.

The browser library is built to the node_modules/share/webclient directory when you install sharejs. This path is exposed programatically at require('share').scriptsDir. You can add this to your express app:

var sharejs = require('share');
app.use(express.static(sharejs.scriptsDir));

Then in your web app include whichever OT types you need in your app and sharejs:

<script src="text.js"></script>
<script src="json0.js"></script>
<script src="share.js"></script>

This will create a global sharejs object in the browser.

Connections

The client exposes 2 classes you care about:

  • The Connection class wraps a socket and handles the communication to the sharejs server. You use the connection instance to create document references in the client.
  • All actual data you edit will be wrapped by the Doc class. The document class stores an in-memory copy of the document data with your local edits applied. Create a document instance by calling connection.get('collection', 'docname').

ShareJS also allows you to make queries to your database. Live-bound queries will return a Query object. These are not currently documented.

To get started, you first need to create a connection:

var sjs = new sharejs.Connection(socket);

The socket must be a websocket-like object. See the section on client server communication 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 named document on the server. This function returns the same document reference each time you call connection.get(). collection and docname 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 a Query object via the callback.

The best documentation for these functions is in a block comment in the code.

For debugging, connections have 2 additional properties:

  • Set connection.debug = true to console.log out all messages sent and recieved over the wire.
  • connection.messageBuffer contains the last 100 messages, for debugging error states.

Documents

Document objects store your actual data in the client. They can be modified syncronously and they can automatically sync their data with the server. Document objects can be modified offline - they will send data to the server when the client reconnects.

Normally you will create a document object by calling connection.get(collection, docname). Destroy the document reference using doc.destroy().

Documents start in a dumb, inert state. You have three options to get started:

  • Normally, you want to call doc.subscribe(callback). This will fetch the current data from the server and subscribe the document object to a feed of changes from other clients. (If you don't want to be subscribed anymore, call doc.unsubscribe([callback])).
  • If you don't want a live feed of changes, call doc.fetch(callback) to get the data from the server. Your local document will be updated automatically every time you submit an operation.
  • If you know the document doesn't exist on the server (for example the doc name is a new GUID), you can immediately call doc.create(type, data, callback).

There's a secret 4th option - if you're doing server-side rendering, you can initialize the document object with bundled data by calling doc.ingestData({type:..., data:...}).

To call a method when a document has the current server data, pair your call to subscribe with doc.whenReady(function() { ... }. Your function will be called immediately if the document already has data.

Both subscribe and fetch take a callback which will be called when the operation is complete. In ShareJS 0.8 this callback is being removed - most of the time you should call whenReady instead. The semantics are a little different in each case - the subscribe / fetch callbacks are called when the operation has completed (successfully or unsuccessfully). Its possible for a subscription to fail, but succeed when the client reconnects. On the other hand, whenReady is called once there's data. It will not be called if there was an error subscribing.

Once you have data, you should call doc.getSnapshot() to get it. Note that this returns the doc's internal doc object. You should never modify the snapshot directly - instead call doc.submitOp.

Editing documents

Documents follow the sharejs / livedb object model. All documents sort of implicitly 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 on the server, and a document on the server can be deleted and recreated without you 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 on the 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 JSON stringify-able data. If unspecified, initial data is an empty string or null for text and JSON, respectively.
  • doc.submitOp(op, [context], [callback]): Submit an operation to the document. The operation must be valid for the given OT type of the document. See the text document OT spec and the JSON document OT spec. Consider using a context instead of calling submitOp directly. (Described below)
  • doc.del([context], [callback]): Delete the document on the server. The document reference will become null.

In all cases, the context argument is a user data object which is passed to all event emitters related to this operation. This is designed so data bindings can easily ignore their own events.

The callback for all editing operations is optional and informational. It will be 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, call doc.pause(). This is useful if a user wants to edit a document without other people seeing their changes. Call doc.resume() to unpause & send any pending changes to the server.

Editing Contexts

The other option to edit documents is to use a Document editing context. Document contexts are thin wrappers around submitOp which provide two benefits:

  1. An editing context does not get notified about its own operations, but it does get notified about the operations performed by other contexts editing the same document. This solves the problem that multiple parts of your app may bind to the same document.
  2. Editing contexts mix in API methods for the OT type of the document. This makes it easier to edit the document. Note that the JSON API is currently a bit broken, so this is currently only useful for text documents.

Create a context using context = doc.createContext(). Contexts have the following 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 API to add your own logic when operations happen. If you're using the text API, bind to context.onInsert = ... and context.onRemove = ... instead.
  • context.destroy(): Destroy the context. The context will stop getting messages.

If you're making a text edit binding, bind to a document context instead of binding to the document itself.

Document events

In the nodejs tradition, documents are event emitters. They emit the following events:

  • ready: Emitted when the document has data from the server. Consider using whenReady(callback) instead of this event so your function is called immediately if the document already has data from the server.

  • subscribe: Emitted when the document is subscribed. This will be re-emitted when the document is resubscribed each time the client reconnects.

  • unsubscribe: Emitted when the document is unsubscribed. This will be re-emitted whenever the document is unsubscribed due to the client being disconnected.

  • nothing pending: Emitted after sending data to the server, when there are no outstanding operations to send. Pair with hasPending to find out when there is outstanding data. This is useful for displaying "Are you sure you want to 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 into smaller 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 call submitOp in the event handlers for create, del, before op or op events. If you want to make changes in response to an operation, register for the after op or unlock events.

Examples

Here's some code to get started editing a text document:

<textarea id='pad' autofocus>Connecting...</textarea>
<script src="channel/bcsocket.js"></script>
<script src="text.js"></script>
<script src="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 changes
doc.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:

var socket = ...;
var sjs = new sharejs.Connection(socket);

var doc = sjs.get('users', 'seph');

// Subscribe to changes
doc.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('json0', {x:5});
});

// later, add 10 to the doc.snapshot.x property
doc.submitOp([{p:['x'], na:10}]);

See the examples directory for more examples.


License

ShareJS is proudly licensed under the MIT license.

More Repositories

1

noisejs

Javascript 2D Perlin & Simplex noise functions
JavaScript
1,569
star
2

diamond-types

The world's fastest CRDT. WIP.
Rust
1,194
star
3

Chipmunk-js

Port of slembcke/Chipmunk-Physics to Javascript
JavaScript
537
star
4

node-browserchannel

An implementation of a google browserchannel server in node.js
CoffeeScript
287
star
5

librope

UTF-8 rope library for C
C
254
star
6

statecraft

Manage state with finesse
TypeScript
177
star
7

sephsplace

My own version of r/place, done in a weekend
JavaScript
130
star
8

jumprope-rs

Rust
104
star
9

reference-crdts

Simple, tiny spec-compliant reference implementations of Yjs and Automerge's list types.
TypeScript
98
star
10

jumprope

Fast string editing in Javascript using skip lists
JavaScript
86
star
11

steamdance

A multiplayer steam-based CPU simulator
JavaScript
45
star
12

editing-traces

Real world text editing traces for benchmarking CRDT and Rope data structures
JavaScript
39
star
13

schemaboi

Mergable, efficient data schema for intercompatible apps
TypeScript
33
star
14

braid-protocol

TypeScript
28
star
15

textot.rs

Text operational transform library, for rust. Compatible with libot, ottypes/text.
Rust
26
star
16

sharedb

ShareJS server, in C.
C
24
star
17

appstate

CoffeeScript
24
star
18

replica

Local first application platform built using CRDTs
TypeScript
18
star
19

wangjs

A javascript Wang tile implementation
CoffeeScript
17
star
20

eg-walker-reference

Simple reimplementation of the diamond types sequence CRDT in simple, pure, unoptimized typescript.
TypeScript
16
star
21

mail-viewer

Pure browser email viewer
JavaScript
15
star
22

diamond-js

Javascript wrapper bindings for diamond types
14
star
23

fdb-tuple

Pure javascript FoundationDB tuple encoder and decoder
TypeScript
12
star
24

node-timerstub

Stubbed out timer objects for fast unit testing in nodejs
CoffeeScript
11
star
25

gmail-jmap

Simple JMAP wrapper for gmail
TypeScript
10
star
26

braid-db

Simple database prototype
Rust
10
star
27

unicount

JavaScript
9
star
28

react-static-example

JavaScript
9
star
29

mime-to-jmap

Tools to convert RFC2822 email messages to JMAP
C
9
star
30

unsplash

JavaScript
8
star
31

egwalker-paper

Eg-walker paper, experiments and data.
Rust
8
star
32

tanksalot

CoffeeScript
7
star
33

lightwave

Fork of Torben Weis's lightwave experimental branch - http://code.google.com/p/lightwave/
Go
7
star
34

TP2

An implementation of OT for text which supports TP2 (for p2p editing)
CoffeeScript
6
star
35

text-crdt-rust2

Rust
6
star
36

miniohm

JavaScript
6
star
37

simple-crdt-text

Simple typescript reference implementation for a simple RGA based plain text CRDT
TypeScript
6
star
38

axidraw

Python
5
star
39

livedb-foundation

CoffeeScript
5
star
40

dt-simple-wiki

World's simplest wiki on top of diamond types
JavaScript
5
star
41

crdt-examples

CRDT examples from a DWEB talk
JavaScript
5
star
42

glassbeadtimer

Svelte
4
star
43

killdinohitler

Stealth jam game
CoffeeScript
4
star
44

lovehot

Lua
4
star
45

keykitten

Stores your public key.
4
star
46

ARTIST

Michael Brough and Andi McClure's 10 second artist game, allowing a starting image
Lua
4
star
47

skiplistrs

Rust
4
star
48

map2

Tuple map type for javascript (a,b) -> x
TypeScript
3
star
49

gossip

Simple proof-of-concept gossip protocol for CRDTs
TypeScript
3
star
50

tp2stuff

CoffeeScript
3
star
51

set2

Tuple set type for javascript
JavaScript
3
star
52

resolvable

TypeScript
2
star
53

libot-swift

Swift
2
star
54

roboname

A simple robot-themed name generator for nodejs
CoffeeScript
2
star
55

braid-explorer

UI experiment with braid for data first software
TypeScript
2
star
56

space

Spaaaaaaaace
JavaScript
2
star
57

robot-party

Robots run robots.
CoffeeScript
2
star
58

streamtoiter

TypeScript
2
star
59

Attila

a little game prototype
CoffeeScript
2
star
60

wave-prototype

Simple prototype of a wave-like email service
TypeScript
2
star
61

braid-kernel

TypeScript
2
star
62

canvasproxy

CoffeeScript
2
star
63

space-server

C
2
star
64

census-couch

A simple couchapp site for exploring census data. Way incomplete.
JavaScript
1
star
65

state-client

TypeScript
1
star
66

Clickboss

Click on the boss.
CoffeeScript
1
star
67

FactoryFactoryImpl

The code from my minecraft machine that makes anything
1
star
68

braid-cli

JavaScript
1
star
69

mars2

JavaScript
1
star
70

boilerplate-sim

Boilerplate simulator reference implementation
CoffeeScript
1
star
71

ietf-sync-prototype

JavaScript
1
star
72

boilerplate-gif

CoffeeScript
1
star
73

mtgox

CoffeeScript
1
star
74

boilerplate-jit

CoffeeScript
1
star
75

raider

Simple ECS-style game prototype for multiplayer dungeon raid type things. https://home.seph.codes/public/game/
Rust
1
star
76

state-server

TypeScript
1
star
77

Cocontroller

Userland USB controller for XBox360 controllers on a mac.
Objective-C
1
star
78

boilerplate-compiler

CoffeeScript
1
star
79

visbindiff

CoffeeScript
1
star
80

rle-utils

Javascript utilities for internal run-length encoding
TypeScript
1
star
81

mars

CSS
1
star
82

boilerbot

CoffeeScript
1
star