diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3b281f2f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..bffb2067 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,17 @@ +{ + "env": { + "browser": true, + "node": true + }, + "rules": { + "eqeqeq": [2, "smart"], + "strict": [2, "global"], + "camelcase": 0, + "no-underscore-dangle": 0, + "quotes": [2, "single"], + "curly": [2, "multi-line"], + "no-unused-vars": [2, {"vars": "all", "args": "none"}], + "no-shadow": 0, + "consistent-return": 0 + } +} diff --git a/.gitignore b/.gitignore index 48819a20..288e9a92 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules dist/* coverage +npm-debug.log diff --git a/.travis.yml b/.travis.yml index 089b3f24..87492300 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,13 @@ language: node_js node_js: - - 0.10 + - "io.js" + - "0.12" + - "0.10" + +script: + - npm run lint + - npm test services: - redis-server - diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..4ead0e1d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,13 @@ +# Contributor Code of Conduct + +As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. + +Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. + +This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..d3284277 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +Contributions are always welcome, no matter how large or small. Before +contributing, please read the +[code of conduct](CODE_OF_CONDUCT.md). + +## Developing + +#### Setup + +```sh +$ git clone https://github.com/share/ShareJS +$ cd ShareJS +$ npm install +``` + +#### Running tests + +You can run tests via: + +```sh +$ npm test +``` + +#### Workflow + +* Fork the repository +* Clone your fork and change directory to it (`git clone git@github.com:yourUserName/ShareJS.git && cd ShareJS`) +* Install the project dependencies (`npm install`) +* Link your forked clone (`npm link`) +* Develop your changes ensuring you're fetching updates from upstream often +* Ensure the test are passing (`npm test`) +* Create new pull request explaining your proposed change or reference an issue in your commit message + +#### Code Standards + +Run linting via `npm run lint`. diff --git a/README.md b/README.md index 12b7632d..d1bd907a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ ShareJS ======= +[![Build Status](https://secure.travis-ci.org/share/ShareJS.svg)](http://travis-ci.org/share/ShareJS) [![Code Climate](https://codeclimate.com/github/share/ShareJS/badges/gpa.svg)](https://codeclimate.com/github/share/ShareJS) [![Dependency Status](https://david-dm.org/share/sharejs.svg)](https://david-dm.org/share/sharejs) [![devDependency Status](https://david-dm.org/share/sharejs/dev-status.svg)](https://david-dm.org/share/sharejs#info=devDependencies) + + 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. @@ -11,9 +14,7 @@ ShareJS currently supports operational transform on plain-text and arbitrary JSO **Check out the [live interactive demos](http://sharejs.org/).** -**Immerse yourself in [API Documentation](https://github.com/josephg/ShareJS/wiki).** - -[![Build Status](https://secure.travis-ci.org/share/ShareJS.png)](http://travis-ci.org/share/ShareJS) +**Immerse yourself in [API Documentation](https://github.com/share/ShareJS/wiki).** Browser support @@ -424,7 +425,7 @@ 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')); @@ -445,7 +446,7 @@ 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}); }); @@ -462,4 +463,3 @@ See the [examples directory](https://github.com/share/ShareJS/tree/master/exampl # License ShareJS is proudly licensed under the [MIT license](LICENSE). - diff --git a/lib/client/connection.js b/lib/client/connection.js index 322105c5..84e3fd95 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -1,3 +1,5 @@ +'use strict'; + var Doc = require('./doc').Doc; var Query = require('./query').Query; var emitter = require('./emitter'); @@ -60,7 +62,7 @@ var Connection = exports.Connection = function (socket) { this.messageBuffer = []; this.bindToSocket(socket); -} +}; emitter.mixin(Connection); @@ -83,10 +85,10 @@ emitter.mixin(Connection); */ Connection.prototype.bindToSocket = function(socket) { if (this.socket) { - delete this.socket.onopen - delete this.socket.onclose - delete this.socket.onmessage - delete this.socket.onerror + delete this.socket.onopen; + delete this.socket.onclose; + delete this.socket.onmessage; + delete this.socket.onerror; } // TODO: Check that the socket is in the 'connecting' state. @@ -98,7 +100,7 @@ Connection.prototype.bindToSocket = function(socket) { this.canSend = this.state === 'connecting' && socket.canSendWhileConnecting; this._setupRetry(); - var connection = this + var connection = this; socket.onmessage = function(msg) { var data = msg.data; @@ -114,7 +116,7 @@ Connection.prototype.bindToSocket = function(socket) { connection.messageBuffer.push({ t: (new Date()).toTimeString(), - recv:JSON.stringify(data) + recv: JSON.stringify(data) }); while (connection.messageBuffer.length > 100) { connection.messageBuffer.shift(); @@ -128,7 +130,7 @@ Connection.prototype.bindToSocket = function(socket) { // in infinite reconnection bugs. throw e; } - } + }; socket.onopen = function() { connection._setState('connecting'); @@ -189,9 +191,9 @@ Connection.prototype.handleMessage = function(msg) { break; } - var msg = result[cName][docName]; - if (typeof msg === 'object') { - doc._handleSubscribe(msg.error, msg); + var localMsg = result[cName][docName]; + if (typeof localMsg === 'object') { + doc._handleSubscribe(localMsg.error, localMsg); } else { // The msg will be true if we simply resubscribed. doc._handleSubscribe(null, null); @@ -203,17 +205,17 @@ Connection.prototype.handleMessage = function(msg) { default: // Document message. Pull out the referenced document and forward the // message. - var collection, docName, doc; + var collection, dName; if (msg.d) { collection = this._lastReceivedCollection = msg.c; - docName = this._lastReceivedDoc = msg.d; + dName = this._lastReceivedDoc = msg.d; } else { collection = msg.c = this._lastReceivedCollection; - docName = msg.d = this._lastReceivedDoc; + dName = msg.d = this._lastReceivedDoc; } - var doc = this.getExisting(collection, docName); - if (doc) doc._onMessage(msg); + var d = this.getExisting(collection, dName); + if (d) d._onMessage(msg); } }; @@ -256,7 +258,7 @@ Connection.prototype._setState = function(newState, data) { // 'connected' from anywhere other than 'connecting'. if ((newState === 'connecting' && (this.state !== 'disconnected' && this.state !== 'stopped')) || (newState === 'connected' && this.state !== 'connecting')) { - throw new Error("Cannot transition directly from " + this.state + " to " + newState); + throw new Error('Cannot transition directly from ' + this.state + ' to ' + newState); } this.state = newState; @@ -301,8 +303,8 @@ Connection.prototype._setState = function(newState, data) { // originally sent. If we don't sort, an op with a high sequence number will // convince the server not to accept any ops with earlier sequence numbers. this.opQueue.sort(function(a, b) { return a.seq - b.seq; }); - for (var i = 0; i < this.opQueue.length; i++) { - this.send(this.opQueue[i]); + for (var j = 0; j < this.opQueue.length; j++) { + this.send(this.opQueue[j]); } this.opQueue = null; @@ -327,10 +329,15 @@ Connection.prototype.bsStart = function() { this.subscribeData = {}; }; +function hasKeys(object) { + for (var key in object) return true; // eslint-disable-line no-unused-vars + return false; +} + Connection.prototype.bsEnd = function() { // Only send bulk subscribe if not empty if (hasKeys(this.subscribeData)) { - this.send({a:'bs', s:this.subscribeData}); + this.send({a: 'bs', s: this.subscribeData}); } this.subscribeData = null; }; @@ -346,7 +353,7 @@ Connection.prototype.sendSubscribe = function(collection, name, v) { data[collection][name] = v || null; } else { - var msg = {a:'sub', c:collection, d:name}; + var msg = {a: 'sub', c: collection, d: name}; if (v != null) msg.v = v; this.send(msg); } @@ -357,8 +364,8 @@ Connection.prototype.sendSubscribe = function(collection, name, v) { * Sends a message down the socket */ Connection.prototype.send = function(msg) { - if (this.debug) console.log("SEND", JSON.stringify(msg)); - this.messageBuffer.push({t:Date.now(), send:JSON.stringify(msg)}); + if (this.debug) console.log('SEND', JSON.stringify(msg)); + this.messageBuffer.push({t: Date.now(), send: JSON.stringify(msg)}); while (this.messageBuffer.length > 100) { this.messageBuffer.shift(); } @@ -375,8 +382,9 @@ Connection.prototype.send = function(msg) { } } - if (!this.socket.canSendJSON) + if (!this.socket.canSendJSON) { msg = JSON.stringify(msg); + } this.socket.send(msg); }; @@ -413,20 +421,23 @@ Connection.prototype.getOrCreate = function(collection, name, data) { */ Connection.prototype.get = function(collection, name, data) { var collectionObject = this.collections[collection]; - if (!collectionObject) + if (!collectionObject) { collectionObject = this.collections[collection] = {}; + } var doc = collectionObject[name]; - if (!doc) + if (!doc) { doc = collectionObject[name] = new Doc(this, collection, name); + } // Even if the document isn't new, its possible the document was created // manually and then tried to be re-created with data (suppose a query // returns with data for the document). We should hydrate the document // immediately if we can because the query callback will expect the document // to have data. - if (data && data.data !== undefined && !doc.state) + if (data && data.data !== undefined && !doc.state) { doc.ingestData(data); + } return doc; }; @@ -446,21 +457,17 @@ Connection.prototype._destroyDoc = function(doc) { // Delete the collection container if its empty. This could be a source of // memory leaks if you slowly make a billion collections, which you probably // won't do anyway, but whatever. - if (!hasKeys(collectionObject)) + if (!hasKeys(collectionObject)) { delete this.collections[doc.collection]; -}; - - -function hasKeys(object) { - for (var key in object) return true; - return false; + } }; // Helper for createFetchQuery and createSubscribeQuery, below. Connection.prototype._createQuery = function(type, collection, q, options, callback) { - if (type !== 'fetch' && type !== 'sub') + if (type !== 'fetch' && type !== 'sub') { throw new Error('Invalid query type: ' + type); + } if (!options) options = {}; var id = this.nextQueryId++; diff --git a/lib/client/doc.js b/lib/client/doc.js index a789643b..da1c0cbb 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1,3 +1,5 @@ +'use strict'; + var types = require('../types').ottypes; var emitter = require('./emitter'); @@ -139,7 +141,7 @@ var Doc = exports.Doc = function(connection, collection, name) { // The OT type of this document. // // The document also responds to the api provided by the type - this.type = null + this.type = null; }; emitter.mixin(Doc); @@ -173,7 +175,7 @@ Doc.prototype.destroy = function(callback) { // @param newType OT type provided by the ottypes library or its name or uri Doc.prototype._setType = function(newType) { if (typeof newType === 'string') { - if (!types[newType]) throw new Error("Missing type " + newType + ' ' + this.collection + ' ' + this.name); + if (!types[newType]) throw new Error('Missing type ' + newType + ' ' + this.collection + ' ' + this.name); newType = types[newType]; } this.removeContexts(); @@ -290,6 +292,80 @@ Doc.prototype._finishQuerySubscribe = function(version) { this._finishSub(); }; +// Operations + + +// ************ Dealing with operations. + +// Helper function to set opData to contain a no-op. +var setNoOp = function(opData) { + delete opData.op; + delete opData.create; + delete opData.del; +}; + +var isNoOp = function(opData) { + return !opData.op && !opData.create && !opData.del; +}; + +// Try to compose data2 into data1. Returns truthy if it succeeds, otherwise falsy. +var tryCompose = function(type, data1, data2) { + if (data1.create && data2.del) { + setNoOp(data1); + } else if (data1.create && data2.op) { + // Compose the data into the create data. + var data = (data1.create.data === undefined) ? type.create() : data1.create.data; + data1.create.data = type.apply(data, data2.op); + } else if (isNoOp(data1)) { + data1.create = data2.create; + data1.del = data2.del; + data1.op = data2.op; + } else if (data1.op && data2.op && type.compose) { + data1.op = type.compose(data1.op, data2.op); + } else { + return false; + } + return true; +}; + +// Transform server op data by a client op, and vice versa. Ops are edited in place. +var xf = function(client, server) { + // In this case, we're in for some fun. There are some local operations + // which are totally invalid - either the client continued editing a + // document that someone else deleted or a document was created both on the + // client and on the server. In either case, the local document is way + // invalid and the client's ops are useless. + // + // The client becomes a no-op, and we keep the server op entirely. + if (server.create || server.del) return setNoOp(client); + if (client.create) throw new Error('Invalid state. This is a bug. ' + this.collection + ' ' + this.name); + + // The client has deleted the document while the server edited it. Kill the + // server's op. + if (client.del) return setNoOp(server); + + // We only get here if either the server or client ops are no-op. Carry on, + // nothing to see here. + if (!server.op || !client.op) return; + + // They both edited the document. This is the normal case for this function - + // as in, most of the time we'll end up down here. + // + // You should be wondering why I'm using client.type instead of this.type. + // The reason is, if we get ops at an old version of the document, this.type + // might be undefined or a totally different type. By pinning the type to the + // op data, we make sure the right type has its transform function called. + if (client.type.transformX) { + var result = client.type.transformX(client.op, server.op); + client.op = result[0]; + server.op = result[1]; + } else { + var _c = client.type.transform(client.op, server.op, 'left'); + var _s = client.type.transform(server.op, client.op, 'right'); + client.op = _c; server.op = _s; + } +}; + // This is called by the connection when it receives a message for the document. Doc.prototype._onMessage = function(msg) { if (!(msg.c === this.collection && msg.d === this.name)) { @@ -377,7 +453,7 @@ Doc.prototype._onMessage = function(msg) { // an old version (and not subscribed), when the document matches a // query the query will send the client a snapshot of the document // instead of the operations in between. - console.warn("Client got future operation from the server", + console.warn('Client got future operation from the server', this.collection, this.name, msg); // Get the operations we missed and catch up @@ -464,10 +540,10 @@ Doc.prototype.flush = function() { this._sendOpData(); } else if (this.subscribed && !this.wantSubscribe) { this.action = 'unsubscribe'; - this._send({a:'unsub'}); + this._send({a: 'unsub'}); } else if (!this.subscribed && this.wantSubscribe === 'fetch') { this.action = 'fetch'; - this._send(this.state === 'ready' ? {a:'fetch', v:this.version} : {a:'fetch'}); + this._send(this.state === 'ready' ? {a: 'fetch', v: this.version} : {a: 'fetch'}); } else if (!this.subscribed && this.wantSubscribe) { this.action = 'subscribe'; // Special send method needed for bulk subscribes on reconnect. @@ -490,8 +566,9 @@ Doc.prototype._setWantSubscribe = function(value, callback, err) { } // If we want to subscribe, don't weaken it to a fetch. - if (value !== 'fetch' || this.wantSubscribe !== true) + if (value !== 'fetch' || this.wantSubscribe !== true) { this.wantSubscribe = value; + } if (callback) this._subscribeCallbacks.push(callback); this.flush(); @@ -526,80 +603,6 @@ Doc.prototype._finishSub = function(err) { }; -// Operations - - -// ************ Dealing with operations. - -// Helper function to set opData to contain a no-op. -var setNoOp = function(opData) { - delete opData.op; - delete opData.create; - delete opData.del; -}; - -var isNoOp = function(opData) { - return !opData.op && !opData.create && !opData.del; -} - -// Try to compose data2 into data1. Returns truthy if it succeeds, otherwise falsy. -var tryCompose = function(type, data1, data2) { - if (data1.create && data2.del) { - setNoOp(data1); - } else if (data1.create && data2.op) { - // Compose the data into the create data. - var data = (data1.create.data === undefined) ? type.create() : data1.create.data; - data1.create.data = type.apply(data, data2.op); - } else if (isNoOp(data1)) { - data1.create = data2.create; - data1.del = data2.del; - data1.op = data2.op; - } else if (data1.op && data2.op && type.compose) { - data1.op = type.compose(data1.op, data2.op); - } else { - return false; - } - return true; -}; - -// Transform server op data by a client op, and vice versa. Ops are edited in place. -var xf = function(client, server) { - // In this case, we're in for some fun. There are some local operations - // which are totally invalid - either the client continued editing a - // document that someone else deleted or a document was created both on the - // client and on the server. In either case, the local document is way - // invalid and the client's ops are useless. - // - // The client becomes a no-op, and we keep the server op entirely. - if (server.create || server.del) return setNoOp(client); - if (client.create) throw new Error('Invalid state. This is a bug. ' + this.collection + ' ' + this.name); - - // The client has deleted the document while the server edited it. Kill the - // server's op. - if (client.del) return setNoOp(server); - - // We only get here if either the server or client ops are no-op. Carry on, - // nothing to see here. - if (!server.op || !client.op) return; - - // They both edited the document. This is the normal case for this function - - // as in, most of the time we'll end up down here. - // - // You should be wondering why I'm using client.type instead of this.type. - // The reason is, if we get ops at an old version of the document, this.type - // might be undefined or a totally different type. By pinning the type to the - // op data, we make sure the right type has its transform function called. - if (client.type.transformX) { - var result = client.type.transformX(client.op, server.op); - client.op = result[0]; - server.op = result[1]; - } else { - var _c = client.type.transform(client.op, server.op, 'left'); - var _s = client.type.transform(server.op, client.op, 'right'); - client.op = _c; server.op = _s; - } -}; - /** * Applies the operation to the snapshot * @@ -644,7 +647,7 @@ Doc.prototype._otApply = function(opData, context) { // to store any extra data. (text-tp2 has this constraint.) for (var i = 0; i < this.editingContexts.length; i++) { var c = this.editingContexts[i]; - if (c != context && c._beforeOp) c._beforeOp(opData.op); + if (c !== context && c._beforeOp) c._beforeOp(opData.op); } this.emit('before op', op, context); @@ -681,12 +684,12 @@ Doc.prototype._otApply = function(opData, context) { // Notify all the contexts about the op (well, all the contexts except // the one which initiated the submit in the first place). // NOTE Handle this with events? - for (var i = 0; i < contexts.length; i++) { - var c = contexts[i]; - if (c != context && c._onOp) c._onOp(opData.op); + for (var j = 0; j < contexts.length; j++) { + var kontext = contexts[j]; + if (kontext !== context && kontext._onOp) kontext._onOp(opData.op); } - for (var i = 0; i < contexts.length; i++) { - if (contexts[i].remove) contexts.splice(i--, 1); + for (var k = 0; k < contexts.length; k++) { + if (contexts[k].remove) contexts.splice(k--, 1); } return this.emit('after op', opData.op, context); @@ -757,7 +760,7 @@ Doc.prototype._submitOpData = function(opData, context, callback) { }; if (this.locked) { - return error("Cannot call submitOp from inside an 'op' event handler. " + this.collection + ' ' + this.name); + return error('Cannot call submitOp from inside an \'op\' event handler. ' + this.collection + ' ' + this.name); } // The opData contains either op, create, delete, or none of the above (a no-op). @@ -827,10 +830,10 @@ Doc.prototype.create = function(type, data, context, callback) { data = undefined; } - var op = {create: {type:type, data:data}}; + var op = {create: {type: type, data: data}}; if (this.type) { if (callback) callback('Document already exists', this._opErrorContext(op)); - return + return; } this._submitOpData(op, context, callback); @@ -881,10 +884,8 @@ Doc.prototype._tryRollback = function(opData) { this._setType(null); // I don't think its possible to get here if we aren't in a floating state. - if (this.state === 'floating') - this.state = null; - else - console.warn('Rollback a create from state ' + this.state); + if (this.state === 'floating') this.state = null; + else console.warn('Rollback a create from state ' + this.state); } else if (opData.op && opData.type.invert) { opData.op = opData.type.invert(opData.op); @@ -907,7 +908,7 @@ Doc.prototype._tryRollback = function(opData) { this.version = null; this.state = null; this.subscribed = false; - this.emit('error', "Op apply failed and the operation could not be reverted"); + this.emit('error', 'Op apply failed and the operation could not be reverted'); // Trigger a fetch. In our invalid state, we can't really do anything. this.fetch(); @@ -1008,7 +1009,7 @@ Doc.prototype.createContext = function() { // This is dangerous, but really really useful for debugging. I hope people // don't depend on it. - _doc: this, + _doc: this }; if (type.api) { diff --git a/lib/client/emitter.js b/lib/client/emitter.js index 2b7827cc..cb39e002 100644 --- a/lib/client/emitter.js +++ b/lib/client/emitter.js @@ -1,3 +1,5 @@ +'use strict'; + var EventEmitter = require('events').EventEmitter; exports.EventEmitter = EventEmitter; diff --git a/lib/client/index.js b/lib/client/index.js index 879fb1f7..019f4ee6 100644 --- a/lib/client/index.js +++ b/lib/client/index.js @@ -1,3 +1,5 @@ +'use strict'; + // Entry point for the client // // Usage: diff --git a/lib/client/query.js b/lib/client/query.js index 359bba14..23bac878 100644 --- a/lib/client/query.js +++ b/lib/client/query.js @@ -1,3 +1,5 @@ +'use strict'; + var emitter = require('./emitter'); // Queries are live requests to the database for particular sets of fields. @@ -14,7 +16,7 @@ var Query = exports.Query = function(type, connection, id, collection, query, op this.id = id; this.collection = collection; - // The query itself. For mongo, this should look something like {"data.x":5} + // The query itself. For mongo, this should look something like {'data.x':5} this.query = query; // Resultant document action for the server. Fetch mode will automatically @@ -75,7 +77,7 @@ Query.prototype._execute = function() { id: this.id, c: this.collection, o: {}, - q: this.query, + q: this.query }; if (this.docMode) { @@ -117,7 +119,7 @@ Query.prototype._dataToDocs = function(data) { // destroying it. Query.prototype.destroy = function() { if (this.connection.canSend && this.type === 'sub') { - this.connection.send({a:'qunsub', id:this.id}); + this.connection.send({a: 'qunsub', id: this.id}); } this.connection._destroyQuery(this); @@ -155,12 +157,14 @@ Query.prototype._onMessage = function(msg) { // around setting documents to be subscribed & unsubscribing documents // in event callbacks. for (var i = 0; i < msg.diff.length; i++) { - var d = msg.diff[i]; - if (d.type === 'insert') d.values = this._dataToDocs(d.values); + var diff = msg.diff[i]; + if (diff.type === 'insert') diff.values = this._dataToDocs(diff.values); } - for (var i = 0; i < msg.diff.length; i++) { - var d = msg.diff[i]; + for (var j = 0; j < msg.diff.length; j++) { + var d = msg.diff[j]; + var howMany; + switch (d.type) { case 'insert': var newDocs = d.values; @@ -168,12 +172,12 @@ Query.prototype._onMessage = function(msg) { this.emit('insert', newDocs, d.index); break; case 'remove': - var howMany = d.howMany || 1; + howMany = d.howMany || 1; var removed = this.results.splice(d.index, howMany); this.emit('remove', removed, d.index); break; case 'move': - var howMany = d.howMany || 1; + howMany = d.howMany || 1; var docs = this.results.splice(d.from, howMany); Array.prototype.splice.apply(this.results, [d.to, 0].concat(docs)); this.emit('move', docs, d.from, d.to); @@ -215,7 +219,7 @@ Query.prototype.setQuery = function(q) { this.query = q; if (this.connection.canSend) { // There's no 'change' message to send to the server. Just resubscribe. - this.connection.send({a:'qunsub', id:this.id}); + this.connection.send({a: 'qunsub', id: this.id}); this._execute(); } }; diff --git a/lib/client/textarea.js b/lib/client/textarea.js index e3649d88..892e07a2 100644 --- a/lib/client/textarea.js +++ b/lib/client/textarea.js @@ -1,3 +1,5 @@ +'use strict'; + /* This contains the textarea binding for ShareJS. This binding is really * simple, and a bit slow on big documents (Its O(N). However, it requires no * changes to the DOM and no heavy libraries like ace. It works for any kind of @@ -8,7 +10,7 @@ * heavier. */ - var Doc = require('./doc').Doc; +var Doc = require('./doc').Doc; /* applyChange creates the edits to convert oldval -> newval. * diff --git a/lib/index.js b/lib/index.js index d216ff4b..44779313 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,5 @@ +'use strict'; + exports.server = require('./server'); exports.client = require('./client'); diff --git a/lib/server/index.js b/lib/server/index.js index 50c4c43d..64088552 100644 --- a/lib/server/index.js +++ b/lib/server/index.js @@ -1,3 +1,5 @@ +'use strict'; + var Session = require('./session'); var UserAgent = require('./useragent'); var livedb = require('livedb'); @@ -17,11 +19,11 @@ var ShareInstance = function(options) { } else if (options.db) { this.backend = livedb.client(options.db); } else { - throw Error("Both options.backend and options.db are missing. Can't function without a database!"); + throw Error('Both options.backend and options.db are missing. Can\'t function without a database!'); } // Map from event name (or '') to a list of middleware. - this.extensions = {'':[]}; + this.extensions = {'': []}; this.docFilters = []; this.opFilters = []; }; @@ -61,7 +63,7 @@ ShareInstance.prototype.use = function(action, middleware) { } if (action === 'getOps') { - throw new Error("The 'getOps' middleware action has been renamed to 'get ops'. Update your code."); + throw new Error('The \'getOps\' middleware action has been renamed to \'get ops\'. Update your code.'); } var extensions = this.extensions[action]; @@ -107,8 +109,9 @@ ShareInstance.prototype._trigger = function(request, callback) { var middlewares = (this.extensions[request.action] || []).concat(this.extensions['']); var next = function() { - if (!middlewares.length) + if (!middlewares.length) { return callback ? callback(null, request) : undefined; + } var middleware = middlewares.shift(); middleware(request, function(err) { @@ -124,4 +127,3 @@ ShareInstance.prototype._trigger = function(request, callback) { exports.createClient = function(options) { return new ShareInstance(options); }; - diff --git a/lib/server/rest.js b/lib/server/rest.js index 46e0c000..e5451ea6 100644 --- a/lib/server/rest.js +++ b/lib/server/rest.js @@ -1,3 +1,5 @@ +'use strict'; + // This implements ShareJS's REST API. var Router = require('express').Router; @@ -31,24 +33,24 @@ var send409 = function(res, message) { var sendError = function(res, message, head) { if (message === 'forbidden') { if (head) { - send403(res, ""); + send403(res, ''); } else { send403(res); } } else if (message === 'Document created remotely') { if (head) { - send409(res, ""); + send409(res, ''); } else { send409(res, message + '\n'); } } else { - //console.warn("REST server does not know how to send error:", message); + //console.warn('REST server does not know how to send error:', message); if (head) { res.writeHead(500, {}); - res.end(""); + res.end(''); } else { res.writeHead(500, {'Content-Type': 'text/plain'}); - res.end("Error: " + message + "\n"); + res.end('Error: ' + message + '\n'); } } }; @@ -59,7 +61,7 @@ var send400 = function(res, message) { }; var send200 = function(res, message) { - if (message == null) message = "OK\n"; + if (message == null) message = 'OK\n'; res.writeHead(200, {'Content-Type': 'text/plain'}); res.end(message); @@ -70,6 +72,18 @@ var sendJSON = function(res, obj) { res.end(JSON.stringify(obj) + '\n'); }; +var pump = function(req, callback) { + // Currently using the old streams API.. + var data = ''; + req.on('data', function(chunk) { + data += chunk; + return data; + }); + return req.on('end', function() { + return callback(data); + }); +}; + // Expect the request to contain JSON data. Read all the data and try to JSON // parse it. var expectJSONObject = function(req, res, callback) { @@ -86,17 +100,6 @@ var expectJSONObject = function(req, res, callback) { }); }; -var pump = function(req, callback) { - // Currently using the old streams API.. - var data = ''; - req.on('data', function(chunk) { - return data += chunk; - }); - return req.on('end', function() { - return callback(data); - }); -}; - // ***** Actual logic @@ -115,13 +118,13 @@ module.exports = function(share) { next(); }; - + // GET returns the document snapshot. The version and type are sent as headers. // I'm not sure what to do with document metadata - it is inaccessable for now. router.get('/:cName/:docName', auth, function(req, res, next) { req._shareAgent.fetch(req.params.cName, req.params.docName, function(err, doc) { if (err) { - if (req.method === "HEAD") { + if (req.method === 'HEAD') { sendError(res, err, true); } else { sendError(res, err); @@ -146,8 +149,8 @@ module.exports = function(share) { } var content; - var query = url.parse(req.url,true).query; - if (query.envelope == 'true') + var query = url.parse(req.url, true).query; + if (query.envelope === 'true') { content = doc; } else { @@ -168,14 +171,12 @@ module.exports = function(share) { var query = url.parse(req.url, true).query; - if (query && query.from) from = parseInt(query.from)|0; - if (query && query.to) to = parseInt(query.to)|0; + if (query && query.from) from = parseInt(query.from) | 0; + if (query && query.to) to = parseInt(query.to) | 0; req._shareAgent.getOps(req.params.cName, req.params.docName, from, to, function(err, ops) { - if (err) - sendError(res, err); - else - sendJSON(res, ops); + if (err) sendError(res, err); + else sendJSON(res, ops); }); }); @@ -187,10 +188,8 @@ module.exports = function(share) { if (err) return sendError(res, err); res.setHeader('X-OT-Version', v); - if (sendOps) - sendJSON(res, ops); - else - send200(res); + if (sendOps) sendJSON(res, ops); + else send200(res); }); }; @@ -200,22 +199,21 @@ module.exports = function(share) { submit(req, res, opData, true); }); }); - + // PUT is used to create a document. The contents are a JSON object with // {type:TYPENAME, data:{initial data} meta:{...}} // PUT {...} is equivalent to POST {create:{...}} router.put('/:cName/:docName', auth, function(req, res, next) { expectJSONObject(req, res, function(create) { - submit(req, res, {create:create}); + submit(req, res, {create: create}); }); }); // DELETE deletes a document. It is equivalent to POST {del:true} router.delete('/:cName/:docName', auth, function(req, res, next) { - submit(req, res, {del:true}); + submit(req, res, {del: true}); }); return router.middleware; }; - diff --git a/lib/server/session.js b/lib/server/session.js index 6093367e..2f6f09bb 100644 --- a/lib/server/session.js +++ b/lib/server/session.js @@ -1,3 +1,7 @@ +'use strict'; + +var hat = require('hat'); + // This implements the network API for ShareJS. // // The wire protocol is speccced out here: @@ -20,15 +24,21 @@ // The wire protocol is documented here: // https://github.com/josephg/ShareJS/wiki/Wire-Protocol -var hat = require('hat'); -var assert = require('assert'); +// Helpers +function hasKeys(object) { + for (var key in object) return true; // eslint-disable-line no-unused-vars + return false; +} + +// Destroy a linked list of streams +function destroyStreams(opstream) { + while (opstream) { + opstream.destroy(); + opstream.removeAllListeners('data'); + opstream = opstream.__previous; + } +} -// stream is a nodejs 0.10 stream object. -/** - * @param {ShareInstance} instance - * @param {Duplex} stream - * @param {Http.Request} req - */ module.exports = Session; /** @@ -74,11 +84,11 @@ function Session(instance, stream) { stream.once('end', this._cleanup.bind(this)); // Initialize the remote client by sending it its session Id. - this._send({a:'init', protocol:0, id:this.agent.sessionId}); + this._send({a: 'init', protocol: 0, id: this.agent.sessionId}); } Session.prototype._cleanup = function() { - if (this.closed) return + if (this.closed) return; this.closed = true; // Remove the pump listener @@ -92,7 +102,7 @@ Session.prototype._cleanup = function() { // process of subscribing the client. if (typeof value === 'object') { destroyStreams(value); - } + } this.collections[c][docName] = false; // cancel the subscribe } } @@ -158,11 +168,13 @@ Session.prototype._subscribeToStream = function(collection, docName, opstream, v this._setSubscribed(collection, docName, opstream, v); var self = this; - // This should use the new streams API instead of the old one. - opstream.on('data', onData); function onData(data) { self._sendOp(collection, docName, data); - }; + } + + // This should use the new streams API instead of the old one. + opstream.on('data', onData); + opstream.once('end', function() { // Livedb has closed the op stream. What do we do here? Normally this // shouldn't happen unless we're cleaning up, so I'll assume thats whats @@ -336,7 +348,7 @@ Session.prototype._sendOp = function(collection, docName, data) { Session.prototype._reply = function(req, err, msg) { if (err) { - msg = {a:req.a, error:err}; + msg = {a: req.a, error: err}; } else { if (!msg.a) msg.a = req.a; } @@ -419,7 +431,7 @@ Session.prototype._processQueryResults = function(collection, results, qopts) { results.forEach(function(r) { var docName = r.docName; - var message = {c:collection, d:docName, v:r.v}; + var message = {c: collection, d: docName, v: r.v}; messages.push(message); if (lastType !== r.type) { @@ -439,7 +451,7 @@ Session.prototype._processQueryResults = function(collection, results, qopts) { // before you get the document's updated data. self.agent.getOps(collection, docName, atVersion, -1, function(err, results) { if (err) { - self._send({a:'fetch', c:collection, d:docName, error:err}); + self._send({a: 'fetch', c: collection, d: docName, error: err}); return; } @@ -512,8 +524,9 @@ Session.prototype._handleMessage = function(req, callback) { if (req.o) { // Do we send back document snapshots for the results? Either 'fetch' or 'sub'. qopts.docMode = req.o.m; - if (qopts.docMode != null && qopts.docMode !== 'fetch' && qopts.docMode !== 'sub') + if (qopts.docMode != null && qopts.docMode !== 'fetch' && qopts.docMode !== 'sub') { return callback('invalid query docmode: ' + qopts.docMode); + } // The client tells us what versions it already has qopts.versions = req.o.vs; @@ -538,7 +551,7 @@ Session.prototype._handleMessage = function(req, callback) { this.lastReceivedDoc = req.d; } else { if (!this.lastReceivedDoc || !this.lastReceivedCollection) { - console.warn("msg.d or collection missing in req " + JSON.stringify(req) + " from " + this.agent.sessionId); + console.warn('msg.d or collection missing in req ' + JSON.stringify(req) + ' from ' + this.agent.sessionId); return callback('collection or docName missing'); } @@ -587,16 +600,14 @@ Session.prototype._handleMessage = function(req, callback) { // yet. We'll send them a snapshot at the most recent version and stream // operations from that version. this._subscribe(collection, docName, req.v, function(err, data) { - if (err) - callback(err); - else - callback(null, {data:data}); + if (err) callback(err); + else callback(null, {data: data}); }); break; case 'bs': this.bulkSubscribe(req.s, function(err, response) { - callback(err, err ? null : {s:response}); + callback(err, err ? null : {s: response}); }); break; @@ -652,8 +663,9 @@ Session.prototype._handleMessage = function(req, callback) { // isn't subscribed, we'll send the ops with the response as if it was // subscribed so the client catches up. if (!self._isSubscribed(collection, docName)) { - for (var i = 0; i < ops.length; i++) + for (var i = 0; i < ops.length; i++) { self._sendOp(collection, docName, ops[i]); + } // Luckily, the op is transformed & etc in place. self._sendOp(collection, docName, opData); } @@ -674,7 +686,7 @@ Session.prototype._handleMessage = function(req, callback) { // until all the documents are subscribed. var data = self._processQueryResults(req.c, results, qopts); - callback(null, {id:qid, data:data, extra:extra}); + callback(null, {id: qid, data: data, extra: extra}); //self._reply(req, null, {id:qid, data:results, extra:extra}); }); break; @@ -709,12 +721,12 @@ Session.prototype._handleMessage = function(req, callback) { // Consider stripping the collection out of the data we send here // if it matches the query's index. - self._send({a:'q', id:qid, diff:diff}); + self._send({a: 'q', id: qid, diff: diff}); }; emitter.onError = function(err) { // Should we destroy the emitter here? - self._send({a:'q', id:qid, error:err}); + self._send({a: 'q', id: qid, error: err}); console.warn('Query ' + index + '.' + JSON.stringify(req.q) + ' emitted an error:', err); emitter.destroy(); delete self.queries[qid]; @@ -757,18 +769,3 @@ Session.prototype.subscribeStats = function() { return stats; }; - -function hasKeys(object) { - for (var key in object) return true; - return false; -} - -// Destroy a linked list of streams -function destroyStreams(opstream) { - while (opstream) { - opstream.destroy(); - opstream.removeAllListeners('data'); - opstream = opstream.__previous; - } -} - diff --git a/lib/server/useragent.js b/lib/server/useragent.js index 2581fc89..2b0a16ff 100644 --- a/lib/server/useragent.js +++ b/lib/server/useragent.js @@ -1,3 +1,5 @@ +'use strict'; + var hat = require('hat'); var TransformStream = require('stream').Transform; var async = require('async'); @@ -41,18 +43,18 @@ var async = require('async'); * filtered with the share instance's `docFilters`. * * instance.filter(function(collection, docName, docData, next) { - * if (docName == "mario") { - * docData.greeting = "It'se me: Mario"; + * if (docName == 'mario') { + * docData.greeting = 'It'se me: Mario'; * next(); * } else { - * next("Document not found!"); + * next('Document not found!'); * } * }); * userAgent.fetch('people', 'mario', function(error, data) { * data.greeting; // It'se me * }); * userAgent.fetch('people', 'peaches', function(error, data) { - * error == "Document not found!"; + * error == 'Document not found!'; * }); * * In a filter `this` is the user agent. @@ -61,7 +63,7 @@ var async = require('async'); * * instance.filterOps(function(collection, docName, opData, next) { * if (opData.op == 'cheat') - * next("Not on my watch!"); + * next('Not on my watch!'); * else * next(); * } @@ -109,19 +111,21 @@ UserAgent.prototype.filterDocs = function(data, callback) { var done = function() { work--; if (work === 0 && callback) callback(null, data); - } + }; + + var filterCb = function(err) { + if (err && callback) { + callback(err); + callback = null; + } + + done(); + }; for (var cName in data) { for (var docName in data[cName]) { work++; - this.filterDoc(cName, docName, data[cName][docName], function(err) { - if (err && callback) { - callback(err); - callback = null; - } - - done(); - }); + this.filterDoc(cName, docName, data[cName][docName], filterCb); } } @@ -199,7 +203,7 @@ UserAgent.prototype.bulkFetch = function(requests, callback) { if (bulkFetchRequestsEmpty(requests)) return callback(null, {}); if (this.instance._hasMiddleware('bulk fetch') || !this.instance._hasMiddleware('fetch')) { - agent.trigger('bulk fetch', null, null, {requests:requests}, function(err, action) { + agent.trigger('bulk fetch', null, null, {requests: requests}, function(err, action) { if (err) return callback(err); requests = action.requests; @@ -225,7 +229,7 @@ UserAgent.prototype.bulkFetch = function(requests, callback) { UserAgent.prototype.getOps = function(collection, docName, start, end, callback) { var agent = this; - agent.trigger('get ops', collection, docName, {start:start, end:end}, function(err, action) { + agent.trigger('get ops', collection, docName, {start: start, end: end}, function(err, action) { if (err) return callback(err); agent.backend.getOps(action.collection, action.docName, start, end, function(err, results) { @@ -250,11 +254,6 @@ OpTransformStream.prototype.destroy = function() { this.stream.destroy(); }; -OpTransformStream.prototype._transform = function(data, encoding, callback) { - var filterOpCallback = getFilterOpCallback(this, callback); - this.agent.filterOp(this.collection, this.docName, data, filterOpCallback); -}; - function getFilterOpCallback(opTransformStream, callback) { return function filterOpCallback(err, data) { opTransformStream.push(err ? {error: err} : data); @@ -262,6 +261,12 @@ function getFilterOpCallback(opTransformStream, callback) { }; } +OpTransformStream.prototype._transform = function(data, encoding, callback) { + var filterOpCallback = getFilterOpCallback(this, callback); + this.agent.filterOp(this.collection, this.docName, data, filterOpCallback); +}; + + /** * Filter the data passed through the stream with `filterOp()` * @@ -303,7 +308,7 @@ UserAgent.prototype.wrapOpStreams = function(streams) { */ UserAgent.prototype.subscribe = function(collection, docName, version, callback) { var agent = this; - agent.trigger('subscribe', collection, docName, {version:version}, function(err, action) { + agent.trigger('subscribe', collection, docName, {version: version}, function(err, action) { if (err) return callback(err); collection = action.collection; docName = action.docName; @@ -367,7 +372,7 @@ UserAgent.prototype.submit = function(collection, docName, opData, options, call } var agent = this; - agent.trigger('submit', collection, docName, {opData: opData, channelPrefix:null}, function(err, action) { + agent.trigger('submit', collection, docName, {opData: opData, channelPrefix: null}, function(err, action) { if (err) return callback(err); collection = action.collection; @@ -415,7 +420,7 @@ UserAgent.prototype._filterQueryResults = function(collection, results, callback UserAgent.prototype.queryFetch = function(collection, query, options, callback) { var agent = this; // Should we emit 'query' or 'query fetch' here? - agent.trigger('query', collection, null, {query:query, fetch:true, options: options}, function(err, action) { + agent.trigger('query', collection, null, {query: query, fetch: true, options: options}, function(err, action) { if (err) return callback(err); collection = action.collection; diff --git a/lib/types/index.js b/lib/types/index.js index ed9c5f33..bba7abaa 100644 --- a/lib/types/index.js +++ b/lib/types/index.js @@ -1,3 +1,4 @@ +'use strict'; exports.ottypes = {}; exports.registerType = function(type) { diff --git a/lib/types/json-api.js b/lib/types/json-api.js index d6e3ae72..9acff9a3 100644 --- a/lib/types/json-api.js +++ b/lib/types/json-api.js @@ -1,3 +1,5 @@ +'use strict'; + // JSON document API for the 'json0' type. var type = require('ot-json0').type; @@ -44,7 +46,7 @@ function pathEquals(p1, p2) { function containsPath(p1, p2) { if (p1.length < p2.length) return false; - return pathEquals( p1.slice(0,p2.length), p2); + return pathEquals( p1.slice(0, p2.length), p2); } // does nothing, used as a default callback @@ -56,11 +58,11 @@ function normalizePath(path) { if (path instanceof Array) { return path; } - if (typeof(path) == "number") { + if (typeof path == 'number') { return [path]; } - // if (typeof(path) == "string") { - // path = path.split("."); + // if (typeof(path) == 'string') { + // path = path.split('.'); // var out = []; // for (var i=0; i 1 && typeof args[args.length-1] !== 'function') { + if (func.length > 1 && typeof args[args.length - 1] !== 'function') { args.push(nullFunction); } @@ -90,7 +92,7 @@ function normalizeArgs(obj, args, func, requiredArgsCount){ args[0] = path_prefix.concat(normalizePath(args[0])); } - return func.apply(obj,args); + return func.apply(obj, args); } @@ -105,8 +107,8 @@ var SubDoc = function(context, path) { SubDoc.prototype._updatePath = function(op){ for (var i = 0; i < op.length; i++) { var c = op[i]; - if(c.lm !== undefined && containsPath(this.path,c.p)){ - var new_path_prefix = c.p.slice(0,c.p.length-1); + if(c.lm !== undefined && containsPath(this.path, c.p)){ + var new_path_prefix = c.p.slice(0, c.p.length - 1); new_path_prefix.push(c.lm); this.path = new_path_prefix.concat(this.path.slice(new_path_prefix.length)); } @@ -114,7 +116,7 @@ SubDoc.prototype._updatePath = function(op){ }; SubDoc.prototype.createContextAt = function() { - var path = 1 <= arguments.length ? Array.prototype.slice.call(arguments, 0) : []; + var path = arguments.length >= 1 ? Array.prototype.slice.call(arguments, 0) : []; return this.context.createContextAt(this.path.concat(depath(path))); }; @@ -228,7 +230,7 @@ type.api = { } else if (xformed.length === 1) { l.path = xformed[0].p; } else { - throw new Error("Bad assumption in json-api: xforming an 'na' op will always result in 0 or 1 components."); + throw new Error('Bad assumption in json-api: xforming an \'na\' op will always result in 0 or 1 components.'); } } @@ -260,35 +262,35 @@ type.api = { }, _addSubDoc: function(subdoc){ - this._subdocs || (this._subdocs = []); + this._subdocs = this._subdocs || []; this._subdocs.push(subdoc); }, _removeSubDoc: function(subdoc){ - this._subdocs || (this._subdocs = []); + this._subdocs = this._subdocs || []; for(var i = 0; i < this._subdocs.length; i++){ - if(this._subdocs[i] === subdoc) this._subdocs.splice(i,1); + if(this._subdocs[i] === subdoc) this._subdocs.splice(i, 1); return; } }, _updateSubdocPaths: function(op){ - this._subdocs || (this._subdocs = []); + this._subdocs = this._subdocs || []; for(var i = 0; i < this._subdocs.length; i++){ this._subdocs[i]._updatePath(op); } }, createContextAt: function() { - var path = 1 <= arguments.length ? Array.prototype.slice.call(arguments, 0) : []; - var subdoc = new SubDoc(this, depath(path)); + var path = arguments.length >= 1 ? Array.prototype.slice.call(arguments, 0) : []; + var subdoc = new SubDoc(this, depath(path)); this._addSubDoc(subdoc); return subdoc; }, get: function(path) { if (!path) return this.getSnapshot(); - return normalizeArgs(this,arguments,function(path){ + return normalizeArgs(this, arguments, function(path){ var _ref = traverse(this.getSnapshot(), path); return _ref.elem[_ref.key]; }); @@ -363,7 +365,7 @@ type.api = { return this._submit([op], cb); } else if (elem[key].constructor === Array) { var ops = []; - for (var i=pos; i= 0.2.4", @@ -46,7 +47,8 @@ "build": "make", "test": "node_modules/mocha/bin/mocha test/server test/browser", "prepublish": "make webclient", - "coverage": "node node_modules/istanbul/lib/cli cover node_modules/mocha/bin/_mocha test/server test/browser" + "coverage": "node node_modules/istanbul/lib/cli cover node_modules/mocha/bin/_mocha test/server test/browser", + "lint": "node_modules/.bin/eslint lib/**/*.js" }, "licenses": [ { diff --git a/test/server/connection.coffee b/test/server/connection.coffee index 5435657a..b322b100 100644 --- a/test/server/connection.coffee +++ b/test/server/connection.coffee @@ -75,7 +75,7 @@ describe 'Connection', -> it 'pushes message buffer', -> assert @connection.messageBuffer.length == 0 - socket.onmessage('"a message"') + socket.onmessage('{"d": 3}') assert @connection.messageBuffer.length == 1 @@ -111,6 +111,6 @@ describe 'Connection', -> doc = @connection.get('food', 'steak', {data: 'content', v: 0, type: 'text'}) assert.equal doc.snapshot, 'content' doc = @connection.get('food', 'steak', {data: 'other content', v: 0, type: 'text'}) - # TODO + # TODO assert.equal doc.snapshot, 'content' #assert.equal doc.snapshot, 'other content' diff --git a/test/server/useragent.coffee b/test/server/useragent.coffee index 4be5791f..a33d7520 100644 --- a/test/server/useragent.coffee +++ b/test/server/useragent.coffee @@ -138,19 +138,19 @@ describe 'UserAgent', -> it 'calls submit on backend', (done) -> sinon.spy backend, 'submit' - @userAgent.submit 'flowers', 'lily', 'pluck', {}, -> - sinon.assert.calledWith backend.submit, 'flowers', 'lily', 'pluck' + @userAgent.submit 'flowers', 'lily', {docName: 'pluck'}, {}, -> + sinon.assert.calledWith backend.submit, 'flowers', 'lily' done() it 'returns version and operations', (done) -> - @userAgent.submit 'flowers', 'lily', 'pluck', {}, (error, version, operations) -> + @userAgent.submit 'flowers', 'lily', {docName: 'pluck'}, {}, (error, version, operations) -> assert.equal version, 41 assert.deepEqual operations, ['operation'] done() it 'triggers after submit', (done) -> sinon.spy @userAgent, 'trigger' - @userAgent.submit 'flowers', 'lily', 'pluck', {}, => + @userAgent.submit 'flowers', 'lily', {docName: 'pluck'}, {}, => sinon.assert.calledWith @userAgent.trigger, 'after submit', 'flowers', 'lily' done()