From 9121bafe48e6a43dfdf29ee7ef9088b30a9b2a08 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Apr 2018 16:36:58 +0100 Subject: [PATCH 01/95] Update tested nodejs versions in .travis.yml See See https://github.com/nodejs/Release --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1b9165051..66e0be28c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,9 @@ language: node_js node_js: - - 6 - - 5 - - 4 - - 0.10 + - "9" + - "8" + - "6" + - "4" script: "npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From 4dbefd14464f234cdc4cb7ee35d3b83ae534cbb2 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 17 Oct 2017 14:15:42 +0100 Subject: [PATCH 02/95] Add .editorconfig --- .editorconfig | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..e29f5e504 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = LF +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true From 2ef8181081b168ce773fca51db248fab518dbe0f Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 23 Apr 2018 14:51:13 +0100 Subject: [PATCH 03/95] Update mocha I had to add the --exit flag workaround to mocha.opts to make it exit when tests are done. A better long-term solution would be to ensure that nothing keeps node running after all tests are done, see https://boneskull.com/mocha-v4-nears-release/#mochawontforceexit. --- package.json | 2 +- test/mocha.opts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 47684ed71..5f51224c8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^3.2.0", + "mocha": "^5.1.1", "sharedb-mingo-memory": "^1.0.0-beta" }, "scripts": { diff --git a/test/mocha.opts b/test/mocha.opts index 34f904192..7ca4707b0 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,3 +1,4 @@ --reporter spec --check-leaks --recursive +--exit From 6b687db2744156665608b86f5ed9d59470a28292 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 18 Apr 2018 16:27:13 +0100 Subject: [PATCH 04/95] Fix Doc.prototype.destroy The problem was that unsubscribe re-added the doc to the connection. Now the doc is removed from the connection after unsubscribe. Additionally, we're no longer waiting for the unsubscribe response before executing the callback. It is consistent with Query, unsubscribe can't fail anyway and the subscribed state is updated synchronously on the client side. --- lib/client/doc.js | 4 ++-- test/client/subscribe.js | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 05e17976d..33621cb9c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -104,10 +104,10 @@ emitter.mixin(Doc); Doc.prototype.destroy = function(callback) { var doc = this; doc.whenNothingPending(function() { - doc.connection._destroyDoc(doc); if (doc.wantSubscribe) { - return doc.unsubscribe(callback); + doc.unsubscribe(); } + doc.connection._destroyDoc(doc); if (callback) callback(); }); }; diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 567031d0a..db2bea1b2 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -405,8 +405,10 @@ describe('client subscribe', function() { }); it('doc destroy stops op updates', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + var doc = connection1.get('dogs', 'fido'); + var doc2 = connection2.get('dogs', 'fido'); doc.create({age: 3}, function(err) { if (err) return done(err); doc2.subscribe(function(err) { @@ -416,6 +418,7 @@ describe('client subscribe', function() { }); doc2.destroy(function(err) { if (err) return done(err); + expect(connection2.getExisting('dogs', 'fido')).equal(undefined); doc.submitOp({p: ['age'], na: 1}, done); }); }); From 1489e36c1e4179b76ba505c8558e6b5bf4619034 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 24 Apr 2018 13:49:59 +0100 Subject: [PATCH 05/95] Fix hasWritePending in op's callback --- lib/client/doc.js | 5 +++-- test/client/submit.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 33621cb9c..a7d1d845e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -888,9 +888,10 @@ Doc.prototype._hardRollback = function(err) { }; Doc.prototype._clearInflightOp = function(err) { - var called = callEach(this.inflightOp.callbacks, err); - + var callbacks = this.inflightOp && this.inflightOp.callbacks; this.inflightOp = null; + var called = callbacks && callEach(callbacks, err); + this.flush(); this._emitNothingPending(); diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..4334b57e8 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1044,6 +1044,39 @@ describe('client submit', function() { }); }); + it('hasWritePending is false when create\'s callback is executed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + expect(doc.hasWritePending()).equal(false); + done(); + }); + }); + + it('hasWritePending is false when submimtOp\'s callback is executed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.hasWritePending()).equal(false); + done(); + }); + }); + }); + + it('hasWritePending is false when del\'s callback is executed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.del(function(err) { + if (err) return done(err); + expect(doc.hasWritePending()).equal(false); + done(); + }); + }); + }); + describe('type.deserialize', function() { it('can create a new doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); From a4499a539cc6961f26174126a8f5d00cc251b757 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 16 Apr 2018 13:30:49 +0100 Subject: [PATCH 06/95] Implement ephemeral "presence" data sync --- README.md | 19 + lib/agent.js | 48 ++ lib/backend.js | 5 + lib/client/connection.js | 22 + lib/client/doc.js | 384 +++++++++- test/client/presence-type.js | 82 +++ test/client/presence.js | 1277 ++++++++++++++++++++++++++++++++++ test/util.js | 6 + 8 files changed, 1832 insertions(+), 11 deletions(-) create mode 100644 test/client/presence-type.js create mode 100644 test/client/presence.js diff --git a/README.md b/README.md index 69770ed33..3cbdea6e8 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ tracker](https://github.com/share/sharedb/issues). - Realtime synchronization of any JSON document - Concurrent multi-user collaboration +- Realtime synchronization of any ephemeral "presence" data - Synchronous editing API with asynchronous eventual consistency - Realtime query subscriptions - Simple integration with any database - [MongoDB](https://github.com/share/sharedb-mongo), [PostgresQL](https://github.com/share/sharedb-postgres) (experimental) @@ -57,6 +58,10 @@ initial data. Then you can submit editing operations on the document (using OT). Finally you can delete the document with a delete operation. By default, ShareDB stores all operations forever - nothing is truly deleted. +## User presence synchronization + +Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. + ## Server API ### Initialization @@ -221,6 +226,9 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. +`doc.presence` _(Object)_ +Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. + `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. @@ -250,6 +258,9 @@ An operation was applied to the data. `source` will be `false` for ops received `doc.on('del', function(data, source) {...})` The document was deleted. Document contents before deletion are passed in as an argument. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. +`doc.on('presence', function(srcList) {...})` +Presence data has changed. `srcList` is an Array of `doc.presence` property names for which values have changed. + `doc.on('error', function(err) {...})` There was an error fetching the document or applying an operation. @@ -283,6 +294,11 @@ Invokes the given callback function after Note that `whenNothingPending` does NOT wait for pending `model.query()` calls. +`doc.submitPresence(presenceData[, function(err) {...}])` +Set local presence data and publish it for other clients. +`presenceData` structure depends on the document type. +Presence is synchronized only when subscribed to the document. + ### Class: `ShareDB.Query` `query.ready` _(Boolean)_ @@ -358,6 +374,9 @@ Additional fields may be added to the error object for debugging context dependi * 4021 - Invalid client id * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type +* 4024 - OT Type does not support presence +* 4025 - Not subscribed to document +* 4026 - Presence data superseded ### 5000 - Internal error diff --git a/lib/agent.js b/lib/agent.js index d1a944de4..f04baa2bd 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,6 +1,7 @@ var hat = require('hat'); var util = require('./util'); var types = require('./types'); +var ShareDBError = require('./error'); /** * Agent deserializes the wire protocol messages received from the stream and @@ -25,6 +26,9 @@ function Agent(backend, stream) { // Map from queryId -> emitter this.subscribedQueries = {}; + // The max presence sequence number received from the client. + this.maxPresenceSeq = 0; + // We need to track this manually to make sure we don't reply to messages // after the stream was closed. this.closed = false; @@ -98,10 +102,17 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { console.error('Doc subscription stream error', collection, id, data.error); return; } + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== agent.clientId) agent.send(data); + return; + } if (agent._isOwnOp(collection, data)) return; agent._sendOp(collection, id, data); }); stream.on('end', function() { + var presence = agent._createPresence(collection, id); + agent.backend.sendPresence(presence); // The op stream is done sending, so release its reference var streams = agent.subscribedDocs[collection]; if (!streams) return; @@ -268,6 +279,13 @@ Agent.prototype._checkRequest = function(request) { // Bulk request if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; + } else if (request.a === 'p') { + // Presence + if (typeof request.c !== 'string') return 'Invalid collection'; + if (typeof request.d !== 'string') return 'Invalid id'; + if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; + if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; + if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') return 'Invalid "request reply" value'; } }; @@ -300,6 +318,9 @@ Agent.prototype._handleMessage = function(request, callback) { var op = this._createOp(request); if (!op) return callback({code: 4000, message: 'Invalid op message'}); return this._submit(request.c, request.d, op, callback); + case 'p': + var presence = this._createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + return this._presence(presence, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -582,3 +603,30 @@ Agent.prototype._createOp = function(request) { return new DeleteOp(src, request.seq, request.v, request.del); } }; + +Agent.prototype._presence = function(presence, callback) { + if (presence.seq <= this.maxPresenceSeq) { + return callback(new ShareDBError(4026, 'Presence data superseded')); + } + this.maxPresenceSeq = presence.seq; + if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) { + return callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + } + this.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); +}; + +Agent.prototype._createPresence = function(collection, id, data, version, requestReply, seq) { + return { + a: 'p', + src: this.clientId, + seq: seq != null ? seq : this.maxPresenceSeq, + c: collection, + d: id, + p: data, + v: version, + r: requestReply + }; +}; diff --git a/lib/backend.js b/lib/backend.js index 22791f30b..40a1ca282 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -515,6 +515,11 @@ Backend.prototype.getChannels = function(collection, id) { ]; }; +Backend.prototype.sendPresence = function(presence, callback) { + var channels = [ this.getDocChannel(presence.c, presence.d) ]; + this.pubsub.publish(channels, presence, callback); +}; + function pluckIds(snapshots) { var ids = []; for (var i = 0; i < snapshots.length; i++) { diff --git a/lib/client/connection.js b/lib/client/connection.js index f4cc298e6..b8d7f1ccc 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -243,6 +243,11 @@ Connection.prototype.handleMessage = function(message) { if (doc) doc._handleOp(err, message); return; + case 'p': + var doc = this.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); + return; + default: console.warn('Ignoring unrecognized message', message); } @@ -408,6 +413,23 @@ Connection.prototype.sendOp = function(doc, op) { this.send(message); }; +Connection.prototype.sendPresence = function(doc, data, requestReply) { + // Ensure the doc is registered so that it receives the reply message + this._addDoc(doc); + var message = { + a: 'p', + c: doc.collection, + d: doc.id, + p: data, + v: doc.version || 0, + seq: this.seq++ + }; + if (requestReply) { + message.r = true; + } + this.send(message); +}; + /** * Sends a message down the socket diff --git a/lib/client/doc.js b/lib/client/doc.js index a7d1d845e..e92c4b644 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -28,6 +28,14 @@ var types = require('../types'); * }) * * + * Presence + * -------- + * + * We can associate transient "presence" data with a document, eg caret position, etc. + * The presence data is synchronized on the best-effort basis between clients subscribed to the same document. + * Each client has their own presence data which is read-write. Other clients' data is read-only. + * + * * Events * ------ * @@ -42,6 +50,7 @@ var types = require('../types'); * the data is null. It is passed the data before delteion as an * arguments * - `load ()` Fired when a new snapshot is ingested from a fetch, subscribe, or query + * - `presence ([src])` Fired after the presence data has changed. */ module.exports = Doc; @@ -57,11 +66,37 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; + // The current presence data + // Map of src -> presence data + // Local src === '' + this.presence = Object.create(null); + // The presence objects received from the server + // Map of src -> presence + this.receivedPresence = Object.create(null); + // The minimum amount of time to wait before removing processed presence from this.receivedPresence. + // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. + // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower + // sequence number arrive after messages with higher sequence numbers. + this.receivedPresenceTimeout = 60000; + // If set to true, then the next time the local presence is sent, + // all other clients will be asked to reply with their own presence data. + this.requestReplyPresence = true; + // A list of ops sent by the server. These are needed for transforming presence data, + // if we get that presence data for an older version of the document. + // The ops are cached for 1 minute by default, which should be lots, considering that the presence + // data is supposed to be synced in real-time. + this.cachedOps = []; + this.cachedOpsTimeout = 60000; + // The sequence number of the inflight presence request. + this.inflightPresenceSeq = 0; + // Array of callbacks or nulls as placeholders this.inflightFetch = []; this.inflightSubscribe = []; this.inflightUnsubscribe = []; + this.inflightPresence = null; this.pendingFetch = []; + this.pendingPresence = null; // Whether we think we are subscribed on the server. Synchronously set to // false on calls to unsubscribe and disconnect. Should never be true when @@ -108,6 +143,24 @@ Doc.prototype.destroy = function(callback) { doc.unsubscribe(); } doc.connection._destroyDoc(doc); + + // Make sure all presence callbacks are called + var callbacks = []; + if (doc.inflightPresence) { + // This shouldn't be possible but check just in case. + callbacks.push.apply(callbacks, doc.inflightPresence); + doc.inflightPresence = null; + doc.inflightPresenceSeq = 0; + } + if (doc.pendingPresence) { + callbacks.push.apply(callbacks, doc.pendingPresence); + doc.pendingPresence = null; + } + + doc.receivedPresence = Object.create(null); + doc.cachedOps.length = 0; + + callEach(callbacks); if (callback) callback(); }); }; @@ -186,12 +239,14 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; + this.cachedOps.length = 0; var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); this.data = (this.type && this.type.deserialize) ? this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); + this._processAllReceivedPresence(); callback && callback(); }; @@ -257,6 +312,7 @@ Doc.prototype._handleSubscribe = function(err, snapshot) { if (this.wantSubscribe) this.subscribed = true; this.ingestSnapshot(snapshot, callback); this._emitNothingPending(); + this.flush(); }; Doc.prototype._handleUnsubscribe = function(err) { @@ -307,6 +363,13 @@ Doc.prototype._handleOp = function(err, message) { return; } + var serverOp = { + src: message.src, + create: !!message.create, + op: message.op, + del: !!message.del + }; + if (this.inflightOp) { var transformErr = transformX(this.inflightOp, message); if (transformErr) return this._hardRollback(transformErr); @@ -318,7 +381,9 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; + this._cacheOp(serverOp); this._otApply(message, false); + this._processAllReceivedPresence(); return; }; @@ -342,7 +407,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; + this._pausePresence(); callEach(callbacks); + } else { + this._pausePresence(); } } }; @@ -402,8 +470,10 @@ Doc.prototype.unsubscribe = function(callback) { if (this.connection.canSend) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); + this._pausePresence(); return; } + this._pausePresence(); if (callback) process.nextTick(callback); }; @@ -426,14 +496,22 @@ function pushActionCallback(inflight, isDuplicate, callback) { // // Only one operation can be in-flight at a time. If an operation is already on // its way, or we're not currently connected, this method does nothing. +// +// If there are no pending ops, this method sends the pending presence data, if possible. Doc.prototype.flush = function() { - // Ignore if we can't send or we are already sending an op - if (!this.connection.canSend || this.inflightOp) return; + if (this.paused) return; - // Send first pending op unless paused - if (!this.paused && this.pendingOps.length) { + if (this.connection.canSend && !this.inflightOp && this.pendingOps.length) { this._sendOp(); } + + if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) { + this.inflightPresence = this.pendingPresence; + this.inflightPresenceSeq = this.connection.seq; + this.pendingPresence = null; + this.connection.sendPresence(this, this.presence[''], this.requestReplyPresence); + this.requestReplyPresence = false; + } }; // Helper function to set op to contain a no-op. @@ -538,6 +616,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); + this._transformAllPresence(op); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -550,6 +629,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place this.data = this.type.apply(this.data, op.op); + this._transformAllPresence(op); // Emit an 'op' event once the local data includes the changes from the // op. For locally submitted ops, this will be synchronously with // submission and before the server or other clients have received the op. @@ -566,6 +646,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); + this._transformAllPresence(op); this.emit('create', source); return; } @@ -573,6 +654,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); + this._transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -820,6 +902,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; + this.cachedOps.length = 0; } else if (message.v !== this.version) { // We should already be at the same version, because the server should @@ -832,8 +915,15 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; + this._cacheOp({ + src: this.inflightOp.src, + create: !!this.inflightOp.create, + op: this.inflightOp.op, + del: !!this.inflightOp.del + }); this._clearInflightOp(); + this._processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { @@ -868,21 +958,45 @@ Doc.prototype._rollback = function(err) { }; Doc.prototype._hardRollback = function(err) { - // Cancel all pending ops and reset if we can't invert - var op = this.inflightOp; - var pending = this.pendingOps; + var callbacks = []; + if (this.inflightPresence) { + callbacks.push.apply(callbacks, this.inflightPresence); + this.inflightPresence = null; + this.inflightPresenceSeq = 0; + } + if (this.pendingPresence) { + callbacks.push.apply(callbacks, this.pendingPresence); + this.pendingPresence = null; + } + if (this.inflightOp) { + callbacks.push.apply(callbacks, this.inflightOp.callbacks); + } + for (var i = 0; i < this.pendingOps.length; i++) { + callbacks.push.apply(callbacks, this.pendingOps[i].callbacks); + } + this._setType(null); this.version = null; this.inflightOp = null; this.pendingOps = []; + this.cachedOps.length = 0; + this.receivedPresence = Object.create(null); + this.requestReplyPresence = true; + + var srcList = Object.keys(this.presence); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList); // Fetch the latest from the server to get us back into a working state var doc = this; this.fetch(function() { - var called = op && callEach(op.callbacks, err); - for (var i = 0; i < pending.length; i++) { - callEach(pending[i].callbacks, err); - } + var called = callEach(callbacks, err); if (err && !called) return doc.emit('error', err); }); }; @@ -909,3 +1023,251 @@ function callEach(callbacks, err) { } return called; } + +// *** Presence + +Doc.prototype.submitPresence = function (data, callback) { + if (data != null) { + if (!this.type) { + var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + this.collection + '.' + this.id); + if (callback) return callback(err); + return this.emit('error', err); + } + + if (!this.type.createPresence || !this.type.transformPresence) { + var err = new ShareDBError(4024, 'Cannot submit presence. Document\'s type does not support presence. ' + this.collection + '.' + this.id); + if (callback) return callback(err); + return this.emit('error', err); + } + + data = this.type.createPresence(data); + } + + if (!this.pendingPresence) this.pendingPresence = []; + if (callback) this.pendingPresence.push(callback); + this._setPresence('', data, true); + + var doc = this; + process.nextTick(function() { + doc.flush(); + }); +}; + +Doc.prototype._handlePresence = function(err, presence) { + if (!this.subscribed) return; + + var src = presence.src; + if (!src) { + // Handle the ACK for the presence data we submitted. + // this.inflightPresenceSeq would not equal presence.seq after a hard rollback, + // when all callbacks are flushed with an error. + if (this.inflightPresenceSeq === presence.seq) { + var callbacks = this.inflightPresence; + this.inflightPresence = null; + this.inflightPresenceSeq = 0; + var called = callbacks && callEach(callbacks, err); + if (err && !called) this.emit('error', err); + this.flush(); + } + return; + } + + // This shouldn't happen but check just in case. + if (err) return this.emit('error', err); + + if (presence.r && !this.pendingPresence) { + // Another client requested us to share our current presence data + this.pendingPresence = []; + this.flush(); + } + + // Ignore older messages which arrived out of order + if ( + this.receivedPresence[src] && ( + this.receivedPresence[src].seq > presence.seq || + (this.receivedPresence[src].seq === presence.seq && presence.v != null) + ) + ) return; + + this.receivedPresence[src] = presence; + + if (presence.v == null) { + // null version should happen only when the server automatically sends + // null presence for an unsubscribed client + presence.processedAt = Date.now(); + return this._setPresence(src, null, true); + } + + // Get missing ops first, if necessary + if (this.version == null || this.version < presence.v) return this.fetch(); + + this._processReceivedPresence(src, true); +}; + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed for src. Otherwise false. +Doc.prototype._processReceivedPresence = function(src, emit) { + if (!src) return false; + var presence = this.receivedPresence[src]; + if (!presence) return false; + + if (presence.processedAt != null) { + if (Date.now() >= presence.processedAt + this.receivedPresenceTimeout) { + // Remove old received and processed presence + delete this.receivedPresence[src]; + } + return false; + } + + if (this.version == null || this.version < presence.v) return false; // keep waiting for the missing snapshot or ops + + if (presence.p == null) { + // Remove presence data as requested + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (!this.type || !this.type.createPresence || !this.type.transformPresence) { + // Remove presence data because the document is not created or its type does not support presence + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (this.inflightOp && this.inflightOp.op == null) { + // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + if (this.pendingOps[i].op == null) { + // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + var startIndex = this.cachedOps.length - (this.version - presence.v); + if (startIndex < 0) { + // Remove presence data because we can't transform receivedPresence + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = startIndex; i < this.cachedOps.length; i++) { + if (this.cachedOps[i].op == null) { + // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + // Make sure the format of the data is correct + var data = this.type.createPresence(presence.p); + + // Transform against past ops + for (var i = startIndex; i < this.cachedOps.length; i++) { + var op = this.cachedOps[i]; + data = this.type.transformPresence(data, op.op, presence.src === op.src); + } + + // Transform against pending ops + if (this.inflightOp) { + data = this.type.transformPresence(data, this.inflightOp.op, false); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + data = this.type.transformPresence(data, this.pendingOps[i].op, false); + } + + // Set presence data + presence.processedAt = Date.now(); + return this._setPresence(src, data, emit); +}; + +Doc.prototype._processAllReceivedPresence = function() { + var srcList = Object.keys(this.receivedPresence); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._processReceivedPresence(src)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList); +}; + +Doc.prototype._transformPresence = function(src, op) { + var presenceData = this.presence[src]; + if (op.op != null) { + var isOwnOperation = src === (op.src || ''); + presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); + } else { + presenceData = null; + } + return this._setPresence(src, presenceData); +}; + +Doc.prototype._transformAllPresence = function(op) { + var srcList = Object.keys(this.presence); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._transformPresence(src, op)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList); +}; + +Doc.prototype._pausePresence = function() { + if (this.inflightPresence) { + this.pendingPresence = + this.pendingPresence ? + this.inflightPresence.concat(this.pendingPresence) : + this.inflightPresence; + this.inflightPresence = null; + this.inflightPresenceSeq = 0; + } + this.receivedPresence = Object.create(null); + this.requestReplyPresence = true; + var srcList = Object.keys(this.presence); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (src && this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList); +}; + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed. Otherwise false. +Doc.prototype._setPresence = function(src, data, emit) { + if (data == null) { + if (this.presence[src] == null) return false; + delete this.presence[src]; + } else { + var isPresenceEqual = + this.presence[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence[src], data)); + if (isPresenceEqual) return false; + this.presence[src] = data; + } + if (emit) this._emitPresence([ src ]); + return true; +}; + +Doc.prototype._emitPresence = function(srcList) { + if (srcList && srcList.length > 0) { + this.emit('presence', srcList); + } +}; + +Doc.prototype._cacheOp = function(op) { + this.cachedOps.push(op); + setTimeout(function() { + if (this.cachedOps[0] === op) this.cachedOps.shift(); + }.bind(this), this.cachedOpsTimeout); +}; diff --git a/test/client/presence-type.js b/test/client/presence-type.js new file mode 100644 index 000000000..51ad272a0 --- /dev/null +++ b/test/client/presence-type.js @@ -0,0 +1,82 @@ +// A simple type for testing presence, where: +// +// - snapshot is a list +// - operation is { index, value } -> insert value at index in snapshot +// - presence is { index } -> an index in the snapshot +exports.type = { + name: 'wrapped-presence-no-compare', + uri: 'http://sharejs.org/types/wrapped-presence-no-compare', + create: create, + apply: apply, + transform: transform, + createPresence: createPresence, + transformPresence: transformPresence +}; + +// The same as `exports.type` but implements `comparePresence`. +exports.type2 = { + name: 'wrapped-presence-with-compare', + uri: 'http://sharejs.org/types/wrapped-presence-with-compare', + create: create, + apply: apply, + transform: transform, + createPresence: createPresence, + transformPresence: transformPresence, + comparePresence: comparePresence +}; + +// The same as `exports.type` but `presence.index` is unwrapped. +exports.type3 = { + name: 'unwrapped-presence', + uri: 'http://sharejs.org/types/unwrapped-presence', + create: create, + apply: apply, + transform: transform, + createPresence: createPresence2, + transformPresence: transformPresence2 +}; + +function create(data) { + return data || []; +} + +function apply(snapshot, op) { + snapshot.splice(op.index, 0, op.value); + return snapshot; +} + +function transform(op1, op2, side) { + return op1.index < op2.index || (op1.index === op2.index && side === 'left') ? + op1 : + { + index: op1.index + 1, + value: op1.value + }; +} + +function createPresence(data) { + return { index: (data && data.index) | 0 }; +} + +function transformPresence(presence, op, isOwnOperation) { + return presence.index < op.index || (presence.index === op.index && !isOwnOperation) ? + presence : + { + index: presence.index + 1 + }; +} + +function comparePresence(presence1, presence2) { + return presence1 === presence2 || + (presence1 == null && presence2 == null) || + (presence1 != null && presence2 != null && presence1.index === presence2.index); +} + +function createPresence2(data) { + return data | 0; +} + +function transformPresence2(presence, op, isOwnOperation) { + return presence < op.index || (presence === op.index && !isOwnOperation) ? + presence : presence + 1; +} diff --git a/test/client/presence.js b/test/client/presence.js new file mode 100644 index 000000000..271b9b063 --- /dev/null +++ b/test/client/presence.js @@ -0,0 +1,1277 @@ +var async = require('async'); +var util = require('../util'); +var errorHandler = util.errorHandler; +var Backend = require('../../lib/backend'); +var ShareDBError = require('../../lib/error'); +var expect = require('expect.js'); +var types = require('../../lib/types'); +var presenceType = require('./presence-type'); +types.register(presenceType.type); +types.register(presenceType.type2); +types.register(presenceType.type3); + +[ + 'wrapped-presence-no-compare', + 'wrapped-presence-with-compare', + 'unwrapped-presence' +].forEach(function(typeName) { + function p(index) { + return typeName === 'unwrapped-presence' ? index : { index: index }; + } + + describe('client presence (' + typeName + ')', function() { + beforeEach(function() { + this.backend = new Backend(); + this.connection = this.backend.connect(); + this.connection2 = this.backend.connect(); + this.doc = this.connection.get('dogs', 'fido'); + this.doc2 = this.connection2.get('dogs', 'fido'); + }); + + afterEach(function(done) { + this.backend.close(done); + }); + + it('sends presence immediately', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + this.doc2.once('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('sends presence after pending ops', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); + this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + this.doc2.once('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('waits for pending ops before processing future presence', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + // A hack to send presence for a future version. + this.doc.version += 2; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), function(err) { + if (err) return done(err); + this.doc.version -= 2; + this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); + this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (own ops, presence.index < op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), + this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'a' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (own ops, presence.index === op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitOp.bind(this.doc, { index: 1, value: 'c' }), + this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'a' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (own ops, presence.index > op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitOp.bind(this.doc, { index: 0, value: 'b' }), + this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'c' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (non-own ops, presence.index < op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), + this.doc2.submitOp.bind(this.doc2, { index: 2, value: 'c' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'a' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (non-own ops, presence.index === op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'c' }), + this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'a' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (non-own ops, presence.index > op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'b' }), + this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'a' }), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'c' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (transform against non-op)', function(allDone) { + async.series([ + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.create.bind(this.doc, [], typeName), + this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }), + this.doc.del.bind(this.doc), + this.doc.create.bind(this.doc, [ 'b' ], typeName), + function(done) { + this.doc2.once('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'b' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + }.bind(this), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'b' ]); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 2; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('handles presence sent for earlier revisions (no cached ops)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), + this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), + function(done) { + this.doc2.once('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + }.bind(this), + function(done) { + this.doc2.cachedOps = []; + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + done(); + }.bind(this)); + // A hack to send presence for an older version. + this.doc.version = 1; + this.doc.data = [ 'a' ]; + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against local delete', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(0)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + done(); + }.bind(this)); + this.doc.del(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against non-local delete', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(0)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + done(); + }.bind(this)); + this.doc2.del(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against local op (presence.index != op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(2)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection2.id ]); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); + done(); + }.bind(this)); + this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against non-local op (presence.index != op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(2)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection2.id ]); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); + done(); + }.bind(this)); + this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against local op (presence.index == op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(1)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList).to.eql([ '' ]); + expect(this.doc.presence['']).to.eql(p(2)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + done(); + }.bind(this)); + this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms presence against non-local op (presence.index == op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(1)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, // wait for the doc2 presence message to reach doc + function(done) { + this.doc.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection2.id ]); + expect(this.doc.presence['']).to.eql(p(1)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(2)); + done(); + }.bind(this)); + this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('caches local ops', function(allDone) { + var op = { index: 1, value: 'b' }; + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.submitOp.bind(this.doc, op), + this.doc.del.bind(this.doc), + function(done) { + expect(this.doc.cachedOps.length).to.equal(3); + expect(this.doc.cachedOps[0].create).to.equal(true); + expect(this.doc.cachedOps[1].op).to.equal(op); + expect(this.doc.cachedOps[2].del).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('caches non-local ops', function(allDone) { + var op = { index: 1, value: 'b' }; + async.series([ + this.doc2.subscribe.bind(this.doc2), + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.submitOp.bind(this.doc, op), + this.doc.del.bind(this.doc), + async.nextTick, + function(done) { + expect(this.doc2.cachedOps.length).to.equal(3); + expect(this.doc2.cachedOps[0].create).to.equal(true); + expect(this.doc2.cachedOps[1].op).to.eql(op); + expect(this.doc2.cachedOps[2].del).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('removes cached ops', function(allDone) { + var op = { index: 1, value: 'b' }; + this.doc.cachedOpsTimeout = 0; + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.submitOp.bind(this.doc, op), + this.doc.del.bind(this.doc), + function(done) { + expect(this.doc.cachedOps.length).to.equal(3); + expect(this.doc.cachedOps[0].create).to.equal(true); + expect(this.doc.cachedOps[1].op).to.equal(op); + expect(this.doc.cachedOps[2].del).to.equal(true); + done(); + }.bind(this), + setTimeout, + function(done) { + expect(this.doc.cachedOps.length).to.equal(0); + done(); + }.bind(this) + ], allDone); + }); + + it('removes correct cached ops', function(allDone) { + var op = { index: 1, value: 'b' }; + this.doc.cachedOpsTimeout = 0; + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.submitOp.bind(this.doc, op), + this.doc.del.bind(this.doc), + function(done) { + expect(this.doc.cachedOps.length).to.equal(3); + expect(this.doc.cachedOps[0].create).to.equal(true); + expect(this.doc.cachedOps[1].op).to.equal(op); + expect(this.doc.cachedOps[2].del).to.equal(true); + this.doc.cachedOps.shift(); + this.doc.cachedOps.push({ op: true }); + done(); + }.bind(this), + setTimeout, + function(done) { + expect(this.doc.cachedOps.length).to.equal(1); + expect(this.doc.cachedOps[0].op).to.equal(true); + done(); + }.bind(this) + ], allDone); + }); + + it('requests reply presence when sending presence for the first time', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + if (srcList[0] === '') { + expect(srcList).to.eql([ '' ]); + expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + } else { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.requestReplyPresence).to.equal(false); + done(); + } + }.bind(this)); + this.doc2.submitPresence(p(1), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('fails to submit presence for uncreated document: callback(err)', function(allDone) { + async.series([ + this.doc.subscribe.bind(this.doc), + function(done) { + this.doc.submitPresence(p(0), function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4015); + done(); + }); + }.bind(this) + ], allDone); + }); + + it('fails to submit presence for uncreated document: emit(err)', function(allDone) { + async.series([ + this.doc.subscribe.bind(this.doc), + function(done) { + this.doc.on('error', function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4015); + done(); + }); + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('fails to submit presence, if type does not support presence: callback(err)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, {}), + this.doc.subscribe.bind(this.doc), + function(done) { + this.doc.submitPresence(p(0), function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4024); + done(); + }); + }.bind(this) + ], allDone); + }); + + it('fails to submit presence, if type does not support presence: emit(err)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, {}), + this.doc.subscribe.bind(this.doc), + function(done) { + this.doc.on('error', function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4024); + done(); + }); + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('submits null presence', function(allDone) { + async.series([ + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, null) + ], allDone); + }); + + it('sends presence once, if submitted multiple times synchronously', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + this.doc.submitPresence(p(1), errorHandler(done)); + this.doc.submitPresence(p(2), errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('buffers presence until subscribed', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + setTimeout(function() { + this.doc.subscribe(function(err) { + if (err) return done(err); + expect(this.doc2.presence).to.eql({}); + }.bind(this)); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('buffers presence when disconnected', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + this.connection.close(); + this.doc.submitPresence(p(1), errorHandler(done)); + process.nextTick(function() { + this.backend.connect(this.connection); + this.doc.requestReplyPresence = false; + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('submits presence without a callback', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('cancels pending presence on destroy', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + function(done) { + this.doc.submitPresence(p(0), done); + console.log(!!this.doc.inflightPresence, !!this.doc.pendingPresence); + this.doc.destroy(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('cancels inflight presence on destroy', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + this.doc.submitPresence(p(0), done); + process.nextTick(function() { + this.doc.destroy(errorHandler(done)); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('receives presence after doc is deleted', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + async.nextTick, + function(done) { + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + this.doc2.del(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('clears peer presence on peer disconnection', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + + var connectionId = this.connection.id; + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ connectionId ]); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); + done(); + }.bind(this)); + this.connection.close(); + + // this.doc.requestReplyPresence = false; + // this.doc.submitPresence(p(0), errorHandler(done)); + // this.doc2.del(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('clears peer presence on own disconnection', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + + var connectionId = this.connection.id; + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ connectionId ]); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); + done(); + }.bind(this)); + this.connection2.close(); + + // this.doc.requestReplyPresence = false; + // this.doc.submitPresence(p(0), errorHandler(done)); + // this.doc2.del(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('clears peer presence on peer unsubscribe', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + + var connectionId = this.connection.id; + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ connectionId ]); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); + done(); + }.bind(this)); + this.doc.unsubscribe(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('clears peer presence on own unsubscribe', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + + var connectionId = this.connection.id; + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ connectionId ]); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); + done(); + }.bind(this)); + this.doc2.unsubscribe(errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('pauses inflight and pending presence on disconnect', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + var called = 0; + function callback(err) { + if (err) return done(err); + if (++called === 2) done(); + } + this.doc.submitPresence(p(0), callback); + process.nextTick(function() { + this.doc.submitPresence(p(1), callback); + this.connection.close(); + process.nextTick(function() { + this.backend.connect(this.connection); + }.bind(this)); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('pauses inflight and pending presence on unsubscribe', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + var called = 0; + function callback(err) { + if (err) return done(err); + if (++called === 2) done(); + } + this.doc.submitPresence(p(0), callback); + process.nextTick(function() { + this.doc.submitPresence(p(1), callback); + this.doc.unsubscribe(errorHandler(done)); + process.nextTick(function() { + this.doc.subscribe(errorHandler(done)); + }.bind(this)); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('transforms received presence against inflight and pending ops (presence.index < op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(0), errorHandler(done)); + this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) + this.doc2.submitOp({ index: 2, value: 'c' }, errorHandler(done)) + }.bind(this) + ], allDone); + }); + + it('transforms received presence against inflight and pending ops (presence.index === op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + this.doc2.submitOp({ index: 1, value: 'c' }, errorHandler(done)) + this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) + }.bind(this) + ], allDone); + }); + + it('transforms received presence against inflight and pending ops (presence.index > op.index)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(1), errorHandler(done)); + this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)) + this.doc2.submitOp({ index: 0, value: 'a' }, errorHandler(done)) + }.bind(this) + ], allDone); + }); + + it('transforms received presence against inflight delete', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(1)), + async.nextTick, + function(done) { + this.doc2.on('presence', function(srcList) { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(2), errorHandler(done)); + this.doc2.del(errorHandler(done)); + this.doc2.create([ 'c' ], typeName, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('transforms received presence against a pending delete', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(1)), + async.nextTick, + function(done) { + var firstCall = true; + this.doc2.on('presence', function(srcList) { + if (firstCall) return firstCall = false; + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence).to.not.have.key(this.connection.id); + done(); + }.bind(this)); + this.doc.requestReplyPresence = false; + this.doc.submitPresence(p(2), errorHandler(done)); + this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); + this.doc2.del(errorHandler(done)); + this.doc2.create([ 'c' ], typeName, errorHandler(done)); + }.bind(this) + ], allDone); + }); + + it('emits the same presence only if comparePresence is not implemented (local presence)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(1)), + function(done) { + this.doc.on('presence', function(srcList) { + if (typeName === 'wrapped-presence-no-compare') { + expect(srcList).to.eql([ '' ]); + expect(this.doc.presence['']).to.eql(p(1)); + done(); + } else { + done(new Error('Unexpected presence event')); + } + }.bind(this)); + this.doc.submitPresence(p(1), typeName === 'wrapped-presence-no-compare' ? errorHandler(done) : done); + }.bind(this) + ], allDone); + }); + + it('emits the same presence only if comparePresence is not implemented (non-local presence)', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(1)), + async.nextTick, + function(done) { + this.doc2.on('presence', function(srcList) { + if (typeName === 'wrapped-presence-no-compare') { + expect(srcList).to.eql([ this.connection.id ]); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + done(); + } else { + done(new Error('Unexpected presence event')); + } + }.bind(this)); + this.doc.submitPresence(p(1), typeName === 'wrapped-presence-no-compare' ? errorHandler(done) : done); + }.bind(this) + ], allDone); + }); + + it('returns an error when not subscribed on the server', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + this.connection.sendUnsubscribe(this.doc); + process.nextTick(done); + }.bind(this), + function(done) { + this.doc.on('error', done); + this.doc.submitPresence(p(0), function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4025); + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('emits an error when not subscribed on the server and no callback is provided', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + this.connection.sendUnsubscribe(this.doc); + process.nextTick(done); + }.bind(this), + function(done) { + this.doc.on('error', function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4025); + done(); + }.bind(this)); + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('returns an error when the server gets an old sequence number', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(0)), + async.nextTick, + function(done) { + this.doc.on('error', done); + this.connection.seq--; + this.doc.submitPresence(p(0), function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4026); + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('emits an error when the server gets an old sequence number and no callback is provided', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(0)), + async.nextTick, + function(done) { + this.doc.on('error', function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(4026); + done(); + }.bind(this)); + this.connection.seq--; + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('returns an error when publishing presence fails', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + async.nextTick, + function(done) { + var sendPresence = this.backend.sendPresence; + this.backend.sendPresence = function(presence, callback) { + if (presence.a === 'p' && presence.v != null) { + return callback(new ShareDBError(-1, 'Test publishing error')); + } + sendPresence.apply(this, arguments); + }; + this.doc.on('error', done); + this.doc.submitPresence(p(0), function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(-1); + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('emits an error when publishing presence fails and no callback is provided', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + async.nextTick, + function(done) { + var sendPresence = this.backend.sendPresence; + this.backend.sendPresence = function(presence, callback) { + if (presence.a === 'p' && presence.v != null) { + return callback(new ShareDBError(-1, 'Test publishing error')); + } + sendPresence.apply(this, arguments); + }; + this.doc.on('error', function(err) { + expect(err).to.be.an(Error); + expect(err.code).to.equal(-1); + done(); + }.bind(this)); + this.doc.submitPresence(p(0)); + }.bind(this) + ], allDone); + }); + + it('clears presence on hard rollback and emits an error', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(0)), + async.nextTick, + function(done) { + // A hack to allow testing of hard rollback of both inflight and pending presence. + var doc = this.doc; + var _handlePresence = this.doc._handlePresence; + this.doc._handlePresence = function(err, presence) { + setTimeout(function() { + _handlePresence.call(doc, err, presence); + }); + }; + + this.doc.submitPresence(p(1)); // inflightPresence + process.nextTick(function() { + this.doc.submitPresence(p(2)); // pendingPresence + + var presenceEmitted = false; + this.doc.on('presence', function(srcList) { + expect(presenceEmitted).to.equal(false); + presenceEmitted = true; + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + }.bind(this)); + + this.doc.on('error', function(err) { + expect(presenceEmitted).to.equal(true); + expect(err).to.be.an(Error); + expect(err.code).to.equal(4000); + done(); + }.bind(this)); + + // send an invalid op + this.doc._submit({ index: 3, value: 'b' }, true); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('clears presence on hard rollback and executes all callbacks', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(0)), + async.nextTick, + function(done) { + // A hack to allow testing of hard rollback of both inflight and pending presence. + var doc = this.doc; + var _handlePresence = this.doc._handlePresence; + this.doc._handlePresence = function(err, presence) { + setTimeout(function() { + _handlePresence.call(doc, err, presence); + }); + }; + + var presenceEmitted = false; + var called = 0; + function callback(err) { + expect(presenceEmitted).to.equal(true); + expect(err).to.be.an(Error); + expect(err.code).to.equal(4000); + if (++called < 3) return; + done(); + } + this.doc.submitPresence(p(1), callback); // inflightPresence + process.nextTick(function() { + this.doc.submitPresence(p(2), callback); // pendingPresence + + this.doc.on('presence', function(srcList) { + expect(presenceEmitted).to.equal(false); + presenceEmitted = true; + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + }.bind(this)); + this.doc.on('error', done); + + // send an invalid op + this.doc._submit({ index: 3, value: 'b' }, true, callback); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + function testReceivedMessageExpiry(expireCache, reduceSequence) { + return function(allDone) { + var lastPresence = null; + var handleMessage = this.connection.handleMessage; + this.connection.handleMessage = function(message) { + if (message.a === 'p' && message.src) { + lastPresence = JSON.parse(JSON.stringify(message)); + } + return handleMessage.apply(this, arguments); + }; + if (expireCache) { + this.doc.receivedPresenceTimeout = 0; + } + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + function(done) { + this.doc2.requestReplyPresence = false; + this.doc2.submitPresence(p(0), done); + }.bind(this), + async.nextTick, // wait for presence to reach this.doc + this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), // forces processing of all received presence + async.nextTick, // wait for op to reach this.doc + function(done) { + expect(this.doc.data).to.eql([ 'a', 'b' ]); + expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); + // Replay the `lastPresence` with modified payload. + lastPresence.p = p(1); + lastPresence.v++; // +1 to account for the op above + if (reduceSequence) { + lastPresence.seq--; + } + this.connection.handleMessage(lastPresence); + process.nextTick(done); + }.bind(this), + function(done) { + expect(this.doc.presence[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + process.nextTick(done); + }.bind(this) + ], allDone); + }; + } + + it('ignores an old message (cache not expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(false, false)); + it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(false, true)); + it('processes an old message (cache expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(true, false)); + it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(true, true)); + }); +}); diff --git a/test/util.js b/test/util.js index 508f81a00..dfbfc0b8f 100644 --- a/test/util.js +++ b/test/util.js @@ -14,3 +14,9 @@ exports.pluck = function(docs, key) { } return values; }; + +exports.errorHandler = function(callback) { + return function(err) { + if (err) callback(err); + }; +}; From 33c72644521c82bf528344b9b6f95bb6debd26d4 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 27 Apr 2018 12:53:46 +0100 Subject: [PATCH 07/95] Execute some callbacks asynchronously --- lib/agent.js | 8 ++++++-- lib/client/doc.js | 18 ++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index f04baa2bd..ac9c12d70 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -606,11 +606,15 @@ Agent.prototype._createOp = function(request) { Agent.prototype._presence = function(presence, callback) { if (presence.seq <= this.maxPresenceSeq) { - return callback(new ShareDBError(4026, 'Presence data superseded')); + return process.nextTick(function() { + callback(new ShareDBError(4026, 'Presence data superseded')); + }); } this.maxPresenceSeq = presence.seq; if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) { - return callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + return process.nextTick(function() { + callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + }); } this.backend.sendPresence(presence, function(err) { if (err) return callback(err); diff --git a/lib/client/doc.js b/lib/client/doc.js index e92c4b644..1900b2d68 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1029,15 +1029,21 @@ function callEach(callbacks, err) { Doc.prototype.submitPresence = function (data, callback) { if (data != null) { if (!this.type) { - var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + this.collection + '.' + this.id); - if (callback) return callback(err); - return this.emit('error', err); + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); } if (!this.type.createPresence || !this.type.transformPresence) { - var err = new ShareDBError(4024, 'Cannot submit presence. Document\'s type does not support presence. ' + this.collection + '.' + this.id); - if (callback) return callback(err); - return this.emit('error', err); + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4024, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); } data = this.type.createPresence(data); From 8ff4b3335f3055ae3d3cefa7ee1773a3656919ba Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 30 Apr 2018 11:42:50 +0100 Subject: [PATCH 08/95] Don't send presence unnecessarily --- lib/client/doc.js | 14 ++++++++--- test/client/presence.js | 55 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 1900b2d68..62d8e17fa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1049,9 +1049,17 @@ Doc.prototype.submitPresence = function (data, callback) { data = this.type.createPresence(data); } - if (!this.pendingPresence) this.pendingPresence = []; - if (callback) this.pendingPresence.push(callback); - this._setPresence('', data, true); + if (this._setPresence('', data, true) || this.pendingPresence || this.inflightPresence) { + if (!this.pendingPresence) { + this.pendingPresence = []; + } + if (callback) { + this.pendingPresence.push(callback); + } + + } else if (callback) { + process.nextTick(callback); + } var doc = this; process.nextTick(function() { diff --git a/test/client/presence.js b/test/client/presence.js index 271b9b063..90371cf6d 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1060,7 +1060,7 @@ types.register(presenceType.type3); function(done) { this.doc.on('error', done); this.connection.seq--; - this.doc.submitPresence(p(0), function(err) { + this.doc.submitPresence(p(1), function(err) { expect(err).to.be.an(Error); expect(err.code).to.equal(4026); done(); @@ -1082,7 +1082,60 @@ types.register(presenceType.type3); done(); }.bind(this)); this.connection.seq--; + this.doc.submitPresence(p(1)); + }.bind(this) + ], allDone); + }); + + it('does not publish presence unnecessarily', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(0)), + async.nextTick, + function(done) { + this.doc.on('error', done); + // Decremented sequence number would cause the server to return an error, however, + // the message won't be sent to the server at all because the presence data has not changed. + this.connection.seq--; + this.doc.submitPresence(p(0), function(err) { + if (typeName === 'wrapped-presence-no-compare') { + // The OT type does not support comparing presence. + expect(err).to.be.an(Error); + expect(err.code).to.equal(4026); + } else { + expect(err).to.not.be.ok(); + } + done(); + }.bind(this)); + }.bind(this) + ], allDone); + }); + + it('does not publish presence unnecessarily when no callback is provided', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc.submitPresence.bind(this.doc, p(0)), + async.nextTick, + function(done) { + this.doc.on('error', function(err) { + if (typeName === 'wrapped-presence-no-compare') { + // The OT type does not support comparing presence. + expect(err).to.be.an(Error); + expect(err.code).to.equal(4026); + done(); + } else { + done(err); + } + }.bind(this)); + // Decremented sequence number would cause the server to return an error, however, + // the message won't be sent to the server at all because the presence data has not changed. + this.connection.seq--; this.doc.submitPresence(p(0)); + if (typeName !== 'wrapped-presence-no-compare') { + process.nextTick(done); + } }.bind(this) ], allDone); }); From 0ff380dda1c6263a31bd3878e73283424877a36e Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 30 Apr 2018 12:06:54 +0100 Subject: [PATCH 09/95] Re-sync presence after re-subscribe and re-connect --- lib/client/doc.js | 2 ++ test/client/presence.js | 51 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/lib/client/doc.js b/lib/client/doc.js index 62d8e17fa..fe689ef2b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1242,6 +1242,8 @@ Doc.prototype._pausePresence = function() { this.inflightPresence; this.inflightPresence = null; this.inflightPresenceSeq = 0; + } else if (!this.pendingPresence && this.presence[''] != null) { + this.pendingPresence = []; } this.receivedPresence = Object.create(null); this.requestReplyPresence = true; diff --git a/test/client/presence.js b/test/client/presence.js index 90371cf6d..ea9c14caf 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -869,6 +869,57 @@ types.register(presenceType.type3); ], allDone); }); + it('re-synchronizes presence after reconnecting', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + this.connection.close(); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + this.backend.connect(this.connection); + process.nextTick(done); + }.bind(this), + setTimeout, // wait for re-sync + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + process.nextTick(done); + }.bind(this) + ], allDone); + }); + + it('re-synchronizes presence after resubscribing', function(allDone) { + async.series([ + this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + this.doc2.subscribe.bind(this.doc2), + this.doc.submitPresence.bind(this.doc, p(0)), + this.doc2.submitPresence.bind(this.doc2, p(1)), + async.nextTick, + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + this.doc.unsubscribe(errorHandler(done)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + this.doc.subscribe(done); + }.bind(this), + setTimeout, // wait for re-sync + function(done) { + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + process.nextTick(done); + }.bind(this) + ], allDone); + }); + it('transforms received presence against inflight and pending ops (presence.index < op.index)', function(allDone) { async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), From d67dd6a777661fad628ff84fa869f2a96d6fe6b9 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 1 May 2018 14:35:24 +0100 Subject: [PATCH 10/95] Emit presence asynchronously --- lib/client/doc.js | 5 +- test/client/presence.js | 130 +++++++++++++++++++++------------------- 2 files changed, 71 insertions(+), 64 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index fe689ef2b..f84ce65e1 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1277,7 +1277,10 @@ Doc.prototype._setPresence = function(src, data, emit) { Doc.prototype._emitPresence = function(srcList) { if (srcList && srcList.length > 0) { - this.emit('presence', srcList); + var doc = this; + process.nextTick(function() { + doc.emit('presence', srcList); + }); } }; diff --git a/test/client/presence.js b/test/client/presence.js index ea9c14caf..b5cec497a 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -295,7 +295,7 @@ types.register(presenceType.type3); this.doc.version = 1; this.doc.data = [ 'a' ]; this.doc.requestReplyPresence = false; - this.doc.submitPresence(p(0), errorHandler(done)); + this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); }); @@ -307,7 +307,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(0)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); @@ -327,7 +327,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(0)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); @@ -347,7 +347,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(2)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList).to.eql([ this.connection2.id ]); @@ -367,7 +367,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(2)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList).to.eql([ this.connection2.id ]); @@ -387,7 +387,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList).to.eql([ '' ]); @@ -407,7 +407,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, // wait for the doc2 presence message to reach doc + setTimeout, function(done) { this.doc.on('presence', function(srcList) { expect(srcList).to.eql([ this.connection2.id ]); @@ -443,7 +443,7 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), - async.nextTick, + setTimeout, function(done) { expect(this.doc2.cachedOps.length).to.equal(3); expect(this.doc2.cachedOps[0].create).to.equal(true); @@ -698,7 +698,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), - async.nextTick, + setTimeout, function(done) { expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList) { @@ -720,7 +720,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -750,7 +750,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -780,7 +780,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -806,7 +806,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -876,7 +876,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -902,7 +902,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(1)), - async.nextTick, + setTimeout, function(done) { expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); @@ -983,7 +983,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), - async.nextTick, + setTimeout, function(done) { this.doc2.on('presence', function(srcList) { expect(srcList).to.eql([ this.connection.id ]); @@ -1004,7 +1004,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), - async.nextTick, + setTimeout, function(done) { var firstCall = true; this.doc2.on('presence', function(srcList) { @@ -1048,7 +1048,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), - async.nextTick, + setTimeout, function(done) { this.doc2.on('presence', function(srcList) { if (typeName === 'wrapped-presence-no-compare') { @@ -1107,7 +1107,7 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), - async.nextTick, + setTimeout, function(done) { this.doc.on('error', done); this.connection.seq--; @@ -1125,7 +1125,7 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), - async.nextTick, + setTimeout, function(done) { this.doc.on('error', function(err) { expect(err).to.be.an(Error); @@ -1143,7 +1143,7 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), - async.nextTick, + setTimeout, function(done) { this.doc.on('error', done); // Decremented sequence number would cause the server to return an error, however, @@ -1168,7 +1168,7 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), - async.nextTick, + setTimeout, function(done) { this.doc.on('error', function(err) { if (typeName === 'wrapped-presence-no-compare') { @@ -1195,7 +1195,7 @@ types.register(presenceType.type3); async.series([ this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), - async.nextTick, + setTimeout, function(done) { var sendPresence = this.backend.sendPresence; this.backend.sendPresence = function(presence, callback) { @@ -1218,7 +1218,7 @@ types.register(presenceType.type3); async.series([ this.doc.create.bind(this.doc, [ 'c' ], typeName), this.doc.subscribe.bind(this.doc), - async.nextTick, + setTimeout, function(done) { var sendPresence = this.backend.sendPresence; this.backend.sendPresence = function(presence, callback) { @@ -1244,7 +1244,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(0)), - async.nextTick, + setTimeout, function(done) { // A hack to allow testing of hard rollback of both inflight and pending presence. var doc = this.doc; @@ -1254,30 +1254,31 @@ types.register(presenceType.type3); _handlePresence.call(doc, err, presence); }); }; + process.nextTick(done); + }.bind(this), + this.doc.submitPresence.bind(this.doc, p(1)), // inflightPresence + process.nextTick, // wait for "presence" event + this.doc.submitPresence.bind(this.doc, p(2)), // pendingPresence + process.nextTick, // wait for "presence" event + function(done) { + var presenceEmitted = false; + this.doc.on('presence', function(srcList) { + expect(presenceEmitted).to.equal(false); + presenceEmitted = true; + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + }.bind(this)); - this.doc.submitPresence(p(1)); // inflightPresence - process.nextTick(function() { - this.doc.submitPresence(p(2)); // pendingPresence - - var presenceEmitted = false; - this.doc.on('presence', function(srcList) { - expect(presenceEmitted).to.equal(false); - presenceEmitted = true; - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); - }.bind(this)); - - this.doc.on('error', function(err) { - expect(presenceEmitted).to.equal(true); - expect(err).to.be.an(Error); - expect(err.code).to.equal(4000); - done(); - }.bind(this)); - - // send an invalid op - this.doc._submit({ index: 3, value: 'b' }, true); + this.doc.on('error', function(err) { + expect(presenceEmitted).to.equal(true); + expect(err).to.be.an(Error); + expect(err.code).to.equal(4000); + done(); }.bind(this)); + + // send an invalid op + this.doc._submit({}, true); }.bind(this) ], allDone); }); @@ -1289,7 +1290,7 @@ types.register(presenceType.type3); this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.submitPresence.bind(this.doc2, p(0)), - async.nextTick, + setTimeout, function(done) { // A hack to allow testing of hard rollback of both inflight and pending presence. var doc = this.doc; @@ -1299,7 +1300,9 @@ types.register(presenceType.type3); _handlePresence.call(doc, err, presence); }); }; - + process.nextTick(done); + }.bind(this), + function(done) { var presenceEmitted = false; var called = 0; function callback(err) { @@ -1310,20 +1313,21 @@ types.register(presenceType.type3); done(); } this.doc.submitPresence(p(1), callback); // inflightPresence - process.nextTick(function() { + process.nextTick(function() { // wait for presence event this.doc.submitPresence(p(2), callback); // pendingPresence - - this.doc.on('presence', function(srcList) { - expect(presenceEmitted).to.equal(false); - presenceEmitted = true; - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + process.nextTick(function() { // wait for presence event + this.doc.on('presence', function(srcList) { + expect(presenceEmitted).to.equal(false); + presenceEmitted = true; + expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); + }.bind(this)); + this.doc.on('error', done); + + // send an invalid op + this.doc._submit({ index: 3, value: 'b' }, true, callback); }.bind(this)); - this.doc.on('error', done); - - // send an invalid op - this.doc._submit({ index: 3, value: 'b' }, true, callback); }.bind(this)); }.bind(this) ], allDone); @@ -1350,9 +1354,9 @@ types.register(presenceType.type3); this.doc2.requestReplyPresence = false; this.doc2.submitPresence(p(0), done); }.bind(this), - async.nextTick, // wait for presence to reach this.doc + setTimeout, this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), // forces processing of all received presence - async.nextTick, // wait for op to reach this.doc + setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); From e8ec2158a46c6cc9c3e1f90c5909723e1acb5580 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 9 May 2018 12:53:18 +0100 Subject: [PATCH 11/95] Add `submitted` param to `presence` event --- README.md | 4 +- lib/client/doc.js | 14 ++--- test/client/presence.js | 130 +++++++++++++++++++++++++--------------- 3 files changed, 92 insertions(+), 56 deletions(-) diff --git a/README.md b/README.md index 3cbdea6e8..2c6dc1293 100644 --- a/README.md +++ b/README.md @@ -258,8 +258,8 @@ An operation was applied to the data. `source` will be `false` for ops received `doc.on('del', function(data, source) {...})` The document was deleted. Document contents before deletion are passed in as an argument. `source` will be `false` for ops received from the server and defaults to `true` for ops generated locally. -`doc.on('presence', function(srcList) {...})` -Presence data has changed. `srcList` is an Array of `doc.presence` property names for which values have changed. +`doc.on('presence', function(srcList, submitted) {...})` +Presence data has changed. `srcList` is an Array of `doc.presence` property names for which values have changed. `submitted` is `true`, if the event is the result of new presence data being submitted by the local or remote user, otherwise it is `false` - eg if the presence data was transformed against an operation or was cleared on unsubscribe, disconnect or roll-back. `doc.on('error', function(err) {...})` There was an error fetching the document or applying an operation. diff --git a/lib/client/doc.js b/lib/client/doc.js index f84ce65e1..fa35b4926 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -991,7 +991,7 @@ Doc.prototype._hardRollback = function(err) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList); + this._emitPresence(changedSrcList, false); // Fetch the latest from the server to get us back into a working state var doc = this; @@ -1208,7 +1208,7 @@ Doc.prototype._processAllReceivedPresence = function() { changedSrcList.push(src); } } - this._emitPresence(changedSrcList); + this._emitPresence(changedSrcList, true); }; Doc.prototype._transformPresence = function(src, op) { @@ -1231,7 +1231,7 @@ Doc.prototype._transformAllPresence = function(op) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList); + this._emitPresence(changedSrcList, false); }; Doc.prototype._pausePresence = function() { @@ -1255,7 +1255,7 @@ Doc.prototype._pausePresence = function() { changedSrcList.push(src); } } - this._emitPresence(changedSrcList); + this._emitPresence(changedSrcList, false); }; // If emit is true and presence has changed, emits a presence event. @@ -1271,15 +1271,15 @@ Doc.prototype._setPresence = function(src, data, emit) { if (isPresenceEqual) return false; this.presence[src] = data; } - if (emit) this._emitPresence([ src ]); + if (emit) this._emitPresence([ src ], true); return true; }; -Doc.prototype._emitPresence = function(srcList) { +Doc.prototype._emitPresence = function(srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this; process.nextTick(function() { - doc.emit('presence', srcList); + doc.emit('presence', srcList, submitted); }); } }; diff --git a/test/client/presence.js b/test/client/presence.js index b5cec497a..3ddc86bff 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -40,8 +40,9 @@ types.register(presenceType.type3); function(done) { this.doc.requestReplyPresence = false; this.doc.submitPresence(p(1), errorHandler(done)); - this.doc2.once('presence', function(srcList) { + this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -60,8 +61,9 @@ types.register(presenceType.type3); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); this.doc.requestReplyPresence = false; this.doc.submitPresence(p(1), errorHandler(done)); - this.doc2.once('presence', function(srcList) { + this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -76,8 +78,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -103,8 +106,9 @@ types.register(presenceType.type3); this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); @@ -126,8 +130,9 @@ types.register(presenceType.type3); this.doc.submitOp.bind(this.doc, { index: 1, value: 'c' }), this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); @@ -149,8 +154,9 @@ types.register(presenceType.type3); this.doc.submitOp.bind(this.doc, { index: 0, value: 'b' }), this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); @@ -172,8 +178,9 @@ types.register(presenceType.type3); this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), this.doc2.submitOp.bind(this.doc2, { index: 2, value: 'c' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); @@ -195,8 +202,9 @@ types.register(presenceType.type3); this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'c' }), this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -218,8 +226,9 @@ types.register(presenceType.type3); this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'b' }), this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'a' }), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); @@ -242,8 +251,9 @@ types.register(presenceType.type3); this.doc.del.bind(this.doc), this.doc.create.bind(this.doc, [ 'b' ], typeName), function(done) { - this.doc2.once('presence', function(srcList) { + this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); @@ -252,8 +262,9 @@ types.register(presenceType.type3); this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); @@ -274,8 +285,9 @@ types.register(presenceType.type3); this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), function(done) { - this.doc2.once('presence', function(srcList) { + this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); @@ -285,8 +297,9 @@ types.register(presenceType.type3); }.bind(this), function(done) { this.doc2.cachedOps = []; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); @@ -309,8 +322,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(0)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); @@ -329,8 +343,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(0)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); @@ -349,8 +364,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(2)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); @@ -369,8 +385,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(2)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); @@ -389,8 +406,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); + expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(2)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); done(); @@ -409,8 +427,9 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(1)); expect(this.doc.presence[this.connection2.id]).to.eql(p(2)); done(); @@ -508,9 +527,10 @@ types.register(presenceType.type3); this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); + expect(submitted).to.equal(true); expect(this.doc2.presence['']).to.eql(p(1)); expect(this.doc2.presence).to.not.have.key(this.connection.id); } else { @@ -595,8 +615,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); @@ -613,8 +634,9 @@ types.register(presenceType.type3); this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); @@ -636,8 +658,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); @@ -657,8 +680,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); @@ -701,13 +725,16 @@ types.register(presenceType.type3); setTimeout, function(done) { expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + // The call to `del` transforms the presence and fires the event. + // The call to `submitPresence` does not fire the event because presence is already null. + expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; - this.doc.submitPresence(p(0), errorHandler(done)); + this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.del(errorHandler(done)); }.bind(this) ], allDone); @@ -728,17 +755,14 @@ types.register(presenceType.type3); expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); + expect(submitted).to.equal(true); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); - - // this.doc.requestReplyPresence = false; - // this.doc.submitPresence(p(0), errorHandler(done)); - // this.doc2.del(errorHandler(done)); }.bind(this) ], allDone); }); @@ -758,17 +782,14 @@ types.register(presenceType.type3); expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); + expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); - - // this.doc.requestReplyPresence = false; - // this.doc.submitPresence(p(0), errorHandler(done)); - // this.doc2.del(errorHandler(done)); }.bind(this) ], allDone); }); @@ -788,8 +809,9 @@ types.register(presenceType.type3); expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); + expect(submitted).to.equal(true); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); done(); @@ -814,8 +836,9 @@ types.register(presenceType.type3); expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); + expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); done(); @@ -926,8 +949,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); @@ -945,8 +969,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); @@ -964,8 +989,9 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); @@ -985,8 +1011,11 @@ types.register(presenceType.type3); this.doc.submitPresence.bind(this.doc, p(1)), setTimeout, function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); + // The call to `del` transforms the presence and fires the event. + // The call to `submitPresence` does not fire the event because presence is already null. + expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); @@ -1007,9 +1036,12 @@ types.register(presenceType.type3); setTimeout, function(done) { var firstCall = true; - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { if (firstCall) return firstCall = false; expect(srcList).to.eql([ this.connection.id ]); + // The call to `del` transforms the presence and fires the event. + // The call to `submitPresence` does not fire the event because presence is already null. + expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); @@ -1028,9 +1060,10 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(1)), function(done) { - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); + expect(submitted).to.equal(true); expect(this.doc.presence['']).to.eql(p(1)); done(); } else { @@ -1050,9 +1083,10 @@ types.register(presenceType.type3); this.doc.submitPresence.bind(this.doc, p(1)), setTimeout, function(done) { - this.doc2.on('presence', function(srcList) { + this.doc2.on('presence', function(srcList, submitted) { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); + expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); } else { @@ -1262,10 +1296,11 @@ types.register(presenceType.type3); process.nextTick, // wait for "presence" event function(done) { var presenceEmitted = false; - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(presenceEmitted).to.equal(false); presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); @@ -1316,10 +1351,11 @@ types.register(presenceType.type3); process.nextTick(function() { // wait for presence event this.doc.submitPresence(p(2), callback); // pendingPresence process.nextTick(function() { // wait for presence event - this.doc.on('presence', function(srcList) { + this.doc.on('presence', function(srcList, submitted) { expect(presenceEmitted).to.equal(false); presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); From 173bf3a58379e61edaacb75ccfa14b69bb5d55af Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Wed, 13 Jun 2018 23:44:42 +0100 Subject: [PATCH 12/95] Use the correct variable The issue could not cause problems in practice because ot-json0 does not support presence. --- lib/client/doc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index fa35b4926..039adcf7f 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -616,7 +616,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); - this._transformAllPresence(op); + this._transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op From 054d34d90e870277372df80293bff03aeb820cd3 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 21 Jun 2018 11:43:00 +0100 Subject: [PATCH 13/95] Small test update --- test/client/presence.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/client/presence.js b/test/client/presence.js index 3ddc86bff..d74532a49 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1313,7 +1313,7 @@ types.register(presenceType.type3); }.bind(this)); // send an invalid op - this.doc._submit({}, true); + this.doc._submit({}, null); }.bind(this) ], allDone); }); @@ -1362,7 +1362,7 @@ types.register(presenceType.type3); this.doc.on('error', done); // send an invalid op - this.doc._submit({ index: 3, value: 'b' }, true, callback); + this.doc._submit({ index: 3, value: 'b' }, null, callback); }.bind(this)); }.bind(this)); }.bind(this) From 56b726bd0f97fee321ecb363a993ee0e1700f65f Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 14:30:09 +0200 Subject: [PATCH 14/95] Make hasPending depend on inflightPresence and pendingPresence --- lib/client/doc.js | 5 ++++- test/client/presence.js | 42 ++++++++++++++++++++++++++++++++--------- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 180f821d7..b07554353 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -260,7 +260,9 @@ Doc.prototype.hasPending = function() { this.inflightFetch.length || this.inflightSubscribe.length || this.inflightUnsubscribe.length || - this.pendingFetch.length + this.pendingFetch.length || + this.inflightPresence || + this.pendingPresence ); }; @@ -1077,6 +1079,7 @@ Doc.prototype._handlePresence = function(err, presence) { var called = callbacks && callEach(callbacks, err); if (err && !called) this.emit('error', err); this.flush(); + this._emitNothingPending(); } return; } diff --git a/test/client/presence.js b/test/client/presence.js index ca07c9b8b..4f9a3d31e 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -692,26 +692,50 @@ types.register(presenceType.type3); ], allDone); }); - it.skip('cancels pending presence on destroy', function(allDone) { + it('hasPending is true, if there is pending presence', function(allDone) { async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.subscribe.bind(this.doc), + function(done) { + expect(this.doc.hasPending()).to.equal(false); + this.doc.submitPresence(p(0)); + expect(this.doc.hasPending()).to.equal(true); + expect(!!this.doc.pendingPresence).to.equal(true); + expect(!!this.doc.inflightPresence).to.equal(false); + this.doc.whenNothingPending(done); + }.bind(this), function(done) { - this.doc.submitPresence(p(0), done); - console.log(!!this.doc.inflightPresence, !!this.doc.pendingPresence); - this.doc.destroy(errorHandler(done)); + expect(this.doc.hasPending()).to.equal(false); + expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.inflightPresence).to.equal(false); + done(); }.bind(this) ], allDone); }); - it.skip('cancels inflight presence on destroy', function(allDone) { + it('hasPending is true, if there is inflight presence', function(allDone) { async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.subscribe.bind(this.doc), function(done) { - this.doc.submitPresence(p(0), done); - process.nextTick(function() { - this.doc.destroy(errorHandler(done)); - }.bind(this)); + expect(this.doc.hasPending()).to.equal(false); + this.doc.submitPresence(p(0)); + expect(this.doc.hasPending()).to.equal(true); + expect(!!this.doc.pendingPresence).to.equal(true); + expect(!!this.doc.inflightPresence).to.equal(false); + process.nextTick(done); + }.bind(this), + function(done) { + expect(this.doc.hasPending()).to.equal(true); + expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.inflightPresence).to.equal(true); + this.doc.whenNothingPending(done); + }.bind(this), + function(done) { + expect(this.doc.hasPending()).to.equal(false); + expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.inflightPresence).to.equal(false); + done(); }.bind(this) ], allDone); }); From 762496aae09e90468c51fe6f1b88dddfb4cbde1b Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 10 Jul 2018 11:30:02 +0200 Subject: [PATCH 15/95] Remove cached ops without using setTimeout See https://github.com/share/sharedb/issues/219 --- lib/client/doc.js | 20 ++++++++++++--- test/client/presence.js | 54 +++++++++++++++++++---------------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index b07554353..42d8d37dc 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -83,7 +83,7 @@ function Doc(connection, collection, id) { this.requestReplyPresence = true; // A list of ops sent by the server. These are needed for transforming presence data, // if we get that presence data for an older version of the document. - // The ops are cached for 1 minute by default, which should be lots, considering that the presence + // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence // data is supposed to be synced in real-time. this.cachedOps = []; this.cachedOpsTimeout = 60000; @@ -362,6 +362,7 @@ Doc.prototype._handleOp = function(err, message) { var serverOp = { src: message.src, + time: Date.now(), create: !!message.create, op: message.op, del: !!message.del @@ -914,6 +915,7 @@ Doc.prototype._opAcknowledged = function(message) { this.version++; this._cacheOp({ src: this.inflightOp.src, + time: Date.now(), create: !!this.inflightOp.create, op: this.inflightOp.op, del: !!this.inflightOp.del @@ -1283,8 +1285,18 @@ Doc.prototype._emitPresence = function(srcList, submitted) { }; Doc.prototype._cacheOp = function(op) { + // Remove the old ops. + var oldOpTime = Date.now() - this.cachedOpsTimeout; + var i; + for (i = 0; i < this.cachedOps.length; i++) { + if (this.cachedOps[i].time >= oldOpTime) { + break; + } + } + if (i > 0) { + this.cachedOps.splice(0, i); + } + + // Cache the new op. this.cachedOps.push(op); - setTimeout(function() { - if (this.cachedOps[0] === op) this.cachedOps.shift(); - }.bind(this), this.cachedOpsTimeout); }; diff --git a/test/client/presence.js b/test/client/presence.js index 4f9a3d31e..2bf7e7dab 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -473,48 +473,44 @@ types.register(presenceType.type3); ], allDone); }); - it('removes cached ops', function(allDone) { - var op = { index: 1, value: 'b' }; - this.doc.cachedOpsTimeout = 0; + it('expires cached ops', function(allDone) { + var op1 = { index: 1, value: 'b' }; + var op2 = { index: 2, value: 'b' }; + var op3 = { index: 3, value: 'b' }; + this.doc.cachedOpsTimeout = 60; async.series([ + // Cache 2 ops. this.doc.create.bind(this.doc, [ 'a' ], typeName), - this.doc.submitOp.bind(this.doc, op), - this.doc.del.bind(this.doc), + this.doc.submitOp.bind(this.doc, op1), function(done) { - expect(this.doc.cachedOps.length).to.equal(3); + expect(this.doc.cachedOps.length).to.equal(2); expect(this.doc.cachedOps[0].create).to.equal(true); - expect(this.doc.cachedOps[1].op).to.equal(op); - expect(this.doc.cachedOps[2].del).to.equal(true); + expect(this.doc.cachedOps[1].op).to.equal(op1); done(); }.bind(this), - setTimeout, - function(done) { - expect(this.doc.cachedOps.length).to.equal(0); - done(); - }.bind(this) - ], allDone); - }); - it('removes correct cached ops', function(allDone) { - var op = { index: 1, value: 'b' }; - this.doc.cachedOpsTimeout = 0; - async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), - this.doc.submitOp.bind(this.doc, op), - this.doc.del.bind(this.doc), + // Cache another op before the first 2 expire. + function (callback) { + setTimeout(callback, 30); + }, + this.doc.submitOp.bind(this.doc, op2), function(done) { expect(this.doc.cachedOps.length).to.equal(3); expect(this.doc.cachedOps[0].create).to.equal(true); - expect(this.doc.cachedOps[1].op).to.equal(op); - expect(this.doc.cachedOps[2].del).to.equal(true); - this.doc.cachedOps.shift(); - this.doc.cachedOps.push({ op: true }); + expect(this.doc.cachedOps[1].op).to.equal(op1); + expect(this.doc.cachedOps[2].op).to.equal(op2); done(); }.bind(this), - setTimeout, + + // Cache another op after the first 2 expire. + function (callback) { + setTimeout(callback, 31); + }, + this.doc.submitOp.bind(this.doc, op3), function(done) { - expect(this.doc.cachedOps.length).to.equal(1); - expect(this.doc.cachedOps[0].op).to.equal(true); + expect(this.doc.cachedOps.length).to.equal(2); + expect(this.doc.cachedOps[0].op).to.equal(op2); + expect(this.doc.cachedOps[1].op).to.equal(op3); done(); }.bind(this) ], allDone); From e4c5e6d827656fe3781be702cdd6b8fc7f512a02 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 13:44:05 +0200 Subject: [PATCH 16/95] Remove --exit mocha option --- test/mocha.opts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/mocha.opts b/test/mocha.opts index 7ca4707b0..34f904192 100644 --- a/test/mocha.opts +++ b/test/mocha.opts @@ -1,4 +1,3 @@ --reporter spec --check-leaks --recursive ---exit From 428c46a61b6ea5fdefb440a3740f677051d7ed8e Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 13:50:46 +0200 Subject: [PATCH 17/95] Workaround for circular dependency --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 21efafe46..736e5fe78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ node_js: - "10" - "8" - "6" -script: "npm run jshint && npm run test-cover" +script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From f43b75281afb325cc2188b48e4561aeaed6f56ab Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:09:47 +0530 Subject: [PATCH 18/95] Restore tests to working order --- lib/client/doc.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index d85560556..8cde2f2fa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -981,7 +981,7 @@ Doc.prototype._hardRollback = function(err) { if (this.inflightOp) pendingOps.push(this.inflightOp); pendingOps = pendingOps.concat(this.pendingOps); - // Apply the same technique for presence. + // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; if (this.inflightPresence) pendingPresence.push(this.inflightPresence); if (this.pendingPresence) pendingPresence.push(this.pendingPresence); @@ -991,6 +991,11 @@ Doc.prototype._hardRollback = function(err) { this.version = null; this.inflightOp = null; this.pendingOps = []; + + // Reset presence-related properties. + this.inflightPresence = null; + this.inflightPresenceSeq = 0; + this.pendingPresence = null; this.cachedOps.length = 0; this.receivedPresence = Object.create(null); this.requestReplyPresence = true; @@ -1011,21 +1016,22 @@ Doc.prototype._hardRollback = function(err) { // We want to check that no errors are swallowed, so we check that: // - there are callbacks to call, and // - that every single pending op called a callback - // If there are no ops queued, or one of them didn't handle the error, - // then we emit the error. var allOpsHadCallbacks = !!pendingOps.length; for (var i = 0; i < pendingOps.length; i++) { allOpsHadCallbacks = callEach(pendingOps[i].callbacks, err) && allOpsHadCallbacks; } - if (err && !allOpsHadCallbacks) return doc.emit('error', err); // Apply the same technique for presence. var allPresenceHadCallbacks = !!pendingPresence.length; for (var i = 0; i < pendingPresence.length; i++) { - console.log(pendingPresence[i]) - allPresenceHadCallbacks = callEach(pendingPresence[i].callbacks, err) && allPresenceHadCallbacks; + allPresenceHadCallbacks = callEach(pendingPresence[i], err) && allPresenceHadCallbacks; + } + + // If there are no ops or presence queued, or one of them didn't handle the error, + // then we emit the error. + if (err && !allOpsHadCallbacks && !allPresenceHadCallbacks) { + return doc.emit('error', err); } - if (err && !allPresenceHadCallbacks) return doc.emit('error', err); }); }; From 940942955803abd5cbf97dc8d6a0325b7a7c3e98 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:11:58 +0530 Subject: [PATCH 19/95] Remove extraneous .editorconfig --- .editorconfig | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index e29f5e504..000000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -indent_style = space -indent_size = 2 -end_of_line = LF -charset = utf-8 -trim_trailing_whitespace = true -insert_final_newline = true From c4cf1b8a9c4eab92da6e3448e2fa7dbf5503573a Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:14:46 +0530 Subject: [PATCH 20/95] Revert extraneous changes in .travis.yml and package.json --- .travis.yml | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 736e5fe78..21efafe46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ node_js: - "10" - "8" - "6" -script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" +script: "npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/package.json b/package.json index 0a1d5bfaa..76dc3f878 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "jshint": "^2.9.2", "lolex": "^3.0.0", "mocha": "^5.2.0", - "sinon": "^6.1.5", - "sharedb-mingo-memory": "^1.0.0-beta" + "sinon": "^6.1.5" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run jshint", From 237d2ad4356c7dbff4a70871bf924a002bf60f12 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 11:59:04 +0530 Subject: [PATCH 21/95] Use lolex to make 'expires cached ops' test more stable. --- test/client/presence.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/client/presence.js b/test/client/presence.js index 9c216c980..ce021add9 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1,4 +1,5 @@ var async = require('async'); +var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); @@ -474,6 +475,7 @@ types.register(presenceType.type3); }); it('expires cached ops', function(allDone) { + var clock = lolex.install(); var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; @@ -492,6 +494,7 @@ types.register(presenceType.type3); // Cache another op before the first 2 expire. function (callback) { setTimeout(callback, 30); + clock.next(); }, this.doc.submitOp.bind(this.doc, op2), function(done) { @@ -505,15 +508,20 @@ types.register(presenceType.type3); // Cache another op after the first 2 expire. function (callback) { setTimeout(callback, 31); + clock.next(); }, this.doc.submitOp.bind(this.doc, op3), function(done) { + console.log('a'); + console.log('b'); expect(this.doc.cachedOps.length).to.equal(2); expect(this.doc.cachedOps[0].op).to.equal(op2); expect(this.doc.cachedOps[1].op).to.equal(op3); + clock.uninstall(); done(); }.bind(this) ], allDone); + console.log('runAll'); }); it('requests reply presence when sending presence for the first time', function(allDone) { From c8d35c5846ad190ea01286008cf55e95d99b98c9 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:07:19 +0530 Subject: [PATCH 22/95] Move doc.presence to doc.presence.current --- README.md | 4 +- lib/client/doc.js | 26 +++--- test/client/presence.js | 173 ++++++++++++++++++++-------------------- 3 files changed, 101 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index fd906bddb..1f8dfffb4 100644 --- a/README.md +++ b/README.md @@ -313,8 +313,8 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. -`doc.presence` _(Object)_ -Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. +`doc.presence.current` _(Object)_ +Each property under `doc.presence.current` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. diff --git a/lib/client/doc.js b/lib/client/doc.js index 8cde2f2fa..359b864fa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -70,7 +70,9 @@ function Doc(connection, collection, id) { // The current presence data // Map of src -> presence data // Local src === '' - this.presence = Object.create(null); + this.presence = { + current: Object.create(null) + }; // The presence objects received from the server // Map of src -> presence this.receivedPresence = Object.create(null); @@ -514,7 +516,7 @@ Doc.prototype.flush = function() { this.inflightPresence = this.pendingPresence; this.inflightPresenceSeq = this.connection.seq; this.pendingPresence = null; - this.connection.sendPresence(this, this.presence[''], this.requestReplyPresence); + this.connection.sendPresence(this, this.presence.current[''], this.requestReplyPresence); this.requestReplyPresence = false; } }; @@ -1000,7 +1002,7 @@ Doc.prototype._hardRollback = function(err) { this.receivedPresence = Object.create(null); this.requestReplyPresence = true; - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1247,7 +1249,7 @@ Doc.prototype._processAllReceivedPresence = function() { }; Doc.prototype._transformPresence = function(src, op) { - var presenceData = this.presence[src]; + var presenceData = this.presence.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); @@ -1258,7 +1260,7 @@ Doc.prototype._transformPresence = function(src, op) { }; Doc.prototype._transformAllPresence = function(op) { - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1277,12 +1279,12 @@ Doc.prototype._pausePresence = function() { this.inflightPresence; this.inflightPresence = null; this.inflightPresenceSeq = 0; - } else if (!this.pendingPresence && this.presence[''] != null) { + } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } this.receivedPresence = Object.create(null); this.requestReplyPresence = true; - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1297,14 +1299,14 @@ Doc.prototype._pausePresence = function() { // Returns true, if presence has changed. Otherwise false. Doc.prototype._setPresence = function(src, data, emit) { if (data == null) { - if (this.presence[src] == null) return false; - delete this.presence[src]; + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; } else { var isPresenceEqual = - this.presence[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence[src], data)); + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); if (isPresenceEqual) return false; - this.presence[src] = data; + this.presence.current[src] = data; } if (emit) this._emitPresence([ src ], true); return true; diff --git a/test/client/presence.js b/test/client/presence.js index ce021add9..ad2ddcc90 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -45,7 +45,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -66,7 +66,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -83,7 +83,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for a future version. @@ -111,7 +111,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -135,7 +135,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -159,7 +159,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -183,7 +183,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -207,7 +207,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -231,7 +231,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -256,7 +256,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -267,7 +267,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -290,7 +290,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -302,7 +302,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -326,8 +326,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -347,8 +347,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -368,8 +368,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -389,8 +389,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -410,8 +410,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(false); - expect(this.doc.presence['']).to.eql(p(2)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(2)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -431,8 +431,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence['']).to.eql(p(1)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(2)); + expect(this.doc.presence.current['']).to.eql(p(1)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -512,8 +512,6 @@ types.register(presenceType.type3); }, this.doc.submitOp.bind(this.doc, op3), function(done) { - console.log('a'); - console.log('b'); expect(this.doc.cachedOps.length).to.equal(2); expect(this.doc.cachedOps[0].op).to.equal(op2); expect(this.doc.cachedOps[1].op).to.equal(op3); @@ -521,7 +519,6 @@ types.register(presenceType.type3); done(); }.bind(this) ], allDone); - console.log('runAll'); }); it('requests reply presence when sending presence for the first time', function(allDone) { @@ -535,12 +532,12 @@ types.register(presenceType.type3); if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc2.presence['']).to.eql(p(1)); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); } else { expect(srcList).to.eql([ this.connection.id ]); - expect(this.doc2.presence['']).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); expect(this.doc2.requestReplyPresence).to.equal(false); done(); } @@ -622,7 +619,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -641,7 +638,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -649,7 +646,7 @@ types.register(presenceType.type3); setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2.presence).to.eql({}); + expect(this.doc2.presence.current).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -665,7 +662,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -687,7 +684,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -752,13 +749,13 @@ types.register(presenceType.type3); this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -777,17 +774,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence).to.not.have.key(connectionId); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -804,17 +801,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence).to.not.have.key(connectionId); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -831,17 +828,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence).to.not.have.key(connectionId); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -858,17 +855,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence).to.not.have.key(connectionId); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -929,18 +926,18 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -955,17 +952,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -980,7 +977,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1000,7 +997,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1020,7 +1017,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1044,7 +1041,7 @@ types.register(presenceType.type3); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1070,7 +1067,7 @@ types.register(presenceType.type3); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1092,7 +1089,7 @@ types.register(presenceType.type3); if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1115,7 +1112,7 @@ types.register(presenceType.type3); if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1329,8 +1326,8 @@ types.register(presenceType.type3); presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1384,8 +1381,8 @@ types.register(presenceType.type3); presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence).to.not.have.key(''); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1423,7 +1420,7 @@ types.register(presenceType.type3); setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1434,7 +1431,7 @@ types.register(presenceType.type3); process.nextTick(done); }.bind(this), function(done) { - expect(this.doc.presence[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); From 3efb82c6d76e77a332de1e78731ecc063fbf6ca8 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:09:06 +0530 Subject: [PATCH 23/95] Move doc.receivedPresence to doc.presence.received --- lib/client/doc.js | 38 +++++++++++++++++++------------------- test/client/presence.js | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 359b864fa..b169e0763 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -75,12 +75,12 @@ function Doc(connection, collection, id) { }; // The presence objects received from the server // Map of src -> presence - this.receivedPresence = Object.create(null); - // The minimum amount of time to wait before removing processed presence from this.receivedPresence. + this.presence.received = Object.create(null); + // The minimum amount of time to wait before removing processed presence from this.presence.received. // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower // sequence number arrive after messages with higher sequence numbers. - this.receivedPresenceTimeout = 60000; + this.presence.receivedTimeout = 60000; // If set to true, then the next time the local presence is sent, // all other clients will be asked to reply with their own presence data. this.requestReplyPresence = true; @@ -148,13 +148,13 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.receivedPresence = Object.create(null); + doc.presence.received = Object.create(null); doc.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.receivedPresence = Object.create(null); + doc.presence.received = Object.create(null); doc.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); @@ -999,7 +999,7 @@ Doc.prototype._hardRollback = function(err) { this.inflightPresenceSeq = 0; this.pendingPresence = null; this.cachedOps.length = 0; - this.receivedPresence = Object.create(null); + this.presence.received = Object.create(null); this.requestReplyPresence = true; var srcList = Object.keys(this.presence.current); @@ -1134,13 +1134,13 @@ Doc.prototype._handlePresence = function(err, presence) { // Ignore older messages which arrived out of order if ( - this.receivedPresence[src] && ( - this.receivedPresence[src].seq > presence.seq || - (this.receivedPresence[src].seq === presence.seq && presence.v != null) + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) ) ) return; - this.receivedPresence[src] = presence; + this.presence.received[src] = presence; if (presence.v == null) { // null version should happen only when the server automatically sends @@ -1159,13 +1159,13 @@ Doc.prototype._handlePresence = function(err, presence) { // Returns true, if presence has changed for src. Otherwise false. Doc.prototype._processReceivedPresence = function(src, emit) { if (!src) return false; - var presence = this.receivedPresence[src]; + var presence = this.presence.received[src]; if (!presence) return false; if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.receivedPresenceTimeout) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { // Remove old received and processed presence - delete this.receivedPresence[src]; + delete this.presence.received[src]; } return false; } @@ -1185,14 +1185,14 @@ Doc.prototype._processReceivedPresence = function(src, emit) { } if (this.inflightOp && this.inflightOp.op == null) { - // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } for (var i = 0; i < this.pendingOps.length; i++) { if (this.pendingOps[i].op == null) { - // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } @@ -1200,14 +1200,14 @@ Doc.prototype._processReceivedPresence = function(src, emit) { var startIndex = this.cachedOps.length - (this.version - presence.v); if (startIndex < 0) { - // Remove presence data because we can't transform receivedPresence + // Remove presence data because we can't transform presence.received presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } for (var i = startIndex; i < this.cachedOps.length; i++) { if (this.cachedOps[i].op == null) { - // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } @@ -1237,7 +1237,7 @@ Doc.prototype._processReceivedPresence = function(src, emit) { }; Doc.prototype._processAllReceivedPresence = function() { - var srcList = Object.keys(this.receivedPresence); + var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1282,7 +1282,7 @@ Doc.prototype._pausePresence = function() { } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } - this.receivedPresence = Object.create(null); + this.presence.received = Object.create(null); this.requestReplyPresence = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; diff --git a/test/client/presence.js b/test/client/presence.js index ad2ddcc90..44f6b9ca8 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1405,7 +1405,7 @@ types.register(presenceType.type3); return handleMessage.apply(this, arguments); }; if (expireCache) { - this.doc.receivedPresenceTimeout = 0; + this.doc.presence.receivedTimeout = 0; } async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), From f0451e3b70abf2800f24768b06282e7076ec380c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:10:35 +0530 Subject: [PATCH 24/95] Move doc.requestReplyPresence to doc.presence.requestReply --- lib/client/doc.js | 10 ++++----- test/client/presence.js | 50 ++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index b169e0763..7f953c403 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -83,7 +83,7 @@ function Doc(connection, collection, id) { this.presence.receivedTimeout = 60000; // If set to true, then the next time the local presence is sent, // all other clients will be asked to reply with their own presence data. - this.requestReplyPresence = true; + this.presence.requestReply = true; // A list of ops sent by the server. These are needed for transforming presence data, // if we get that presence data for an older version of the document. // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence @@ -516,8 +516,8 @@ Doc.prototype.flush = function() { this.inflightPresence = this.pendingPresence; this.inflightPresenceSeq = this.connection.seq; this.pendingPresence = null; - this.connection.sendPresence(this, this.presence.current[''], this.requestReplyPresence); - this.requestReplyPresence = false; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; } }; @@ -1000,7 +1000,7 @@ Doc.prototype._hardRollback = function(err) { this.pendingPresence = null; this.cachedOps.length = 0; this.presence.received = Object.create(null); - this.requestReplyPresence = true; + this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; @@ -1283,7 +1283,7 @@ Doc.prototype._pausePresence = function() { this.pendingPresence = []; } this.presence.received = Object.create(null); - this.requestReplyPresence = true; + this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { diff --git a/test/client/presence.js b/test/client/presence.js index 44f6b9ca8..902be9752 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -39,7 +39,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); @@ -60,7 +60,7 @@ types.register(presenceType.type3); function(done) { this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); @@ -88,7 +88,7 @@ types.register(presenceType.type3); }.bind(this)); // A hack to send presence for a future version. this.doc.version += 2; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), function(err) { if (err) return done(err); this.doc.version -= 2; @@ -117,7 +117,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -141,7 +141,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -165,7 +165,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -189,7 +189,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -213,7 +213,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -237,7 +237,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -259,7 +259,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -272,7 +272,7 @@ types.register(presenceType.type3); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 2; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -293,7 +293,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -308,7 +308,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -538,7 +538,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(this.doc2.presence.current['']).to.eql(p(1)); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.requestReplyPresence).to.equal(false); + expect(this.doc2.presence.requestReply).to.equal(false); done(); } }.bind(this)); @@ -622,7 +622,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); this.doc.submitPresence(p(1), errorHandler(done)); this.doc.submitPresence(p(2), errorHandler(done)); @@ -641,7 +641,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); setTimeout(function() { this.doc.subscribe(function(err) { @@ -669,7 +669,7 @@ types.register(presenceType.type3); this.doc.submitPresence(p(1), errorHandler(done)); process.nextTick(function() { this.backend.connect(this.connection); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; }.bind(this)); }.bind(this) ], allDone); @@ -687,7 +687,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -758,7 +758,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.del(errorHandler(done)); }.bind(this) @@ -980,7 +980,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) this.doc2.submitOp({ index: 2, value: 'c' }, errorHandler(done)) @@ -1000,7 +1000,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.submitOp({ index: 1, value: 'c' }, errorHandler(done)) this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) @@ -1020,7 +1020,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)) this.doc2.submitOp({ index: 0, value: 'a' }, errorHandler(done)) @@ -1044,7 +1044,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.del(errorHandler(done)); this.doc2.create([ 'c' ], typeName, errorHandler(done)); @@ -1070,7 +1070,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); this.doc2.del(errorHandler(done)); @@ -1412,7 +1412,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.requestReplyPresence = false; + this.doc2.presence.requestReply = false; this.doc2.submitPresence(p(0), done); }.bind(this), setTimeout, From 5217635b167bd462831344e297ac8e0025602486 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:11:57 +0530 Subject: [PATCH 25/95] Move doc.cachedOps to doc.presence.cachedOps --- lib/client/doc.js | 34 +++++++++++++++++----------------- test/client/presence.js | 40 ++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 7f953c403..3abde219e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -88,8 +88,8 @@ function Doc(connection, collection, id) { // if we get that presence data for an older version of the document. // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence // data is supposed to be synced in real-time. - this.cachedOps = []; - this.cachedOpsTimeout = 60000; + this.presence.cachedOps = []; + this.presence.cachedOpsTimeout = 60000; // The sequence number of the inflight presence request. this.inflightPresenceSeq = 0; @@ -149,13 +149,13 @@ Doc.prototype.destroy = function(callback) { return doc.emit('error', err); } doc.presence.received = Object.create(null); - doc.cachedOps.length = 0; + doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { doc.presence.received = Object.create(null); - doc.cachedOps.length = 0; + doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -236,7 +236,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); this.data = (this.type && this.type.deserialize) ? @@ -913,7 +913,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; } else if (message.v !== this.version) { // We should already be at the same version, because the server should @@ -998,7 +998,7 @@ Doc.prototype._hardRollback = function(err) { this.inflightPresence = null; this.inflightPresenceSeq = 0; this.pendingPresence = null; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); this.presence.requestReply = true; @@ -1198,15 +1198,15 @@ Doc.prototype._processReceivedPresence = function(src, emit) { } } - var startIndex = this.cachedOps.length - (this.version - presence.v); + var startIndex = this.presence.cachedOps.length - (this.version - presence.v); if (startIndex < 0) { // Remove presence data because we can't transform presence.received presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } - for (var i = startIndex; i < this.cachedOps.length; i++) { - if (this.cachedOps[i].op == null) { + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); return this._setPresence(src, null, emit); @@ -1217,8 +1217,8 @@ Doc.prototype._processReceivedPresence = function(src, emit) { var data = this.type.createPresence(presence.p); // Transform against past ops - for (var i = startIndex; i < this.cachedOps.length; i++) { - var op = this.cachedOps[i]; + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.cachedOps[i]; data = this.type.transformPresence(data, op.op, presence.src === op.src); } @@ -1323,17 +1323,17 @@ Doc.prototype._emitPresence = function(srcList, submitted) { Doc.prototype._cacheOp = function(op) { // Remove the old ops. - var oldOpTime = Date.now() - this.cachedOpsTimeout; + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; var i; - for (i = 0; i < this.cachedOps.length; i++) { - if (this.cachedOps[i].time >= oldOpTime) { + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { break; } } if (i > 0) { - this.cachedOps.splice(0, i); + this.presence.cachedOps.splice(0, i); } // Cache the new op. - this.cachedOps.push(op); + this.presence.cachedOps.push(op); }; diff --git a/test/client/presence.js b/test/client/presence.js index 902be9752..27f499227 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -297,7 +297,7 @@ types.register(presenceType.type3); this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { - this.doc2.cachedOps = []; + this.doc2.presence.cachedOps = []; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); @@ -447,10 +447,10 @@ types.register(presenceType.type3); this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), function(done) { - expect(this.doc.cachedOps.length).to.equal(3); - expect(this.doc.cachedOps[0].create).to.equal(true); - expect(this.doc.cachedOps[1].op).to.equal(op); - expect(this.doc.cachedOps[2].del).to.equal(true); + expect(this.doc.presence.cachedOps.length).to.equal(3); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op); + expect(this.doc.presence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -465,10 +465,10 @@ types.register(presenceType.type3); this.doc.del.bind(this.doc), setTimeout, function(done) { - expect(this.doc2.cachedOps.length).to.equal(3); - expect(this.doc2.cachedOps[0].create).to.equal(true); - expect(this.doc2.cachedOps[1].op).to.eql(op); - expect(this.doc2.cachedOps[2].del).to.equal(true); + expect(this.doc2.presence.cachedOps.length).to.equal(3); + expect(this.doc2.presence.cachedOps[0].create).to.equal(true); + expect(this.doc2.presence.cachedOps[1].op).to.eql(op); + expect(this.doc2.presence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -479,15 +479,15 @@ types.register(presenceType.type3); var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; - this.doc.cachedOpsTimeout = 60; + this.doc.presence.cachedOpsTimeout = 60; async.series([ // Cache 2 ops. this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.submitOp.bind(this.doc, op1), function(done) { - expect(this.doc.cachedOps.length).to.equal(2); - expect(this.doc.cachedOps[0].create).to.equal(true); - expect(this.doc.cachedOps[1].op).to.equal(op1); + expect(this.doc.presence.cachedOps.length).to.equal(2); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op1); done(); }.bind(this), @@ -498,10 +498,10 @@ types.register(presenceType.type3); }, this.doc.submitOp.bind(this.doc, op2), function(done) { - expect(this.doc.cachedOps.length).to.equal(3); - expect(this.doc.cachedOps[0].create).to.equal(true); - expect(this.doc.cachedOps[1].op).to.equal(op1); - expect(this.doc.cachedOps[2].op).to.equal(op2); + expect(this.doc.presence.cachedOps.length).to.equal(3); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op1); + expect(this.doc.presence.cachedOps[2].op).to.equal(op2); done(); }.bind(this), @@ -512,9 +512,9 @@ types.register(presenceType.type3); }, this.doc.submitOp.bind(this.doc, op3), function(done) { - expect(this.doc.cachedOps.length).to.equal(2); - expect(this.doc.cachedOps[0].op).to.equal(op2); - expect(this.doc.cachedOps[1].op).to.equal(op3); + expect(this.doc.presence.cachedOps.length).to.equal(2); + expect(this.doc.presence.cachedOps[0].op).to.equal(op2); + expect(this.doc.presence.cachedOps[1].op).to.equal(op3); clock.uninstall(); done(); }.bind(this) From ac26dae36f2b1ed5ce14351238f8e72afccfdd0c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:13:01 +0530 Subject: [PATCH 26/95] Move doc.inflightPresenceSeq to doc.presence.inflightSeq --- lib/client/doc.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 3abde219e..eb0e4d4ae 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -91,7 +91,7 @@ function Doc(connection, collection, id) { this.presence.cachedOps = []; this.presence.cachedOpsTimeout = 60000; // The sequence number of the inflight presence request. - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -514,7 +514,7 @@ Doc.prototype.flush = function() { if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) { this.inflightPresence = this.pendingPresence; - this.inflightPresenceSeq = this.connection.seq; + this.presence.inflightSeq = this.connection.seq; this.pendingPresence = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); this.presence.requestReply = false; @@ -996,7 +996,7 @@ Doc.prototype._hardRollback = function(err) { // Reset presence-related properties. this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; this.pendingPresence = null; this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); @@ -1109,12 +1109,12 @@ Doc.prototype._handlePresence = function(err, presence) { var src = presence.src; if (!src) { // Handle the ACK for the presence data we submitted. - // this.inflightPresenceSeq would not equal presence.seq after a hard rollback, + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. - if (this.inflightPresenceSeq === presence.seq) { + if (this.presence.inflightSeq === presence.seq) { var callbacks = this.inflightPresence; this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; var called = callbacks && callEach(callbacks, err); if (err && !called) this.emit('error', err); this.flush(); @@ -1278,7 +1278,7 @@ Doc.prototype._pausePresence = function() { this.inflightPresence.concat(this.pendingPresence) : this.inflightPresence; this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } From 48accccc8dff310dd27bed44721c13b8c73d44af Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:13:59 +0530 Subject: [PATCH 27/95] Move doc.inflightPresence to doc.presence.inflight --- lib/client/doc.js | 26 +++++++++++++------------- test/client/presence.js | 14 +++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index eb0e4d4ae..faa7d4d30 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -97,7 +97,7 @@ function Doc(connection, collection, id) { this.inflightFetch = []; this.inflightSubscribe = []; this.inflightUnsubscribe = []; - this.inflightPresence = null; + this.presence.inflight = null; this.pendingFetch = []; this.pendingPresence = null; @@ -266,7 +266,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.inflightPresence || + this.presence.inflight || this.pendingPresence ); }; @@ -512,8 +512,8 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) { - this.inflightPresence = this.pendingPresence; + if (this.subscribed && !this.presence.inflight && this.pendingPresence && !this.hasWritePending()) { + this.presence.inflight = this.pendingPresence; this.presence.inflightSeq = this.connection.seq; this.pendingPresence = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); @@ -985,7 +985,7 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; - if (this.inflightPresence) pendingPresence.push(this.inflightPresence); + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); if (this.pendingPresence) pendingPresence.push(this.pendingPresence); // Cancel all pending ops and reset if we can't invert @@ -995,7 +995,7 @@ Doc.prototype._hardRollback = function(err) { this.pendingOps = []; // Reset presence-related properties. - this.inflightPresence = null; + this.presence.inflight = null; this.presence.inflightSeq = 0; this.pendingPresence = null; this.presence.cachedOps.length = 0; @@ -1085,7 +1085,7 @@ Doc.prototype.submitPresence = function (data, callback) { data = this.type.createPresence(data); } - if (this._setPresence('', data, true) || this.pendingPresence || this.inflightPresence) { + if (this._setPresence('', data, true) || this.pendingPresence || this.presence.inflight) { if (!this.pendingPresence) { this.pendingPresence = []; } @@ -1112,8 +1112,8 @@ Doc.prototype._handlePresence = function(err, presence) { // this.presence.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.inflightPresence; - this.inflightPresence = null; + var callbacks = this.presence.inflight; + this.presence.inflight = null; this.presence.inflightSeq = 0; var called = callbacks && callEach(callbacks, err); if (err && !called) this.emit('error', err); @@ -1272,12 +1272,12 @@ Doc.prototype._transformAllPresence = function(op) { }; Doc.prototype._pausePresence = function() { - if (this.inflightPresence) { + if (this.presence.inflight) { this.pendingPresence = this.pendingPresence ? - this.inflightPresence.concat(this.pendingPresence) : - this.inflightPresence; - this.inflightPresence = null; + this.presence.inflight.concat(this.pendingPresence) : + this.presence.inflight; + this.presence.inflight = null; this.presence.inflightSeq = 0; } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; diff --git a/test/client/presence.js b/test/client/presence.js index 27f499227..bfc47cf19 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -702,13 +702,13 @@ types.register(presenceType.type3); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); expect(!!this.doc.pendingPresence).to.equal(true); - expect(!!this.doc.inflightPresence).to.equal(false); + expect(!!this.doc.presence.inflight).to.equal(false); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); expect(!!this.doc.pendingPresence).to.equal(false); - expect(!!this.doc.inflightPresence).to.equal(false); + expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -723,19 +723,19 @@ types.register(presenceType.type3); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); expect(!!this.doc.pendingPresence).to.equal(true); - expect(!!this.doc.inflightPresence).to.equal(false); + expect(!!this.doc.presence.inflight).to.equal(false); process.nextTick(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(true); expect(!!this.doc.pendingPresence).to.equal(false); - expect(!!this.doc.inflightPresence).to.equal(true); + expect(!!this.doc.presence.inflight).to.equal(true); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); expect(!!this.doc.pendingPresence).to.equal(false); - expect(!!this.doc.inflightPresence).to.equal(false); + expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -1315,7 +1315,7 @@ types.register(presenceType.type3); }; process.nextTick(done); }.bind(this), - this.doc.submitPresence.bind(this.doc, p(1)), // inflightPresence + this.doc.submitPresence.bind(this.doc, p(1)), // presence.inflight process.nextTick, // wait for "presence" event this.doc.submitPresence.bind(this.doc, p(2)), // pendingPresence process.nextTick, // wait for "presence" event @@ -1372,7 +1372,7 @@ types.register(presenceType.type3); if (++called < 3) return; done(); } - this.doc.submitPresence(p(1), callback); // inflightPresence + this.doc.submitPresence(p(1), callback); // presence.inflight process.nextTick(function() { // wait for presence event this.doc.submitPresence(p(2), callback); // pendingPresence process.nextTick(function() { // wait for presence event From cab69fb8e20771ab7343f8af6973d9b17ba7e760 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:16:48 +0530 Subject: [PATCH 28/95] Move doc.pendingPresence to doc.presence.pending --- lib/client/doc.js | 37 ++++++++++++++++++------------------- test/client/presence.js | 14 +++++++------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index faa7d4d30..42b15df41 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -99,7 +99,7 @@ function Doc(connection, collection, id) { this.inflightUnsubscribe = []; this.presence.inflight = null; this.pendingFetch = []; - this.pendingPresence = null; + this.presence.pending = null; // Whether we think we are subscribed on the server. Synchronously set to // false on calls to unsubscribe and disconnect. Should never be true when @@ -267,7 +267,7 @@ Doc.prototype.hasPending = function() { this.inflightUnsubscribe.length || this.pendingFetch.length || this.presence.inflight || - this.pendingPresence + this.presence.pending ); }; @@ -512,10 +512,10 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.subscribed && !this.presence.inflight && this.pendingPresence && !this.hasWritePending()) { - this.presence.inflight = this.pendingPresence; + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; - this.pendingPresence = null; + this.presence.pending = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); this.presence.requestReply = false; } @@ -986,7 +986,7 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.pendingPresence) pendingPresence.push(this.pendingPresence); + if (this.presence.pending) pendingPresence.push(this.presence.pending); // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -997,7 +997,7 @@ Doc.prototype._hardRollback = function(err) { // Reset presence-related properties. this.presence.inflight = null; this.presence.inflightSeq = 0; - this.pendingPresence = null; + this.presence.pending = null; this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); this.presence.requestReply = true; @@ -1085,12 +1085,12 @@ Doc.prototype.submitPresence = function (data, callback) { data = this.type.createPresence(data); } - if (this._setPresence('', data, true) || this.pendingPresence || this.presence.inflight) { - if (!this.pendingPresence) { - this.pendingPresence = []; + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; } if (callback) { - this.pendingPresence.push(callback); + this.presence.pending.push(callback); } } else if (callback) { @@ -1126,9 +1126,9 @@ Doc.prototype._handlePresence = function(err, presence) { // This shouldn't happen but check just in case. if (err) return this.emit('error', err); - if (presence.r && !this.pendingPresence) { + if (presence.r && !this.presence.pending) { // Another client requested us to share our current presence data - this.pendingPresence = []; + this.presence.pending = []; this.flush(); } @@ -1273,14 +1273,13 @@ Doc.prototype._transformAllPresence = function(op) { Doc.prototype._pausePresence = function() { if (this.presence.inflight) { - this.pendingPresence = - this.pendingPresence ? - this.presence.inflight.concat(this.pendingPresence) : - this.presence.inflight; + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; this.presence.inflight = null; this.presence.inflightSeq = 0; - } else if (!this.pendingPresence && this.presence.current[''] != null) { - this.pendingPresence = []; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; } this.presence.received = Object.create(null); this.presence.requestReply = true; diff --git a/test/client/presence.js b/test/client/presence.js index bfc47cf19..34f753e64 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -701,13 +701,13 @@ types.register(presenceType.type3); expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.pendingPresence).to.equal(true); + expect(!!this.doc.presence.pending).to.equal(true); expect(!!this.doc.presence.inflight).to.equal(false); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); - expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.presence.pending).to.equal(false); expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) @@ -722,19 +722,19 @@ types.register(presenceType.type3); expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.pendingPresence).to.equal(true); + expect(!!this.doc.presence.pending).to.equal(true); expect(!!this.doc.presence.inflight).to.equal(false); process.nextTick(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.presence.pending).to.equal(false); expect(!!this.doc.presence.inflight).to.equal(true); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); - expect(!!this.doc.pendingPresence).to.equal(false); + expect(!!this.doc.presence.pending).to.equal(false); expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) @@ -1317,7 +1317,7 @@ types.register(presenceType.type3); }.bind(this), this.doc.submitPresence.bind(this.doc, p(1)), // presence.inflight process.nextTick, // wait for "presence" event - this.doc.submitPresence.bind(this.doc, p(2)), // pendingPresence + this.doc.submitPresence.bind(this.doc, p(2)), // presence.pending process.nextTick, // wait for "presence" event function(done) { var presenceEmitted = false; @@ -1374,7 +1374,7 @@ types.register(presenceType.type3); } this.doc.submitPresence(p(1), callback); // presence.inflight process.nextTick(function() { // wait for presence event - this.doc.submitPresence(p(2), callback); // pendingPresence + this.doc.submitPresence(p(2), callback); // presence.pending process.nextTick(function() { // wait for presence event this.doc.on('presence', function(srcList, submitted) { expect(presenceEmitted).to.equal(false); From 6a0ecc4709b8601fc6bccaf32813fff5590eae6c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:27:36 +0530 Subject: [PATCH 29/95] Refactor presence fields into object declaration. --- lib/client/doc.js | 60 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 42b15df41..c6524c4d2 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -67,39 +67,49 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - // The current presence data - // Map of src -> presence data - // Local src === '' + // Properties related to presence are grouped within this object. this.presence = { - current: Object.create(null) + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: Object.create(null), + + // The presence objects received from the server. + // Map of src -> presence + received: Object.create(null), + + // The minimum amount of time to wait before removing processed presence from this.presence.received. + // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. + // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower + // sequence number arrive after messages with higher sequence numbers. + receivedTimeout: 60000, + + // If set to true, then the next time the local presence is sent, + // all other clients will be asked to reply with their own presence data. + requestReply: true, + + // A list of ops sent by the server. These are needed for transforming presence data, + // if we get that presence data for an older version of the document. + cachedOps: [], + + // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence + // data is supposed to be synced in real-time. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null }; - // The presence objects received from the server - // Map of src -> presence - this.presence.received = Object.create(null); - // The minimum amount of time to wait before removing processed presence from this.presence.received. - // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. - // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower - // sequence number arrive after messages with higher sequence numbers. - this.presence.receivedTimeout = 60000; - // If set to true, then the next time the local presence is sent, - // all other clients will be asked to reply with their own presence data. - this.presence.requestReply = true; - // A list of ops sent by the server. These are needed for transforming presence data, - // if we get that presence data for an older version of the document. - // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence - // data is supposed to be synced in real-time. - this.presence.cachedOps = []; - this.presence.cachedOpsTimeout = 60000; - // The sequence number of the inflight presence request. - this.presence.inflightSeq = 0; // Array of callbacks or nulls as placeholders this.inflightFetch = []; this.inflightSubscribe = []; this.inflightUnsubscribe = []; - this.presence.inflight = null; this.pendingFetch = []; - this.presence.pending = null; // Whether we think we are subscribed on the server. Synchronously set to // false on calls to unsubscribe and disconnect. Should never be true when From d41c961af735df4bff9edbb12ff08b77c8a64c1d Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:35:45 +0530 Subject: [PATCH 30/95] Simplify object creation; 'change Object.create(null)' to '{}'. --- lib/client/doc.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c6524c4d2..c9ea0950e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -73,11 +73,11 @@ function Doc(connection, collection, id) { // The current presence data. // Map of src -> presence data // Local src === '' - current: Object.create(null), + current: {}, // The presence objects received from the server. // Map of src -> presence - received: Object.create(null), + received: {}, // The minimum amount of time to wait before removing processed presence from this.presence.received. // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. @@ -158,13 +158,13 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.presence.received = Object.create(null); + doc.presence.received = {}; doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.presence.received = Object.create(null); + doc.presence.received = {}; doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); @@ -1009,7 +1009,7 @@ Doc.prototype._hardRollback = function(err) { this.presence.inflightSeq = 0; this.presence.pending = null; this.presence.cachedOps.length = 0; - this.presence.received = Object.create(null); + this.presence.received = {}; this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); @@ -1291,7 +1291,7 @@ Doc.prototype._pausePresence = function() { } else if (!this.presence.pending && this.presence.current[''] != null) { this.presence.pending = []; } - this.presence.received = Object.create(null); + this.presence.received = {}; this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; From fc351fa36112c1d1befbb9a70b87202ef4e504c0 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 14:10:29 +0530 Subject: [PATCH 31/95] Introduce enablePresence option. Closes #128 --- README.md | 4 +++ lib/backend.js | 7 ++++ lib/client/doc.js | 71 ++++++++++++++++++++++++++--------------- test/client/presence.js | 11 ++++++- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1f8dfffb4..d82b92ad7 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ default, ShareDB stores all operations forever - nothing is truly deleted. ## User presence synchronization +ShareDB supports synchronization of user presence data. This feature is opt-in, not enabled by default. To enable this feature, pass the `enablePresence: true` option to the ShareDB constructor (e.g. `var share = new ShareDB({ enablePresence: true })`). + Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. ## Server API @@ -96,6 +98,8 @@ __Options__ * `options.pubsub` _(instance of `ShareDB.PubSub`)_ Notify other ShareDB processes when data changes through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`. +* `options.enablePresence` _(optional boolean)_ + Enable user presence synchronization. #### Database Adapters * `ShareDB.MemoryDB`, backed by a non-persistent database with no queries diff --git a/lib/backend.js b/lib/backend.js index e23bebd53..6e3145df6 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -48,6 +48,8 @@ function Backend(options) { if (!options.disableSpaceDelimitedActions) { this._shimAfterSubmit(); } + + this.enablePresence = options.enablePresence; } module.exports = Backend; emitter.mixin(Backend); @@ -155,6 +157,11 @@ Backend.prototype.connect = function(connection, req) { // not used internal to ShareDB, but it is handy for server-side only user // code that may cache state on the agent and read it in middleware connection.agent = agent; + + // Pass through information on whether or not presence is enabled, + // so that Doc instances can use it. + connection.enablePresence = this.enablePresence; + return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index c9ea0950e..e6288f6d9 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,7 +68,7 @@ function Doc(connection, collection, id) { this.data = undefined; // Properties related to presence are grouped within this object. - this.presence = { + this.presence = connection.enablePresence && { // The current presence data. // Map of src -> presence data @@ -158,14 +158,18 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; + if (doc.presence) { + doc.presence.received = {}; + doc.presence.cachedOps.length = 0; + } doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; + if (doc.presence) { + doc.presence.received = {}; + doc.presence.cachedOps.length = 0; + } doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -246,7 +250,11 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; - this.presence.cachedOps.length = 0; + + if (this.presence) { + this.presence.cachedOps.length = 0; + } + var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); this.data = (this.type && this.type.deserialize) ? @@ -276,8 +284,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence.inflight || - this.presence.pending + this.presence && (this.presence.inflight || this.presence.pending) ); }; @@ -522,6 +529,8 @@ Doc.prototype.flush = function() { this._sendOp(); } + if (!this.presence) return; + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; @@ -923,7 +932,10 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.presence.cachedOps.length = 0; + + if (this.presence) { + this.presence.cachedOps.length = 0; + } } else if (message.v !== this.version) { // We should already be at the same version, because the server should @@ -995,8 +1007,10 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); + if (this.presence) { + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + } // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -1005,22 +1019,24 @@ Doc.prototype._hardRollback = function(err) { this.pendingOps = []; // Reset presence-related properties. - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._setPresence(src, null)) { - changedSrcList.push(src); + if (this.presence) { + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._setPresence(src, null)) { + changedSrcList.push(src); + } } + this._emitPresence(changedSrcList, false); } - this._emitPresence(changedSrcList, false); // Fetch the latest version from the server to get us back into a working state var doc = this; @@ -1247,6 +1263,7 @@ Doc.prototype._processReceivedPresence = function(src, emit) { }; Doc.prototype._processAllReceivedPresence = function() { + if (!this.presence) return; var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -1270,6 +1287,7 @@ Doc.prototype._transformPresence = function(src, op) { }; Doc.prototype._transformAllPresence = function(op) { + if (!this.presence) return; var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -1282,6 +1300,8 @@ Doc.prototype._transformAllPresence = function(op) { }; Doc.prototype._pausePresence = function() { + if (!this.presence) return; + if (this.presence.inflight) { this.presence.pending = this.presence.pending ? this.presence.inflight.concat(this.presence.pending) @@ -1331,6 +1351,7 @@ Doc.prototype._emitPresence = function(srcList, submitted) { }; Doc.prototype._cacheOp = function(op) { + if (!this.presence) return; // Remove the old ops. var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; var i; diff --git a/test/client/presence.js b/test/client/presence.js index 34f753e64..aff5ab887 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -11,6 +11,15 @@ types.register(presenceType.type); types.register(presenceType.type2); types.register(presenceType.type3); +describe('client presence', function() { + it('does not expose doc.presence if enablePresence is false', function() { + var backend = new Backend(); + var connection = backend.connect(); + var doc = connection.get('dogs', 'fido'); + expect(typeof doc.presence).to.equal('undefined'); + }); +}); + [ 'wrapped-presence-no-compare', 'wrapped-presence-with-compare', @@ -22,7 +31,7 @@ types.register(presenceType.type3); describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend(); + this.backend = new Backend({ enablePresence: true }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From 6cd16f3f8f2a105c1badd2ef126d0ec6f56fa6e4 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 14:45:05 +0530 Subject: [PATCH 32/95] Misc cleanup, finishing touches. --- lib/client/doc.js | 57 +++++++++++++++++++----------------- test/client/presence-type.js | 22 ++++++-------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index e6288f6d9..12c923d52 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,6 +68,9 @@ function Doc(connection, collection, id) { this.data = undefined; // Properties related to presence are grouped within this object. + // If this.presence is falsy (undefined), it means that + // the enablePresence flag was not passed into the ShareDB constructor, + // so the presence features should be disabled.. this.presence = connection.enablePresence && { // The current presence data. @@ -382,14 +385,6 @@ Doc.prototype._handleOp = function(err, message) { return; } - var serverOp = { - src: message.src, - time: Date.now(), - create: !!message.create, - op: message.op, - del: !!message.del - }; - if (this.inflightOp) { var transformErr = transformX(this.inflightOp, message); if (transformErr) return this._hardRollback(transformErr); @@ -401,7 +396,13 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._cacheOp(serverOp); + this._cacheOp({ + src: message.src, + time: Date.now(), + create: !!message.create, + op: message.op, + del: !!message.del + }); try { this._otApply(message, false); this._processAllReceivedPresence(); @@ -523,15 +524,15 @@ function pushActionCallback(inflight, isDuplicate, callback) { // // If there are no pending ops, this method sends the pending presence data, if possible. Doc.prototype.flush = function() { - if (this.paused) return; + // Ignore if we can't send or we are already sending an op + if (!this.connection.canSend || this.inflightOp) return; - if (this.connection.canSend && !this.inflightOp && this.pendingOps.length) { + // Send first pending op unless paused + if (!this.paused && this.pendingOps.length) { this._sendOp(); } - if (!this.presence) return; - - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + if (this.presence && this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; this.presence.pending = null; @@ -1065,7 +1066,9 @@ Doc.prototype._hardRollback = function(err) { Doc.prototype._clearInflightOp = function(err) { var inflightOp = this.inflightOp; + this.inflightOp = null; + var called = callEach(inflightOp.callbacks, err); this.flush(); @@ -1123,10 +1126,7 @@ Doc.prototype.submitPresence = function (data, callback) { process.nextTick(callback); } - var doc = this; - process.nextTick(function() { - doc.flush(); - }); + process.nextTick(this.flush.bind(this)); }; Doc.prototype._handlePresence = function(err, presence) { @@ -1169,10 +1169,10 @@ Doc.prototype._handlePresence = function(err, presence) { this.presence.received[src] = presence; if (presence.v == null) { - // null version should happen only when the server automatically sends - // null presence for an unsubscribed client - presence.processedAt = Date.now(); - return this._setPresence(src, null, true); + // null version should happen only when the server automatically sends + // null presence for an unsubscribed client + presence.processedAt = Date.now(); + return this._setPresence(src, null, true); } // Get missing ops first, if necessary @@ -1190,16 +1190,19 @@ Doc.prototype._processReceivedPresence = function(src, emit) { if (presence.processedAt != null) { if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence - delete this.presence.received[src]; + // Remove old received and processed presence. + delete this.presence.received[src]; } return false; } - if (this.version == null || this.version < presence.v) return false; // keep waiting for the missing snapshot or ops + if (this.version == null || this.version < presence.v) { + // keep waiting for the missing snapshot or ops. + return false; + } if (presence.p == null) { - // Remove presence data as requested + // Remove presence data as requested. presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } @@ -1269,7 +1272,7 @@ Doc.prototype._processAllReceivedPresence = function() { for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; if (this._processReceivedPresence(src)) { - changedSrcList.push(src); + changedSrcList.push(src); } } this._emitPresence(changedSrcList, true); diff --git a/test/client/presence-type.js b/test/client/presence-type.js index 51ad272a0..6138eae7f 100644 --- a/test/client/presence-type.js +++ b/test/client/presence-type.js @@ -46,12 +46,9 @@ function apply(snapshot, op) { } function transform(op1, op2, side) { - return op1.index < op2.index || (op1.index === op2.index && side === 'left') ? - op1 : - { - index: op1.index + 1, - value: op1.value - }; + return op1.index < op2.index || (op1.index === op2.index && side === 'left') + ? op1 + : { index: op1.index + 1, value: op1.value }; } function createPresence(data) { @@ -59,11 +56,9 @@ function createPresence(data) { } function transformPresence(presence, op, isOwnOperation) { - return presence.index < op.index || (presence.index === op.index && !isOwnOperation) ? - presence : - { - index: presence.index + 1 - }; + return presence.index < op.index || (presence.index === op.index && !isOwnOperation) + ? presence + : { index: presence.index + 1 }; } function comparePresence(presence1, presence2) { @@ -77,6 +72,7 @@ function createPresence2(data) { } function transformPresence2(presence, op, isOwnOperation) { - return presence < op.index || (presence === op.index && !isOwnOperation) ? - presence : presence + 1; + return presence < op.index || (presence === op.index && !isOwnOperation) + ? presence + : presence + 1; } From ad6a5282133de6a3c8d3bd61c28375b1d5c05e49 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 15:15:27 +0530 Subject: [PATCH 33/95] Split out presence methods into separate module --- lib/client/doc.js | 322 +------------------------------------ lib/client/presence.js | 349 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 317 deletions(-) create mode 100644 lib/client/presence.js diff --git a/lib/client/doc.js b/lib/client/doc.js index 12c923d52..293d36441 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var presenceMethods = require('./presence'); /** * A Doc is a client's view on a sharejs document. @@ -55,6 +56,7 @@ var types = require('../types'); */ module.exports = Doc; +Object.assign(Doc.prototype, presenceMethods); function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -71,42 +73,7 @@ function Doc(connection, collection, id) { // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled.. - this.presence = connection.enablePresence && { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, - - // The presence objects received from the server. - // Map of src -> presence - received: {}, - - // The minimum amount of time to wait before removing processed presence from this.presence.received. - // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. - // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower - // sequence number arrive after messages with higher sequence numbers. - receivedTimeout: 60000, - - // If set to true, then the next time the local presence is sent, - // all other clients will be asked to reply with their own presence data. - requestReply: true, - - // A list of ops sent by the server. These are needed for transforming presence data, - // if we get that presence data for an older version of the document. - cachedOps: [], - - // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence - // data is supposed to be synced in real-time. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; + this.presence = connection.enablePresence && this._initializePresence(); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -1089,284 +1056,5 @@ function callEach(callbacks, err) { return called; } -// *** Presence - -Doc.prototype.submitPresence = function (data, callback) { - if (data != null) { - if (!this.type) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); - if (callback) return callback(err); - doc.emit('error', err); - }); - } - - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); - if (callback) return callback(err); - doc.emit('error', err); - }); - } - - data = this.type.createPresence(data); - } - - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; - } - if (callback) { - this.presence.pending.push(callback); - } - - } else if (callback) { - process.nextTick(callback); - } - - process.nextTick(this.flush.bind(this)); -}; - -Doc.prototype._handlePresence = function(err, presence) { - if (!this.subscribed) return; - - var src = presence.src; - if (!src) { - // Handle the ACK for the presence data we submitted. - // this.presence.inflightSeq would not equal presence.seq after a hard rollback, - // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - var called = callbacks && callEach(callbacks, err); - if (err && !called) this.emit('error', err); - this.flush(); - this._emitNothingPending(); - } - return; - } - - // This shouldn't happen but check just in case. - if (err) return this.emit('error', err); - - if (presence.r && !this.presence.pending) { - // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); - } - - // Ignore older messages which arrived out of order - if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) - ) - ) return; - - this.presence.received[src] = presence; - - if (presence.v == null) { - // null version should happen only when the server automatically sends - // null presence for an unsubscribed client - presence.processedAt = Date.now(); - return this._setPresence(src, null, true); - } - - // Get missing ops first, if necessary - if (this.version == null || this.version < presence.v) return this.fetch(); - - this._processReceivedPresence(src, true); -}; - -// If emit is true and presence has changed, emits a presence event. -// Returns true, if presence has changed for src. Otherwise false. -Doc.prototype._processReceivedPresence = function(src, emit) { - if (!src) return false; - var presence = this.presence.received[src]; - if (!presence) return false; - - if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence. - delete this.presence.received[src]; - } - return false; - } - - if (this.version == null || this.version < presence.v) { - // keep waiting for the missing snapshot or ops. - return false; - } - - if (presence.p == null) { - // Remove presence data as requested. - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - if (!this.type || !this.type.createPresence || !this.type.transformPresence) { - // Remove presence data because the document is not created or its type does not support presence - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - if (this.inflightOp && this.inflightOp.op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - for (var i = 0; i < this.pendingOps.length; i++) { - if (this.pendingOps[i].op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - } - - var startIndex = this.presence.cachedOps.length - (this.version - presence.v); - if (startIndex < 0) { - // Remove presence data because we can't transform presence.received - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - } - - // Make sure the format of the data is correct - var data = this.type.createPresence(presence.p); - - // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.cachedOps[i]; - data = this.type.transformPresence(data, op.op, presence.src === op.src); - } - - // Transform against pending ops - if (this.inflightOp) { - data = this.type.transformPresence(data, this.inflightOp.op, false); - } - - for (var i = 0; i < this.pendingOps.length; i++) { - data = this.type.transformPresence(data, this.pendingOps[i].op, false); - } - - // Set presence data - presence.processedAt = Date.now(); - return this._setPresence(src, data, emit); -}; - -Doc.prototype._processAllReceivedPresence = function() { - if (!this.presence) return; - var srcList = Object.keys(this.presence.received); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._processReceivedPresence(src)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, true); -}; - -Doc.prototype._transformPresence = function(src, op) { - var presenceData = this.presence.current[src]; - if (op.op != null) { - var isOwnOperation = src === (op.src || ''); - presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); - } else { - presenceData = null; - } - return this._setPresence(src, presenceData); -}; - -Doc.prototype._transformAllPresence = function(op) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._transformPresence(src, op)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); -}; - -Doc.prototype._pausePresence = function() { - if (!this.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; - } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (src && this._setPresence(src, null)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); -}; - -// If emit is true and presence has changed, emits a presence event. -// Returns true, if presence has changed. Otherwise false. -Doc.prototype._setPresence = function(src, data, emit) { - if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; - } else { - var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); - if (isPresenceEqual) return false; - this.presence.current[src] = data; - } - if (emit) this._emitPresence([ src ], true); - return true; -}; - -Doc.prototype._emitPresence = function(srcList, submitted) { - if (srcList && srcList.length > 0) { - var doc = this; - process.nextTick(function() { - doc.emit('presence', srcList, submitted); - }); - } -}; - -Doc.prototype._cacheOp = function(op) { - if (!this.presence) return; - // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; - var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { - break; - } - } - if (i > 0) { - this.presence.cachedOps.splice(0, i); - } - - // Cache the new op. - this.presence.cachedOps.push(op); -}; +// Expose callEach to presence methods via Doc prototype. +Doc.prototype._callEach = callEach; diff --git a/lib/client/presence.js b/lib/client/presence.js new file mode 100644 index 000000000..5e9cf01bd --- /dev/null +++ b/lib/client/presence.js @@ -0,0 +1,349 @@ +/* + * Presence Methods + * ---------------- + * + * This module contains definitions for presence-related methods + * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * + * The value of 'this' in these functions will be the Doc instance. + */ +var ShareDBError = require('../error'); + +// Submit presence data to a document. +// This is the only public facing method. +// All the others are marked as internal with a leading "_". +function submitPresence(data, callback) { + if (data != null) { + if (!this.type) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); + } + + if (!this.type.createPresence || !this.type.transformPresence) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); + } + + data = this.type.createPresence(data); + } + + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; + } + if (callback) { + this.presence.pending.push(callback); + } + + } else if (callback) { + process.nextTick(callback); + } + + process.nextTick(this.flush.bind(this)); +} + +// This function generates the initial value for doc.presence. +function _initializePresence() { + + // Return a new object each time, otherwise mutations would bleed across documents. + return { + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: {}, + + // The presence objects received from the server. + // Map of src -> presence + received: {}, + + // The minimum amount of time to wait before removing processed presence from this.presence.received. + // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. + // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower + // sequence number arrive after messages with higher sequence numbers. + receivedTimeout: 60000, + + // If set to true, then the next time the local presence is sent, + // all other clients will be asked to reply with their own presence data. + requestReply: true, + + // A list of ops sent by the server. These are needed for transforming presence data, + // if we get that presence data for an older version of the document. + cachedOps: [], + + // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence + // data is supposed to be synced in real-time. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null + }; +} + +function _handlePresence(err, presence) { + if (!this.subscribed) return; + + var src = presence.src; + if (!src) { + // Handle the ACK for the presence data we submitted. + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // when all callbacks are flushed with an error. + if (this.presence.inflightSeq === presence.seq) { + var callbacks = this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + var called = callbacks && this._callEach(callbacks, err); + if (err && !called) this.emit('error', err); + this.flush(); + this._emitNothingPending(); + } + return; + } + + // This shouldn't happen but check just in case. + if (err) return this.emit('error', err); + + if (presence.r && !this.presence.pending) { + // Another client requested us to share our current presence data + this.presence.pending = []; + this.flush(); + } + + // Ignore older messages which arrived out of order + if ( + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) + ) + ) return; + + this.presence.received[src] = presence; + + if (presence.v == null) { + // null version should happen only when the server automatically sends + // null presence for an unsubscribed client + presence.processedAt = Date.now(); + return this._setPresence(src, null, true); + } + + // Get missing ops first, if necessary + if (this.version == null || this.version < presence.v) return this.fetch(); + + this._processReceivedPresence(src, true); +} + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed for src. Otherwise false. +function _processReceivedPresence(src, emit) { + if (!src) return false; + var presence = this.presence.received[src]; + if (!presence) return false; + + if (presence.processedAt != null) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + // Remove old received and processed presence. + delete this.presence.received[src]; + } + return false; + } + + if (this.version == null || this.version < presence.v) { + // keep waiting for the missing snapshot or ops. + return false; + } + + if (presence.p == null) { + // Remove presence data as requested. + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (!this.type || !this.type.createPresence || !this.type.transformPresence) { + // Remove presence data because the document is not created or its type does not support presence + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (this.inflightOp && this.inflightOp.op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + if (this.pendingOps[i].op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + var startIndex = this.presence.cachedOps.length - (this.version - presence.v); + if (startIndex < 0) { + // Remove presence data because we can't transform presence.received + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + // Make sure the format of the data is correct + var data = this.type.createPresence(presence.p); + + // Transform against past ops + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.cachedOps[i]; + data = this.type.transformPresence(data, op.op, presence.src === op.src); + } + + // Transform against pending ops + if (this.inflightOp) { + data = this.type.transformPresence(data, this.inflightOp.op, false); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + data = this.type.transformPresence(data, this.pendingOps[i].op, false); + } + + // Set presence data + presence.processedAt = Date.now(); + return this._setPresence(src, data, emit); +} + +function _processAllReceivedPresence() { + if (!this.presence) return; + var srcList = Object.keys(this.presence.received); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._processReceivedPresence(src)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, true); +} + +function _transformPresence(src, op) { + var presenceData = this.presence.current[src]; + if (op.op != null) { + var isOwnOperation = src === (op.src || ''); + presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); + } else { + presenceData = null; + } + return this._setPresence(src, presenceData); +} + +function _transformAllPresence(op) { + if (!this.presence) return; + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._transformPresence(src, op)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); +} + +function _pausePresence() { + if (!this.presence) return; + + if (this.presence.inflight) { + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; + } + this.presence.received = {}; + this.presence.requestReply = true; + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (src && this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); +} + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed. Otherwise false. +function _setPresence(src, data, emit) { + if (data == null) { + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; + } else { + var isPresenceEqual = + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + if (isPresenceEqual) return false; + this.presence.current[src] = data; + } + if (emit) this._emitPresence([ src ], true); + return true; +} + +function _emitPresence(srcList, submitted) { + if (srcList && srcList.length > 0) { + var doc = this; + process.nextTick(function() { + doc.emit('presence', srcList, submitted); + }); + } +} + +function _cacheOp(op) { + if (!this.presence) return; + // Remove the old ops. + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var i; + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { + break; + } + } + if (i > 0) { + this.presence.cachedOps.splice(0, i); + } + + // Cache the new op. + this.presence.cachedOps.push(op); +} + +module.exports = { + submitPresence, + _initializePresence, + _handlePresence, + _processReceivedPresence, + _processAllReceivedPresence, + _transformPresence, + _transformAllPresence, + _pausePresence, + _setPresence, + _emitPresence, + _cacheOp +}; From eaafc98772213e1da7fd2204320cbbbcdad2807e Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 15:33:45 +0530 Subject: [PATCH 34/95] Move more presence-related logic into presence methods module. --- lib/client/doc.js | 56 +++++++++--------------------------------- lib/client/presence.js | 46 +++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 293d36441..b134f666a 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -56,7 +56,10 @@ var presenceMethods = require('./presence'); */ module.exports = Doc; + +// Expose presence-related methods on the Doc prototype. Object.assign(Doc.prototype, presenceMethods); + function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -72,7 +75,7 @@ function Doc(connection, collection, id) { // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled.. + // so the presence features should be disabled. this.presence = connection.enablePresence && this._initializePresence(); // Array of callbacks or nulls as placeholders @@ -128,18 +131,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; - } + if (doc.presence) doc._destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; - } + if (doc.presence) doc._destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -376,7 +373,6 @@ Doc.prototype._handleOp = function(err, message) { } catch (error) { return this._hardRollback(error); } - return; }; // Called whenever (you guessed it!) the connection state changes. This will @@ -488,8 +484,6 @@ function pushActionCallback(inflight, isDuplicate, callback) { // // Only one operation can be in-flight at a time. If an operation is already on // its way, or we're not currently connected, this method does nothing. -// -// If there are no pending ops, this method sends the pending presence data, if possible. Doc.prototype.flush = function() { // Ignore if we can't send or we are already sending an op if (!this.connection.canSend || this.inflightOp) return; @@ -499,12 +493,8 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.presence && this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; + if (this.presence) { + this._flushPresence(); } }; @@ -973,12 +963,8 @@ Doc.prototype._hardRollback = function(err) { if (this.inflightOp) pendingOps.push(this.inflightOp); pendingOps = pendingOps.concat(this.pendingOps); - // Apply the same technique for presence, cleaning up as we go. - var pendingPresence = []; - if (this.presence) { - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); - } + // Apply a similar technique for presence. + var pendingPresence = this.presence ? this._hardRollbackPresence() : []; // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -986,26 +972,6 @@ Doc.prototype._hardRollback = function(err) { this.inflightOp = null; this.pendingOps = []; - // Reset presence-related properties. - if (this.presence) { - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._setPresence(src, null)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); - } - // Fetch the latest version from the server to get us back into a working state var doc = this; this.fetch(function() { @@ -1026,7 +992,7 @@ Doc.prototype._hardRollback = function(err) { // If there are no ops or presence queued, or one of them didn't handle the error, // then we emit the error. if (err && !allOpsHadCallbacks && !allPresenceHadCallbacks) { - return doc.emit('error', err); + doc.emit('error', err); } }); }; diff --git a/lib/client/presence.js b/lib/client/presence.js index 5e9cf01bd..91f635758 100644 --- a/lib/client/presence.js +++ b/lib/client/presence.js @@ -334,6 +334,47 @@ function _cacheOp(op) { this.presence.cachedOps.push(op); } +// If there are no pending ops, this method sends the pending presence data, if possible. +function _flushPresence() { + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; + this.presence.inflightSeq = this.connection.seq; + this.presence.pending = null; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; + } +} + +function _destroyPresence() { + this.presence.received = {}; + this.presence.cachedOps.length = 0; +} + +// Reset presence-related properties. +function _hardRollbackPresence() { + var pendingPresence = []; + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); + return pendingPresence; +} + module.exports = { submitPresence, _initializePresence, @@ -345,5 +386,8 @@ module.exports = { _pausePresence, _setPresence, _emitPresence, - _cacheOp + _cacheOp, + _flushPresence, + _destroyPresence, + _hardRollbackPresence }; From 7259f7e4fe200f828a98c5fa1bb52a76a23b227c Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 17:47:59 +0530 Subject: [PATCH 35/95] Move presence methods such that they are passed into Backend --- .gitignore | 1 + lib/backend.js | 6 +- lib/client/doc.js | 42 ++-- lib/client/presence.js | 1 - lib/presence/dummy.js | 14 ++ lib/presence/stateless.js | 393 ++++++++++++++++++++++++++++++++++++++ test/client/presence.js | 3 +- 7 files changed, 434 insertions(+), 26 deletions(-) create mode 100644 lib/presence/dummy.js create mode 100644 lib/presence/stateless.js diff --git a/.gitignore b/.gitignore index 3005c1397..abd0d58e5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ coverage # Dependency directories node_modules package-lock.json +yarn.lock jspm_packages diff --git a/lib/backend.js b/lib/backend.js index 6e3145df6..1d86a8603 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -49,7 +49,7 @@ function Backend(options) { this._shimAfterSubmit(); } - this.enablePresence = options.enablePresence; + this.Presence = options.Presence; } module.exports = Backend; emitter.mixin(Backend); @@ -158,9 +158,7 @@ Backend.prototype.connect = function(connection, req) { // code that may cache state on the agent and read it in middleware connection.agent = agent; - // Pass through information on whether or not presence is enabled, - // so that Doc instances can use it. - connection.enablePresence = this.enablePresence; + connection.Presence = this.Presence; return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index b134f666a..1c42b0538 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,7 +2,6 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); -var presenceMethods = require('./presence'); /** * A Doc is a client's view on a sharejs document. @@ -57,9 +56,6 @@ var presenceMethods = require('./presence'); module.exports = Doc; -// Expose presence-related methods on the Doc prototype. -Object.assign(Doc.prototype, presenceMethods); - function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -72,11 +68,17 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - // Properties related to presence are grouped within this object. - // If this.presence is falsy (undefined), it means that - // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled. - this.presence = connection.enablePresence && this._initializePresence(); + if (connection.Presence) { + // TODO don't decorate + // Expose presence-related methods on the Doc prototype. + Object.assign(Doc.prototype, connection.Presence); + + // Properties related to presence are grouped within this object. + // If this.presence is falsy (undefined), it means that + // the enablePresence flag was not passed into the ShareDB constructor, + // so the presence features should be disabled. + this.presence = this._initializePresence(); + } // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -228,7 +230,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); callback && callback(); }; @@ -360,7 +362,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._cacheOp({ + if (this.presence) this._cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -369,7 +371,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -395,10 +397,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this._pausePresence(); + if (this.presence) this._pausePresence(); callEach(callbacks); } else { - this._pausePresence(); + if (this.presence) this._pausePresence(); } } }; @@ -600,7 +602,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); - this._transformAllPresence(componentOp); + if (this.presence) this._transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -613,7 +615,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place this.data = this.type.apply(this.data, op.op); - this._transformAllPresence(op); + if (this.presence) this._transformAllPresence(op); // Emit an 'op' event once the local data includes the changes from the // op. For locally submitted ops, this will be synchronously with // submission and before the server or other clients have received the op. @@ -630,7 +632,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - this._transformAllPresence(op); + if (this.presence) this._transformAllPresence(op); this.emit('create', source); return; } @@ -638,7 +640,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this._transformAllPresence(op); + if (this.presence) this._transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -906,7 +908,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this._cacheOp({ + if (this.presence) this._cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, @@ -915,7 +917,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { diff --git a/lib/client/presence.js b/lib/client/presence.js index 91f635758..20b522008 100644 --- a/lib/client/presence.js +++ b/lib/client/presence.js @@ -229,7 +229,6 @@ function _processReceivedPresence(src, emit) { } function _processAllReceivedPresence() { - if (!this.presence) return; var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js new file mode 100644 index 000000000..9fe310999 --- /dev/null +++ b/lib/presence/dummy.js @@ -0,0 +1,14 @@ +function DummyPresence () { } +function noop () {} + +DummyPresence.prototype.flushPresence = noop; +DummyPresence.prototype.destroyPresence = noop; +DummyPresence.prototype.clearCachedOps = noop; // this.presence.cachedOps.length = 0; +DummyPresence.prototype.processAllReceivedPresence = noop; +DummyPresence.prototype.hardRollbackPresence = function () { return []; }; +DummyPresence.prototype.transformAllPresence = noop; +DummyPresence.prototype.cacheOp = noop; +DummyPresence.prototype.hasPending = function () { return false }; // (this.presence.inflight || this.presence.pending) +DummyPresence.prototype.pause = noop; + +module.exports = DummyPresence; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js new file mode 100644 index 000000000..91f635758 --- /dev/null +++ b/lib/presence/stateless.js @@ -0,0 +1,393 @@ +/* + * Presence Methods + * ---------------- + * + * This module contains definitions for presence-related methods + * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * + * The value of 'this' in these functions will be the Doc instance. + */ +var ShareDBError = require('../error'); + +// Submit presence data to a document. +// This is the only public facing method. +// All the others are marked as internal with a leading "_". +function submitPresence(data, callback) { + if (data != null) { + if (!this.type) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); + } + + if (!this.type.createPresence || !this.type.transformPresence) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); + if (callback) return callback(err); + doc.emit('error', err); + }); + } + + data = this.type.createPresence(data); + } + + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; + } + if (callback) { + this.presence.pending.push(callback); + } + + } else if (callback) { + process.nextTick(callback); + } + + process.nextTick(this.flush.bind(this)); +} + +// This function generates the initial value for doc.presence. +function _initializePresence() { + + // Return a new object each time, otherwise mutations would bleed across documents. + return { + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: {}, + + // The presence objects received from the server. + // Map of src -> presence + received: {}, + + // The minimum amount of time to wait before removing processed presence from this.presence.received. + // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. + // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower + // sequence number arrive after messages with higher sequence numbers. + receivedTimeout: 60000, + + // If set to true, then the next time the local presence is sent, + // all other clients will be asked to reply with their own presence data. + requestReply: true, + + // A list of ops sent by the server. These are needed for transforming presence data, + // if we get that presence data for an older version of the document. + cachedOps: [], + + // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence + // data is supposed to be synced in real-time. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null + }; +} + +function _handlePresence(err, presence) { + if (!this.subscribed) return; + + var src = presence.src; + if (!src) { + // Handle the ACK for the presence data we submitted. + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // when all callbacks are flushed with an error. + if (this.presence.inflightSeq === presence.seq) { + var callbacks = this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + var called = callbacks && this._callEach(callbacks, err); + if (err && !called) this.emit('error', err); + this.flush(); + this._emitNothingPending(); + } + return; + } + + // This shouldn't happen but check just in case. + if (err) return this.emit('error', err); + + if (presence.r && !this.presence.pending) { + // Another client requested us to share our current presence data + this.presence.pending = []; + this.flush(); + } + + // Ignore older messages which arrived out of order + if ( + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) + ) + ) return; + + this.presence.received[src] = presence; + + if (presence.v == null) { + // null version should happen only when the server automatically sends + // null presence for an unsubscribed client + presence.processedAt = Date.now(); + return this._setPresence(src, null, true); + } + + // Get missing ops first, if necessary + if (this.version == null || this.version < presence.v) return this.fetch(); + + this._processReceivedPresence(src, true); +} + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed for src. Otherwise false. +function _processReceivedPresence(src, emit) { + if (!src) return false; + var presence = this.presence.received[src]; + if (!presence) return false; + + if (presence.processedAt != null) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + // Remove old received and processed presence. + delete this.presence.received[src]; + } + return false; + } + + if (this.version == null || this.version < presence.v) { + // keep waiting for the missing snapshot or ops. + return false; + } + + if (presence.p == null) { + // Remove presence data as requested. + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (!this.type || !this.type.createPresence || !this.type.transformPresence) { + // Remove presence data because the document is not created or its type does not support presence + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + if (this.inflightOp && this.inflightOp.op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + if (this.pendingOps[i].op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + var startIndex = this.presence.cachedOps.length - (this.version - presence.v); + if (startIndex < 0) { + // Remove presence data because we can't transform presence.received + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].op == null) { + // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" + presence.processedAt = Date.now(); + return this._setPresence(src, null, emit); + } + } + + // Make sure the format of the data is correct + var data = this.type.createPresence(presence.p); + + // Transform against past ops + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.cachedOps[i]; + data = this.type.transformPresence(data, op.op, presence.src === op.src); + } + + // Transform against pending ops + if (this.inflightOp) { + data = this.type.transformPresence(data, this.inflightOp.op, false); + } + + for (var i = 0; i < this.pendingOps.length; i++) { + data = this.type.transformPresence(data, this.pendingOps[i].op, false); + } + + // Set presence data + presence.processedAt = Date.now(); + return this._setPresence(src, data, emit); +} + +function _processAllReceivedPresence() { + if (!this.presence) return; + var srcList = Object.keys(this.presence.received); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._processReceivedPresence(src)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, true); +} + +function _transformPresence(src, op) { + var presenceData = this.presence.current[src]; + if (op.op != null) { + var isOwnOperation = src === (op.src || ''); + presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); + } else { + presenceData = null; + } + return this._setPresence(src, presenceData); +} + +function _transformAllPresence(op) { + if (!this.presence) return; + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._transformPresence(src, op)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); +} + +function _pausePresence() { + if (!this.presence) return; + + if (this.presence.inflight) { + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; + } + this.presence.received = {}; + this.presence.requestReply = true; + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (src && this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); +} + +// If emit is true and presence has changed, emits a presence event. +// Returns true, if presence has changed. Otherwise false. +function _setPresence(src, data, emit) { + if (data == null) { + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; + } else { + var isPresenceEqual = + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + if (isPresenceEqual) return false; + this.presence.current[src] = data; + } + if (emit) this._emitPresence([ src ], true); + return true; +} + +function _emitPresence(srcList, submitted) { + if (srcList && srcList.length > 0) { + var doc = this; + process.nextTick(function() { + doc.emit('presence', srcList, submitted); + }); + } +} + +function _cacheOp(op) { + if (!this.presence) return; + // Remove the old ops. + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var i; + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { + break; + } + } + if (i > 0) { + this.presence.cachedOps.splice(0, i); + } + + // Cache the new op. + this.presence.cachedOps.push(op); +} + +// If there are no pending ops, this method sends the pending presence data, if possible. +function _flushPresence() { + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; + this.presence.inflightSeq = this.connection.seq; + this.presence.pending = null; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; + } +} + +function _destroyPresence() { + this.presence.received = {}; + this.presence.cachedOps.length = 0; +} + +// Reset presence-related properties. +function _hardRollbackPresence() { + var pendingPresence = []; + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + var changedSrcList = []; + for (var i = 0; i < srcList.length; i++) { + var src = srcList[i]; + if (this._setPresence(src, null)) { + changedSrcList.push(src); + } + } + this._emitPresence(changedSrcList, false); + return pendingPresence; +} + +module.exports = { + submitPresence, + _initializePresence, + _handlePresence, + _processReceivedPresence, + _processAllReceivedPresence, + _transformPresence, + _transformAllPresence, + _pausePresence, + _setPresence, + _emitPresence, + _cacheOp, + _flushPresence, + _destroyPresence, + _hardRollbackPresence +}; diff --git a/test/client/presence.js b/test/client/presence.js index aff5ab887..f5ee3f1fa 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); +var StatelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -31,7 +32,7 @@ describe('client presence', function() { describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend({ enablePresence: true }); + this.backend = new Backend({ Presence: StatelessPresence }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From 09f641507ce44ac3d8763dc3c70971079925ffaa Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:22:34 +0530 Subject: [PATCH 36/95] Migrate hardRollbackPresence to presence instance --- lib/client/doc.js | 7 +- lib/client/presence.js | 392 -------------------------------------- lib/presence/stateless.js | 26 +-- 3 files changed, 19 insertions(+), 406 deletions(-) delete mode 100644 lib/client/presence.js diff --git a/lib/client/doc.js b/lib/client/doc.js index 1c42b0538..3513826c6 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,11 @@ function Doc(connection, collection, id) { // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. this.presence = this._initializePresence(); + + this.presence.doc = this; + + this.presence.hardRollbackPresence = connection.Presence.hardRollbackPresence; + delete Doc.prototype.hardRollbackPresence; } // Array of callbacks or nulls as placeholders @@ -966,7 +971,7 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence ? this._hardRollbackPresence() : []; + var pendingPresence = this.presence ? this.presence.hardRollbackPresence() : []; // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/lib/client/presence.js b/lib/client/presence.js deleted file mode 100644 index 20b522008..000000000 --- a/lib/client/presence.js +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Presence Methods - * ---------------- - * - * This module contains definitions for presence-related methods - * that are added as methods to the Doc prototype (e.g. doc.submitPresence). - * - * The value of 'this' in these functions will be the Doc instance. - */ -var ShareDBError = require('../error'); - -// Submit presence data to a document. -// This is the only public facing method. -// All the others are marked as internal with a leading "_". -function submitPresence(data, callback) { - if (data != null) { - if (!this.type) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); - if (callback) return callback(err); - doc.emit('error', err); - }); - } - - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); - if (callback) return callback(err); - doc.emit('error', err); - }); - } - - data = this.type.createPresence(data); - } - - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; - } - if (callback) { - this.presence.pending.push(callback); - } - - } else if (callback) { - process.nextTick(callback); - } - - process.nextTick(this.flush.bind(this)); -} - -// This function generates the initial value for doc.presence. -function _initializePresence() { - - // Return a new object each time, otherwise mutations would bleed across documents. - return { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, - - // The presence objects received from the server. - // Map of src -> presence - received: {}, - - // The minimum amount of time to wait before removing processed presence from this.presence.received. - // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. - // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower - // sequence number arrive after messages with higher sequence numbers. - receivedTimeout: 60000, - - // If set to true, then the next time the local presence is sent, - // all other clients will be asked to reply with their own presence data. - requestReply: true, - - // A list of ops sent by the server. These are needed for transforming presence data, - // if we get that presence data for an older version of the document. - cachedOps: [], - - // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence - // data is supposed to be synced in real-time. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; -} - -function _handlePresence(err, presence) { - if (!this.subscribed) return; - - var src = presence.src; - if (!src) { - // Handle the ACK for the presence data we submitted. - // this.presence.inflightSeq would not equal presence.seq after a hard rollback, - // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - var called = callbacks && this._callEach(callbacks, err); - if (err && !called) this.emit('error', err); - this.flush(); - this._emitNothingPending(); - } - return; - } - - // This shouldn't happen but check just in case. - if (err) return this.emit('error', err); - - if (presence.r && !this.presence.pending) { - // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); - } - - // Ignore older messages which arrived out of order - if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) - ) - ) return; - - this.presence.received[src] = presence; - - if (presence.v == null) { - // null version should happen only when the server automatically sends - // null presence for an unsubscribed client - presence.processedAt = Date.now(); - return this._setPresence(src, null, true); - } - - // Get missing ops first, if necessary - if (this.version == null || this.version < presence.v) return this.fetch(); - - this._processReceivedPresence(src, true); -} - -// If emit is true and presence has changed, emits a presence event. -// Returns true, if presence has changed for src. Otherwise false. -function _processReceivedPresence(src, emit) { - if (!src) return false; - var presence = this.presence.received[src]; - if (!presence) return false; - - if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence. - delete this.presence.received[src]; - } - return false; - } - - if (this.version == null || this.version < presence.v) { - // keep waiting for the missing snapshot or ops. - return false; - } - - if (presence.p == null) { - // Remove presence data as requested. - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - if (!this.type || !this.type.createPresence || !this.type.transformPresence) { - // Remove presence data because the document is not created or its type does not support presence - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - if (this.inflightOp && this.inflightOp.op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - for (var i = 0; i < this.pendingOps.length; i++) { - if (this.pendingOps[i].op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - } - - var startIndex = this.presence.cachedOps.length - (this.version - presence.v); - if (startIndex < 0) { - // Remove presence data because we can't transform presence.received - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].op == null) { - // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" - presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); - } - } - - // Make sure the format of the data is correct - var data = this.type.createPresence(presence.p); - - // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.cachedOps[i]; - data = this.type.transformPresence(data, op.op, presence.src === op.src); - } - - // Transform against pending ops - if (this.inflightOp) { - data = this.type.transformPresence(data, this.inflightOp.op, false); - } - - for (var i = 0; i < this.pendingOps.length; i++) { - data = this.type.transformPresence(data, this.pendingOps[i].op, false); - } - - // Set presence data - presence.processedAt = Date.now(); - return this._setPresence(src, data, emit); -} - -function _processAllReceivedPresence() { - var srcList = Object.keys(this.presence.received); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._processReceivedPresence(src)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, true); -} - -function _transformPresence(src, op) { - var presenceData = this.presence.current[src]; - if (op.op != null) { - var isOwnOperation = src === (op.src || ''); - presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); - } else { - presenceData = null; - } - return this._setPresence(src, presenceData); -} - -function _transformAllPresence(op) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._transformPresence(src, op)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); -} - -function _pausePresence() { - if (!this.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; - } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (src && this._setPresence(src, null)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); -} - -// If emit is true and presence has changed, emits a presence event. -// Returns true, if presence has changed. Otherwise false. -function _setPresence(src, data, emit) { - if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; - } else { - var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); - if (isPresenceEqual) return false; - this.presence.current[src] = data; - } - if (emit) this._emitPresence([ src ], true); - return true; -} - -function _emitPresence(srcList, submitted) { - if (srcList && srcList.length > 0) { - var doc = this; - process.nextTick(function() { - doc.emit('presence', srcList, submitted); - }); - } -} - -function _cacheOp(op) { - if (!this.presence) return; - // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; - var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { - break; - } - } - if (i > 0) { - this.presence.cachedOps.splice(0, i); - } - - // Cache the new op. - this.presence.cachedOps.push(op); -} - -// If there are no pending ops, this method sends the pending presence data, if possible. -function _flushPresence() { - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; - } -} - -function _destroyPresence() { - this.presence.received = {}; - this.presence.cachedOps.length = 0; -} - -// Reset presence-related properties. -function _hardRollbackPresence() { - var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); - - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._setPresence(src, null)) { - changedSrcList.push(src); - } - } - this._emitPresence(changedSrcList, false); - return pendingPresence; -} - -module.exports = { - submitPresence, - _initializePresence, - _handlePresence, - _processReceivedPresence, - _processAllReceivedPresence, - _transformPresence, - _transformAllPresence, - _pausePresence, - _setPresence, - _emitPresence, - _cacheOp, - _flushPresence, - _destroyPresence, - _hardRollbackPresence -}; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 91f635758..fd7136aea 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -351,27 +351,27 @@ function _destroyPresence() { } // Reset presence-related properties. -function _hardRollbackPresence() { +function hardRollbackPresence() { var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); + if (this.inflight) pendingPresence.push(this.inflight); + if (this.pending) pendingPresence.push(this.pending); - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; + this.inflight = null; + this.inflightSeq = 0; + this.pending = null; + this.cachedOps.length = 0; + this.received = {}; + this.requestReply = true; - var srcList = Object.keys(this.presence.current); + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._setPresence(src, null)) { + if (this.doc._setPresence(src, null)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); return pendingPresence; } @@ -389,5 +389,5 @@ module.exports = { _cacheOp, _flushPresence, _destroyPresence, - _hardRollbackPresence + hardRollbackPresence }; From 23a06c348764e9f135f0f0a9dade1f938422e9c5 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:30:18 +0530 Subject: [PATCH 37/95] Migrate _initializePresence --- lib/client/doc.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 3513826c6..6115ad4b4 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -73,16 +73,18 @@ function Doc(connection, collection, id) { // Expose presence-related methods on the Doc prototype. Object.assign(Doc.prototype, connection.Presence); + delete Doc.prototype.hardRollbackPresence; + delete Doc.prototype._initializePresence; + // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. - this.presence = this._initializePresence(); + this.presence = connection.Presence._initializePresence(); this.presence.doc = this; + Object.assign(this.presence, connection.Presence); - this.presence.hardRollbackPresence = connection.Presence.hardRollbackPresence; - delete Doc.prototype.hardRollbackPresence; } // Array of callbacks or nulls as placeholders From 824346ffcb5662ed1e9b62f753aa8b3e375561d9 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:51:25 +0530 Subject: [PATCH 38/95] Migrate _handlePresence --- lib/client/doc.js | 6 +++++ lib/presence/stateless.js | 46 +++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 6115ad4b4..582bcec51 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,10 +76,16 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; + Doc.prototype._handlePresence = function(err, presence) { + this.presence.handlePresence(err, presence); + }; + // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. + // + // TODO convert to constructor. this.presence = connection.Presence._initializePresence(); this.presence.doc = this; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index fd7136aea..a5eb63557 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -92,56 +92,56 @@ function _initializePresence() { }; } -function _handlePresence(err, presence) { - if (!this.subscribed) return; +function handlePresence(err, presence) { + if (!this.doc.subscribed) return; var src = presence.src; if (!src) { // Handle the ACK for the presence data we submitted. - // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // this.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - var called = callbacks && this._callEach(callbacks, err); - if (err && !called) this.emit('error', err); - this.flush(); - this._emitNothingPending(); + if (this.inflightSeq === presence.seq) { + var callbacks = this.inflight; + this.inflight = null; + this.inflightSeq = 0; + var called = callbacks && this.doc._callEach(callbacks, err); + if (err && !called) this.doc.emit('error', err); + this.doc.flush(); + this.doc._emitNothingPending(); } return; } // This shouldn't happen but check just in case. - if (err) return this.emit('error', err); + if (err) return this.doc.emit('error', err); - if (presence.r && !this.presence.pending) { + if (presence.r && !this.pending) { // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); + this.pending = []; + this.doc.flush(); } // Ignore older messages which arrived out of order if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) + this.received[src] && ( + this.received[src].seq > presence.seq || + (this.received[src].seq === presence.seq && presence.v != null) ) ) return; - this.presence.received[src] = presence; + this.received[src] = presence; if (presence.v == null) { // null version should happen only when the server automatically sends // null presence for an unsubscribed client presence.processedAt = Date.now(); - return this._setPresence(src, null, true); + return this.doc._setPresence(src, null, true); } // Get missing ops first, if necessary - if (this.version == null || this.version < presence.v) return this.fetch(); + if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch(); - this._processReceivedPresence(src, true); + this.doc._processReceivedPresence(src, true); } // If emit is true and presence has changed, emits a presence event. @@ -377,8 +377,8 @@ function hardRollbackPresence() { module.exports = { submitPresence, + handlePresence, _initializePresence, - _handlePresence, _processReceivedPresence, _processAllReceivedPresence, _transformPresence, From d6e3e3d6e5ac7e897b4c4c5a6d8d13af21d4a3b7 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:56:07 +0530 Subject: [PATCH 39/95] Migrate _processReceivedPresence --- lib/client/doc.js | 2 ++ lib/presence/stateless.js | 56 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 582bcec51..f545a6aa2 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -75,7 +75,9 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; + delete Doc.prototype._processReceivedPresence; + // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { this.presence.handlePresence(err, presence); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index a5eb63557..56734d5f3 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -141,25 +141,25 @@ function handlePresence(err, presence) { // Get missing ops first, if necessary if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch(); - this.doc._processReceivedPresence(src, true); + this._processReceivedPresence(src, true); } // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed for src. Otherwise false. function _processReceivedPresence(src, emit) { if (!src) return false; - var presence = this.presence.received[src]; + var presence = this.received[src]; if (!presence) return false; if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + if (Date.now() >= presence.processedAt + this.receivedTimeout) { // Remove old received and processed presence. - delete this.presence.received[src]; + delete this.received[src]; } return false; } - if (this.version == null || this.version < presence.v) { + if (this.doc.version == null || this.doc.version < presence.v) { // keep waiting for the missing snapshot or ops. return false; } @@ -167,65 +167,65 @@ function _processReceivedPresence(src, emit) { if (presence.p == null) { // Remove presence data as requested. presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } - if (!this.type || !this.type.createPresence || !this.type.transformPresence) { + if (!this.doc.type || !this.doc.type.createPresence || !this.doc.type.transformPresence) { // Remove presence data because the document is not created or its type does not support presence presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } - if (this.inflightOp && this.inflightOp.op == null) { + if (this.doc.inflightOp && this.doc.inflightOp.op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } - for (var i = 0; i < this.pendingOps.length; i++) { - if (this.pendingOps[i].op == null) { + for (var i = 0; i < this.doc.pendingOps.length; i++) { + if (this.doc.pendingOps[i].op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } } - var startIndex = this.presence.cachedOps.length - (this.version - presence.v); + var startIndex = this.cachedOps.length - (this.doc.version - presence.v); if (startIndex < 0) { // Remove presence data because we can't transform presence.received presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].op == null) { + for (var i = startIndex; i < this.cachedOps.length; i++) { + if (this.cachedOps[i].op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } } // Make sure the format of the data is correct - var data = this.type.createPresence(presence.p); + var data = this.doc.type.createPresence(presence.p); // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.cachedOps[i]; - data = this.type.transformPresence(data, op.op, presence.src === op.src); + for (var i = startIndex; i < this.cachedOps.length; i++) { + var op = this.cachedOps[i]; + data = this.doc.type.transformPresence(data, op.op, presence.src === op.src); } // Transform against pending ops - if (this.inflightOp) { - data = this.type.transformPresence(data, this.inflightOp.op, false); + if (this.doc.inflightOp) { + data = this.doc.type.transformPresence(data, this.doc.inflightOp.op, false); } - for (var i = 0; i < this.pendingOps.length; i++) { - data = this.type.transformPresence(data, this.pendingOps[i].op, false); + for (var i = 0; i < this.doc.pendingOps.length; i++) { + data = this.doc.type.transformPresence(data, this.doc.pendingOps[i].op, false); } // Set presence data presence.processedAt = Date.now(); - return this._setPresence(src, data, emit); + return this.doc._setPresence(src, data, emit); } function _processAllReceivedPresence() { @@ -234,7 +234,7 @@ function _processAllReceivedPresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._processReceivedPresence(src)) { + if (this.presence._processReceivedPresence(src)) { changedSrcList.push(src); } } From 0382c03a9749fac082a37211dd126de9f32aeb6b Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:02:56 +0530 Subject: [PATCH 40/95] Migrate processAllReceivedPresence --- lib/client/doc.js | 8 +++++--- lib/presence/stateless.js | 11 +++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index f545a6aa2..691e59ffd 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -77,6 +77,8 @@ function Doc(connection, collection, id) { delete Doc.prototype._initializePresence; delete Doc.prototype._processReceivedPresence; + delete Doc.prototype.processAllReceivedPresence; + // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { this.presence.handlePresence(err, presence); @@ -245,7 +247,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); callback && callback(); }; @@ -386,7 +388,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -932,7 +934,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 56734d5f3..a8efc9681 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -228,17 +228,16 @@ function _processReceivedPresence(src, emit) { return this.doc._setPresence(src, data, emit); } -function _processAllReceivedPresence() { - if (!this.presence) return; - var srcList = Object.keys(this.presence.received); +function processAllReceivedPresence() { + var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.presence._processReceivedPresence(src)) { + if (this._processReceivedPresence(src)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, true); + this.doc._emitPresence(changedSrcList, true); } function _transformPresence(src, op) { @@ -380,7 +379,7 @@ module.exports = { handlePresence, _initializePresence, _processReceivedPresence, - _processAllReceivedPresence, + processAllReceivedPresence, _transformPresence, _transformAllPresence, _pausePresence, From 46d8a1b6098e39087e9c0236daf8ca431e3741aa Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:06:08 +0530 Subject: [PATCH 41/95] Migrate _transformPresence --- lib/client/doc.js | 2 +- lib/presence/stateless.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 691e59ffd..1931ae718 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,8 +76,8 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; delete Doc.prototype._processReceivedPresence; - delete Doc.prototype.processAllReceivedPresence; + delete Doc.prototype._transformPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index a8efc9681..c281ba8f8 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -241,14 +241,14 @@ function processAllReceivedPresence() { } function _transformPresence(src, op) { - var presenceData = this.presence.current[src]; + var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); - presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); + presenceData = this.doc.type.transformPresence(presenceData, op.op, isOwnOperation); } else { presenceData = null; } - return this._setPresence(src, presenceData); + return this.doc._setPresence(src, presenceData); } function _transformAllPresence(op) { @@ -257,7 +257,7 @@ function _transformAllPresence(op) { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._transformPresence(src, op)) { + if (this.presence._transformPresence(src, op)) { changedSrcList.push(src); } } From fc16be76946717c6bdd24ccd51f22b4a7ee41978 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:47:36 +0530 Subject: [PATCH 42/95] Migrate pausePresence --- lib/client/doc.js | 10 ++++++---- lib/presence/stateless.js | 34 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 1931ae718..96e39b674 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._processReceivedPresence; delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; + delete Doc.prototype.pausePresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -414,10 +415,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - if (this.presence) this._pausePresence(); + if (this.presence) this.presence.pausePresence(); callEach(callbacks); } else { - if (this.presence) this._pausePresence(); + if (this.presence) this.presence.pausePresence(); } } }; @@ -477,10 +478,11 @@ Doc.prototype.unsubscribe = function(callback) { if (this.connection.canSend) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - this._pausePresence(); + + if (this.presence) this.presence.pausePresence(); return; } - this._pausePresence(); + if (this.presence) this.presence.pausePresence(); if (callback) process.nextTick(callback); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index c281ba8f8..b5addc6a4 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -264,29 +264,29 @@ function _transformAllPresence(op) { this._emitPresence(changedSrcList, false); } -function _pausePresence() { - if (!this.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; +function pausePresence() { + if (!this) return; + + if (this.inflight) { + this.pending = this.pending + ? this.inflight.concat(this.pending) + : this.inflight; + this.inflight = null; + this.inflightSeq = 0; + } else if (!this.pending && this.current[''] != null) { + this.pending = []; } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); + this.received = {}; + this.requestReply = true; + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (src && this._setPresence(src, null)) { + if (src && this.doc._setPresence(src, null)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); } // If emit is true and presence has changed, emits a presence event. @@ -382,7 +382,7 @@ module.exports = { processAllReceivedPresence, _transformPresence, _transformAllPresence, - _pausePresence, + pausePresence, _setPresence, _emitPresence, _cacheOp, From 9cb5564d1b3b894ec3f963ad77abe529b3cd6cfd Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:59:36 +0530 Subject: [PATCH 43/95] Migrate cacheOp --- lib/client/doc.js | 5 +++-- lib/presence/dummy.js | 1 + lib/presence/stateless.js | 15 +++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 96e39b674..446e1199b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -79,6 +79,7 @@ function Doc(connection, collection, id) { delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; delete Doc.prototype.pausePresence; + delete Doc.prototype.cacheOp; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -380,7 +381,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - if (this.presence) this._cacheOp({ + if (this.presence) this.presence.cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -927,7 +928,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - if (this.presence) this._cacheOp({ + if (this.presence) this.presence.cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 9fe310999..ac328f67a 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,3 +1,4 @@ +// TODO use this function DummyPresence () { } function noop () {} diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index b5addc6a4..1166306b0 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -315,22 +315,21 @@ function _emitPresence(srcList, submitted) { } } -function _cacheOp(op) { - if (!this.presence) return; +function cacheOp(op) { // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { + for (i = 0; i < this.cachedOps.length; i++) { + if (this.cachedOps[i].time >= oldOpTime) { break; } } if (i > 0) { - this.presence.cachedOps.splice(0, i); + this.cachedOps.splice(0, i); } // Cache the new op. - this.presence.cachedOps.push(op); + this.cachedOps.push(op); } // If there are no pending ops, this method sends the pending presence data, if possible. @@ -385,7 +384,7 @@ module.exports = { pausePresence, _setPresence, _emitPresence, - _cacheOp, + cacheOp, _flushPresence, _destroyPresence, hardRollbackPresence From 6461a79a66c49f464e9d188c2eca560f53ac4092 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:00:56 +0530 Subject: [PATCH 44/95] Migrate flushPresence --- lib/client/doc.js | 3 ++- lib/presence/stateless.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 446e1199b..d354bf18c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -80,6 +80,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._transformPresence; delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; + delete Doc.prototype._flushPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -516,7 +517,7 @@ Doc.prototype.flush = function() { } if (this.presence) { - this._flushPresence(); + this.presence.flushPresence(); } }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 1166306b0..99b67fb37 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -333,13 +333,13 @@ function cacheOp(op) { } // If there are no pending ops, this method sends the pending presence data, if possible. -function _flushPresence() { - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; +function flushPresence() { + if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { + this.inflight = this.pending; + this.inflightSeq = this.doc.connection.seq; + this.pending = null; + this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); + this.requestReply = false; } } @@ -385,7 +385,7 @@ module.exports = { _setPresence, _emitPresence, cacheOp, - _flushPresence, + flushPresence, _destroyPresence, hardRollbackPresence }; From 2358022223fb2735d2a5d755e49b2aeb802b6d8e Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:03:40 +0530 Subject: [PATCH 45/95] Migrate transformAllPresence --- lib/client/doc.js | 9 +++++---- lib/presence/stateless.js | 11 +++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index d354bf18c..c66c4240b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._processReceivedPresence; delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; + delete Doc.prototype.transformAllPresence; delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; @@ -623,7 +624,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); - if (this.presence) this._transformAllPresence(componentOp); + if (this.presence) this.presence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -636,7 +637,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place this.data = this.type.apply(this.data, op.op); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.transformAllPresence(op); // Emit an 'op' event once the local data includes the changes from the // op. For locally submitted ops, this will be synchronously with // submission and before the server or other clients have received the op. @@ -653,7 +654,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.transformAllPresence(op); this.emit('create', source); return; } @@ -661,7 +662,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.transformAllPresence(op); this.emit('del', oldData, source); return; } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 99b67fb37..7c1f2d780 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -251,17 +251,16 @@ function _transformPresence(src, op) { return this.doc._setPresence(src, presenceData); } -function _transformAllPresence(op) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); +function transformAllPresence(op) { + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.presence._transformPresence(src, op)) { + if (this._transformPresence(src, op)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); } function pausePresence() { @@ -380,7 +379,7 @@ module.exports = { _processReceivedPresence, processAllReceivedPresence, _transformPresence, - _transformAllPresence, + transformAllPresence, pausePresence, _setPresence, _emitPresence, From 41a474373d09347f6790c17592e9e3cf8981137b Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:07:19 +0530 Subject: [PATCH 46/95] Migrate emitPresence --- lib/client/doc.js | 1 + lib/presence/stateless.js | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c66c4240b..eddc55259 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -82,6 +82,7 @@ function Doc(connection, collection, id) { delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; + delete Doc.prototype._emitPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 7c1f2d780..12a746da1 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -237,7 +237,7 @@ function processAllReceivedPresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, true); + this._emitPresence(changedSrcList, true); } function _transformPresence(src, op) { @@ -260,7 +260,7 @@ function transformAllPresence(op) { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); } function pausePresence() { @@ -285,7 +285,7 @@ function pausePresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); } // If emit is true and presence has changed, emits a presence event. @@ -301,13 +301,13 @@ function _setPresence(src, data, emit) { if (isPresenceEqual) return false; this.presence.current[src] = data; } - if (emit) this._emitPresence([ src ], true); + if (emit) this.presence._emitPresence([ src ], true); return true; } function _emitPresence(srcList, submitted) { if (srcList && srcList.length > 0) { - var doc = this; + var doc = this.doc; process.nextTick(function() { doc.emit('presence', srcList, submitted); }); @@ -368,7 +368,7 @@ function hardRollbackPresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); return pendingPresence; } From ba7d8802458bd6f2d8a65cb28add03fe3aeb5c13 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:10:09 +0530 Subject: [PATCH 47/95] Migrate submitPresence --- lib/client/doc.js | 7 ++++++- lib/presence/stateless.js | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index eddc55259..ef581ede0 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -83,9 +83,14 @@ function Doc(connection, collection, id) { delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; delete Doc.prototype._emitPresence; + delete Doc.prototype.submitPresence; + + Doc.prototype.submitPresence = function (data, callback) { + this.presence.submitPresence(data, callback); + }; // TODO move this with other _handle... definitions. - Doc.prototype._handlePresence = function(err, presence) { + Doc.prototype._handlePresence = function (err, presence) { this.presence.handlePresence(err, presence); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 12a746da1..20b005301 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -14,8 +14,8 @@ var ShareDBError = require('../error'); // All the others are marked as internal with a leading "_". function submitPresence(data, callback) { if (data != null) { - if (!this.type) { - var doc = this; + if (!this.doc.type) { + var doc = this.doc; return process.nextTick(function() { var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); if (callback) return callback(err); @@ -23,8 +23,8 @@ function submitPresence(data, callback) { }); } - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; + if (!this.doc.type.createPresence || !this.doc.type.transformPresence) { + var doc = this.doc; return process.nextTick(function() { var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); if (callback) return callback(err); @@ -32,22 +32,22 @@ function submitPresence(data, callback) { }); } - data = this.type.createPresence(data); + data = this.doc.type.createPresence(data); } - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; + if (this.doc._setPresence('', data, true) || this.pending || this.inflight) { + if (!this.pending) { + this.pending = []; } if (callback) { - this.presence.pending.push(callback); + this.pending.push(callback); } } else if (callback) { process.nextTick(callback); } - process.nextTick(this.flush.bind(this)); + process.nextTick(this.doc.flush.bind(this.doc)); } // This function generates the initial value for doc.presence. From 3ffd1abbccff6ca366d85520b563ae98e0f685ec Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:13:37 +0530 Subject: [PATCH 48/95] Migrate _setPresence --- lib/client/doc.js | 3 +++ lib/presence/stateless.js | 47 ++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index ef581ede0..c6e3a1f4c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -84,7 +84,10 @@ function Doc(connection, collection, id) { delete Doc.prototype._flushPresence; delete Doc.prototype._emitPresence; delete Doc.prototype.submitPresence; + delete Doc.prototype._destroyPresence; + delete Doc.prototype._setPresence; + // TODO move this outside Doc.prototype.submitPresence = function (data, callback) { this.presence.submitPresence(data, callback); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 20b005301..87e59654a 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -35,7 +35,7 @@ function submitPresence(data, callback) { data = this.doc.type.createPresence(data); } - if (this.doc._setPresence('', data, true) || this.pending || this.inflight) { + if (this._setPresence('', data, true) || this.pending || this.inflight) { if (!this.pending) { this.pending = []; } @@ -135,7 +135,7 @@ function handlePresence(err, presence) { // null version should happen only when the server automatically sends // null presence for an unsubscribed client presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, true); + return this._setPresence(src, null, true); } // Get missing ops first, if necessary @@ -167,26 +167,26 @@ function _processReceivedPresence(src, emit) { if (presence.p == null) { // Remove presence data as requested. presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } if (!this.doc.type || !this.doc.type.createPresence || !this.doc.type.transformPresence) { // Remove presence data because the document is not created or its type does not support presence presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } if (this.doc.inflightOp && this.doc.inflightOp.op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } for (var i = 0; i < this.doc.pendingOps.length; i++) { if (this.doc.pendingOps[i].op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } } @@ -194,14 +194,14 @@ function _processReceivedPresence(src, emit) { if (startIndex < 0) { // Remove presence data because we can't transform presence.received presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } for (var i = startIndex; i < this.cachedOps.length; i++) { if (this.cachedOps[i].op == null) { // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del" presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } } @@ -225,7 +225,7 @@ function _processReceivedPresence(src, emit) { // Set presence data presence.processedAt = Date.now(); - return this.doc._setPresence(src, data, emit); + return this._setPresence(src, data, emit); } function processAllReceivedPresence() { @@ -248,7 +248,7 @@ function _transformPresence(src, op) { } else { presenceData = null; } - return this.doc._setPresence(src, presenceData); + return this._setPresence(src, presenceData); } function transformAllPresence(op) { @@ -281,7 +281,7 @@ function pausePresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (src && this.doc._setPresence(src, null)) { + if (src && this._setPresence(src, null)) { changedSrcList.push(src); } } @@ -292,16 +292,16 @@ function pausePresence() { // Returns true, if presence has changed. Otherwise false. function _setPresence(src, data, emit) { if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; + if (this.current[src] == null) return false; + delete this.current[src]; } else { var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + this.current[src] === data || + (this.doc.type.comparePresence && this.doc.type.comparePresence(this.current[src], data)); if (isPresenceEqual) return false; - this.presence.current[src] = data; + this.current[src] = data; } - if (emit) this.presence._emitPresence([ src ], true); + if (emit) this._emitPresence([ src ], true); return true; } @@ -342,10 +342,11 @@ function flushPresence() { } } -function _destroyPresence() { - this.presence.received = {}; - this.presence.cachedOps.length = 0; -} +// TODO cover with a test +//function _destroyPresence() { +// this.presence.received = {}; +// this.presence.cachedOps.length = 0; +//} // Reset presence-related properties. function hardRollbackPresence() { @@ -364,7 +365,7 @@ function hardRollbackPresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.doc._setPresence(src, null)) { + if (this._setPresence(src, null)) { changedSrcList.push(src); } } @@ -385,6 +386,6 @@ module.exports = { _emitPresence, cacheOp, flushPresence, - _destroyPresence, + //_destroyPresence, hardRollbackPresence }; From 6114bade0b4311f63fb664952b26815499fb043d Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:15:07 +0530 Subject: [PATCH 49/95] Clean up intermediate migration steps --- lib/client/doc.js | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c6e3a1f4c..ee667b305 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -69,33 +69,6 @@ function Doc(connection, collection, id) { this.data = undefined; if (connection.Presence) { - // TODO don't decorate - // Expose presence-related methods on the Doc prototype. - Object.assign(Doc.prototype, connection.Presence); - - delete Doc.prototype.hardRollbackPresence; - delete Doc.prototype._initializePresence; - delete Doc.prototype._processReceivedPresence; - delete Doc.prototype.processAllReceivedPresence; - delete Doc.prototype._transformPresence; - delete Doc.prototype.transformAllPresence; - delete Doc.prototype.pausePresence; - delete Doc.prototype.cacheOp; - delete Doc.prototype._flushPresence; - delete Doc.prototype._emitPresence; - delete Doc.prototype.submitPresence; - delete Doc.prototype._destroyPresence; - delete Doc.prototype._setPresence; - - // TODO move this outside - Doc.prototype.submitPresence = function (data, callback) { - this.presence.submitPresence(data, callback); - }; - - // TODO move this with other _handle... definitions. - Doc.prototype._handlePresence = function (err, presence) { - this.presence.handlePresence(err, presence); - }; // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that @@ -407,6 +380,14 @@ Doc.prototype._handleOp = function(err, message) { } }; +Doc.prototype._handlePresence = function (err, presence) { + this.presence.handlePresence(err, presence); +}; + +Doc.prototype.submitPresence = function (data, callback) { + this.presence.submitPresence(data, callback); +}; + // Called whenever (you guessed it!) the connection state changes. This will // happen when we get disconnected & reconnect. Doc.prototype._onConnectionStateChanged = function() { From 19446cd613f3d9c40df453545851196f46429908 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 21:23:04 +0530 Subject: [PATCH 50/95] Convert StatelessPresence to a class --- lib/client/doc.js | 3 +-- lib/presence/stateless.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index ee667b305..05ecb1325 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,9 +76,8 @@ function Doc(connection, collection, id) { // so the presence features should be disabled. // // TODO convert to constructor. - this.presence = connection.Presence._initializePresence(); + this.presence = new connection.Presence(this); - this.presence.doc = this; Object.assign(this.presence, connection.Presence); } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 87e59654a..43325fccd 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -373,7 +373,12 @@ function hardRollbackPresence() { return pendingPresence; } -module.exports = { +function StatelessPresence(doc) { + this.doc = doc; + Object.assign(this, this._initializePresence()); +} + +Object.assign(StatelessPresence.prototype, { submitPresence, handlePresence, _initializePresence, @@ -388,4 +393,6 @@ module.exports = { flushPresence, //_destroyPresence, hardRollbackPresence -}; +}); + +module.exports = StatelessPresence; From 0089f80f80ca976aafa412cc8fad82e099817e6a Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 21:29:48 +0530 Subject: [PATCH 51/95] Convert StatelessPresence into idiomatic JS class. --- lib/presence/stateless.js | 172 +++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 96 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 43325fccd..4586c36bd 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -1,18 +1,62 @@ /* - * Presence Methods - * ---------------- - * - * This module contains definitions for presence-related methods - * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * Stateless Presence + * ------------------ + * + * This module provides an implementation of presence that works, + * but has some scalability problems. Each time a client joins a document, + * this implementation requests current presence information from all other clients, + * via the server. The server does not store any state at all regarding presence, + * it exists only in clients, hence the name "Stateless Presence". * - * The value of 'this' in these functions will be the Doc instance. */ var ShareDBError = require('../error'); +// TODO inherit from Presence, add test for that. +function StatelessPresence(doc) { + + // The Doc instance that this Presence is attached to. + this.doc = doc; + + // The current presence data. + // Map of src -> presence data + // Local src === '' + this.current = {}; + + // The presence objects received from the server. + // Map of src -> presence + this.received = {}; + + // The minimum amount of time to wait before removing processed presence from this.presence.received. + // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. + // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower + // sequence number arrive after messages with higher sequence numbers. + this.receivedTimeout = 60000; + + // If set to true, then the next time the local presence is sent, + // all other clients will be asked to reply with their own presence data. + this.requestReply = true; + + // A list of ops sent by the server. These are needed for transforming presence data, + // if we get that presence data for an older version of the document. + this.cachedOps = []; + + // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence + // data is supposed to be synced in real-time. + this.cachedOpsTimeout = 60000; + + // The sequence number of the inflight presence request. + this.inflightSeq = 0; + + // Callbacks (or null) for pending and inflight presence requests. + this.pending = null; + this.inflight = null; +} + + // Submit presence data to a document. // This is the only public facing method. // All the others are marked as internal with a leading "_". -function submitPresence(data, callback) { +StatelessPresence.prototype.submitPresence = function (data, callback) { if (data != null) { if (!this.doc.type) { var doc = this.doc; @@ -48,51 +92,9 @@ function submitPresence(data, callback) { } process.nextTick(this.doc.flush.bind(this.doc)); -} - -// This function generates the initial value for doc.presence. -function _initializePresence() { - - // Return a new object each time, otherwise mutations would bleed across documents. - return { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, +}; - // The presence objects received from the server. - // Map of src -> presence - received: {}, - - // The minimum amount of time to wait before removing processed presence from this.presence.received. - // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot. - // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower - // sequence number arrive after messages with higher sequence numbers. - receivedTimeout: 60000, - - // If set to true, then the next time the local presence is sent, - // all other clients will be asked to reply with their own presence data. - requestReply: true, - - // A list of ops sent by the server. These are needed for transforming presence data, - // if we get that presence data for an older version of the document. - cachedOps: [], - - // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence - // data is supposed to be synced in real-time. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; -} - -function handlePresence(err, presence) { +StatelessPresence.prototype.handlePresence = function (err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -142,11 +144,11 @@ function handlePresence(err, presence) { if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch(); this._processReceivedPresence(src, true); -} +}; // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed for src. Otherwise false. -function _processReceivedPresence(src, emit) { +StatelessPresence.prototype._processReceivedPresence = function (src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -226,9 +228,9 @@ function _processReceivedPresence(src, emit) { // Set presence data presence.processedAt = Date.now(); return this._setPresence(src, data, emit); -} +}; -function processAllReceivedPresence() { +StatelessPresence.prototype.processAllReceivedPresence = function () { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -238,9 +240,9 @@ function processAllReceivedPresence() { } } this._emitPresence(changedSrcList, true); -} +}; -function _transformPresence(src, op) { +StatelessPresence.prototype._transformPresence = function (src, op) { var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -249,9 +251,9 @@ function _transformPresence(src, op) { presenceData = null; } return this._setPresence(src, presenceData); -} +}; -function transformAllPresence(op) { +StatelessPresence.prototype.transformAllPresence = function (op) { var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -261,9 +263,9 @@ function transformAllPresence(op) { } } this._emitPresence(changedSrcList, false); -} +}; -function pausePresence() { +StatelessPresence.prototype.pausePresence = function () { if (!this) return; if (this.inflight) { @@ -286,11 +288,11 @@ function pausePresence() { } } this._emitPresence(changedSrcList, false); -} +}; // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed. Otherwise false. -function _setPresence(src, data, emit) { +StatelessPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { if (this.current[src] == null) return false; delete this.current[src]; @@ -303,18 +305,18 @@ function _setPresence(src, data, emit) { } if (emit) this._emitPresence([ src ], true); return true; -} +}; -function _emitPresence(srcList, submitted) { +StatelessPresence.prototype._emitPresence = function (srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { doc.emit('presence', srcList, submitted); }); } -} +}; -function cacheOp(op) { +StatelessPresence.prototype.cacheOp = function (op) { // Remove the old ops. var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; @@ -329,10 +331,10 @@ function cacheOp(op) { // Cache the new op. this.cachedOps.push(op); -} +}; // If there are no pending ops, this method sends the pending presence data, if possible. -function flushPresence() { +StatelessPresence.prototype.flushPresence = function () { if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; @@ -340,16 +342,16 @@ function flushPresence() { this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); this.requestReply = false; } -} +}; // TODO cover with a test -//function _destroyPresence() { +//StatelessPresence.prototype._destroyPresence = function () { // this.presence.received = {}; // this.presence.cachedOps.length = 0; -//} +//}; // Reset presence-related properties. -function hardRollbackPresence() { +StatelessPresence.prototype.hardRollbackPresence = function () { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); @@ -371,28 +373,6 @@ function hardRollbackPresence() { } this._emitPresence(changedSrcList, false); return pendingPresence; -} - -function StatelessPresence(doc) { - this.doc = doc; - Object.assign(this, this._initializePresence()); -} - -Object.assign(StatelessPresence.prototype, { - submitPresence, - handlePresence, - _initializePresence, - _processReceivedPresence, - processAllReceivedPresence, - _transformPresence, - transformAllPresence, - pausePresence, - _setPresence, - _emitPresence, - cacheOp, - flushPresence, - //_destroyPresence, - hardRollbackPresence -}); +}; module.exports = StatelessPresence; From 205405706b43bd2bc7cfe95c4f60554a2b3ea8e2 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:31:30 +0530 Subject: [PATCH 52/95] Add test case that doc invokes presence.destroy inside doc.destroy --- lib/client/doc.js | 4 ++-- lib/presence/stateless.js | 9 ++++----- test/client/presence.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 05ecb1325..6d05cfca6 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -135,12 +135,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) doc._destroyPresence(); + if (doc.presence) doc.presence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) doc._destroyPresence(); + if (doc.presence) doc.presence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 4586c36bd..37f48ef5a 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -344,11 +344,10 @@ StatelessPresence.prototype.flushPresence = function () { } }; -// TODO cover with a test -//StatelessPresence.prototype._destroyPresence = function () { -// this.presence.received = {}; -// this.presence.cachedOps.length = 0; -//}; +StatelessPresence.prototype.destroyPresence = function () { + this.received = {}; + this.cachedOps.length = 0; +}; // Reset presence-related properties. StatelessPresence.prototype.hardRollbackPresence = function () { diff --git a/test/client/presence.js b/test/client/presence.js index f5ee3f1fa..07547af4c 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1452,5 +1452,16 @@ describe('client presence', function() { it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(false, true)); it('processes an old message (cache expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(true, false)); it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(true, true)); + + it('invokes presence.destroy inside doc.destroy', function(done) { + var presence = this.doc.presence; + presence.cachedOps = ['foo']; + presence.received = { bar: true }; + this.doc.destroy(function(err) { + expect(presence.cachedOps).to.eql([]); + expect(presence.received).to.eql({}); + done(); + }); + }); }); }); From 1a64a06865a8a50c87d7f3742a958c01cb4aaa6f Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:52:33 +0530 Subject: [PATCH 53/95] Introduce DummyPresence, use it by default --- lib/client/doc.js | 12 ++++++------ lib/presence/dummy.js | 27 +++++++++++++++++++++------ lib/presence/stateless.js | 8 ++++++++ test/client/presence.js | 5 +++-- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 6d05cfca6..c8d950da7 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var DummyPresence = require('../presence/dummy'); /** * A Doc is a client's view on a sharejs document. @@ -77,9 +78,8 @@ function Doc(connection, collection, id) { // // TODO convert to constructor. this.presence = new connection.Presence(this); - - Object.assign(this.presence, connection.Presence); - + } else { + this.presence = new DummyPresence(); } // Array of callbacks or nulls as placeholders @@ -223,7 +223,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; if (this.presence) { - this.presence.cachedOps.length = 0; + this.presence.clearCachedOps(); } var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; @@ -255,7 +255,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence && (this.presence.inflight || this.presence.pending) + this.presence.hasPendingPresence() ); }; @@ -905,7 +905,7 @@ Doc.prototype._opAcknowledged = function(message) { this.version = message.v; if (this.presence) { - this.presence.cachedOps.length = 0; + this.presence.clearCachedOps(); } } else if (message.v !== this.version) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index ac328f67a..ad2633a6d 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,15 +1,30 @@ +/* + * Dummy Presence + * ------------------ + * + * This module provides a dummy implementation of presence that does nothing. + * Its purpose is to stand in for a real implementation, to simplify code in doc.js. + */ + // TODO use this +// TODO inherit from Presence, add test for that. function DummyPresence () { } function noop () {} -DummyPresence.prototype.flushPresence = noop; -DummyPresence.prototype.destroyPresence = noop; -DummyPresence.prototype.clearCachedOps = noop; // this.presence.cachedOps.length = 0; +DummyPresence.prototype.submitPresence = noop; +DummyPresence.prototype.handlePresence = noop; DummyPresence.prototype.processAllReceivedPresence = noop; -DummyPresence.prototype.hardRollbackPresence = function () { return []; }; DummyPresence.prototype.transformAllPresence = noop; +DummyPresence.prototype.pausePresence = noop; DummyPresence.prototype.cacheOp = noop; -DummyPresence.prototype.hasPending = function () { return false }; // (this.presence.inflight || this.presence.pending) -DummyPresence.prototype.pause = noop; +DummyPresence.prototype.flushPresence = noop; +DummyPresence.prototype.destroyPresence = noop; +DummyPresence.prototype.clearCachedOps = noop; +DummyPresence.prototype.hardRollbackPresence = function () { return []; }; +DummyPresence.prototype.hasPendingPresence = function () { return false }; +DummyPresence.prototype._processReceivedPresence = noop; +DummyPresence.prototype._transformPresence = noop; +DummyPresence.prototype._setPresence = noop; +DummyPresence.prototype._emitPresence = noop; module.exports = DummyPresence; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 37f48ef5a..35361acab 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -346,6 +346,10 @@ StatelessPresence.prototype.flushPresence = function () { StatelessPresence.prototype.destroyPresence = function () { this.received = {}; + this.clearCachedOps(); +}; + +StatelessPresence.prototype.clearCachedOps = function () { this.cachedOps.length = 0; }; @@ -374,4 +378,8 @@ StatelessPresence.prototype.hardRollbackPresence = function () { return pendingPresence; }; +StatelessPresence.prototype.hasPendingPresence = function () { + return this.inflight || this.pending; +}; + module.exports = StatelessPresence; diff --git a/test/client/presence.js b/test/client/presence.js index 07547af4c..ec85172ff 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); +var DummyPresence = require('../../lib/presence/dummy'); var StatelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); var expect = require('expect.js'); @@ -13,11 +14,11 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('does not expose doc.presence if enablePresence is false', function() { + it('should use DummyPresence if Presence option not provided', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(typeof doc.presence).to.equal('undefined'); + expect(doc.presence instanceof DummyPresence); }); }); From 47193daf4ab13a76cef2b0357c779b8c577e4178 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:57:26 +0530 Subject: [PATCH 54/95] Remove if(this.presence) guards. --- lib/client/doc.js | 57 +++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c8d950da7..c2afdff87 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -69,18 +69,9 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - if (connection.Presence) { - - // Properties related to presence are grouped within this object. - // If this.presence is falsy (undefined), it means that - // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled. - // - // TODO convert to constructor. - this.presence = new connection.Presence(this); - } else { - this.presence = new DummyPresence(); - } + this.presence = connection.Presence + ? new connection.Presence(this) + : new DummyPresence(); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -222,9 +213,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; - if (this.presence) { - this.presence.clearCachedOps(); - } + this.presence.clearCachedOps(); var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); @@ -232,7 +221,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); callback && callback(); }; @@ -364,7 +353,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - if (this.presence) this.presence.cacheOp({ + this.presence.cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -373,7 +362,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -407,10 +396,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); callEach(callbacks); } else { - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); } } }; @@ -471,10 +460,10 @@ Doc.prototype.unsubscribe = function(callback) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); return; } - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -506,9 +495,7 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.presence) { - this.presence.flushPresence(); - } + this.presence.flushPresence(); }; // Helper function to set op to contain a no-op. @@ -613,7 +600,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); - if (this.presence) this.presence.transformAllPresence(componentOp); + this.presence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -626,7 +613,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place this.data = this.type.apply(this.data, op.op); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.transformAllPresence(op); // Emit an 'op' event once the local data includes the changes from the // op. For locally submitted ops, this will be synchronously with // submission and before the server or other clients have received the op. @@ -643,7 +630,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.transformAllPresence(op); this.emit('create', source); return; } @@ -651,7 +638,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -903,11 +890,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - - if (this.presence) { - this.presence.clearCachedOps(); - } - + this.presence.clearCachedOps(); } else if (message.v !== this.version) { // We should already be at the same version, because the server should // have sent all the ops that have happened before acknowledging our op @@ -919,7 +902,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - if (this.presence) this.presence.cacheOp({ + this.presence.cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, @@ -928,7 +911,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { @@ -977,7 +960,7 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence ? this.presence.hardRollbackPresence() : []; + var pendingPresence = this.presence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); From b23661c7eef4f6fe830b95371953bc6999b052c0 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:59:59 +0530 Subject: [PATCH 55/95] Optimize cacheOp --- lib/client/doc.js | 16 ++-------------- lib/presence/stateless.js | 9 ++++++++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c2afdff87..119032671 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -353,13 +353,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this.presence.cacheOp({ - src: message.src, - time: Date.now(), - create: !!message.create, - op: message.op, - del: !!message.del - }); + this.presence.cacheOp(message); try { this._otApply(message, false); this.presence.processAllReceivedPresence(); @@ -902,14 +896,8 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this.presence.cacheOp({ - src: this.inflightOp.src, - time: Date.now(), - create: !!this.inflightOp.create, - op: this.inflightOp.op, - del: !!this.inflightOp.del - }); + this.presence.cacheOp(this.inflightOp); this._clearInflightOp(); this.presence.processAllReceivedPresence(); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 35361acab..1f85f4dda 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -316,7 +316,14 @@ StatelessPresence.prototype._emitPresence = function (srcList, submitted) { } }; -StatelessPresence.prototype.cacheOp = function (op) { +StatelessPresence.prototype.cacheOp = function (message) { + var op = { + src: message.src, + time: Date.now(), + create: !!message.create, + op: message.op, + del: !!message.del + } // Remove the old ops. var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; From 521f77b03398cb08175af1b26779e530c29089be Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:10:48 +0530 Subject: [PATCH 56/95] Split out getPendingPresence logic from hardRollbackPresence. --- lib/presence/stateless.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 1f85f4dda..23a4610ef 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -362,10 +362,6 @@ StatelessPresence.prototype.clearCachedOps = function () { // Reset presence-related properties. StatelessPresence.prototype.hardRollbackPresence = function () { - var pendingPresence = []; - if (this.inflight) pendingPresence.push(this.inflight); - if (this.pending) pendingPresence.push(this.pending); - this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -382,11 +378,17 @@ StatelessPresence.prototype.hardRollbackPresence = function () { } } this._emitPresence(changedSrcList, false); - return pendingPresence; }; StatelessPresence.prototype.hasPendingPresence = function () { return this.inflight || this.pending; }; +StatelessPresence.prototype.getPendingPresence = function () { + var pendingPresence = []; + if (this.inflight) pendingPresence.push(this.inflight); + if (this.pending) pendingPresence.push(this.pending); + return pendingPresence; +}; + module.exports = StatelessPresence; From bdb6424c5199e68ea21d6bdc8ea737beeca1daf6 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:11:12 +0530 Subject: [PATCH 57/95] Clean up DummyPresence --- lib/client/doc.js | 3 ++- lib/presence/dummy.js | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 119032671..60edde765 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -948,7 +948,8 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence.hardRollbackPresence(); + var pendingPresence = this.presence.getPendingPresence(); + this.presence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index ad2633a6d..c7d23d9fc 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -10,21 +10,26 @@ // TODO inherit from Presence, add test for that. function DummyPresence () { } function noop () {} +function returnEmptyArray () { return []; }; +function returnFalse () { return false; }; -DummyPresence.prototype.submitPresence = noop; -DummyPresence.prototype.handlePresence = noop; -DummyPresence.prototype.processAllReceivedPresence = noop; -DummyPresence.prototype.transformAllPresence = noop; -DummyPresence.prototype.pausePresence = noop; -DummyPresence.prototype.cacheOp = noop; -DummyPresence.prototype.flushPresence = noop; -DummyPresence.prototype.destroyPresence = noop; -DummyPresence.prototype.clearCachedOps = noop; -DummyPresence.prototype.hardRollbackPresence = function () { return []; }; -DummyPresence.prototype.hasPendingPresence = function () { return false }; -DummyPresence.prototype._processReceivedPresence = noop; -DummyPresence.prototype._transformPresence = noop; -DummyPresence.prototype._setPresence = noop; -DummyPresence.prototype._emitPresence = noop; +Object.assign(DummyPresence.prototype, { + submitPresence: noop, + handlePresence: noop, + processAllReceivedPresence: noop, + transformAllPresence: noop, + pausePresence: noop, + cacheOp: noop, + flushPresence: noop, + destroyPresence: noop, + clearCachedOps: noop, + hardRollbackPresence: returnEmptyArray, + hasPendingPresence: returnFalse, + getPendingPresence: returnEmptyArray, + _processReceivedPresence: noop, + _transformPresence: noop, + _setPresence: noop, + _emitPresence: noop +}); module.exports = DummyPresence; From 0f3084aac391b2c5ccbe4e9b1e854e5f4859f61c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:23:25 +0530 Subject: [PATCH 58/95] Introduce Presence base class inherited by DummyPresence and StatelessPresence --- lib/presence/dummy.js | 7 +++++-- lib/presence/stateless.js | 4 +++- test/client/presence.js | 11 ++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index c7d23d9fc..a893b5a2c 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -5,10 +5,13 @@ * This module provides a dummy implementation of presence that does nothing. * Its purpose is to stand in for a real implementation, to simplify code in doc.js. */ +var Presence = require('.'); -// TODO use this -// TODO inherit from Presence, add test for that. function DummyPresence () { } + +// Inherit from Presence to support instanceof type checking. +DummyPresence.prototype = Object.create(Presence.prototype); + function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 23a4610ef..d3642b877 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -10,8 +10,8 @@ * */ var ShareDBError = require('../error'); +var Presence = require('.'); -// TODO inherit from Presence, add test for that. function StatelessPresence(doc) { // The Doc instance that this Presence is attached to. @@ -52,6 +52,8 @@ function StatelessPresence(doc) { this.inflight = null; } +// Inherit from Presence to support instanceof type checking. +StatelessPresence.prototype = Object.create(Presence.prototype); // Submit presence data to a document. // This is the only public facing method. diff --git a/test/client/presence.js b/test/client/presence.js index ec85172ff..abca8ea54 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); +var Presence = require('../../lib/presence'); var DummyPresence = require('../../lib/presence/dummy'); var StatelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); @@ -18,7 +19,15 @@ describe('client presence', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof DummyPresence); + expect(doc.presence instanceof DummyPresence).to.be(true); + }); + + it('DummyPresence should subclass Presence', function() { + expect(DummyPresence.prototype instanceof Presence).to.be(true); + }); + + it('StatelessPresence should subclass Presence', function() { + expect(StatelessPresence.prototype instanceof Presence).to.be(true); }); }); From 41cd2eae8171ac1a1ca4d70f3cb128ae548334eb Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 11:37:10 +0530 Subject: [PATCH 59/95] Add Presence base class module --- lib/presence/index.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/presence/index.js diff --git a/lib/presence/index.js b/lib/presence/index.js new file mode 100644 index 000000000..2ce09b6d3 --- /dev/null +++ b/lib/presence/index.js @@ -0,0 +1 @@ +module.exports = function Presence () {}; From 986a695348c33732e5787bfc0aa5c6efe35232f9 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 11:51:59 +0530 Subject: [PATCH 60/95] Start disentangling presence logic from Agent --- lib/agent.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index b4da48c0f..351667674 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -27,6 +27,17 @@ function Agent(backend, stream) { // Map from queryId -> emitter this.subscribedQueries = {}; + this.presence = { + agent: this, + processPresenceData: function (data) { + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== this.agent.clientId) this.agent.send(data); + return true; + } + } + }; + // The max presence sequence number received from the client. this.maxPresenceSeq = 0; @@ -110,17 +121,11 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (data.a === 'p') { - // Send other clients' presence data - if (data.src !== agent.clientId) agent.send(data); - return; - } + if (agent.presence.processPresenceData(data)) return; if (agent._isOwnOp(collection, data)) return; agent._sendOp(collection, id, data); }); stream.on('end', function() { - var presence = agent._createPresence(collection, id); - agent.backend.sendPresence(presence); // The op stream is done sending, so release its reference var streams = agent.subscribedDocs[collection]; if (!streams || streams[id] !== stream) return; @@ -128,6 +133,10 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); + stream.on('end', function() { + var presence = agent._createPresence(collection, id); + agent.backend.sendPresence(presence); + }); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { From ac55884b3aaaca784a6e6239052402243c4063f5 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:17:28 +0530 Subject: [PATCH 61/95] Migrate Agent._createPresence --- lib/agent.js | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 351667674..718e5b354 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -35,12 +35,23 @@ function Agent(backend, stream) { if (data.src !== this.agent.clientId) this.agent.send(data); return true; } + }, + // The max presence sequence number received from the client. + maxPresenceSeq: 0, + createPresence: function(collection, id, data, version, requestReply, seq) { + return { + a: 'p', + src: this.agent.clientId, + seq: seq != null ? seq : this.maxPresenceSeq, + c: collection, + d: id, + p: data, + v: version, + r: requestReply + }; } }; - // The max presence sequence number received from the client. - this.maxPresenceSeq = 0; - // We need to track this manually to make sure we don't reply to messages // after the stream was closed. this.closed = false; @@ -134,7 +145,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { delete agent.subscribedDocs[collection]; }); stream.on('end', function() { - var presence = agent._createPresence(collection, id); + var presence = agent.presence.createPresence(collection, id); agent.backend.sendPresence(presence); }); }; @@ -352,8 +363,7 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); case 'p': - var presence = this._createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - return this._presence(presence, callback); + return this._handlePresenceMessage(request, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -645,13 +655,14 @@ Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); }; -Agent.prototype._presence = function(presence, callback) { - if (presence.seq <= this.maxPresenceSeq) { +Agent.prototype._handlePresenceMessage = function(request, callback) { + var presence = this.presence.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + if (presence.seq <= this.presence.maxPresenceSeq) { return process.nextTick(function() { callback(new ShareDBError(4026, 'Presence data superseded')); }); } - this.maxPresenceSeq = presence.seq; + this.presence.maxPresenceSeq = presence.seq; if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) { return process.nextTick(function() { callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); @@ -662,16 +673,3 @@ Agent.prototype._presence = function(presence, callback) { callback(null, { seq: presence.seq }); }); }; - -Agent.prototype._createPresence = function(collection, id, data, version, requestReply, seq) { - return { - a: 'p', - src: this.clientId, - seq: seq != null ? seq : this.maxPresenceSeq, - c: collection, - d: id, - p: data, - v: version, - r: requestReply - }; -}; From 9187b346a236198c883e46a39809ca78f7217c9c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:24:10 +0530 Subject: [PATCH 62/95] Migrate subscribeToStream --- lib/agent.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 718e5b354..ba0e0b06b 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -49,6 +49,12 @@ function Agent(backend, stream) { v: version, r: requestReply }; + }, + subscribeToStream: function (collection, id, stream) { + var agent = this.agent; + stream.on('end', function() { + agent.backend.sendPresence(agent.presence.createPresence(collection, id)); + }); } }; @@ -144,10 +150,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); - stream.on('end', function() { - var presence = agent.presence.createPresence(collection, id); - agent.backend.sendPresence(presence); - }); + this.presence.subscribeToStream(collection, id, stream); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { From 75077314ee5f892031cbd2560af758e4edf1413d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:27:59 +0530 Subject: [PATCH 63/95] Migrate _subscribeToQuery --- lib/agent.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index ba0e0b06b..7380e1287 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -55,6 +55,17 @@ function Agent(backend, stream) { stream.on('end', function() { agent.backend.sendPresence(agent.presence.createPresence(collection, id)); }); + }, + checkRequest: function (request) { + if (request.a === 'p') { + if (typeof request.c !== 'string') return 'Invalid collection'; + if (typeof request.d !== 'string') return 'Invalid id'; + if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; + if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; + if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') { + return 'Invalid "request reply" value'; + } + } } }; @@ -322,13 +333,8 @@ Agent.prototype._checkRequest = function(request) { // Bulk request if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; - } else if (request.a === 'p') { - // Presence - if (typeof request.c !== 'string') return 'Invalid collection'; - if (typeof request.d !== 'string') return 'Invalid id'; - if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; - if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; - if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') return 'Invalid "request reply" value'; + } else { + return this.presence.checkRequest(request); } }; From 1a1f52aca6c7ed37bf3b8f952dc1127d3d7e4050 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:35:22 +0530 Subject: [PATCH 64/95] Migrate handlePresenceMessage --- lib/agent.js | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 7380e1287..53891b306 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -29,6 +29,9 @@ function Agent(backend, stream) { this.presence = { agent: this, + isPresenceData: function (data) { + return data.a === 'p'; + }, processPresenceData: function (data) { if (data.a === 'p') { // Send other clients' presence data @@ -66,6 +69,24 @@ function Agent(backend, stream) { return 'Invalid "request reply" value'; } } + }, + handlePresenceMessage: function(request, callback) { + var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + if (presence.seq <= this.maxPresenceSeq) { + return process.nextTick(function() { + callback(new ShareDBError(4026, 'Presence data superseded')); + }); + } + this.maxPresenceSeq = presence.seq; + if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) { + return process.nextTick(function() { + callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + }); + } + this.agent.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); } }; @@ -149,7 +170,10 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent.presence.processPresenceData(data)) return; + if (agent.presence.isPresenceData(data)) { + agent.presence.processPresenceData(data); + return; + } if (agent._isOwnOp(collection, data)) return; agent._sendOp(collection, id, data); }); @@ -371,9 +395,10 @@ Agent.prototype._handleMessage = function(request, callback) { return this._fetchSnapshot(request.c, request.d, request.v, callback); case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); - case 'p': - return this._handlePresenceMessage(request, callback); default: + if (this.presence.isPresenceData(request)) { + return this.presence.handlePresenceMessage(request, callback); + } callback({code: 4000, message: 'Invalid or unknown message'}); } } catch (err) { @@ -663,22 +688,3 @@ Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); }; - -Agent.prototype._handlePresenceMessage = function(request, callback) { - var presence = this.presence.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - if (presence.seq <= this.presence.maxPresenceSeq) { - return process.nextTick(function() { - callback(new ShareDBError(4026, 'Presence data superseded')); - }); - } - this.presence.maxPresenceSeq = presence.seq; - if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) { - return process.nextTick(function() { - callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); - }); - } - this.backend.sendPresence(presence, function(err) { - if (err) return callback(err); - callback(null, { seq: presence.seq }); - }); -}; From 49ff5c2b141b8062360d9a282799a2a04853c264 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:46:21 +0530 Subject: [PATCH 65/95] Use only flushPresence(), not flush(), _handleSubscribe --- lib/client/doc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 60edde765..4cd25f1e0 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -291,7 +291,7 @@ Doc.prototype._handleSubscribe = function(err, snapshot) { if (this.wantSubscribe) this.subscribed = true; this.ingestSnapshot(snapshot, callback); this._emitNothingPending(); - this.flush(); + this.presence.flushPresence(); }; Doc.prototype._handleUnsubscribe = function(err) { From 87aa90b81e3b609b6ff0503dc03c1edaaa5f9822 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:13:26 +0530 Subject: [PATCH 66/95] Disentangle doc internals from flushPresence --- lib/client/doc.js | 6 ++++-- lib/presence/stateless.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 4cd25f1e0..7d3cc967b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -291,7 +291,7 @@ Doc.prototype._handleSubscribe = function(err, snapshot) { if (this.wantSubscribe) this.subscribed = true; this.ingestSnapshot(snapshot, callback); this._emitNothingPending(); - this.presence.flushPresence(); + this.flush(); }; Doc.prototype._handleUnsubscribe = function(err) { @@ -489,7 +489,9 @@ Doc.prototype.flush = function() { this._sendOp(); } - this.presence.flushPresence(); + if (this.subscribed && !this.hasWritePending()) { + this.presence.flushPresence(); + } }; // Helper function to set op to contain a no-op. diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index d3642b877..389f53f94 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -344,7 +344,7 @@ StatelessPresence.prototype.cacheOp = function (message) { // If there are no pending ops, this method sends the pending presence data, if possible. StatelessPresence.prototype.flushPresence = function () { - if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { + if(!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; From 2198e8e46e5a6201b0ce06f40393e9676be0a6d4 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:32:55 +0530 Subject: [PATCH 67/95] Begin disentangling presence logic from connection.js --- lib/client/connection.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 222d82338..7b1215f8d 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -63,6 +63,18 @@ function Connection(socket) { this.state = connectionState(socket); this.bindToSocket(socket); + + this.presence = { + connection: this, + // TODO unify with code in agent.js + isPresenceMessage: function (message) { + return message.a === 'p'; + }, + handlePresenceMessage: function (err, message) { + var doc = this.connection.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); + } + }; } emitter.mixin(Connection); @@ -254,12 +266,10 @@ Connection.prototype.handleMessage = function(message) { if (doc) doc._handleOp(err, message); return; - case 'p': - var doc = this.getExisting(message.c, message.d); - if (doc) doc._handlePresence(err, message); - return; - default: + if (this.presence.isPresenceMessage(message)) { + return this.presence.handlePresenceMessage(err, message); + } logger.warn('Ignoring unrecognized message', message); } }; From cf0168dcf9dde236ad905ca5cdb4ac073c9dc454 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:37:02 +0530 Subject: [PATCH 68/95] Decouple sendPresence --- lib/client/connection.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 7b1215f8d..5be290676 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -73,6 +73,22 @@ function Connection(socket) { handlePresenceMessage: function (err, message) { var doc = this.connection.getExisting(message.c, message.d); if (doc) doc._handlePresence(err, message); + }, + sendPresence: function(doc, data, requestReply) { + // Ensure the doc is registered so that it receives the reply message + this.connection._addDoc(doc); + var message = { + a: 'p', + c: doc.collection, + d: doc.id, + p: data, + v: doc.version || 0, + seq: this.connection.seq++ + }; + if (requestReply) { + message.r = true; + } + this.connection.send(message); } }; } @@ -439,21 +455,11 @@ Connection.prototype.sendOp = function(doc, op) { this.send(message); }; +/** + * Sends presence data down the socket + */ Connection.prototype.sendPresence = function(doc, data, requestReply) { - // Ensure the doc is registered so that it receives the reply message - this._addDoc(doc); - var message = { - a: 'p', - c: doc.collection, - d: doc.id, - p: data, - v: doc.version || 0, - seq: this.seq++ - }; - if (requestReply) { - message.r = true; - } - this.send(message); + this.presence.sendPresence(doc, data, requestReply); }; From 4c46a05af7914e52d5dbbd1fb25d1973cec879ba Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 15:16:24 +0530 Subject: [PATCH 69/95] Move Presence class to presence.DocPresence --- lib/backend.js | 5 +++-- lib/client/doc.js | 5 +---- lib/presence/dummy.js | 14 +++++++------ lib/presence/index.js | 6 +++++- lib/presence/stateless.js | 44 ++++++++++++++++++++------------------- test/client/presence.js | 16 +++++++------- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 1d86a8603..192636a76 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -12,6 +12,7 @@ var Snapshot = require('./snapshot'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); var types = require('./types'); +var dummyPresence = require('./presence/dummy'); var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; @@ -49,7 +50,7 @@ function Backend(options) { this._shimAfterSubmit(); } - this.Presence = options.Presence; + this.presence = options.presence || dummyPresence; } module.exports = Backend; emitter.mixin(Backend); @@ -158,7 +159,7 @@ Backend.prototype.connect = function(connection, req) { // code that may cache state on the agent and read it in middleware connection.agent = agent; - connection.Presence = this.Presence; + connection.DocPresence = this.presence.DocPresence; return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index 7d3cc967b..86336ec7e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,7 +2,6 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); -var DummyPresence = require('../presence/dummy'); /** * A Doc is a client's view on a sharejs document. @@ -69,9 +68,7 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - this.presence = connection.Presence - ? new connection.Presence(this) - : new DummyPresence(); + this.presence = new connection.DocPresence(this); // Array of callbacks or nulls as placeholders this.inflightFetch = []; diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index a893b5a2c..8a4bac340 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,22 +1,22 @@ /* * Dummy Presence - * ------------------ + * -------------- * * This module provides a dummy implementation of presence that does nothing. * Its purpose is to stand in for a real implementation, to simplify code in doc.js. */ -var Presence = require('.'); +var presence = require('./index'); -function DummyPresence () { } +function DocPresence () { } // Inherit from Presence to support instanceof type checking. -DummyPresence.prototype = Object.create(Presence.prototype); +DocPresence.prototype = Object.create(presence.DocPresence.prototype); function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; -Object.assign(DummyPresence.prototype, { +Object.assign(DocPresence.prototype, { submitPresence: noop, handlePresence: noop, processAllReceivedPresence: noop, @@ -35,4 +35,6 @@ Object.assign(DummyPresence.prototype, { _emitPresence: noop }); -module.exports = DummyPresence; +module.exports = { + DocPresence: DocPresence +}; diff --git a/lib/presence/index.js b/lib/presence/index.js index 2ce09b6d3..e4de5669a 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1 +1,5 @@ -module.exports = function Presence () {}; +var DocPresence = function () {}; + +module.exports = { + DocPresence: DocPresence +}; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 389f53f94..2f65ca930 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -6,13 +6,13 @@ * but has some scalability problems. Each time a client joins a document, * this implementation requests current presence information from all other clients, * via the server. The server does not store any state at all regarding presence, - * it exists only in clients, hence the name "Stateless Presence". + * it exists only in clients, hence the name "Doc Presence". * */ var ShareDBError = require('../error'); -var Presence = require('.'); +var presence = require('./index'); -function StatelessPresence(doc) { +function DocPresence(doc) { // The Doc instance that this Presence is attached to. this.doc = doc; @@ -53,12 +53,12 @@ function StatelessPresence(doc) { } // Inherit from Presence to support instanceof type checking. -StatelessPresence.prototype = Object.create(Presence.prototype); +DocPresence.prototype = Object.create(presence.DocPresence.prototype); // Submit presence data to a document. // This is the only public facing method. // All the others are marked as internal with a leading "_". -StatelessPresence.prototype.submitPresence = function (data, callback) { +DocPresence.prototype.submitPresence = function (data, callback) { if (data != null) { if (!this.doc.type) { var doc = this.doc; @@ -96,7 +96,7 @@ StatelessPresence.prototype.submitPresence = function (data, callback) { process.nextTick(this.doc.flush.bind(this.doc)); }; -StatelessPresence.prototype.handlePresence = function (err, presence) { +DocPresence.prototype.handlePresence = function (err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -150,7 +150,7 @@ StatelessPresence.prototype.handlePresence = function (err, presence) { // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed for src. Otherwise false. -StatelessPresence.prototype._processReceivedPresence = function (src, emit) { +DocPresence.prototype._processReceivedPresence = function (src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -232,7 +232,7 @@ StatelessPresence.prototype._processReceivedPresence = function (src, emit) { return this._setPresence(src, data, emit); }; -StatelessPresence.prototype.processAllReceivedPresence = function () { +DocPresence.prototype.processAllReceivedPresence = function () { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -244,7 +244,7 @@ StatelessPresence.prototype.processAllReceivedPresence = function () { this._emitPresence(changedSrcList, true); }; -StatelessPresence.prototype._transformPresence = function (src, op) { +DocPresence.prototype._transformPresence = function (src, op) { var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -255,7 +255,7 @@ StatelessPresence.prototype._transformPresence = function (src, op) { return this._setPresence(src, presenceData); }; -StatelessPresence.prototype.transformAllPresence = function (op) { +DocPresence.prototype.transformAllPresence = function (op) { var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -267,7 +267,7 @@ StatelessPresence.prototype.transformAllPresence = function (op) { this._emitPresence(changedSrcList, false); }; -StatelessPresence.prototype.pausePresence = function () { +DocPresence.prototype.pausePresence = function () { if (!this) return; if (this.inflight) { @@ -294,7 +294,7 @@ StatelessPresence.prototype.pausePresence = function () { // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed. Otherwise false. -StatelessPresence.prototype._setPresence = function (src, data, emit) { +DocPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { if (this.current[src] == null) return false; delete this.current[src]; @@ -309,7 +309,7 @@ StatelessPresence.prototype._setPresence = function (src, data, emit) { return true; }; -StatelessPresence.prototype._emitPresence = function (srcList, submitted) { +DocPresence.prototype._emitPresence = function (srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { @@ -318,7 +318,7 @@ StatelessPresence.prototype._emitPresence = function (srcList, submitted) { } }; -StatelessPresence.prototype.cacheOp = function (message) { +DocPresence.prototype.cacheOp = function (message) { var op = { src: message.src, time: Date.now(), @@ -343,7 +343,7 @@ StatelessPresence.prototype.cacheOp = function (message) { }; // If there are no pending ops, this method sends the pending presence data, if possible. -StatelessPresence.prototype.flushPresence = function () { +DocPresence.prototype.flushPresence = function () { if(!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; @@ -353,17 +353,17 @@ StatelessPresence.prototype.flushPresence = function () { } }; -StatelessPresence.prototype.destroyPresence = function () { +DocPresence.prototype.destroyPresence = function () { this.received = {}; this.clearCachedOps(); }; -StatelessPresence.prototype.clearCachedOps = function () { +DocPresence.prototype.clearCachedOps = function () { this.cachedOps.length = 0; }; // Reset presence-related properties. -StatelessPresence.prototype.hardRollbackPresence = function () { +DocPresence.prototype.hardRollbackPresence = function () { this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -382,15 +382,17 @@ StatelessPresence.prototype.hardRollbackPresence = function () { this._emitPresence(changedSrcList, false); }; -StatelessPresence.prototype.hasPendingPresence = function () { +DocPresence.prototype.hasPendingPresence = function () { return this.inflight || this.pending; }; -StatelessPresence.prototype.getPendingPresence = function () { +DocPresence.prototype.getPendingPresence = function () { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); return pendingPresence; }; -module.exports = StatelessPresence; +module.exports = { + DocPresence: DocPresence +}; diff --git a/test/client/presence.js b/test/client/presence.js index abca8ea54..c7ee09d93 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,9 +3,9 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); -var Presence = require('../../lib/presence'); -var DummyPresence = require('../../lib/presence/dummy'); -var StatelessPresence = require('../../lib/presence/stateless'); +var presence = require('../../lib/presence'); +var dummyPresence = require('../../lib/presence/dummy'); +var statelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -15,19 +15,19 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('should use DummyPresence if Presence option not provided', function() { + it('should use dummyPresence.DocPresence if presence option not provided', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof DummyPresence).to.be(true); + expect(doc.presence instanceof dummyPresence.DocPresence).to.be(true); }); it('DummyPresence should subclass Presence', function() { - expect(DummyPresence.prototype instanceof Presence).to.be(true); + expect(dummyPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true); }); it('StatelessPresence should subclass Presence', function() { - expect(StatelessPresence.prototype instanceof Presence).to.be(true); + expect(statelessPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true); }); }); @@ -42,7 +42,7 @@ describe('client presence', function() { describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend({ Presence: StatelessPresence }); + this.backend = new Backend({ presence: statelessPresence }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From f0b7b97fbd03cb93348e5c6403c5842912c19b7d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 16:58:02 +0530 Subject: [PATCH 70/95] Rename doc.presence to doc._docPresence --- lib/client/doc.js | 48 +++---- test/client/presence.js | 286 ++++++++++++++++++++-------------------- 2 files changed, 167 insertions(+), 167 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 86336ec7e..963e9fa01 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,7 +68,7 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - this.presence = new connection.DocPresence(this); + this._docPresence = new connection.DocPresence(this); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -123,12 +123,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) doc.presence.destroyPresence(); + doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) doc.presence.destroyPresence(); + doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -210,7 +210,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; - this.presence.clearCachedOps(); + this._docPresence.clearCachedOps(); var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); @@ -218,7 +218,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); callback && callback(); }; @@ -241,7 +241,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence.hasPendingPresence() + this._docPresence.hasPendingPresence() ); }; @@ -350,21 +350,21 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this.presence.cacheOp(message); + this._docPresence.cacheOp(message); try { this._otApply(message, false); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } }; Doc.prototype._handlePresence = function (err, presence) { - this.presence.handlePresence(err, presence); + this._docPresence.handlePresence(err, presence); }; Doc.prototype.submitPresence = function (data, callback) { - this.presence.submitPresence(data, callback); + this._docPresence.submitPresence(data, callback); }; // Called whenever (you guessed it!) the connection state changes. This will @@ -387,10 +387,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this.presence.pausePresence(); + this._docPresence.pausePresence(); callEach(callbacks); } else { - this.presence.pausePresence(); + this._docPresence.pausePresence(); } } }; @@ -451,10 +451,10 @@ Doc.prototype.unsubscribe = function(callback) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - this.presence.pausePresence(); + this._docPresence.pausePresence(); return; } - this.presence.pausePresence(); + this._docPresence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -487,7 +487,7 @@ Doc.prototype.flush = function() { } if (this.subscribed && !this.hasWritePending()) { - this.presence.flushPresence(); + this._docPresence.flushPresence(); } }; @@ -593,7 +593,7 @@ Doc.prototype._otApply = function(op, source) { // Apply the individual op component this.emit('before op', componentOp.op, source); this.data = this.type.apply(this.data, componentOp.op); - this.presence.transformAllPresence(componentOp); + this._docPresence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -606,7 +606,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('before op', op.op, source); // Apply the operation to the local data, mutating it in place this.data = this.type.apply(this.data, op.op); - this.presence.transformAllPresence(op); + this._docPresence.transformAllPresence(op); // Emit an 'op' event once the local data includes the changes from the // op. For locally submitted ops, this will be synchronously with // submission and before the server or other clients have received the op. @@ -623,7 +623,7 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - this.presence.transformAllPresence(op); + this._docPresence.transformAllPresence(op); this.emit('create', source); return; } @@ -631,7 +631,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this.presence.transformAllPresence(op); + this._docPresence.transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -883,7 +883,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.presence.clearCachedOps(); + this._docPresence.clearCachedOps(); } else if (message.v !== this.version) { // We should already be at the same version, because the server should // have sent all the ops that have happened before acknowledging our op @@ -896,9 +896,9 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this.presence.cacheOp(this.inflightOp); + this._docPresence.cacheOp(this.inflightOp); this._clearInflightOp(); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { @@ -947,8 +947,8 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence.getPendingPresence(); - this.presence.hardRollbackPresence(); + var pendingPresence = this._docPresence.getPendingPresence(); + this._docPresence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/test/client/presence.js b/test/client/presence.js index c7ee09d93..1dd6a48a2 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -19,7 +19,7 @@ describe('client presence', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof dummyPresence.DocPresence).to.be(true); + expect(doc._docPresence instanceof dummyPresence.DocPresence).to.be(true); }); it('DummyPresence should subclass Presence', function() { @@ -59,13 +59,13 @@ describe('client presence', function() { this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -80,13 +80,13 @@ describe('client presence', function() { function(done) { this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -103,12 +103,12 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for a future version. this.doc.version += 2; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), function(err) { if (err) return done(err); this.doc.version -= 2; @@ -131,13 +131,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -155,13 +155,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -179,13 +179,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -203,13 +203,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -227,13 +227,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -251,13 +251,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -276,10 +276,10 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -287,12 +287,12 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 2; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -310,25 +310,25 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { - this.doc2.presence.cachedOps = []; + this.doc2._docPresence.cachedOps = []; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -346,8 +346,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -367,8 +367,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -388,8 +388,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -409,8 +409,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -430,8 +430,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(2)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(2)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -451,8 +451,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(1)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(2)); + expect(this.doc._docPresence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -467,10 +467,10 @@ describe('client presence', function() { this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(3); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.cachedOps[1].op).to.equal(op); - expect(this.doc.presence.cachedOps[2].del).to.equal(true); + expect(this.doc._docPresence.cachedOps.length).to.equal(3); + expect(this.doc._docPresence.cachedOps[0].create).to.equal(true); + expect(this.doc._docPresence.cachedOps[1].op).to.equal(op); + expect(this.doc._docPresence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -485,10 +485,10 @@ describe('client presence', function() { this.doc.del.bind(this.doc), setTimeout, function(done) { - expect(this.doc2.presence.cachedOps.length).to.equal(3); - expect(this.doc2.presence.cachedOps[0].create).to.equal(true); - expect(this.doc2.presence.cachedOps[1].op).to.eql(op); - expect(this.doc2.presence.cachedOps[2].del).to.equal(true); + expect(this.doc2._docPresence.cachedOps.length).to.equal(3); + expect(this.doc2._docPresence.cachedOps[0].create).to.equal(true); + expect(this.doc2._docPresence.cachedOps[1].op).to.eql(op); + expect(this.doc2._docPresence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -499,15 +499,15 @@ describe('client presence', function() { var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; - this.doc.presence.cachedOpsTimeout = 60; + this.doc._docPresence.cachedOpsTimeout = 60; async.series([ // Cache 2 ops. this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.submitOp.bind(this.doc, op1), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(2); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.cachedOps[1].op).to.equal(op1); + expect(this.doc._docPresence.cachedOps.length).to.equal(2); + expect(this.doc._docPresence.cachedOps[0].create).to.equal(true); + expect(this.doc._docPresence.cachedOps[1].op).to.equal(op1); done(); }.bind(this), @@ -518,10 +518,10 @@ describe('client presence', function() { }, this.doc.submitOp.bind(this.doc, op2), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(3); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.cachedOps[1].op).to.equal(op1); - expect(this.doc.presence.cachedOps[2].op).to.equal(op2); + expect(this.doc._docPresence.cachedOps.length).to.equal(3); + expect(this.doc._docPresence.cachedOps[0].create).to.equal(true); + expect(this.doc._docPresence.cachedOps[1].op).to.equal(op1); + expect(this.doc._docPresence.cachedOps[2].op).to.equal(op2); done(); }.bind(this), @@ -532,9 +532,9 @@ describe('client presence', function() { }, this.doc.submitOp.bind(this.doc, op3), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(2); - expect(this.doc.presence.cachedOps[0].op).to.equal(op2); - expect(this.doc.presence.cachedOps[1].op).to.equal(op3); + expect(this.doc._docPresence.cachedOps.length).to.equal(2); + expect(this.doc._docPresence.cachedOps[0].op).to.equal(op2); + expect(this.doc._docPresence.cachedOps[1].op).to.equal(op3); clock.uninstall(); done(); }.bind(this) @@ -552,13 +552,13 @@ describe('client presence', function() { if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current['']).to.eql(p(1)); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); } else { expect(srcList).to.eql([ this.connection.id ]); - expect(this.doc2.presence.current['']).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.requestReply).to.equal(false); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.requestReply).to.equal(false); done(); } }.bind(this)); @@ -639,10 +639,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); this.doc.submitPresence(p(1), errorHandler(done)); this.doc.submitPresence(p(2), errorHandler(done)); @@ -658,15 +658,15 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2.presence.current).to.eql({}); + expect(this.doc2._docPresence.current).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -682,14 +682,14 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); this.doc.submitPresence(p(1), errorHandler(done)); process.nextTick(function() { this.backend.connect(this.connection); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; }.bind(this)); }.bind(this) ], allDone); @@ -704,10 +704,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -721,14 +721,14 @@ describe('client presence', function() { expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.presence.pending).to.equal(true); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(true); + expect(!!this.doc._docPresence.inflight).to.equal(false); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); - expect(!!this.doc.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(false); + expect(!!this.doc._docPresence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -742,20 +742,20 @@ describe('client presence', function() { expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.presence.pending).to.equal(true); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(true); + expect(!!this.doc._docPresence.inflight).to.equal(false); process.nextTick(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).to.equal(true); + expect(!!this.doc._docPresence.pending).to.equal(false); + expect(!!this.doc._docPresence.inflight).to.equal(true); this.doc.whenNothingPending(done); }.bind(this), function(done) { expect(this.doc.hasPending()).to.equal(false); - expect(!!this.doc.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(false); + expect(!!this.doc._docPresence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -769,16 +769,16 @@ describe('client presence', function() { this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.del(errorHandler(done)); }.bind(this) @@ -794,17 +794,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -821,17 +821,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -848,17 +848,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -875,17 +875,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -946,18 +946,18 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -972,17 +972,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -997,10 +997,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) this.doc2.submitOp({ index: 2, value: 'c' }, errorHandler(done)) @@ -1017,10 +1017,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.submitOp({ index: 1, value: 'c' }, errorHandler(done)) this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) @@ -1037,10 +1037,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)) this.doc2.submitOp({ index: 0, value: 'a' }, errorHandler(done)) @@ -1061,10 +1061,10 @@ describe('client presence', function() { // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.del(errorHandler(done)); this.doc2.create([ 'c' ], typeName, errorHandler(done)); @@ -1087,10 +1087,10 @@ describe('client presence', function() { // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); this.doc2.del(errorHandler(done)); @@ -1109,7 +1109,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1132,7 +1132,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1346,8 +1346,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1401,8 +1401,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1425,14 +1425,14 @@ describe('client presence', function() { return handleMessage.apply(this, arguments); }; if (expireCache) { - this.doc.presence.receivedTimeout = 0; + this.doc._docPresence.receivedTimeout = 0; } async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.presence.requestReply = false; + this.doc2._docPresence.requestReply = false; this.doc2.submitPresence(p(0), done); }.bind(this), setTimeout, @@ -1440,7 +1440,7 @@ describe('client presence', function() { setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1451,7 +1451,7 @@ describe('client presence', function() { process.nextTick(done); }.bind(this), function(done) { - expect(this.doc.presence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); @@ -1464,7 +1464,7 @@ describe('client presence', function() { it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(true, true)); it('invokes presence.destroy inside doc.destroy', function(done) { - var presence = this.doc.presence; + var presence = this.doc._docPresence; presence.cachedOps = ['foo']; presence.received = { bar: true }; this.doc.destroy(function(err) { From db39b697d2cc627262f974080fc8317737058237 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:01:20 +0530 Subject: [PATCH 71/95] Move doc._docPresence.current back to original API doc.presence. --- lib/presence/stateless.js | 24 +++--- test/client/presence.js | 170 +++++++++++++++++++------------------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 2f65ca930..d35b22613 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -20,7 +20,7 @@ function DocPresence(doc) { // The current presence data. // Map of src -> presence data // Local src === '' - this.current = {}; + this.doc.presence = {}; // The presence objects received from the server. // Map of src -> presence @@ -245,7 +245,7 @@ DocPresence.prototype.processAllReceivedPresence = function () { }; DocPresence.prototype._transformPresence = function (src, op) { - var presenceData = this.current[src]; + var presenceData = this.doc.presence[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); presenceData = this.doc.type.transformPresence(presenceData, op.op, isOwnOperation); @@ -256,7 +256,7 @@ DocPresence.prototype._transformPresence = function (src, op) { }; DocPresence.prototype.transformAllPresence = function (op) { - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -276,12 +276,12 @@ DocPresence.prototype.pausePresence = function () { : this.inflight; this.inflight = null; this.inflightSeq = 0; - } else if (!this.pending && this.current[''] != null) { + } else if (!this.pending && this.doc.presence[''] != null) { this.pending = []; } this.received = {}; this.requestReply = true; - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -296,14 +296,14 @@ DocPresence.prototype.pausePresence = function () { // Returns true, if presence has changed. Otherwise false. DocPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { - if (this.current[src] == null) return false; - delete this.current[src]; + if (this.doc.presence[src] == null) return false; + delete this.doc.presence[src]; } else { var isPresenceEqual = - this.current[src] === data || - (this.doc.type.comparePresence && this.doc.type.comparePresence(this.current[src], data)); + this.doc.presence[src] === data || + (this.doc.type.comparePresence && this.doc.type.comparePresence(this.doc.presence[src], data)); if (isPresenceEqual) return false; - this.current[src] = data; + this.doc.presence[src] = data; } if (emit) this._emitPresence([ src ], true); return true; @@ -348,7 +348,7 @@ DocPresence.prototype.flushPresence = function () { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; - this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); + this.doc.connection.sendPresence(this.doc, this.doc.presence[''], this.requestReply); this.requestReply = false; } }; @@ -371,7 +371,7 @@ DocPresence.prototype.hardRollbackPresence = function () { this.received = {}; this.requestReply = true; - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; diff --git a/test/client/presence.js b/test/client/presence.js index 1dd6a48a2..498438ad6 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -65,7 +65,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -86,7 +86,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -103,7 +103,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for a future version. @@ -131,7 +131,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -155,7 +155,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -179,7 +179,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -203,7 +203,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -227,7 +227,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -251,7 +251,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -276,7 +276,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -287,7 +287,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -310,7 +310,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -322,7 +322,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -346,8 +346,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -367,8 +367,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -388,8 +388,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -409,8 +409,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -430,8 +430,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(2)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(2)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -451,8 +451,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(1)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(2)); + expect(this.doc.presence['']).to.eql(p(1)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -552,12 +552,12 @@ describe('client presence', function() { if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(this.connection.id); } else { expect(srcList).to.eql([ this.connection.id ]); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); expect(this.doc2._docPresence.requestReply).to.equal(false); done(); } @@ -639,7 +639,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(2)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -658,7 +658,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -666,7 +666,7 @@ describe('client presence', function() { setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2._docPresence.current).to.eql({}); + expect(this.doc2.presence).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -682,7 +682,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -704,7 +704,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -769,13 +769,13 @@ describe('client presence', function() { this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -794,17 +794,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -821,17 +821,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -848,17 +848,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -875,17 +875,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -946,18 +946,18 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -972,17 +972,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -997,7 +997,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1017,7 +1017,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1037,7 +1037,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1061,7 +1061,7 @@ describe('client presence', function() { // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1087,7 +1087,7 @@ describe('client presence', function() { // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1109,7 +1109,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1132,7 +1132,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1346,8 +1346,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1401,8 +1401,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1440,7 +1440,7 @@ describe('client presence', function() { setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1451,7 +1451,7 @@ describe('client presence', function() { process.nextTick(done); }.bind(this), function(done) { - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); From 60a567b2f7b0ace9911efba6c766396e4f7bce21 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:22:40 +0530 Subject: [PATCH 72/95] Split out implementation of ConnectionPresence. --- lib/backend.js | 4 ++++ lib/client/connection.js | 33 +++------------------------- lib/presence/dummy.js | 21 +++++++++++++----- lib/presence/index.js | 5 ++--- lib/presence/stateless.js | 46 +++++++++++++++++++++++++++++++++++---- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 192636a76..98b052abd 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -159,8 +159,12 @@ Backend.prototype.connect = function(connection, req) { // code that may cache state on the agent and read it in middleware connection.agent = agent; + // Expose the DocPresence passed in through the constructor + // to the Doc class, which has access to the connection. connection.DocPresence = this.presence.DocPresence; + connection._connectionPresence = new this.presence.ConnectionPresence(connection); + return connection; }; diff --git a/lib/client/connection.js b/lib/client/connection.js index 5be290676..c67b1e6da 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -64,33 +64,6 @@ function Connection(socket) { this.bindToSocket(socket); - this.presence = { - connection: this, - // TODO unify with code in agent.js - isPresenceMessage: function (message) { - return message.a === 'p'; - }, - handlePresenceMessage: function (err, message) { - var doc = this.connection.getExisting(message.c, message.d); - if (doc) doc._handlePresence(err, message); - }, - sendPresence: function(doc, data, requestReply) { - // Ensure the doc is registered so that it receives the reply message - this.connection._addDoc(doc); - var message = { - a: 'p', - c: doc.collection, - d: doc.id, - p: data, - v: doc.version || 0, - seq: this.connection.seq++ - }; - if (requestReply) { - message.r = true; - } - this.connection.send(message); - } - }; } emitter.mixin(Connection); @@ -283,8 +256,8 @@ Connection.prototype.handleMessage = function(message) { return; default: - if (this.presence.isPresenceMessage(message)) { - return this.presence.handlePresenceMessage(err, message); + if (this._connectionPresence.isPresenceMessage(message)) { + return this._connectionPresence.handlePresenceMessage(err, message); } logger.warn('Ignoring unrecognized message', message); } @@ -459,7 +432,7 @@ Connection.prototype.sendOp = function(doc, op) { * Sends presence data down the socket */ Connection.prototype.sendPresence = function(doc, data, requestReply) { - this.presence.sendPresence(doc, data, requestReply); + this._connectionPresence.sendPresence(doc, data, requestReply); }; diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 8a4bac340..7853c9066 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -7,15 +7,13 @@ */ var presence = require('./index'); -function DocPresence () { } - -// Inherit from Presence to support instanceof type checking. -DocPresence.prototype = Object.create(presence.DocPresence.prototype); - function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; +function DocPresence () { } +DocPresence.prototype = Object.create(presence.DocPresence.prototype); + Object.assign(DocPresence.prototype, { submitPresence: noop, handlePresence: noop, @@ -35,6 +33,17 @@ Object.assign(DocPresence.prototype, { _emitPresence: noop }); + +function ConnectionPresence() {} +ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); + +Object.assign(ConnectionPresence.prototype, { + isPresenceMessage: returnFalse, + handlePresenceMessage: noop, + sendPresence: noop +}); + module.exports = { - DocPresence: DocPresence + DocPresence: DocPresence, + ConnectionPresence: ConnectionPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index e4de5669a..42898870a 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,5 +1,4 @@ -var DocPresence = function () {}; - module.exports = { - DocPresence: DocPresence + DocPresence: function DocPresence () {}, + ConnectionPresence: function ConnectionPresence () {}, }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index d35b22613..610a9246f 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -12,9 +12,11 @@ var ShareDBError = require('../error'); var presence = require('./index'); +/* + * Stateless Presence implementation of DocPresence + * ------------------------------------------------ + */ function DocPresence(doc) { - - // The Doc instance that this Presence is attached to. this.doc = doc; // The current presence data. @@ -52,7 +54,6 @@ function DocPresence(doc) { this.inflight = null; } -// Inherit from Presence to support instanceof type checking. DocPresence.prototype = Object.create(presence.DocPresence.prototype); // Submit presence data to a document. @@ -393,6 +394,43 @@ DocPresence.prototype.getPendingPresence = function () { return pendingPresence; }; + +/* + * Stateless Presence implementation of ConnectionPresence + * ------------------------------------------------ + */ +function ConnectionPresence(connection) { + this.connection = connection; + Object.assign(this, { + // TODO unify with code in agent.js + isPresenceMessage: function (message) { + return message.a === 'p'; + }, + handlePresenceMessage: function (err, message) { + var doc = this.connection.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); + }, + sendPresence: function(doc, data, requestReply) { + // Ensure the doc is registered so that it receives the reply message + this.connection._addDoc(doc); + var message = { + a: 'p', + c: doc.collection, + d: doc.id, + p: data, + v: doc.version || 0, + seq: this.connection.seq++ + }; + if (requestReply) { + message.r = true; + } + this.connection.send(message); + } + }); +} +ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); + module.exports = { - DocPresence: DocPresence + DocPresence: DocPresence, + ConnectionPresence: ConnectionPresence }; From 8b6872eb1cea0acac612635297b8062d7f3513ee Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:26:08 +0530 Subject: [PATCH 73/95] Refactor ConnectionPresence to idiomatic JS class. --- lib/presence/stateless.js | 56 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 610a9246f..15ccc359c 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -397,39 +397,41 @@ DocPresence.prototype.getPendingPresence = function () { /* * Stateless Presence implementation of ConnectionPresence - * ------------------------------------------------ + * ------------------------------------------------------- */ function ConnectionPresence(connection) { this.connection = connection; - Object.assign(this, { - // TODO unify with code in agent.js - isPresenceMessage: function (message) { - return message.a === 'p'; - }, - handlePresenceMessage: function (err, message) { - var doc = this.connection.getExisting(message.c, message.d); - if (doc) doc._handlePresence(err, message); - }, - sendPresence: function(doc, data, requestReply) { - // Ensure the doc is registered so that it receives the reply message - this.connection._addDoc(doc); - var message = { - a: 'p', - c: doc.collection, - d: doc.id, - p: data, - v: doc.version || 0, - seq: this.connection.seq++ - }; - if (requestReply) { - message.r = true; - } - this.connection.send(message); - } - }); } ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); +// TODO unify with code in agent.js +ConnectionPresence.prototype.isPresenceMessage = function (message) { + return message.a === 'p'; +}; + +ConnectionPresence.prototype.handlePresenceMessage = function (err, message) { + var doc = this.connection.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); +}; + +ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) { + // Ensure the doc is registered so that it receives the reply message + this.connection._addDoc(doc); + var message = { + a: 'p', + c: doc.collection, + d: doc.id, + p: data, + v: doc.version || 0, + seq: this.connection.seq++ + }; + if (requestReply) { + message.r = true; + } + this.connection.send(message); +}; + + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence From 55e1a4d82d505a5fffe6fae9615b20750ace5608 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:35:23 +0530 Subject: [PATCH 74/95] Split out implementation of AgentPresence. --- lib/agent.js | 75 ++++----------------------------------- lib/presence/dummy.js | 20 ++++++++--- lib/presence/index.js | 1 + lib/presence/stateless.js | 72 ++++++++++++++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 74 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 53891b306..66e1f86f1 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -27,68 +27,7 @@ function Agent(backend, stream) { // Map from queryId -> emitter this.subscribedQueries = {}; - this.presence = { - agent: this, - isPresenceData: function (data) { - return data.a === 'p'; - }, - processPresenceData: function (data) { - if (data.a === 'p') { - // Send other clients' presence data - if (data.src !== this.agent.clientId) this.agent.send(data); - return true; - } - }, - // The max presence sequence number received from the client. - maxPresenceSeq: 0, - createPresence: function(collection, id, data, version, requestReply, seq) { - return { - a: 'p', - src: this.agent.clientId, - seq: seq != null ? seq : this.maxPresenceSeq, - c: collection, - d: id, - p: data, - v: version, - r: requestReply - }; - }, - subscribeToStream: function (collection, id, stream) { - var agent = this.agent; - stream.on('end', function() { - agent.backend.sendPresence(agent.presence.createPresence(collection, id)); - }); - }, - checkRequest: function (request) { - if (request.a === 'p') { - if (typeof request.c !== 'string') return 'Invalid collection'; - if (typeof request.d !== 'string') return 'Invalid id'; - if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; - if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; - if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') { - return 'Invalid "request reply" value'; - } - } - }, - handlePresenceMessage: function(request, callback) { - var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - if (presence.seq <= this.maxPresenceSeq) { - return process.nextTick(function() { - callback(new ShareDBError(4026, 'Presence data superseded')); - }); - } - this.maxPresenceSeq = presence.seq; - if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) { - return process.nextTick(function() { - callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); - }); - } - this.agent.backend.sendPresence(presence, function(err) { - if (err) return callback(err); - callback(null, { seq: presence.seq }); - }); - } - }; + this._agentPresence = new backend.presence.AgentPresence(this); // We need to track this manually to make sure we don't reply to messages // after the stream was closed. @@ -170,8 +109,8 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent.presence.isPresenceData(data)) { - agent.presence.processPresenceData(data); + if (agent._agentPresence.isPresenceData(data)) { + agent._agentPresence.processPresenceData(data); return; } if (agent._isOwnOp(collection, data)) return; @@ -185,7 +124,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); - this.presence.subscribeToStream(collection, id, stream); + this._agentPresence.subscribeToStream(collection, id, stream); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { @@ -358,7 +297,7 @@ Agent.prototype._checkRequest = function(request) { if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; } else { - return this.presence.checkRequest(request); + return this._agentPresence.checkRequest(request); } }; @@ -396,8 +335,8 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - if (this.presence.isPresenceData(request)) { - return this.presence.handlePresenceMessage(request, callback); + if (this._agentPresence.isPresenceData(request)) { + return this._agentPresence.handlePresenceMessage(request, callback); } callback({code: 4000, message: 'Invalid or unknown message'}); } diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 7853c9066..c0e871849 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -11,9 +11,8 @@ function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; -function DocPresence () { } +function DocPresence () {} DocPresence.prototype = Object.create(presence.DocPresence.prototype); - Object.assign(DocPresence.prototype, { submitPresence: noop, handlePresence: noop, @@ -33,17 +32,28 @@ Object.assign(DocPresence.prototype, { _emitPresence: noop }); - function ConnectionPresence() {} ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); - Object.assign(ConnectionPresence.prototype, { isPresenceMessage: returnFalse, handlePresenceMessage: noop, sendPresence: noop }); +function AgentPresence() {} +AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); +Object.assign(AgentPresence.prototype, { + isPresenceData: returnFalse, + processPresenceData: returnFalse, + //maxPresenceSeq: 0, + createPresence: noop, + subscribeToStream: noop, + checkRequest: noop, + handlePresenceMessage: noop +}); + module.exports = { DocPresence: DocPresence, - ConnectionPresence: ConnectionPresence + ConnectionPresence: ConnectionPresence, + AgentPresence: AgentPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index 42898870a..0a0b43e11 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,4 +1,5 @@ module.exports = { DocPresence: function DocPresence () {}, ConnectionPresence: function ConnectionPresence () {}, + AgentPresence: function AgentPresence () {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 15ccc359c..f4874d023 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -431,8 +431,78 @@ ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) { this.connection.send(message); }; +/* + * Stateless Presence implementation of AgentPresence + * ------------------------------------------------------- + */ +function AgentPresence(agent) { + Object.assign(this, { + agent: agent, + isPresenceData: function (data) { + return data.a === 'p'; + }, + processPresenceData: function (data) { + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== this.agent.clientId) this.agent.send(data); + return true; + } + }, + // The max presence sequence number received from the client. + maxPresenceSeq: 0, + createPresence: function(collection, id, data, version, requestReply, seq) { + return { + a: 'p', + src: this.agent.clientId, + seq: seq != null ? seq : this.maxPresenceSeq, + c: collection, + d: id, + p: data, + v: version, + r: requestReply + }; + }, + subscribeToStream: function (collection, id, stream) { + var agent = this.agent; + stream.on('end', function() { + agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); + }); + }, + checkRequest: function (request) { + if (request.a === 'p') { + if (typeof request.c !== 'string') return 'Invalid collection'; + if (typeof request.d !== 'string') return 'Invalid id'; + if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; + if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; + if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') { + return 'Invalid "request reply" value'; + } + } + }, + handlePresenceMessage: function(request, callback) { + var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + if (presence.seq <= this.maxPresenceSeq) { + return process.nextTick(function() { + callback(new ShareDBError(4026, 'Presence data superseded')); + }); + } + this.maxPresenceSeq = presence.seq; + if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) { + return process.nextTick(function() { + callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + }); + } + this.agent.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); + } + }); +} +AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); module.exports = { DocPresence: DocPresence, - ConnectionPresence: ConnectionPresence + ConnectionPresence: ConnectionPresence, + AgentPresence: AgentPresence }; From 7a79d98736bc49a6e52be32f1fc95a1f2468a52e Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:39:34 +0530 Subject: [PATCH 75/95] Refactor AgentPresence to idiomatic JS class. --- lib/presence/stateless.js | 137 ++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index f4874d023..008aeabee 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -431,76 +431,87 @@ ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) { this.connection.send(message); }; + /* * Stateless Presence implementation of AgentPresence - * ------------------------------------------------------- + * -------------------------------------------------- */ function AgentPresence(agent) { - Object.assign(this, { - agent: agent, - isPresenceData: function (data) { - return data.a === 'p'; - }, - processPresenceData: function (data) { - if (data.a === 'p') { - // Send other clients' presence data - if (data.src !== this.agent.clientId) this.agent.send(data); - return true; - } - }, - // The max presence sequence number received from the client. - maxPresenceSeq: 0, - createPresence: function(collection, id, data, version, requestReply, seq) { - return { - a: 'p', - src: this.agent.clientId, - seq: seq != null ? seq : this.maxPresenceSeq, - c: collection, - d: id, - p: data, - v: version, - r: requestReply - }; - }, - subscribeToStream: function (collection, id, stream) { - var agent = this.agent; - stream.on('end', function() { - agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); - }); - }, - checkRequest: function (request) { - if (request.a === 'p') { - if (typeof request.c !== 'string') return 'Invalid collection'; - if (typeof request.d !== 'string') return 'Invalid id'; - if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; - if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; - if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') { - return 'Invalid "request reply" value'; - } - } - }, - handlePresenceMessage: function(request, callback) { - var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - if (presence.seq <= this.maxPresenceSeq) { - return process.nextTick(function() { - callback(new ShareDBError(4026, 'Presence data superseded')); - }); - } - this.maxPresenceSeq = presence.seq; - if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) { - return process.nextTick(function() { - callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); - }); - } - this.agent.backend.sendPresence(presence, function(err) { - if (err) return callback(err); - callback(null, { seq: presence.seq }); - }); - } - }); + this.agent = agent; + + // The max presence sequence number received from the client. + this.maxPresenceSeq = 0; } AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); +AgentPresence.prototype.isPresenceData = function (data) { + return data.a === 'p'; +}; + +AgentPresence.prototype.processPresenceData = function (data) { + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== this.agent.clientId) this.agent.send(data); + return true; + } +}; + +AgentPresence.prototype.createPresence = function(collection, id, data, version, requestReply, seq) { + return { + a: 'p', + src: this.agent.clientId, + seq: seq != null ? seq : this.maxPresenceSeq, + c: collection, + d: id, + p: data, + v: version, + r: requestReply + }; +}; + +AgentPresence.prototype.subscribeToStream = function (collection, id, stream) { + var agent = this.agent; + stream.on('end', function() { + agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); + }); +}; + +AgentPresence.prototype.checkRequest = function (request) { + if (request.a === 'p') { + if (typeof request.c !== 'string') return 'Invalid collection'; + if (typeof request.d !== 'string') return 'Invalid id'; + if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version'; + if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq'; + if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') { + return 'Invalid "request reply" value'; + } + } +}; + +AgentPresence.prototype.handlePresenceMessage = function(request, callback) { + var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + if (presence.seq <= this.maxPresenceSeq) { + return process.nextTick(function() { + callback(new ShareDBError(4026, 'Presence data superseded')); + }); + } + this.maxPresenceSeq = presence.seq; + if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) { + return process.nextTick(function() { + callback(new ShareDBError(4025, [ + 'Cannot send presence. Not subscribed to document:', + presence.c, + presence.d + ].join(' '))); + }); + } + this.agent.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); +}; + + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, From 693492d9f3067f8a6a3fe1f74825c64e2b8aa07d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:45:17 +0530 Subject: [PATCH 76/95] Unify isPresenceMessage between ConnectionPresence and AgentPresence --- lib/agent.js | 4 ++-- lib/presence/dummy.js | 4 ++-- lib/presence/stateless.js | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 66e1f86f1..7801eb80c 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -109,7 +109,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent._agentPresence.isPresenceData(data)) { + if (agent._agentPresence.isPresenceMessage(data)) { agent._agentPresence.processPresenceData(data); return; } @@ -335,7 +335,7 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - if (this._agentPresence.isPresenceData(request)) { + if (this._agentPresence.isPresenceMessage(request)) { return this._agentPresence.handlePresenceMessage(request, callback); } callback({code: 4000, message: 'Invalid or unknown message'}); diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index c0e871849..28eae5b58 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -4,6 +4,7 @@ * * This module provides a dummy implementation of presence that does nothing. * Its purpose is to stand in for a real implementation, to simplify code in doc.js. + * */ var presence = require('./index'); @@ -43,9 +44,8 @@ Object.assign(ConnectionPresence.prototype, { function AgentPresence() {} AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); Object.assign(AgentPresence.prototype, { - isPresenceData: returnFalse, + isPresenceMessage: returnFalse, processPresenceData: returnFalse, - //maxPresenceSeq: 0, createPresence: noop, subscribeToStream: noop, checkRequest: noop, diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 008aeabee..7f208d882 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -12,6 +12,12 @@ var ShareDBError = require('../error'); var presence = require('./index'); +// Check if a message represence presence. +// Used in both ConnectionPresence and AgentPresence. +function isPresenceMessage(data) { + return data.a === 'p'; +}; + /* * Stateless Presence implementation of DocPresence * ------------------------------------------------ @@ -404,10 +410,7 @@ function ConnectionPresence(connection) { } ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); -// TODO unify with code in agent.js -ConnectionPresence.prototype.isPresenceMessage = function (message) { - return message.a === 'p'; -}; +ConnectionPresence.prototype.isPresenceMessage = isPresenceMessage; ConnectionPresence.prototype.handlePresenceMessage = function (err, message) { var doc = this.connection.getExisting(message.c, message.d); @@ -444,9 +447,7 @@ function AgentPresence(agent) { } AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); -AgentPresence.prototype.isPresenceData = function (data) { - return data.a === 'p'; -}; +AgentPresence.prototype.isPresenceMessage = isPresenceMessage; AgentPresence.prototype.processPresenceData = function (data) { if (data.a === 'p') { From 538c3c15f3e18f410539c2bd2672ca5a69f56a98 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:56:36 +0530 Subject: [PATCH 77/95] Update README to document presence API --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d82b92ad7..fc7af89cb 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,19 @@ initial data. Then you can submit editing operations on the document (using OT). Finally you can delete the document with a delete operation. By default, ShareDB stores all operations forever - nothing is truly deleted. -## User presence synchronization +## User Presence Synchronization -ShareDB supports synchronization of user presence data. This feature is opt-in, not enabled by default. To enable this feature, pass the `enablePresence: true` option to the ShareDB constructor (e.g. `var share = new ShareDB({ enablePresence: true })`). +ShareDB supports synchronization of user presence data such as cursor positions and text selections. This feature is opt-in, not enabled by default. To enable this feature, pass a presence implementation as the `presence` option to the ShareDB constructor. + +ShareDB includes an implementation of presence called `StatelessPresence`. This provides an implementation of presence that works out of the box, but it has some scalability problems. Each time a client joins a document, this implementation requests current presence information from all other clients, via the server, which may be problematic in terms of performance when a large number of users are present on the same documentsimultaneously. If you don't expect too many simultaneous users, this should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence". + +To use `StatelessPresence`, pass it into the ShareDB constructor like this: + +```js +var ShareDB = require('sharedb'); +var statelessPresence = require('sharedb/lib/presence/stateless'); +var share = new ShareDB({ presence: statelessPresence })`). +``` Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. @@ -98,7 +108,7 @@ __Options__ * `options.pubsub` _(instance of `ShareDB.PubSub`)_ Notify other ShareDB processes when data changes through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`. -* `options.enablePresence` _(optional boolean)_ +* `options.presence` _(implementation of presence API)_ Enable user presence synchronization. #### Database Adapters From 963affae4d2d9257dac5a0d8b0cbb18fd29eda4c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 18:03:21 +0530 Subject: [PATCH 78/95] Iterate README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc7af89cb..2ce8a9070 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,9 @@ default, ShareDB stores all operations forever - nothing is truly deleted. ShareDB supports synchronization of user presence data such as cursor positions and text selections. This feature is opt-in, not enabled by default. To enable this feature, pass a presence implementation as the `presence` option to the ShareDB constructor. -ShareDB includes an implementation of presence called `StatelessPresence`. This provides an implementation of presence that works out of the box, but it has some scalability problems. Each time a client joins a document, this implementation requests current presence information from all other clients, via the server, which may be problematic in terms of performance when a large number of users are present on the same documentsimultaneously. If you don't expect too many simultaneous users, this should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence". +ShareDB includes an implementation of presence called `StatelessPresence`. This provides an implementation of presence that works out of the box, but it has some scalability problems. Each time a client joins a document, this implementation requests current presence information from all other clients, via the server. This approach may be problematic in terms of performance when a large number of users are present on the same document simultaneously. If you don't expect too many simultaneous users per document, `StatelessPresence` should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence". + +In `StatelessPresence`, presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs) (specifically, by [`transformPresence`, `createPresence`, and `comparePresence`](https://github.com/teamwork/ot-docs#optional-properties)). All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. To use `StatelessPresence`, pass it into the ShareDB constructor like this: @@ -88,8 +90,6 @@ var statelessPresence = require('sharedb/lib/presence/stateless'); var share = new ShareDB({ presence: statelessPresence })`). ``` -Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document. - ## Server API ### Initialization From 810175ed2c88e4a67625743b85c313457b868326 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 18:18:53 +0530 Subject: [PATCH 79/95] Minor cleanup --- lib/client/connection.js | 2 -- lib/client/doc.js | 24 +++--------------------- lib/presence/stateless.js | 3 ++- lib/util.js | 12 ++++++++++++ 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index c67b1e6da..336050ebe 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -63,7 +63,6 @@ function Connection(socket) { this.state = connectionState(socket); this.bindToSocket(socket); - } emitter.mixin(Connection); @@ -435,7 +434,6 @@ Connection.prototype.sendPresence = function(doc, data, requestReply) { this._connectionPresence.sendPresence(doc, data, requestReply); }; - /** * Sends a message down the socket */ diff --git a/lib/client/doc.js b/lib/client/doc.js index 963e9fa01..e1124c7f4 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var callEach = require('../util').callEach; /** * A Doc is a client's view on a sharejs document. @@ -387,11 +388,9 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this._docPresence.pausePresence(); callEach(callbacks); - } else { - this._docPresence.pausePresence(); } + this._docPresence.pausePresence(); } }; @@ -447,14 +446,12 @@ Doc.prototype.unsubscribe = function(callback) { // between sending the message and hearing back, but we cannot know exactly // when. Thus, immediately mark us as not subscribed this.subscribed = false; + this._docPresence.pausePresence(); if (this.connection.canSend) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - - this._docPresence.pausePresence(); return; } - this._docPresence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -993,18 +990,3 @@ Doc.prototype._clearInflightOp = function(err) { if (err && !called) return this.emit('error', err); }; - -function callEach(callbacks, err) { - var called = false; - for (var i = 0; i < callbacks.length; i++) { - var callback = callbacks[i]; - if (callback) { - callback(err); - called = true; - } - } - return called; -} - -// Expose callEach to presence methods via Doc prototype. -Doc.prototype._callEach = callEach; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 7f208d882..8feccaaaa 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -11,6 +11,7 @@ */ var ShareDBError = require('../error'); var presence = require('./index'); +var callEach = require('../util').callEach; // Check if a message represence presence. // Used in both ConnectionPresence and AgentPresence. @@ -115,7 +116,7 @@ DocPresence.prototype.handlePresence = function (err, presence) { var callbacks = this.inflight; this.inflight = null; this.inflightSeq = 0; - var called = callbacks && this.doc._callEach(callbacks, err); + var called = callbacks && callEach(callbacks, err); if (err && !called) this.doc.emit('error', err); this.doc.flush(); this.doc._emitNothingPending(); diff --git a/lib/util.js b/lib/util.js index 6ca346ffe..ad7048a58 100644 --- a/lib/util.js +++ b/lib/util.js @@ -22,3 +22,15 @@ exports.isValidVersion = function (version) { exports.isValidTimestamp = function (timestamp) { return exports.isValidVersion(timestamp); }; + +exports.callEach = function (callbacks, err) { + var called = false; + for (var i = 0; i < callbacks.length; i++) { + var callback = callbacks[i]; + if (callback) { + callback(err); + called = true; + } + } + return called; +}; From 0724fd7a1b57bab50644b215b19209ba927886b0 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 23:40:31 +0530 Subject: [PATCH 80/95] Migrate backend presence logic to decoupled BackendPresence class. --- lib/backend.js | 5 +++-- lib/presence/dummy.js | 9 ++++++++- lib/presence/index.js | 3 ++- lib/presence/stateless.js | 12 +++++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 98b052abd..116bd139c 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -51,6 +51,8 @@ function Backend(options) { } this.presence = options.presence || dummyPresence; + + this._backendPresence = new this.presence.BackendPresence(this); } module.exports = Backend; emitter.mixin(Backend); @@ -731,8 +733,7 @@ Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, c }; Backend.prototype.sendPresence = function(presence, callback) { - var channels = [ this.getDocChannel(presence.c, presence.d) ]; - this.pubsub.publish(channels, presence, callback); + this._backendPresence.sendPresence(presence, callback); }; function pluckIds(snapshots) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 28eae5b58..7eda4b13e 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -52,8 +52,15 @@ Object.assign(AgentPresence.prototype, { handlePresenceMessage: noop }); +function BackendPresence() {} +BackendPresence.prototype = Object.create(presence.BackendPresence.prototype); +Object.assign(BackendPresence.prototype, { + sendPresence: noop +}); + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, - AgentPresence: AgentPresence + AgentPresence: AgentPresence, + BackendPresence: BackendPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index 0a0b43e11..2d85e8fc5 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,5 +1,6 @@ module.exports = { DocPresence: function DocPresence () {}, ConnectionPresence: function ConnectionPresence () {}, - AgentPresence: function AgentPresence () {} + AgentPresence: function AgentPresence () {}, + BackendPresence: function BackendPresence () {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 8feccaaaa..58dcbaa22 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -513,9 +513,19 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { }); }; +function BackendPresence(backend) { + this.backend = backend; +} +BackendPresence.prototype = Object.create(presence.BackendPresence.prototype); + +BackendPresence.prototype.sendPresence = function(presence, callback) { + var channels = [ this.backend.getDocChannel(presence.c, presence.d) ]; + this.backend.pubsub.publish(channels, presence, callback); +} module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, - AgentPresence: AgentPresence + AgentPresence: AgentPresence, + BackendPresence: BackendPresence }; From 7af8427771f9721907e6419b2b3f4f1fbd0edd57 Mon Sep 17 00:00:00 2001 From: curran Date: Sat, 20 Apr 2019 00:25:57 +0530 Subject: [PATCH 81/95] Finishing touches --- README.md | 8 ++++---- lib/client/connection.js | 3 --- lib/client/doc.js | 8 +++----- lib/presence/stateless.js | 7 +++++++ test/client/presence.js | 9 ++++++++- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2ce8a9070..a5b6e3bac 100644 --- a/README.md +++ b/README.md @@ -108,8 +108,8 @@ __Options__ * `options.pubsub` _(instance of `ShareDB.PubSub`)_ Notify other ShareDB processes when data changes through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`. -* `options.presence` _(implementation of presence API)_ - Enable user presence synchronization. +* `options.presence` _(implementation of presence classes)_ + Enable user presence synchronization. The value of `options.presence` option is expected to contain implementations of the classes `DocPresence`, `ConnectionPresence`, `AgentPresence`, and `BackendPresence`. Logic related to presence is encapsulated within these classes, so it is possible develop additional third party presence implementations external to ShareDB. #### Database Adapters * `ShareDB.MemoryDB`, backed by a non-persistent database with no queries @@ -327,8 +327,8 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. -`doc.presence.current` _(Object)_ -Each property under `doc.presence.current` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. +`doc.presence` _(Object)_ +Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. The structure of the presence object is defined by the OT type of the document (for example, in [ot-rich-text](https://github.com/Teamwork/ot-rich-text#presence) and [@datavis-tech/json0](https://github.com/datavis-tech/json0#presence)). `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. diff --git a/lib/client/connection.js b/lib/client/connection.js index 336050ebe..c37c639f1 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -427,9 +427,6 @@ Connection.prototype.sendOp = function(doc, op) { this.send(message); }; -/** - * Sends presence data down the socket - */ Connection.prototype.sendPresence = function(doc, data, requestReply) { this._connectionPresence.sendPresence(doc, data, requestReply); }; diff --git a/lib/client/doc.js b/lib/client/doc.js index e1124c7f4..bd92e6506 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -56,7 +56,6 @@ var callEach = require('../util').callEach; */ module.exports = Doc; - function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -118,18 +117,17 @@ emitter.mixin(Doc); Doc.prototype.destroy = function(callback) { var doc = this; doc.whenNothingPending(function() { + doc._docPresence.destroyPresence(); if (doc.wantSubscribe) { doc.unsubscribe(function(err) { if (err) { if (callback) return callback(err); return doc.emit('error', err); } - doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -612,6 +610,8 @@ Doc.prototype._otApply = function(op, source) { this.emit('op', op.op, source); return; } + + this._docPresence.transformAllPresence(op); if (op.create) { this._setType(op.create.type); @@ -620,7 +620,6 @@ Doc.prototype._otApply = function(op, source) { this.type.createDeserialized(op.create.data) : this.type.deserialize(this.type.create(op.create.data)) : this.type.create(op.create.data); - this._docPresence.transformAllPresence(op); this.emit('create', source); return; } @@ -628,7 +627,6 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this._docPresence.transformAllPresence(op); this.emit('del', oldData, source); return; } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 58dcbaa22..f59f48f0c 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -19,6 +19,7 @@ function isPresenceMessage(data) { return data.a === 'p'; }; + /* * Stateless Presence implementation of DocPresence * ------------------------------------------------ @@ -513,6 +514,11 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { }); }; + +/* + * Stateless Presence implementation of BackendPresence + * ---------------------------------------------------- + */ function BackendPresence(backend) { this.backend = backend; } @@ -523,6 +529,7 @@ BackendPresence.prototype.sendPresence = function(presence, callback) { this.backend.pubsub.publish(channels, presence, callback); } + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, diff --git a/test/client/presence.js b/test/client/presence.js index 498438ad6..88cf1fee4 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -15,13 +15,20 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('should use dummyPresence.DocPresence if presence option not provided', function() { + it('should use dummyPresence if presence option not provided', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); expect(doc._docPresence instanceof dummyPresence.DocPresence).to.be(true); }); + it('should use presence option if provided', function() { + var backend = new Backend({ presence: statelessPresence }); + var connection = backend.connect(); + var doc = connection.get('dogs', 'fido'); + expect(doc._docPresence instanceof statelessPresence.DocPresence).to.be(true); + }); + it('DummyPresence should subclass Presence', function() { expect(dummyPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true); }); From 910b384ef870af98aa36a712d525fda5f4b1ea73 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 2 May 2019 18:15:24 +0200 Subject: [PATCH 82/95] fix: repair broken link --- examples/leaderboard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/leaderboard/README.md b/examples/leaderboard/README.md index 5a4c8e002..e3b520501 100644 --- a/examples/leaderboard/README.md +++ b/examples/leaderboard/README.md @@ -2,7 +2,7 @@ ![Demo](demo.gif) -This is a port of [https://github.com/percolatestudio/react-leaderboard](Leaderboard) to +This is a port of [Leaderboard](https://github.com/percolatestudio/react-leaderboard) to ShareDB. In this demo, data is not persisted. To persist data, run a Mongo From 406d4e09705e6eada15d39313fb2501d821c6bee Mon Sep 17 00:00:00 2001 From: qinyang Date: Thu, 9 May 2019 13:51:46 +0800 Subject: [PATCH 83/95] Update logger.js in chrome 51, will throw error Uncaught TypeError: Illegal invocation --- lib/logger/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 6d193e169..60bd7ebca 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -15,7 +15,7 @@ Logger.prototype.setMethods = function (overrides) { SUPPORTED_METHODS.forEach(function (method) { if (typeof overrides[method] === 'function') { - logger[method] = overrides[method]; + logger[method] = overrides[method].bind(overrides); } }); }; From 7c729d7ee9ea4ff4e6f7ea43004ee134dc376478 Mon Sep 17 00:00:00 2001 From: qinyang Date: Fri, 10 May 2019 18:45:22 +0800 Subject: [PATCH 84/95] fix: change the default logger Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628 --- lib/logger/logger.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 60bd7ebca..02bb45177 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -5,7 +5,12 @@ var SUPPORTED_METHODS = [ ]; function Logger() { - this.setMethods(console); + var defaultMethods = {}; + SUPPORTED_METHODS.forEach(function (method) { + // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628 + defaultMethods[method] = console[method].bind(console); + }); + this.setMethods(defaultMethods); } module.exports = Logger; @@ -15,7 +20,7 @@ Logger.prototype.setMethods = function (overrides) { SUPPORTED_METHODS.forEach(function (method) { if (typeof overrides[method] === 'function') { - logger[method] = overrides[method].bind(overrides); + logger[method] = overrides[method]; } }); }; From 65ce13154049d244a7d3a4e8c8ca22e0d7cf6d1d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 15 May 2019 09:20:44 -0700 Subject: [PATCH 85/95] 1.0.0-beta.23 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76dc3f878..af6e8eb6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.22", + "version": "1.0.0-beta.23", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From f09ad6221ff06b9bd04e6b0e23a4f195104505cf Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 15 May 2019 09:46:10 -0700 Subject: [PATCH 86/95] Update rest of examples to @teamwork/websocket-json-stream --- examples/counter/package.json | 2 +- examples/counter/server.js | 2 +- examples/leaderboard/package.json | 2 +- examples/leaderboard/server/index.js | 2 +- examples/rich-text/package.json | 2 +- examples/rich-text/server.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/counter/package.json b/examples/counter/package.json index 49b84460c..42b2e5c90 100644 --- a/examples/counter/package.json +++ b/examples/counter/package.json @@ -17,7 +17,7 @@ "dependencies": { "express": "^4.14.0", "sharedb": "^1.0.0-beta", - "websocket-json-stream": "^0.0.1", + "@teamwork/websocket-json-stream": "^2.0.0", "ws": "^1.1.0" }, "devDependencies": { diff --git a/examples/counter/server.js b/examples/counter/server.js index d4b466965..8817a4584 100644 --- a/examples/counter/server.js +++ b/examples/counter/server.js @@ -2,7 +2,7 @@ var http = require('http'); var express = require('express'); var ShareDB = require('sharedb'); var WebSocket = require('ws'); -var WebSocketJSONStream = require('websocket-json-stream'); +var WebSocketJSONStream = require('@teamwork/websocket-json-stream'); var backend = new ShareDB(); createDoc(startServer); diff --git a/examples/leaderboard/package.json b/examples/leaderboard/package.json index 7defaaa02..6ee5782c8 100644 --- a/examples/leaderboard/package.json +++ b/examples/leaderboard/package.json @@ -24,7 +24,7 @@ "sharedb-mingo-memory": "^1.0.0-beta", "through2": "^2.0.1", "underscore": "^1.8.3", - "websocket-json-stream": "^0.0.3", + "@teamwork/websocket-json-stream": "^2.0.0", "ws": "^1.1.0" }, "devDependencies": { diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js index 6b7972169..25410f8bb 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -3,7 +3,7 @@ var ShareDB = require("sharedb"); var connect = require("connect"); var serveStatic = require('serve-static'); var ShareDBMingoMemory = require('sharedb-mingo-memory'); -var WebSocketJSONStream = require('websocket-json-stream'); +var WebSocketJSONStream = require('@teamwork/websocket-json-stream'); var WebSocket = require('ws'); var util = require('util'); diff --git a/examples/rich-text/package.json b/examples/rich-text/package.json index 0bf0a48e8..2249e29c1 100644 --- a/examples/rich-text/package.json +++ b/examples/rich-text/package.json @@ -18,7 +18,7 @@ "quill": "^1.0.0-beta.11", "rich-text": "^3.0.1", "sharedb": "^1.0.0-beta", - "websocket-json-stream": "^0.0.1", + "@teamwork/websocket-json-stream": "^2.0.0", "ws": "^1.1.0" }, "devDependencies": { diff --git a/examples/rich-text/server.js b/examples/rich-text/server.js index 5dfb02bcc..486130f95 100644 --- a/examples/rich-text/server.js +++ b/examples/rich-text/server.js @@ -3,7 +3,7 @@ var express = require('express'); var ShareDB = require('sharedb'); var richText = require('rich-text'); var WebSocket = require('ws'); -var WebSocketJSONStream = require('websocket-json-stream'); +var WebSocketJSONStream = require('@teamwork/websocket-json-stream'); ShareDB.types.register(richText.type); var backend = new ShareDB(); From 2fb0637990390a712cdd39843f3f24ea225d6cd6 Mon Sep 17 00:00:00 2001 From: Abe Pazos Date: Sun, 19 May 2019 11:35:48 +0200 Subject: [PATCH 87/95] Fix broken status indication in textarea example --- examples/textarea/client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/textarea/client.js b/examples/textarea/client.js index c47035a1c..dadaef7fa 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -8,21 +8,21 @@ var connection = new sharedb.Connection(socket); var element = document.querySelector('textarea'); var statusSpan = document.getElementById('status-span'); -status.innerHTML = "Not Connected" +statusSpan.innerHTML = "Not Connected" element.style.backgroundColor = "gray"; socket.onopen = function(){ - status.innerHTML = "Connected" + statusSpan.innerHTML = "Connected" element.style.backgroundColor = "white"; }; socket.onclose = function(){ - status.innerHTML = "Closed" + statusSpan.innerHTML = "Closed" element.style.backgroundColor = "gray"; }; socket.onerror = function() { - status.innerHTML = "Error" + statusSpan.innerHTML = "Error" element.style.backgroundColor = "red"; } From 95ae394b02cbe4f075e6b3024ea23f679aac7d7f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 23 May 2019 13:39:30 +0100 Subject: [PATCH 88/95] Allow options to be passed for `fetch` and `getOps` The database adapters allow an `options` object to be passed through to them for enabling the return of metadata with a snapshot or ops. Consumers could query the database directly, or even use the database adapters, but this may give inconsistent results when comparing ops with those fetched through `getOps`. For example, the Mongo adapter makes sure that a valid set of ops with unique versions are returned, which may not be the case when querying the database directly. Fetching ops and snapshots through `Backend` methods also ensures that we call the appropriate "sanitize" methods, and trigger the corresponding middleware. However, we don't expose this on `Backend.getOps` or `Backend.fetch`. This change adds an optional `options` argument to these methods, which can then be used to ask for metadata. Note that an options argument has been added to `Backend.subscribe`, but using it will return an error. This is to keep the signature consistent with `fetch` and `getOps`. However, the implementation is beyond the scope of this change, because we'd need to add some way to configure `SubmitRequest.commit` to optionally pass metadata to the appropriate clients, who provided that given option on `subscribe`. --- README.md | 1 + lib/backend.js | 49 ++++++++++++++++++----- test/backend.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 test/backend.js diff --git a/README.md b/README.md index 8cbbfecf0..adee7b51e 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,7 @@ Additional fields may be added to the error object for debugging context dependi * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type * 4024 - Invalid version +* 4025 - Passing options to subscribe has not been implemented ### 5000 - Internal error diff --git a/lib/backend.js b/lib/backend.js index 442da075c..96d086479 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -313,7 +313,11 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) { // Non inclusive - gets ops from [from, to). Ie, all relevant ops. If to is // not defined (null or undefined) then it returns all ops. -Backend.prototype.getOps = function(agent, index, id, from, to, callback) { +Backend.prototype.getOps = function(agent, index, id, from, to, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -326,7 +330,8 @@ Backend.prototype.getOps = function(agent, index, id, from, to, callback) { from: from, to: to }; - backend.db.getOps(collection, id, from, to, null, function(err, ops) { + var opsOptions = options && options.opsOptions; + backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) { if (err) return callback(err); backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { if (err) return callback(err); @@ -336,7 +341,11 @@ Backend.prototype.getOps = function(agent, index, id, from, to, callback) { }); }; -Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) { +Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -348,7 +357,8 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) fromMap: fromMap, toMap: toMap }; - backend.db.getOpsBulk(collection, fromMap, toMap, null, function(err, opsMap) { + var opsOptions = options && options.opsOptions; + backend.db.getOpsBulk(collection, fromMap, toMap, opsOptions, function(err, opsMap) { if (err) return callback(err); backend._sanitizeOpsBulk(agent, projection, collection, opsMap, function(err) { if (err) return callback(err); @@ -358,7 +368,11 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) }); }; -Backend.prototype.fetch = function(agent, index, id, callback) { +Backend.prototype.fetch = function(agent, index, id, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -370,7 +384,8 @@ Backend.prototype.fetch = function(agent, index, id, callback) { collection: collection, id: id }; - backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) { + var snapshotOptions = options && options.snapshotOptions; + backend.db.getSnapshot(collection, id, fields, snapshotOptions, function(err, snapshot) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; @@ -382,7 +397,11 @@ Backend.prototype.fetch = function(agent, index, id, callback) { }); }; -Backend.prototype.fetchBulk = function(agent, index, ids, callback) { +Backend.prototype.fetchBulk = function(agent, index, ids, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -394,7 +413,8 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { collection: collection, ids: ids }; - backend.db.getSnapshotBulk(collection, ids, fields, null, function(err, snapshotMap) { + var snapshotOptions = options && options.snapshotOptions; + backend.db.getSnapshotBulk(collection, ids, fields, snapshotOptions, function(err, snapshotMap) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); @@ -407,7 +427,18 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { }; // Subscribe to the document from the specified version or null version -Backend.prototype.subscribe = function(agent, index, id, version, callback) { +Backend.prototype.subscribe = function(agent, index, id, version, options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + if (options) { + // We haven't yet implemented the ability to pass options to subscribe. This is because we need to + // add the ability to SubmitRequest.commit to optionally pass the metadata to other clients on + // PubSub. This behaviour is not needed right now, but we have added an options object to the + // subscribe() signature so that it remains consistent with getOps() and fetch(). + return callback({code: 4025, message: 'Passing options to subscribe has not been implemented'}); + } var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; diff --git a/test/backend.js b/test/backend.js new file mode 100644 index 000000000..d04746e2b --- /dev/null +++ b/test/backend.js @@ -0,0 +1,104 @@ +var Backend = require('../lib/backend'); +var expect = require('expect.js'); + +describe('Backend', function () { + var backend; + + beforeEach(function () { + backend = new Backend(); + }); + + afterEach(function (done) { + backend.close(done); + }); + + describe('a simple document', function () { + beforeEach(function (done) { + var doc = backend.connect().get('books', '1984'); + doc.create({ title: '1984' }, function (error) { + if (error) return done(error); + doc.submitOp({ p: ['author'], oi: 'George Orwell' }, done); + }); + }); + + describe('getOps', function () { + it('fetches all the ops', function (done) { + backend.getOps(null, 'books', '1984', 0, null, function (error, ops) { + if (error) return done(error); + expect(ops).to.have.length(2); + expect(ops[0].create.data).to.eql({ title: '1984' }); + expect(ops[1].op).to.eql([{ p: ['author'], oi: 'George Orwell' }]); + done(); + }); + }); + + it('fetches the ops with metadata', function (done) { + var options = { + opsOptions: {metadata: true} + }; + backend.getOps(null, 'books', '1984', 0, null, options, function (error, ops) { + if (error) return done(error); + expect(ops).to.have.length(2); + expect(ops[0].m).to.be.ok(); + expect(ops[1].m).to.be.ok(); + done(); + }); + }); + }); + + describe('fetch', function () { + it('fetches the document', function (done) { + backend.fetch(null, 'books', '1984', function (error, doc) { + if (error) return done(error); + expect(doc.data).to.eql({ + title: '1984', + author: 'George Orwell' + }); + done(); + }); + }); + + it('fetches the document with metadata', function (done) { + var options = { + snapshotOptions: {metadata: true} + }; + backend.fetch(null, 'books', '1984', options, function (error, doc) { + if (error) return done(error); + expect(doc.m).to.be.ok(); + done(); + }); + }); + }); + + describe('subscribe', function () { + it('subscribes to the document', function (done) { + backend.subscribe(null, 'books', '1984', null, function (error, stream, snapshot) { + if (error) return done(error); + expect(stream.open).to.be(true); + expect(snapshot.data).to.eql({ + title: '1984', + author: 'George Orwell' + }); + var op = {op: {p: ['publication'], oi: 1949}}; + stream.on('data', function (data) { + expect(data.op).to.eql(op.op); + done(); + }); + backend.submit(null, 'books', '1984', op, null, function (error) { + if (error) return done(error); + }); + }); + }); + + it('does not support subscribing to the document with options', function (done) { + var options = { + opsOptions: { metadata: true } + }; + backend.subscribe(null, 'books', '1984', null, options, function (error) { + expect(error.code).to.be(4025); + done(); + }); + }); + }); + }); +}); From 1a7bc3e86586965761a3caa653146044000b4d72 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 4 Jul 2019 15:09:38 +0100 Subject: [PATCH 89/95] Move from `jshint` to `eslint` This change addresses three things: 1. Move us from `jshint` to `eslint` (which is the more commonly used linter) 2. Fix our linting glob patterns, which currently only address files at the top of their respective directories (and ignore all files in subdirectories) 3. Enforce stricter linting rules, based roughly on the [Google][1] rules [1]: https://github.com/google/eslint-config-google --- .eslintrc | 139 +++++++ .jshintrc | 18 - .travis.yml | 2 +- lib/agent.js | 10 +- lib/backend.js | 85 ++-- lib/client/connection.js | 12 +- lib/client/doc.js | 13 +- lib/client/query.js | 1 - .../snapshot-request/snapshot-request.js | 6 +- .../snapshot-timestamp-request.js | 2 +- .../snapshot-version-request.js | 4 +- lib/logger/logger.js | 6 +- lib/milestone-db/index.js | 14 +- lib/milestone-db/memory.js | 32 +- lib/milestone-db/no-op.js | 8 +- lib/ot.js | 10 +- lib/projections.js | 2 +- lib/query-emitter.js | 38 +- lib/submit-request.js | 58 +-- lib/util.js | 6 +- package.json | 6 +- test/backend.js | 54 +-- test/client/connection.js | 56 ++- test/client/doc.js | 71 ++-- test/client/pending.js | 2 - test/client/projections.js | 11 +- test/client/query-subscribe.js | 83 +++- test/client/query.js | 52 ++- test/client/snapshot-timestamp-request.js | 204 +++++----- test/client/snapshot-version-request.js | 184 ++++----- test/client/submit.js | 38 +- test/client/subscribe.js | 133 +++++-- test/db.js | 6 +- test/logger.js | 14 +- test/middleware.js | 8 - test/milestone-db.js | 366 +++++++++--------- test/ot.js | 12 +- test/projections.js | 21 +- test/pubsub.js | 1 - test/util.js | 2 +- 40 files changed, 1038 insertions(+), 752 deletions(-) create mode 100644 .eslintrc delete mode 100644 .jshintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..0ce02cbc0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,139 @@ +{ + "parserOptions": { + "ecmaVersion": 3 + }, + "rules": { + "no-cond-assign": 0, + "no-irregular-whitespace": 2, + "no-unexpected-multiline": 2, + "curly": [ + 2, + "multi-line" + ], + "guard-for-in": 0, + "no-caller": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-invalid-this": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-new-wrappers": 2, + "no-throw-literal": 2, + "no-with": 2, + "prefer-promise-reject-errors": 2, + "no-unused-vars": [ + 2, + { + "args": "none" + } + ], + "array-bracket-newline": 0, + "array-bracket-spacing": [ + 2, + "never" + ], + "array-element-newline": 0, + "block-spacing": [ + 2, + "never" + ], + "brace-style": 2, + "camelcase": [ + 2, + { + "properties": "never" + } + ], + "comma-spacing": 2, + "comma-style": 2, + "computed-property-spacing": 2, + "eol-last": 2, + "func-call-spacing": 2, + "indent": [ + "error", + 2, + { + "CallExpression": { + "arguments": 1 + }, + "FunctionDeclaration": { + "body": 1, + "parameters": 1 + }, + "FunctionExpression": { + "body": 1, + "parameters": 1 + }, + "MemberExpression": 1, + "ObjectExpression": 1, + "SwitchCase": 1, + "ignoredNodes": [ + "ConditionalExpression" + ] + } + ], + "key-spacing": 2, + "keyword-spacing": 2, + "linebreak-style": 2, + "max-len": [ + 2, + { + "code": 120, + "tabWidth": 2, + "ignoreUrls": true, + } + ], + "new-cap": 2, + "no-array-constructor": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [ + 2, + { + "max": 2 + } + ], + "no-new-object": 2, + "no-tabs": 2, + "no-trailing-spaces": 2, + "object-curly-spacing": 2, + "one-var": [ + 2, + { + "var": "never", + "let": "never", + "const": "never" + } + ], + "padded-blocks": [ + 2, + "never" + ], + "quote-props": [ + 2, + "as-needed" + ], + "quotes": [ + 2, + "single", + { + "allowTemplateLiterals": true + } + ], + "semi": 2, + "semi-spacing": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + { + "asyncArrow": "always", + "anonymous": "never", + "named": "never" + } + ], + "spaced-comment": [ + 2, + "always" + ], + "switch-colon-spacing": 2, + }, +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index cf514a151..000000000 --- a/.jshintrc +++ /dev/null @@ -1,18 +0,0 @@ -{ - "node": true, - "laxcomma": true, - "eqnull": true, - "eqeqeq": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "undef": true, - "trailing": true, - "shadow": true, - "expr": true, - "boss": true, - "globals": { - "window": false, - "document": false - } -} diff --git a/.travis.yml b/.travis.yml index 21efafe46..5dc6cbaa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ node_js: - "10" - "8" - "6" -script: "npm run jshint && npm run test-cover" +script: "npm run lint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/lib/agent.js b/lib/agent.js index b5cef65c1..db18c4a5d 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -56,7 +56,6 @@ Agent.prototype.close = function(err) { }; Agent.prototype._cleanup = function() { - // Only clean up once if the stream emits both 'end' and 'close'. if (this.closed) return; @@ -265,7 +264,7 @@ Agent.prototype._open = function() { agent._handleMessage(request.data, callback); }); }); - + var cleanup = agent._cleanup.bind(agent); this.stream.on('end', cleanup); this.stream.on('close', cleanup); @@ -333,7 +332,7 @@ Agent.prototype._handleMessage = function(request, callback) { }; function getQueryOptions(request) { var results = request.r; - var ids, fetch, fetchOps; + var ids; var fetch; var fetchOps; if (results) { ids = []; for (var i = 0; i < results.length; i++) { @@ -362,7 +361,6 @@ function getQueryOptions(request) { Agent.prototype._queryFetch = function(queryId, collection, query, options, callback) { // Fetch the results of a query once - var agent = this; this.backend.queryFetch(this, collection, query, options, function(err, results, extra) { if (err) return callback(err); var message = { @@ -607,10 +605,10 @@ Agent.prototype._createOp = function(request) { } }; -Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { +Agent.prototype._fetchSnapshot = function(collection, id, version, callback) { this.backend.fetchSnapshot(this, collection, id, version, callback); }; -Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); }; diff --git a/lib/backend.js b/lib/backend.js index 96d086479..1d5c4fe4f 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -11,7 +11,6 @@ var QueryEmitter = require('./query-emitter'); var Snapshot = require('./snapshot'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); -var types = require('./types'); var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; @@ -95,7 +94,10 @@ Backend.prototype.SNAPSHOT_TYPES = { Backend.prototype._shimDocAction = function() { if (warnDeprecatedDoc) { warnDeprecatedDoc = false; - console.warn('DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.'); + console.warn([ + 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead.', + 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.' + ].join(' ')); } var backend = this; @@ -112,7 +114,10 @@ Backend.prototype._shimDocAction = function() { Backend.prototype._shimAfterSubmit = function() { if (warnDeprecatedAfterSubmit) { warnDeprecatedAfterSubmit = false; - console.warn('DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.'); + console.warn([ + 'DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass', + '`disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.' + ].join(' ')); } var backend = this; @@ -389,11 +394,17 @@ Backend.prototype.fetch = function(agent, index, id, options, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { - if (err) return callback(err); - backend.emit('timing', 'fetch', Date.now() - start, request); - callback(null, snapshot); - }); + backend._sanitizeSnapshots( + agent, + snapshotProjection, + collection, + snapshots, + backend.SNAPSHOT_TYPES.current, + function(err) { + if (err) return callback(err); + backend.emit('timing', 'fetch', Date.now() - start, request); + callback(null, snapshot); + }); }); }; @@ -418,11 +429,17 @@ Backend.prototype.fetchBulk = function(agent, index, ids, options, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { - if (err) return callback(err); - backend.emit('timing', 'fetchBulk', Date.now() - start, request); - callback(null, snapshotMap); - }); + backend._sanitizeSnapshots( + agent, + snapshotProjection, + collection, + snapshots, + backend.SNAPSHOT_TYPES.current, + function(err) { + if (err) return callback(err); + backend.emit('timing', 'fetchBulk', Date.now() - start, request); + callback(null, snapshotMap); + }); }); }; @@ -617,9 +634,15 @@ Backend.prototype._query = function(agent, request, callback) { var backend = this; request.db.query(request.collection, request.query, request.fields, request.options, function(err, snapshots, extra) { if (err) return callback(err); - backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { - callback(err, snapshots, extra); - }); + backend._sanitizeSnapshots( + agent, + request.snapshotProjection, + request.collection, + snapshots, + backend.SNAPSHOT_TYPES.current, + function(err) { + callback(err, snapshots, extra); + }); }); }; @@ -651,12 +674,12 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) version: version }; - this._fetchSnapshot(collection, id, version, function (error, snapshot) { + this._fetchSnapshot(collection, id, version, function(error, snapshot) { if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; var snapshotType = backend.SNAPSHOT_TYPES.byVersion; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function (error) { + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); callback(null, snapshot); @@ -664,25 +687,25 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) }); }; -Backend.prototype._fetchSnapshot = function (collection, id, version, callback) { +Backend.prototype._fetchSnapshot = function(collection, id, version, callback) { var db = this.db; var backend = this; - this.milestoneDb.getMilestoneSnapshot(collection, id, version, function (error, milestoneSnapshot) { + this.milestoneDb.getMilestoneSnapshot(collection, id, version, function(error, milestoneSnapshot) { if (error) return callback(error); // Bypass backend.getOps so that we don't call _sanitizeOps. We want to avoid this, because: // - we want to avoid the 'op' middleware, because we later use the 'readSnapshots' middleware in _sanitizeSnapshots // - we handle the projection in _sanitizeSnapshots var from = milestoneSnapshot ? milestoneSnapshot.v : 0; - db.getOps(collection, id, from, version, null, function (error, ops) { + db.getOps(collection, id, from, version, null, function(error, ops) { if (error) return callback(error); - backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function (error, snapshot) { + backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function(error, snapshot) { if (error) return callback(error); if (version > snapshot.v) { - return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); + return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'}); } callback(null, snapshot); @@ -691,7 +714,7 @@ Backend.prototype._fetchSnapshot = function (collection, id, version, callback) }); }; -Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timestamp, callback) { +Backend.prototype.fetchSnapshotByTimestamp = function(agent, index, id, timestamp, callback) { var start = Date.now(); var backend = this; var projection = this.projections[index]; @@ -704,12 +727,12 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta timestamp: timestamp }; - this._fetchSnapshotByTimestamp(collection, id, timestamp, function (error, snapshot) { + this._fetchSnapshotByTimestamp(collection, id, timestamp, function(error, snapshot) { if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; var snapshotType = backend.SNAPSHOT_TYPES.byTimestamp; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function (error) { + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); callback(null, snapshot); @@ -717,7 +740,7 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta }); }; -Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { var db = this.db; var milestoneDb = this.milestoneDb; var backend = this; @@ -726,17 +749,17 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam var from = 0; var to = null; - milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function (error, snapshot) { + milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) { if (error) return callback(error); milestoneSnapshot = snapshot; if (snapshot) from = snapshot.v; - milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function (error, snapshot) { + milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) { if (error) return callback(error); if (snapshot) to = snapshot.v; var options = {metadata: true}; - db.getOps(collection, id, from, to, options, function (error, ops) { + db.getOps(collection, id, from, to, options, function(error, ops) { if (error) return callback(error); filterOpsInPlaceBeforeTimestamp(ops, timestamp); backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback); @@ -745,7 +768,7 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam }); }; -Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback) { +Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, callback) { var snapshot = startingSnapshot || new Snapshot(id, 0, null, undefined, null); var error = ot.applyOps(snapshot, ops); callback(error, snapshot); diff --git a/lib/client/connection.js b/lib/client/connection.js index cd56306b2..779b71383 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -158,10 +158,8 @@ Connection.prototype.bindToSocket = function(socket) { if (reason === 'closed' || reason === 'Closed') { connection._setState('closed', reason); - } else if (reason === 'stopped' || reason === 'Stopped by server') { connection._setState('stopped', reason); - } else { connection._setState('disconnected', reason); } @@ -295,8 +293,8 @@ Connection.prototype._setState = function(newState, reason) { // 'connecting' from anywhere other than 'disconnected' and getting to // 'connected' from anywhere other than 'connecting'. if ( - (newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed') || - (newState === 'connected' && this.state !== 'connecting') + (newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed') + || (newState === 'connected' && this.state !== 'connecting') ) { var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState); return this.emit('error', err); @@ -607,7 +605,7 @@ Connection.prototype._firstQuery = function(fn) { } }; -Connection.prototype._firstSnapshotRequest = function () { +Connection.prototype._firstSnapshotRequest = function() { for (var id in this._snapshotRequests) { return this._snapshotRequests[id]; } @@ -657,7 +655,7 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) * } * */ -Connection.prototype.fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +Connection.prototype.fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { if (typeof timestamp === 'function') { callback = timestamp; timestamp = null; @@ -669,7 +667,7 @@ Connection.prototype.fetchSnapshotByTimestamp = function (collection, id, timest snapshotRequest.send(); }; -Connection.prototype._handleSnapshotFetch = function (error, message) { +Connection.prototype._handleSnapshotFetch = function(error, message) { var snapshotRequest = this._snapshotRequests[message.id]; if (!snapshotRequest) return; delete this._snapshotRequests[message.id]; diff --git a/lib/client/doc.js b/lib/client/doc.js index 15798f0e5..d9c6f19fb 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -136,12 +136,10 @@ Doc.prototype._setType = function(newType) { if (newType) { this.type = newType; - } else if (newType === null) { this.type = newType; // If we removed the type from the object, also remove its data this.data = undefined; - } else { var err = new ShareDBError(4008, 'Missing type ' + newType); return this.emit('error', err); @@ -180,7 +178,10 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { return callback && this.once('no write pending', callback); } // Otherwise, we've encounted an error state - var err = new ShareDBError(5009, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id); + var err = new ShareDBError( + 5009, + 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id + ); if (callback) return callback(err); return this.emit('error', err); } @@ -653,7 +654,10 @@ Doc.prototype._submit = function(op, source, callback) { // The op contains either op, create, delete, or none of the above (a no-op). if (op.op) { if (!this.type) { - var err = new ShareDBError(4015, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id); + var err = new ShareDBError( + 4015, + 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id + ); if (callback) return callback(err); return this.emit('error', err); } @@ -839,7 +843,6 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - } else if (message.v !== this.version) { // We should already be at the same version, because the server should // have sent all the ops that have happened before acknowledging our op diff --git a/lib/client/query.js b/lib/client/query.js index 4f8bae764..c406fb4f0 100644 --- a/lib/client/query.js +++ b/lib/client/query.js @@ -119,7 +119,6 @@ Query.prototype._handleResponse = function(err, data, extra) { wait += data.length; this.results = this._ingestSnapshots(data, finish); this.extra = extra; - } else { for (var id in data) { wait++; diff --git a/lib/client/snapshot-request/snapshot-request.js b/lib/client/snapshot-request/snapshot-request.js index 00ed9b90f..95f68055b 100644 --- a/lib/client/snapshot-request/snapshot-request.js +++ b/lib/client/snapshot-request/snapshot-request.js @@ -20,7 +20,7 @@ function SnapshotRequest(connection, requestId, collection, id, callback) { } emitter.mixin(SnapshotRequest); -SnapshotRequest.prototype.send = function () { +SnapshotRequest.prototype.send = function() { if (!this.connection.canSend) { return; } @@ -29,7 +29,7 @@ SnapshotRequest.prototype.send = function () { this.sent = true; }; -SnapshotRequest.prototype._onConnectionStateChanged = function () { +SnapshotRequest.prototype._onConnectionStateChanged = function() { if (this.connection.canSend) { if (!this.sent) this.send(); } else { @@ -40,7 +40,7 @@ SnapshotRequest.prototype._onConnectionStateChanged = function () { } }; -SnapshotRequest.prototype._handleResponse = function (error, message) { +SnapshotRequest.prototype._handleResponse = function(error, message) { this.emit('ready'); if (error) { diff --git a/lib/client/snapshot-request/snapshot-timestamp-request.js b/lib/client/snapshot-request/snapshot-timestamp-request.js index 53c3b2437..0e8af04af 100644 --- a/lib/client/snapshot-request/snapshot-timestamp-request.js +++ b/lib/client/snapshot-request/snapshot-timestamp-request.js @@ -15,7 +15,7 @@ function SnapshotTimestampRequest(connection, requestId, collection, id, timesta SnapshotTimestampRequest.prototype = Object.create(SnapshotRequest.prototype); -SnapshotTimestampRequest.prototype._message = function () { +SnapshotTimestampRequest.prototype._message = function() { return { a: 'nt', id: this.requestId, diff --git a/lib/client/snapshot-request/snapshot-version-request.js b/lib/client/snapshot-request/snapshot-version-request.js index 60a2e3a3c..0b64b3821 100644 --- a/lib/client/snapshot-request/snapshot-version-request.js +++ b/lib/client/snapshot-request/snapshot-version-request.js @@ -3,7 +3,7 @@ var util = require('../../util'); module.exports = SnapshotVersionRequest; -function SnapshotVersionRequest (connection, requestId, collection, id, version, callback) { +function SnapshotVersionRequest(connection, requestId, collection, id, version, callback) { SnapshotRequest.call(this, connection, requestId, collection, id, callback); if (!util.isValidVersion(version)) { @@ -15,7 +15,7 @@ function SnapshotVersionRequest (connection, requestId, collection, id, version, SnapshotVersionRequest.prototype = Object.create(SnapshotRequest.prototype); -SnapshotVersionRequest.prototype._message = function () { +SnapshotVersionRequest.prototype._message = function() { return { a: 'nf', id: this.requestId, diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 02bb45177..3c70e5a53 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -6,7 +6,7 @@ var SUPPORTED_METHODS = [ function Logger() { var defaultMethods = {}; - SUPPORTED_METHODS.forEach(function (method) { + SUPPORTED_METHODS.forEach(function(method) { // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628 defaultMethods[method] = console[method].bind(console); }); @@ -14,11 +14,11 @@ function Logger() { } module.exports = Logger; -Logger.prototype.setMethods = function (overrides) { +Logger.prototype.setMethods = function(overrides) { overrides = overrides || {}; var logger = this; - SUPPORTED_METHODS.forEach(function (method) { + SUPPORTED_METHODS.forEach(function(method) { if (typeof overrides[method] === 'function') { logger[method] = overrides[method]; } diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index 1b04ad30e..3726b2ca8 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -24,7 +24,7 @@ MilestoneDB.prototype.close = function(callback) { * @param {Function} callback - a callback to invoke once the snapshot has been fetched. Should have * the signature (error, snapshot) => void; */ -MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { +MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; @@ -35,30 +35,30 @@ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, * @param {Function} callback (optional) - a callback to invoke after the snapshot has been saved. * Should have the signature (error) => void; */ -MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { +MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { var error = new ShareDBError(5020, 'saveMilestoneSnapshot MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; -MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { +MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { var error = new ShareDBError(5021, 'getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; -MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { +MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { var error = new ShareDBError(5022, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; -MilestoneDB.prototype._isValidVersion = function (version) { +MilestoneDB.prototype._isValidVersion = function(version) { return util.isValidVersion(version); }; -MilestoneDB.prototype._isValidTimestamp = function (timestamp) { +MilestoneDB.prototype._isValidTimestamp = function(timestamp) { return util.isValidTimestamp(timestamp); }; -MilestoneDB.prototype._callBackOrEmitError = function (error, callback) { +MilestoneDB.prototype._callBackOrEmitError = function(error, callback) { if (callback) return process.nextTick(callback, error); this.emit('error', error); }; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 52f1ba522..7b64dee36 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -22,15 +22,15 @@ function MemoryMilestoneDB(options) { MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); -MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { +MemoryMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version')); var predicate = versionLessThanOrEqualTo(version); this._findMilestoneSnapshot(collection, id, predicate, callback); }; -MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { - callback = callback || function (error) { +MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { + callback = callback || function(error) { if (error) return this.emit('error', error); this.emit('save', collection, snapshot); }.bind(this); @@ -40,25 +40,29 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); - milestoneSnapshots.sort(function (a, b) { + milestoneSnapshots.sort(function(a, b) { return a.v - b.v; }); process.nextTick(callback, null); }; -MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { - if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); +MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { + if (!this._isValidTimestamp(timestamp)) { + return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); + } var filter = timestampLessThanOrEqualTo(timestamp); this._findMilestoneSnapshot(collection, id, filter, callback); }; -MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { - if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); +MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { + if (!this._isValidTimestamp(timestamp)) { + return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); + } var filter = timestampGreaterThanOrEqualTo(timestamp); - this._findMilestoneSnapshot(collection, id, filter, function (error, snapshot) { + this._findMilestoneSnapshot(collection, id, filter, function(error, snapshot) { if (error) return process.nextTick(callback, error); var mtime = snapshot && snapshot.m && snapshot.m.mtime; @@ -70,7 +74,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collec }); }; -MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, breakCondition, callback) { +MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, breakCondition, callback) { if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Missing collection')); if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Missing ID')); @@ -89,13 +93,13 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, b process.nextTick(callback, null, milestoneSnapshot); }; -MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { +MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) { var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {}); return collectionSnapshots[id] || (collectionSnapshots[id] = []); }; function versionLessThanOrEqualTo(version) { - return function (currentSnapshot, nextSnapshot) { + return function(currentSnapshot, nextSnapshot) { if (version === null) { return false; } @@ -105,7 +109,7 @@ function versionLessThanOrEqualTo(version) { } function timestampGreaterThanOrEqualTo(timestamp) { - return function (currentSnapshot) { + return function(currentSnapshot) { if (timestamp === null) { return false; } @@ -116,7 +120,7 @@ function timestampGreaterThanOrEqualTo(timestamp) { } function timestampLessThanOrEqualTo(timestamp) { - return function (currentSnapshot, nextSnapshot) { + return function(currentSnapshot, nextSnapshot) { if (timestamp === null) { return !!currentSnapshot; } diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js index 82d66ba10..fc235d248 100644 --- a/lib/milestone-db/no-op.js +++ b/lib/milestone-db/no-op.js @@ -13,22 +13,22 @@ function NoOpMilestoneDB(options) { NoOpMilestoneDB.prototype = Object.create(MilestoneDB.prototype); -NoOpMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { +NoOpMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { var snapshot = undefined; process.nextTick(callback, null, snapshot); }; -NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { +NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { if (callback) return process.nextTick(callback, null); this.emit('save', collection, snapshot); }; -NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { +NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { var snapshot = undefined; process.nextTick(callback, null, snapshot); }; -NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { +NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { var snapshot = undefined; process.nextTick(callback, null, snapshot); }; diff --git a/lib/ot.js b/lib/ot.js index f44b77835..f04da0145 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -23,10 +23,8 @@ exports.checkOp = function(op) { if (type == null || typeof type !== 'object') { return {code: 4008, message: 'Unknown type'}; } - } else if (op.del != null) { if (op.del !== true) return {code: 4009, message: 'del value must be true'}; - } else if (op.op == null) { return {code: 4010, message: 'Missing op, create, or del'}; } @@ -155,14 +153,14 @@ exports.transform = function(type, op, appliedOp) { * * @param snapshot - a Snapshot object which will be mutated by the provided ops * @param ops - an array of ops to apply to the snapshot - * @returns an error object if applicable + * @return an error object if applicable */ -exports.applyOps = function (snapshot, ops) { +exports.applyOps = function(snapshot, ops) { var type = null; if (snapshot.type) { type = types[snapshot.type]; - if (!type) return { code: 4008, message: 'Unknown type' }; + if (!type) return {code: 4008, message: 'Unknown type'}; } for (var index = 0; index < ops.length; index++) { @@ -172,7 +170,7 @@ exports.applyOps = function (snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return { code: 4008, message: 'Unknown type' }; + if (!type) return {code: 4008, message: 'Unknown type'}; snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { diff --git a/lib/projections.js b/lib/projections.js index 48baa5ae7..d9a48ea03 100644 --- a/lib/projections.js +++ b/lib/projections.js @@ -41,7 +41,7 @@ function projectEdit(fields, op) { var path = c.p; if (path.length === 0) { - var newC = {p:[]}; + var newC = {p: []}; if (c.od !== undefined || c.oi !== undefined) { if (c.od !== undefined) { diff --git a/lib/query-emitter.js b/lib/query-emitter.js index f79c9a684..77b91f8b8 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -187,13 +187,19 @@ QueryEmitter.prototype.queryPoll = function(callback) { if (err) return emitter._finishPoll(err, callback, pending); var snapshots = emitter.backend._getSnapshotsFromMap(inserted, snapshotMap); var snapshotType = emitter.backend.SNAPSHOT_TYPES.current; - emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, snapshotType, function(err) { - if (err) return emitter._finishPoll(err, callback, pending); - emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start); - var diff = mapDiff(idsDiff, snapshotMap); - emitter.onDiff(diff); - emitter._finishPoll(err, callback, pending); - }); + emitter.backend._sanitizeSnapshots( + emitter.agent, + emitter.snapshotProjection, + emitter.collection, + snapshots, + snapshotType, + function(err) { + if (err) return emitter._finishPoll(err, callback, pending); + emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start); + var diff = mapDiff(idsDiff, snapshotMap); + emitter.onDiff(diff); + emitter._finishPoll(err, callback, pending); + }); }); } else { emitter.onDiff(idsDiff); @@ -236,12 +242,18 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) { if (err) return callback(err); var snapshots = [snapshot]; var snapshotType = emitter.backend.SNAPSHOT_TYPES.current; - emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, snapshotType, function(err) { - if (err) return callback(err); - emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]); - emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start); - callback(); - }); + emitter.backend._sanitizeSnapshots( + emitter.agent, + emitter.snapshotProjection, + emitter.collection, + snapshots, + snapshotType, + function(err) { + if (err) return callback(err); + emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]); + emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start); + callback(); + }); }); return; } diff --git a/lib/submit-request.js b/lib/submit-request.js index ddd9f6f13..068be3123 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -54,7 +54,6 @@ SubmitRequest.prototype.submit = function(callback) { request._addSnapshotMeta(); if (op.v == null) { - if (op.create && snapshot.type && op.src) { // If the document was already created by another op, we will return a // 'Document already exists' error in response and fail to submit this @@ -147,28 +146,34 @@ SubmitRequest.prototype.commit = function(callback) { if (err) return callback(err); // Try committing the operation and snapshot to the database atomically - backend.db.commit(request.collection, request.id, request.op, request.snapshot, request.options, function(err, succeeded) { - if (err) return callback(err); - if (!succeeded) { - // Between our fetch and our call to commit, another client committed an - // operation. We expect this to be relatively infrequent but normal. - return request.retry(callback); - } - if (!request.suppressPublish) { - var op = request.op; - op.c = request.collection; - op.d = request.id; - op.m = undefined; - // Needed for agent to detect if it can ignore sending the op back to - // the client that submitted it in subscriptions - if (request.collection !== request.index) op.i = request.index; - backend.pubsub.publish(request.channels, op); - } - if (request._shouldSaveMilestoneSnapshot(request.snapshot)) { - request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot); - } - callback(); - }); + backend.db.commit( + request.collection, + request.id, + request.op, + request.snapshot, + request.options, + function(err, succeeded) { + if (err) return callback(err); + if (!succeeded) { + // Between our fetch and our call to commit, another client committed an + // operation. We expect this to be relatively infrequent but normal. + return request.retry(callback); + } + if (!request.suppressPublish) { + var op = request.op; + op.c = request.collection; + op.d = request.id; + op.m = undefined; + // Needed for agent to detect if it can ignore sending the op back to + // the client that submitted it in subscriptions + if (request.collection !== request.index) op.i = request.index; + backend.pubsub.publish(request.channels, op); + } + if (request._shouldSaveMilestoneSnapshot(request.snapshot)) { + request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot); + } + callback(); + }); }); }; @@ -224,7 +229,7 @@ SubmitRequest.prototype._addSnapshotMeta = function() { meta.mtime = this.start; }; -SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { +SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) { // If the flag is null, it's not been overridden by the consumer, so apply the interval if (this.saveMilestoneSnapshot === null) { return snapshot && snapshot.v % this.backend.milestoneDb.interval === 0; @@ -252,7 +257,10 @@ SubmitRequest.prototype.projectionError = function() { }; // Fatal internal errors: SubmitRequest.prototype.missingOpsError = function() { - return {code: 5001, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'}; + return { + code: 5001, + message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version' + }; }; SubmitRequest.prototype.versionDuringTransformError = function() { return {code: 5002, message: 'Op submit failed. Versions mismatched during op transform'}; diff --git a/lib/util.js b/lib/util.js index 6ca346ffe..7ad8a23fb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -8,17 +8,17 @@ exports.hasKeys = function(object) { }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill -exports.isInteger = Number.isInteger || function (value) { +exports.isInteger = Number.isInteger || function(value) { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; }; -exports.isValidVersion = function (version) { +exports.isValidVersion = function(version) { if (version === null) return true; return exports.isInteger(version) && version >= 0; }; -exports.isValidTimestamp = function (timestamp) { +exports.isValidTimestamp = function(timestamp) { return exports.isValidVersion(timestamp); }; diff --git a/package.json b/package.json index af6e8eb6f..9e4e5b09c 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,17 @@ }, "devDependencies": { "coveralls": "^2.11.8", + "eslint": "^5.16.0", "expect.js": "^0.3.1", "istanbul": "^0.4.2", - "jshint": "^2.9.2", "lolex": "^3.0.0", "mocha": "^5.2.0", "sinon": "^6.1.5" }, "scripts": { - "test": "./node_modules/.bin/mocha && npm run jshint", + "test": "./node_modules/.bin/mocha && npm run lint", "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha", - "jshint": "./node_modules/.bin/jshint lib/*.js test/*.js" + "lint": "./node_modules/.bin/eslint 'lib/**/*.js' 'test/**/*.js'" }, "repository": { "type": "git", diff --git a/test/backend.js b/test/backend.js index d04746e2b..991c6716b 100644 --- a/test/backend.js +++ b/test/backend.js @@ -1,42 +1,42 @@ var Backend = require('../lib/backend'); var expect = require('expect.js'); -describe('Backend', function () { +describe('Backend', function() { var backend; - beforeEach(function () { + beforeEach(function() { backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { backend.close(done); }); - describe('a simple document', function () { - beforeEach(function (done) { + describe('a simple document', function() { + beforeEach(function(done) { var doc = backend.connect().get('books', '1984'); - doc.create({ title: '1984' }, function (error) { + doc.create({title: '1984'}, function(error) { if (error) return done(error); - doc.submitOp({ p: ['author'], oi: 'George Orwell' }, done); + doc.submitOp({p: ['author'], oi: 'George Orwell'}, done); }); }); - describe('getOps', function () { - it('fetches all the ops', function (done) { - backend.getOps(null, 'books', '1984', 0, null, function (error, ops) { + describe('getOps', function() { + it('fetches all the ops', function(done) { + backend.getOps(null, 'books', '1984', 0, null, function(error, ops) { if (error) return done(error); expect(ops).to.have.length(2); - expect(ops[0].create.data).to.eql({ title: '1984' }); - expect(ops[1].op).to.eql([{ p: ['author'], oi: 'George Orwell' }]); + expect(ops[0].create.data).to.eql({title: '1984'}); + expect(ops[1].op).to.eql([{p: ['author'], oi: 'George Orwell'}]); done(); }); }); - it('fetches the ops with metadata', function (done) { + it('fetches the ops with metadata', function(done) { var options = { opsOptions: {metadata: true} }; - backend.getOps(null, 'books', '1984', 0, null, options, function (error, ops) { + backend.getOps(null, 'books', '1984', 0, null, options, function(error, ops) { if (error) return done(error); expect(ops).to.have.length(2); expect(ops[0].m).to.be.ok(); @@ -46,9 +46,9 @@ describe('Backend', function () { }); }); - describe('fetch', function () { - it('fetches the document', function (done) { - backend.fetch(null, 'books', '1984', function (error, doc) { + describe('fetch', function() { + it('fetches the document', function(done) { + backend.fetch(null, 'books', '1984', function(error, doc) { if (error) return done(error); expect(doc.data).to.eql({ title: '1984', @@ -58,11 +58,11 @@ describe('Backend', function () { }); }); - it('fetches the document with metadata', function (done) { + it('fetches the document with metadata', function(done) { var options = { snapshotOptions: {metadata: true} }; - backend.fetch(null, 'books', '1984', options, function (error, doc) { + backend.fetch(null, 'books', '1984', options, function(error, doc) { if (error) return done(error); expect(doc.m).to.be.ok(); done(); @@ -70,9 +70,9 @@ describe('Backend', function () { }); }); - describe('subscribe', function () { - it('subscribes to the document', function (done) { - backend.subscribe(null, 'books', '1984', null, function (error, stream, snapshot) { + describe('subscribe', function() { + it('subscribes to the document', function(done) { + backend.subscribe(null, 'books', '1984', null, function(error, stream, snapshot) { if (error) return done(error); expect(stream.open).to.be(true); expect(snapshot.data).to.eql({ @@ -80,21 +80,21 @@ describe('Backend', function () { author: 'George Orwell' }); var op = {op: {p: ['publication'], oi: 1949}}; - stream.on('data', function (data) { + stream.on('data', function(data) { expect(data.op).to.eql(op.op); done(); }); - backend.submit(null, 'books', '1984', op, null, function (error) { + backend.submit(null, 'books', '1984', op, null, function(error) { if (error) return done(error); }); }); }); - it('does not support subscribing to the document with options', function (done) { + it('does not support subscribing to the document with options', function(done) { var options = { - opsOptions: { metadata: true } + opsOptions: {metadata: true} }; - backend.subscribe(null, 'books', '1984', null, options, function (error) { + backend.subscribe(null, 'books', '1984', null, options, function(error) { expect(error.code).to.be(4025); done(); }); diff --git a/test/client/connection.js b/test/client/connection.js index aa00e2db8..b38ff53fa 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -3,7 +3,6 @@ var Backend = require('../../lib/backend'); var Connection = require('../../lib/client/connection'); describe('client connection', function() { - beforeEach(function() { this.backend = new Backend(); }); @@ -42,8 +41,8 @@ describe('client connection', function() { request.agent.close(); next(); }); - var connection = this.backend.connect(); - }) + this.backend.connect(); + }); it('emits stopped event on call to agent.close()', function(done) { this.backend.use('connect', function(request, next) { @@ -105,7 +104,7 @@ describe('client connection', function() { var backend = this.backend; var connection = backend.connect(); connection.on('connected', function() { - connection.socket.stream.emit('close') + connection.socket.stream.emit('close'); expect(backend.agentsCount).equal(0); done(); }); @@ -115,8 +114,8 @@ describe('client connection', function() { var backend = this.backend; var connection = backend.connect(); connection.on('connected', function() { - connection.socket.stream.emit('end') - connection.socket.stream.emit('close') + connection.socket.stream.emit('end'); + connection.socket.stream.emit('close'); expect(backend.agentsCount).equal(0); done(); }); @@ -128,39 +127,38 @@ describe('client connection', function() { next({message: 'Error'}); }); expect(backend.agentsCount).equal(0); - var connection = backend.connect(); + backend.connect(); expect(backend.agentsCount).equal(0); }); }); describe('state management using setSocket', function() { - - it('initial connection.state is connecting, if socket.readyState is CONNECTING', function () { - // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting - var socket = {readyState: 0}; - var connection = new Connection(socket); - expect(connection.state).equal('connecting'); + it('initial connection.state is connecting, if socket.readyState is CONNECTING', function() { + // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting + var socket = {readyState: 0}; + var connection = new Connection(socket); + expect(connection.state).equal('connecting'); }); - it('initial connection.state is connecting, if socket.readyState is OPEN', function () { - // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open - var socket = {readyState: 1}; - var connection = new Connection(socket); - expect(connection.state).equal('connecting'); + it('initial connection.state is connecting, if socket.readyState is OPEN', function() { + // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open + var socket = {readyState: 1}; + var connection = new Connection(socket); + expect(connection.state).equal('connecting'); }); - it('initial connection.state is disconnected, if socket.readyState is CLOSING', function () { - // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing - var socket = {readyState: 2}; - var connection = new Connection(socket); - expect(connection.state).equal('disconnected'); + it('initial connection.state is disconnected, if socket.readyState is CLOSING', function() { + // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing + var socket = {readyState: 2}; + var connection = new Connection(socket); + expect(connection.state).equal('disconnected'); }); - it('initial connection.state is disconnected, if socket.readyState is CLOSED', function () { - // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed - var socket = {readyState: 3}; - var connection = new Connection(socket); - expect(connection.state).equal('disconnected'); + it('initial connection.state is disconnected, if socket.readyState is CLOSED', function() { + // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed + var socket = {readyState: 3}; + var connection = new Connection(socket); + expect(connection.state).equal('disconnected'); }); it('initial state is connecting', function() { @@ -198,7 +196,5 @@ describe('client connection', function() { done(); }); }); - }); - }); diff --git a/test/client/doc.js b/test/client/doc.js index 738c3055a..9b5316e3f 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,9 +1,8 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var util = require('../util') +var util = require('../util'); describe('Doc', function() { - beforeEach(function() { this.backend = new Backend(); this.connection = this.backend.connect(); @@ -51,7 +50,6 @@ describe('Doc', function() { }); describe('applyStack', function() { - beforeEach(function(done) { this.doc = this.connection.get('dogs', 'fido'); this.doc2 = this.backend.connect().get('dogs', 'fido'); @@ -221,24 +219,23 @@ describe('Doc', function() { verifyConsistency(doc, doc2, doc3, handlers, done); }); }); - }); describe('submitting ops in callbacks', function() { - beforeEach(function () { + beforeEach(function() { this.doc = this.connection.get('dogs', 'scooby'); }); it('succeeds with valid op', function(done) { var doc = this.doc; - doc.create({ name: 'Scooby Doo' }, function(error) { + doc.create({name: 'Scooby Doo'}, function(error) { expect(error).to.not.be.ok(); // Build valid op that deletes a substring at index 0 of name. - var textOpComponents = [{ p: 0, d: 'Scooby '}]; - var op = [{ p: ['name'], t: 'text0', o: textOpComponents }]; + var textOpComponents = [{p: 0, d: 'Scooby '}]; + var op = [{p: ['name'], t: 'text0', o: textOpComponents}]; doc.submitOp(op, function(error) { if (error) return done(error); - expect(doc.data).eql({ name: 'Doo' }); + expect(doc.data).eql({name: 'Doo'}); done(); }); }); @@ -246,11 +243,11 @@ describe('Doc', function() { it('fails with invalid op', function(done) { var doc = this.doc; - doc.create({ name: 'Scooby Doo' }, function(error) { + doc.create({name: 'Scooby Doo'}, function(error) { expect(error).to.not.be.ok(); // Build op that tries to delete an invalid substring at index 0 of name. - var textOpComponents = [{ p: 0, d: 'invalid'}]; - var op = [{ p: ['name'], t: 'text0', o: textOpComponents }]; + var textOpComponents = [{p: 0, d: 'invalid'}]; + var op = [{p: ['name'], t: 'text0', o: textOpComponents}]; doc.submitOp(op, function(error) { expect(error).to.be.ok(); done(); @@ -259,48 +256,48 @@ describe('Doc', function() { }); }); - describe('submitting an invalid op', function () { + describe('submitting an invalid op', function() { var doc; var invalidOp; var validOp; - beforeEach(function (done) { + beforeEach(function(done) { // This op is invalid because we try to perform a list deletion // on something that isn't a list invalidOp = {p: ['name'], ld: 'Scooby'}; - validOp = {p:['snacks'], oi: true}; + validOp = {p: ['snacks'], oi: true}; doc = this.connection.get('dogs', 'scooby'); - doc.create({ name: 'Scooby' }, function (error) { + doc.create({name: 'Scooby'}, function(error) { if (error) return done(error); doc.whenNothingPending(done); }); }); - it('returns an error to the submitOp callback', function (done) { - doc.submitOp(invalidOp, function (error) { + it('returns an error to the submitOp callback', function(done) { + doc.submitOp(invalidOp, function(error) { expect(error.message).to.equal('Referenced element not a list'); done(); }); }); - it('rolls the doc back to a usable state', function (done) { + it('rolls the doc back to a usable state', function(done) { util.callInSeries([ - function (next) { - doc.submitOp(invalidOp, function (error) { + function(next) { + doc.submitOp(invalidOp, function(error) { expect(error).to.be.ok(); next(); }); }, - function (next) { + function(next) { doc.whenNothingPending(next); }, - function (next) { + function(next) { expect(doc.data).to.eql({name: 'Scooby'}); doc.submitOp(validOp, next); }, - function (next) { + function(next) { expect(doc.data).to.eql({name: 'Scooby', snacks: true}); next(); }, @@ -308,7 +305,7 @@ describe('Doc', function() { ]); }); - it('rescues an irreversible op collision', function (done) { + it('rescues an irreversible op collision', function(done) { // This test case attempts to reconstruct the following corner case, with // two independent references to the same document. We submit two simultaneous, but // incompatible operations (eg one of them changes the data structure the other op is @@ -321,9 +318,9 @@ describe('Doc', function() { var pauseSubmit = false; var fireSubmit; - this.backend.use('submit', function (request, callback) { + this.backend.use('submit', function(request, callback) { if (pauseSubmit) { - fireSubmit = function () { + fireSubmit = function() { pauseSubmit = false; callback(); }; @@ -334,26 +331,26 @@ describe('Doc', function() { }); util.callInSeries([ - function (next) { + function(next) { doc1.create({colours: ['white']}, next); }, - function (next) { + function(next) { doc1.whenNothingPending(next); }, - function (next) { + function(next) { doc2.fetch(next); }, - function (next) { + function(next) { doc2.whenNothingPending(next); }, // Both documents start off at the same v1 state, with colours as a list - function (next) { + function(next) { expect(doc1.data).to.eql({colours: ['white']}); expect(doc2.data).to.eql({colours: ['white']}); next(); }, // doc1 successfully submits an op which changes our list into a string in v2 - function (next) { + function(next) { doc1.submitOp({p: ['colours'], oi: 'white,black'}, next); }, // This next step is a little fiddly. We abuse the middleware to pause the op submission and @@ -365,21 +362,21 @@ describe('Doc', function() { // 5. doc2 attempts to roll back the inflight op by turning a list insertion into a list deletion // 6. doc2 applies this list deletion to a field that is no longer a list // 7. type.apply throws, because this is an invalid op - function (next) { + function(next) { pauseSubmit = true; - doc2.submitOp({p: ['colours', '0'], li: 'black'}, function (error) { + doc2.submitOp({p: ['colours', '0'], li: 'black'}, function(error) { expect(error.message).to.equal('Referenced element not a list'); next(); }); - doc2.fetch(function (error) { + doc2.fetch(function(error) { if (error) return next(error); fireSubmit(); }); }, // Validate that - despite the error in doc2.submitOp - doc2 has been returned to a // workable state in v2 - function (next) { + function(next) { expect(doc1.data).to.eql({colours: 'white,black'}); expect(doc2.data).to.eql(doc1.data); doc2.submitOp({p: ['colours'], oi: 'white,black,red'}, next); diff --git a/test/client/pending.js b/test/client/pending.js index 4896440d4..2b22c7a08 100644 --- a/test/client/pending.js +++ b/test/client/pending.js @@ -2,7 +2,6 @@ var expect = require('expect.js'); var Backend = require('../../lib/backend'); describe('client connection', function() { - beforeEach(function() { this.backend = new Backend(); }); @@ -89,5 +88,4 @@ describe('client connection', function() { }); }); }); - }); diff --git a/test/client/projections.js b/test/client/projections.js index fb1c993e5..471cbe909 100644 --- a/test/client/projections.js +++ b/test/client/projections.js @@ -1,9 +1,10 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var util = require('../util'); module.exports = function() { describe('client projections', function() { - beforeEach(function(done) { this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true}); this.connection = this.backend.connect(); @@ -56,7 +57,12 @@ describe('client projections', function() { it('parent field replace', function(done) { test.call(this, - {p: [], oi: {age: 2, color: 'brown', owner: false}, od: {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}}, + { + p: [], + oi: {age: 2, color: 'brown', owner: false}, + od: {age: 3, color: 'gold', owner: {name: 'jim'}, + litter: {count: 4}} + }, {age: 2, owner: false}, done ); @@ -324,6 +330,5 @@ describe('client projections', function() { }); }); }); - }); }; diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 108fe0de5..d7c685ce7 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); @@ -31,8 +33,12 @@ describe('client query subscribe', function() { var connection = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -55,8 +61,12 @@ describe('client query subscribe', function() { var connection = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -80,8 +90,12 @@ describe('client query subscribe', function() { var connection2 = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -102,8 +116,12 @@ describe('client query subscribe', function() { var connection2 = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -123,8 +141,12 @@ describe('client query subscribe', function() { var connection2 = backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -147,8 +169,12 @@ describe('client query subscribe', function() { var connection2 = backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -188,8 +214,12 @@ describe('client query subscribe', function() { var connection = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { @@ -360,8 +390,12 @@ describe('client query subscribe', function() { it('changing a filtered property removes from a subscribed query', function(done) { var connection = this.backend.connect(); async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 3}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 3}, cb); + } ], function(err) { if (err) return done(err); var dbQuery = getQuery({query: {age: 3}}); @@ -387,8 +421,12 @@ describe('client query subscribe', function() { it('changing a filtered property inserts to a subscribed query', function(done) { var connection = this.backend.connect(); async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var dbQuery = getQuery({query: {age: 3}}); @@ -415,8 +453,12 @@ describe('client query subscribe', function() { var connection = this.backend.connect(); async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } ], function(err) { if (err) return done(err); var dbQuery = getQuery({query: {}, sort: [['age', 1]]}); @@ -441,6 +483,5 @@ describe('client query subscribe', function() { }); }); }); - }); }; diff --git a/test/client/query.js b/test/client/query.js index 0b7912cb3..423cdd2a3 100644 --- a/test/client/query.js +++ b/test/client/query.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); @@ -25,9 +27,15 @@ describe('client query', function() { var connection = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); connection[method]('dogs', matchAllDbQuery, null, function(err, results) { @@ -45,9 +53,15 @@ describe('client query', function() { var connection2 = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); connection2[method]('dogs', matchAllDbQuery, null, function(err, results) { @@ -65,9 +79,15 @@ describe('client query', function() { var connection2 = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); connection2.get('dogs', 'fido').fetch(function(err) { @@ -88,9 +108,15 @@ describe('client query', function() { var connection2 = this.backend.connect(); var matchAllDbQuery = this.matchAllDbQuery; async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); connection2.get('dogs', 'fido').fetch(function(err) { @@ -116,8 +142,6 @@ describe('client query', function() { }); }); }); - }); - }); }; diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 9c2aeaad2..0aeae9f9d 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -6,7 +6,7 @@ var MemoryDb = require('../../lib/db/memory'); var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); -describe('SnapshotTimestampRequest', function () { +describe('SnapshotTimestampRequest', function() { var backend; var clock; var day0 = new Date(2017, 11, 31).getTime(); @@ -17,17 +17,17 @@ describe('SnapshotTimestampRequest', function () { var day5 = new Date(2018, 0, 5).getTime(); var ONE_DAY = 1000 * 60 * 60 * 24; - beforeEach(function () { - clock = lolex.install({ now: day1 }); + beforeEach(function() { + clock = lolex.install({now: day1}); backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { clock.uninstall(); backend.close(done); }); - describe('a document with some simple versions separated by a day', function () { + describe('a document with some simple versions separated by a day', function() { var v0 = { id: 'time-machine', v: 0, @@ -68,30 +68,30 @@ describe('SnapshotTimestampRequest', function () { m: null }; - beforeEach(function (done) { + beforeEach(function(done) { var doc = backend.connect().get('books', 'time-machine'); util.callInSeries([ - function (next) { - doc.create({ title: 'The Time Machine' }, next); + function(next) { + doc.create({title: 'The Time Machine'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], oi: 'HG Wells' }, next); + doc.submitOp({p: ['author'], oi: 'HG Wells'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], od: 'HG Wells', oi: 'H.G. Wells' }, next); + doc.submitOp({p: ['author'], od: 'HG Wells', oi: 'H.G. Wells'}, next); }, done ]); }); - it('fetches the version at exactly day 1', function (done) { + it('fetches the version at exactly day 1', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v1); next(); }, @@ -99,12 +99,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the version at exactly day 2', function (done) { + it('fetches the version at exactly day 2', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v2); next(); }, @@ -112,12 +112,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the version at exactly day 3', function (done) { + it('fetches the version at exactly day 3', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -125,13 +125,13 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function (done) { + it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function(done) { var halfwayBetweenDays2and3 = (day2 + day3) * 0.5; util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', halfwayBetweenDays2and3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v2); next(); }, @@ -139,12 +139,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the day 3 version when asking for a time after day 3', function (done) { + it('fetches the day 3 version when asking for a time after day 3', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -152,12 +152,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the most recent version when not specifying a timestamp', function (done) { + it('fetches the most recent version when not specifying a timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -165,12 +165,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches an empty snapshot if the timestamp is before the document creation', function (done) { + it('fetches an empty snapshot if the timestamp is before the document creation', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day0, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v0); next(); }, @@ -178,40 +178,40 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('throws if the timestamp is undefined', function () { - var fetch = function () { - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function () {}); + it('throws if the timestamp is undefined', function() { + var fetch = function() { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function() {}); }; expect(fetch).to.throwError(); }); - it('throws without a callback', function () { - var fetch = function () { + it('throws without a callback', function() { + var fetch = function() { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine'); }; expect(fetch).to.throwError(); }); - it('throws if the timestamp is -1', function () { - var fetch = function () { - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function () { }); + it('throws if the timestamp is -1', function() { + var fetch = function() { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function() { }); }; expect(fetch).to.throwError(); }); - it('errors if the timestamp is a string', function () { - var fetch = function () { - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function () { }); - } + it('errors if the timestamp is a string', function() { + var fetch = function() { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function() { }); + }; expect(fetch).to.throwError(); }); - it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { - backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function (error, snapshot) { + it('returns an empty snapshot if trying to fetch a non-existent document', function(done) { + backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'does-not-exist', @@ -224,10 +224,10 @@ describe('SnapshotTimestampRequest', function () { }); }); - it('starts pending, and finishes not pending', function (done) { + it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function (error, snapshot) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error, snapshot) { expect(connection.hasPending()).to.be(false); done(); }); @@ -235,10 +235,10 @@ describe('SnapshotTimestampRequest', function () { expect(connection.hasPending()).to.be(true); }); - it('deletes the request from the connection', function (done) { + it('deletes the request from the connection', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); expect(connection._snapshotRequests).to.eql({}); done(); @@ -247,10 +247,10 @@ describe('SnapshotTimestampRequest', function () { expect(connection._snapshotRequests).to.not.eql({}); }); - it('emits a ready event when done', function (done) { + it('emits a ready event when done', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); }); @@ -258,22 +258,22 @@ describe('SnapshotTimestampRequest', function () { snapshotRequest.on('ready', done); }); - it('fires the connection.whenNothingPending', function (done) { + it('fires the connection.whenNothingPending', function(done) { var connection = backend.connect(); var snapshotFetched = false; - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); snapshotFetched = true; }); - connection.whenNothingPending(function () { + connection.whenNothingPending(function() { expect(snapshotFetched).to.be(true); done(); }); }); - it('can drop its connection and reconnect, and the callback is just called once', function (done) { + it('can drop its connection and reconnect, and the callback is just called once', function(done) { var connection = backend.connect(); // Here we hook into middleware to make sure that we get the following flow: @@ -286,7 +286,7 @@ describe('SnapshotTimestampRequest', function () { // - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag) // - The done callback is called just once (if it's called twice, then mocha will complain) var connectionInterrupted = false; - backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!connectionInterrupted) { connection.close(); backend.connect(connection); @@ -299,7 +299,7 @@ describe('SnapshotTimestampRequest', function () { connection.fetchSnapshotByTimestamp('books', 'time-machine', done); }); - it('cannot send the same request twice over a connection', function (done) { + it('cannot send the same request twice over a connection', function(done) { var connection = backend.connect(); // Here we hook into the middleware to make sure that we get the following flow: @@ -310,7 +310,7 @@ describe('SnapshotTimestampRequest', function () { // - The done callback is call just once, because the second request does not get sent // (if the done callback is called twice, then mocha will complain) var hasResent = false; - backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!hasResent) { connection._snapshotRequests[1]._onConnectionStateChanged(); hasResent = true; @@ -322,55 +322,55 @@ describe('SnapshotTimestampRequest', function () { connection.fetchSnapshotByTimestamp('books', 'time-machine', done); }); - describe('readSnapshots middleware', function () { - it('triggers the middleware', function (done) { + describe('readSnapshots middleware', function() { + it('triggers the middleware', function(done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, - function (request) { + function(request) { expect(request.snapshots[0]).to.eql(v3); expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp); done(); } ); - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function () { }); + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function() { }); }); - it('can have its snapshot manipulated in the middleware', function (done) { + it('can have its snapshot manipulated in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { + function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); }, ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function (error, snapshot) { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Alice in Wonderland'); done(); }); }); - it('respects errors thrown in the middleware', function (done) { + it('respects errors thrown in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { - callback({ message: 'foo' }); + function(request, callback) { + callback({message: 'foo'}); }, ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function (error, snapshot) { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { expect(error.message).to.be('foo'); done(); }); }); }); - describe('with a registered projection', function () { - beforeEach(function () { - backend.addProjection('bookTitles', 'books', { title: true }); + describe('with a registered projection', function() { + beforeEach(function() { + backend.addProjection('bookTitles', 'books', {title: true}); }); - it('applies the projection to a snapshot', function (done) { - backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function (error, snapshot) { + it('applies the projection to a snapshot', function(done) { + backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function(error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('The Time Machine'); @@ -381,13 +381,13 @@ describe('SnapshotTimestampRequest', function () { }); }); - describe('milestone snapshots enabled for every other version', function () { + describe('milestone snapshots enabled for every other version', function() { var milestoneDb; var db; var backendWithMilestones; - beforeEach(function () { - var options = { interval: 2 }; + beforeEach(function() { + var options = {interval: 2}; db = new MemoryDb(); milestoneDb = new MemoryMilestoneDb(options); backendWithMilestones = new Backend({ @@ -396,41 +396,41 @@ describe('SnapshotTimestampRequest', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { backendWithMilestones.close(done); }); - describe('a doc with some versions in the milestone database', function () { - beforeEach(function (done) { + describe('a doc with some versions in the milestone database', function() { + beforeEach(function(done) { clock.reset(); var doc = backendWithMilestones.connect().get('books', 'mocking-bird'); util.callInSeries([ - function (next) { - doc.create({ title: 'To Kill a Mocking Bird' }, next); + function(next) { + doc.create({title: 'To Kill a Mocking Bird'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, next); + doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, next); + doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['year'], oi: 1959 }, next); + doc.submitOp({p: ['year'], oi: 1959}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['year'], od: 1959, oi: 1960 }, next); + doc.submitOp({p: ['year'], od: 1959, oi: 1960}, next); }, done ]); }); - it('fetches a snapshot between two milestones using the milestones', function (done) { + it('fetches a snapshot between two milestones using the milestones', function(done) { sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime'); sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime'); sinon.spy(db, 'getOps'); @@ -445,35 +445,35 @@ describe('SnapshotTimestampRequest', function () { expect(db.getOps.calledWith('books', 'mocking-bird', 2, 4)).to.be(true); expect(snapshot.v).to.be(3); - expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lee' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'}); done(); }); }); - it('fetches a snapshot that matches a milestone snapshot', function (done) { + it('fetches a snapshot that matches a milestone snapshot', function(done) { sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime'); sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime'); backendWithMilestones.connect() - .fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function (error, snapshot) { + .fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function(error, snapshot) { if (error) return done(error); expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true); expect(snapshot.v).to.be(2); - expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lea' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'}); done(); }); }); - it('fetches a snapshot before any milestones', function (done) { + it('fetches a snapshot before any milestones', function(done) { sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime'); sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime'); sinon.spy(db, 'getOps'); backendWithMilestones.connect() - .fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function (error, snapshot) { + .fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function(error, snapshot) { if (error) return done(error); expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); @@ -481,18 +481,18 @@ describe('SnapshotTimestampRequest', function () { expect(db.getOps.calledWith('books', 'mocking-bird', 0, 2)).to.be(true); expect(snapshot.v).to.be(1); - expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'}); done(); }); }); - it('fetches a snapshot after any milestones', function (done) { + it('fetches a snapshot after any milestones', function(done) { sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime'); sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime'); sinon.spy(db, 'getOps'); backendWithMilestones.connect() - .fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function (error, snapshot) { + .fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function(error, snapshot) { if (error) return done(error); expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index 4b7101e56..2f959be07 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -5,18 +5,18 @@ var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); var util = require('../util'); -describe('SnapshotVersionRequest', function () { +describe('SnapshotVersionRequest', function() { var backend; - beforeEach(function () { + beforeEach(function() { backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { backend.close(done); }); - describe('a document with some simple versions', function () { + describe('a document with some simple versions', function() { var v0 = { id: 'don-quixote', v: 0, @@ -57,99 +57,99 @@ describe('SnapshotVersionRequest', function () { m: null }; - beforeEach(function (done) { + beforeEach(function(done) { var doc = backend.connect().get('books', 'don-quixote'); - doc.create({ title: 'Don Quixote' }, function (error) { + doc.create({title: 'Don Quixote'}, function(error) { if (error) return done(error); - doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (error) { + doc.submitOp({p: ['author'], oi: 'Miguel de Cervante'}, function(error) { if (error) return done(error); - doc.submitOp({ p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes' }, done); + doc.submitOp({p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes'}, done); }); }); }); - it('fetches v1', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 1, function (error, snapshot) { + it('fetches v1', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', 1, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v1); done(); }); }); - it('fetches v2', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 2, function (error, snapshot) { + it('fetches v2', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', 2, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); }); - it('fetches v3', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 3, function (error, snapshot) { + it('fetches v3', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', 3, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v3); done(); }); }); - it('returns an empty snapshot if the version is 0', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + it('returns an empty snapshot if the version is 0', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v0); done(); }); }); - it('throws if the version is undefined', function () { - var fetch = function () { - backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function () {}); + it('throws if the version is undefined', function() { + var fetch = function() { + backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {}); }; expect(fetch).to.throwError(); }); - it('fetches the latest version when the optional version is not provided', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) { + it('fetches the latest version when the optional version is not provided', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v3); done(); }); }); - it('throws without a callback', function () { - var fetch = function () { + it('throws without a callback', function() { + var fetch = function() { backend.connect().fetchSnapshot('books', 'don-quixote'); }; expect(fetch).to.throwError(); }); - it('throws if the version is -1', function () { - var fetch = function () { - backend.connect().fetchSnapshot('books', 'don-quixote', -1, function () {}); + it('throws if the version is -1', function() { + var fetch = function() { + backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {}); }; expect(fetch).to.throwError(); }); - it('errors if the version is a string', function () { - var fetch = function () { - backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function () { }); - } + it('errors if the version is a string', function() { + var fetch = function() { + backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { }); + }; expect(fetch).to.throwError(); }); - it('errors if asking for a version that does not exist', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 4, function (error, snapshot) { + it('errors if asking for a version that does not exist', function(done) { + backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) { expect(error.code).to.be(4024); expect(snapshot).to.be(undefined); done(); }); }); - it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { - backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { + it('returns an empty snapshot if trying to fetch a non-existent document', function(done) { + backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'does-not-exist', @@ -162,10 +162,10 @@ describe('SnapshotVersionRequest', function () { }); }); - it('starts pending, and finishes not pending', function (done) { + it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) { + connection.fetchSnapshot('books', 'don-quixote', null, function(error, snapshot) { expect(connection.hasPending()).to.be(false); done(); }); @@ -173,10 +173,10 @@ describe('SnapshotVersionRequest', function () { expect(connection.hasPending()).to.be(true); }); - it('deletes the request from the connection', function (done) { + it('deletes the request from the connection', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); expect(connection._snapshotRequests).to.eql({}); done(); @@ -185,10 +185,10 @@ describe('SnapshotVersionRequest', function () { expect(connection._snapshotRequests).to.not.eql({}); }); - it('emits a ready event when done', function (done) { + it('emits a ready event when done', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); }); @@ -196,22 +196,22 @@ describe('SnapshotVersionRequest', function () { snapshotRequest.on('ready', done); }); - it('fires the connection.whenNothingPending', function (done) { + it('fires the connection.whenNothingPending', function(done) { var connection = backend.connect(); var snapshotFetched = false; - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); snapshotFetched = true; }); - connection.whenNothingPending(function () { + connection.whenNothingPending(function() { expect(snapshotFetched).to.be(true); done(); }); }); - it('can drop its connection and reconnect, and the callback is just called once', function (done) { + it('can drop its connection and reconnect, and the callback is just called once', function(done) { var connection = backend.connect(); // Here we hook into middleware to make sure that we get the following flow: @@ -224,7 +224,7 @@ describe('SnapshotVersionRequest', function () { // - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag) // - The done callback is called just once (if it's called twice, then mocha will complain) var connectionInterrupted = false; - backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!connectionInterrupted) { connection.close(); backend.connect(connection); @@ -237,7 +237,7 @@ describe('SnapshotVersionRequest', function () { connection.fetchSnapshot('books', 'don-quixote', done); }); - it('cannot send the same request twice over a connection', function (done) { + it('cannot send the same request twice over a connection', function(done) { var connection = backend.connect(); // Here we hook into the middleware to make sure that we get the following flow: @@ -248,7 +248,7 @@ describe('SnapshotVersionRequest', function () { // - The done callback is call just once, because the second request does not get sent // (if the done callback is called twice, then mocha will complain) var hasResent = false; - backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!hasResent) { connection._snapshotRequests[1]._onConnectionStateChanged(); hasResent = true; @@ -260,55 +260,55 @@ describe('SnapshotVersionRequest', function () { connection.fetchSnapshot('books', 'don-quixote', done); }); - describe('readSnapshots middleware', function () { - it('triggers the middleware', function (done) { + describe('readSnapshots middleware', function() { + it('triggers the middleware', function(done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, - function (request) { + function(request) { expect(request.snapshots[0]).to.eql(v3); expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion); done(); } ); - backend.connect().fetchSnapshot('books', 'don-quixote', 3, function () { }); + backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { }); }); - it('can have its snapshot manipulated in the middleware', function (done) { + it('can have its snapshot manipulated in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { + function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); }, ]; - backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Alice in Wonderland'); done(); }); }); - it('respects errors thrown in the middleware', function (done) { + it('respects errors thrown in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { - callback({ message: 'foo' }); + function(request, callback) { + callback({message: 'foo'}); }, ]; - backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { expect(error.message).to.be('foo'); done(); }); }); }); - describe('with a registered projection', function () { - beforeEach(function () { - backend.addProjection('bookTitles', 'books', { title: true }); + describe('with a registered projection', function() { + beforeEach(function() { + backend.addProjection('bookTitles', 'books', {title: true}); }); - it('applies the projection to a snapshot', function (done) { - backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { + it('applies the projection to a snapshot', function(done) { + backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Don Quixote'); @@ -319,19 +319,19 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('a document that is currently deleted', function () { - beforeEach(function (done) { + describe('a document that is currently deleted', function() { + beforeEach(function(done) { var doc = backend.connect().get('books', 'catch-22'); - doc.create({ title: 'Catch 22' }, function (error) { + doc.create({title: 'Catch 22'}, function(error) { if (error) return done(error); - doc.del(function (error) { + doc.del(function(error) { done(error); }); }); }); - it('returns a null type', function (done) { - backend.connect().fetchSnapshot('books', 'catch-22', null, function (error, snapshot) { + it('returns a null type', function(done) { + backend.connect().fetchSnapshot('books', 'catch-22', null, function(error, snapshot) { expect(snapshot).to.eql({ id: 'catch-22', v: 2, @@ -344,8 +344,8 @@ describe('SnapshotVersionRequest', function () { }); }); - it('fetches v1', function (done) { - backend.connect().fetchSnapshot('books', 'catch-22', 1, function (error, snapshot) { + it('fetches v1', function(done) { + backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ @@ -363,22 +363,22 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('a document that was deleted and then created again', function () { - beforeEach(function (done) { + describe('a document that was deleted and then created again', function() { + beforeEach(function(done) { var doc = backend.connect().get('books', 'hitchhikers-guide'); - doc.create({ title: 'Hitchhiker\'s Guide to the Galaxy' }, function (error) { + doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) { if (error) return done(error); - doc.del(function (error) { + doc.del(function(error) { if (error) return done(error); - doc.create({ title: 'The Restaurant at the End of the Universe' }, function (error) { + doc.create({title: 'The Restaurant at the End of the Universe'}, function(error) { done(error); }); }); }); }); - it('fetches the latest version of the document', function (done) { - backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) { + it('fetches the latest version of the document', function(done) { + backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ @@ -396,13 +396,13 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('milestone snapshots enabled for every other version', function () { + describe('milestone snapshots enabled for every other version', function() { var milestoneDb; var db; var backendWithMilestones; - beforeEach(function () { - var options = { interval: 2 }; + beforeEach(function() { + var options = {interval: 2}; db = new MemoryDb(); milestoneDb = new MemoryMilestoneDb(options); backendWithMilestones = new Backend({ @@ -411,33 +411,33 @@ describe('SnapshotVersionRequest', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { backendWithMilestones.close(done); }); - it('fetches a snapshot using the milestone', function (done) { + it('fetches a snapshot using the milestone', function(done) { var doc = backendWithMilestones.connect().get('books', 'mocking-bird'); util.callInSeries([ - function (next) { - doc.create({ title: 'To Kill a Mocking Bird' }, next); + function(next) { + doc.create({title: 'To Kill a Mocking Bird'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, next); + function(next) { + doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, next); + function(next) { + doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next); }, - function (next) { + function(next) { sinon.spy(milestoneDb, 'getMilestoneSnapshot'); sinon.spy(db, 'getOps'); backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true); expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.be(true); expect(snapshot.v).to.be(3); - expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lee' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'}); next(); }, done diff --git a/test/client/submit.js b/test/client/submit.js index 82cecbbe3..546f4ec9e 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var async = require('async'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -9,7 +11,6 @@ types.register(numberType.type); module.exports = function() { describe('client submit', function() { - it('can fetch an uncreated doc', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); expect(doc.data).equal(undefined); @@ -527,7 +528,7 @@ describe('client submit', function() { // other fetches the snapshot to apply to). By storing the callbacks, we can then // manually trigger the callbacks, first calling doc, and when we know that's been committed, // we then commit doc2. - backend.use('commit', function (request, callback) { + backend.use('commit', function(request, callback) { if (request.op.op[0].na === 2) docCallback = callback; if (request.op.op[0].na === 7) doc2Callback = callback; @@ -538,14 +539,14 @@ describe('client submit', function() { docCallback(); } }); - doc.submitOp({p: ['age'], na: 2}, function (error) { + doc.submitOp({p: ['age'], na: 2}, function(error) { if (error) return done(error); // When we know the first op has been committed, we try to commit the second op, which will // fail because it's working on an out-of-date snapshot. It will retry, but exceed the // maxSubmitRetries limit of 0 doc2Callback(); }); - doc2.submitOp({p: ['age'], na: 7}, function (error) { + doc2.submitOp({p: ['age'], na: 7}, function(error) { expect(error).ok(); done(); }); @@ -563,8 +564,12 @@ describe('client submit', function() { doc2.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc.del(cb); }, - function(cb) { doc.create({age: 5}, cb); } + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } ], function(err) { if (err) return done(err); expect(doc.version).equal(4); @@ -586,8 +591,12 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc.del(cb); }, - function(cb) { doc.create({age: 5}, cb); } + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } ], function(err) { if (err) return done(err); expect(doc.version).equal(4); @@ -722,8 +731,12 @@ describe('client submit', function() { expect(doc2.version).equal(3); async.parallel([ - function(cb) { doc.fetch(cb); }, - function(cb) { doc2.fetch(cb); } + function(cb) { + doc.fetch(cb); + }, + function(cb) { + doc2.fetch(cb); + } ], function(err) { if (err) return done(err); expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'}); @@ -1067,7 +1080,7 @@ describe('client submit', function() { it('allows snapshot and op to be a non-object', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); - doc.create(5, numberType.type.uri, function (err) { + doc.create(5, numberType.type.uri, function(err) { if (err) return done(err); expect(doc.data).to.equal(5); doc.submitOp(2, function(err) { @@ -1105,7 +1118,6 @@ describe('client submit', function() { it('deserializes on fetch', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); - var backend = this.backend; doc.create([3], deserializedType.type.uri, function(err) { if (err) return done(err); doc2.fetch(function(err) { @@ -1132,7 +1144,6 @@ describe('client submit', function() { it('server fetches and transforms by already committed op', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); - var backend = this.backend; doc.create([3], deserializedType.type.uri, function(err) { if (err) return done(err); doc2.fetch(function(err) { @@ -1171,6 +1182,5 @@ describe('client submit', function() { }); }); }); - }); }; diff --git a/test/client/subscribe.js b/test/client/subscribe.js index b24a94749..ed45b1b90 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -1,9 +1,10 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); module.exports = function() { describe('client subscribe', function() { - it('can call bulk without doing any actions', function() { var connection = this.backend.connect(); connection.startBulk(); @@ -31,8 +32,12 @@ describe('client subscribe', function() { doc.create({age: 3}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc2[method](cb); }, - function(cb) { doc2[method](cb); } + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } ], function(err) { if (err) return done(err); expect(doc2.version).eql(1); @@ -49,8 +54,12 @@ describe('client subscribe', function() { if (err) return done(err); doc2.connection.startBulk(); async.parallel([ - function(cb) { doc2[method](cb); }, - function(cb) { doc2[method](cb); } + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } ], function(err) { if (err) return done(err); expect(doc2.version).eql(1); @@ -65,9 +74,15 @@ describe('client subscribe', function() { var connection = this.backend.connect(); var connection2 = this.backend.connect(); async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); var fido = connection2.get('dogs', 'fido'); @@ -75,9 +90,15 @@ describe('client subscribe', function() { var finn = connection2.get('cats', 'finn'); connection2.startBulk(); async.parallel([ - function(cb) { fido[method](cb); }, - function(cb) { spot[method](cb); }, - function(cb) { finn[method](cb); } + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } ], function(err) { if (err) return done(err); expect(fido.data).eql({age: 3}); @@ -97,9 +118,15 @@ describe('client subscribe', function() { var finn = connection2.get('cats', 'finn'); connection2.startBulk(); async.parallel([ - function(cb) { fido[method](cb); }, - function(cb) { spot[method](cb); }, - function(cb) { finn[method](cb); } + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } ], function(err) { if (err) return done(err); expect(fido.version).equal(0); @@ -110,16 +137,28 @@ describe('client subscribe', function() { expect(finn.data).equal(undefined); async.parallel([ - function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); }, - function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }, - function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); } + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } ], function(err) { if (err) return done(err); connection2.startBulk(); async.parallel([ - function(cb) { fido[method](cb); }, - function(cb) { spot[method](cb); }, - function(cb) { finn[method](cb); } + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } ], function(err) { if (err) return done(err); expect(fido.data).eql({age: 3}); @@ -129,24 +168,42 @@ describe('client subscribe', function() { // Test sending a fetch without any new ops being created connection2.startBulk(); async.parallel([ - function(cb) { fido[method](cb); }, - function(cb) { spot[method](cb); }, - function(cb) { finn[method](cb); } + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } ], function(err) { if (err) return done(err); // Create new ops and test if they are received async.parallel([ - function(cb) { connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); }, - function(cb) { connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); }, - function(cb) { connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); } + function(cb) { + connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); + }, + function(cb) { + connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); + }, + function(cb) { + connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); + } ], function(err) { if (err) return done(err); connection2.startBulk(); async.parallel([ - function(cb) { fido[method](cb); }, - function(cb) { spot[method](cb); }, - function(cb) { finn[method](cb); } + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } ], function(err) { if (err) return done(err); expect(fido.data).eql({age: 4}); @@ -302,7 +359,6 @@ describe('client subscribe', function() { }); it('unsubscribe calls back immediately on disconnect', function(done) { - var backend = this.backend; var doc = this.backend.connect().get('dogs', 'fido'); doc.subscribe(function(err) { if (err) return done(err); @@ -312,7 +368,6 @@ describe('client subscribe', function() { }); it('unsubscribe calls back immediately when already disconnected', function(done) { - var backend = this.backend; var doc = this.backend.connect().get('dogs', 'fido'); doc.subscribe(function(err) { if (err) return done(err); @@ -445,14 +500,22 @@ describe('client subscribe', function() { doc.create({age: 3}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { fido.subscribe(cb); }, - function(cb) { spot.subscribe(cb); } + function(cb) { + fido.subscribe(cb); + }, + function(cb) { + spot.subscribe(cb); + } ], function(err) { if (err) return done(err); fido.connection.startBulk(); async.parallel([ - function(cb) { fido.unsubscribe(cb); }, - function(cb) { spot.unsubscribe(cb); } + function(cb) { + fido.unsubscribe(cb); + }, + function(cb) { + spot.unsubscribe(cb); + } ], function(err) { if (err) return done(err); fido.on('op', function(op, context) { diff --git a/test/db.js b/test/db.js index 257763173..270a2ea92 100644 --- a/test/db.js +++ b/test/db.js @@ -790,9 +790,9 @@ module.exports = function(options) { // sorts by foo first, then bar var snapshots = [ {type: 'json0', id: '0', v: 1, data: {foo: 1, bar: 1}, m: null}, - {type: 'json0', id: '1', v: 1, data: { foo: 2, bar: 1 }, m: null}, - {type: 'json0', id: '2', v: 1, data: { foo: 1, bar: 2 }, m: null}, - {type: 'json0', id: '3', v: 1, data: { foo: 2, bar: 2 }, m: null} + {type: 'json0', id: '1', v: 1, data: {foo: 2, bar: 1}, m: null}, + {type: 'json0', id: '2', v: 1, data: {foo: 1, bar: 2}, m: null}, + {type: 'json0', id: '3', v: 1, data: {foo: 2, bar: 2}, m: null} ]; var db = this.db; var dbQuery = getQuery({query: {}, sort: [['foo', 1], ['bar', -1]]}); diff --git a/test/logger.js b/test/logger.js index aba680eb7..4efbb0cfa 100644 --- a/test/logger.js +++ b/test/logger.js @@ -2,23 +2,23 @@ var Logger = require('../lib/logger/logger'); var expect = require('expect.js'); var sinon = require('sinon'); -describe('Logger', function () { - describe('Stubbing console.warn', function () { - beforeEach(function () { +describe('Logger', function() { + describe('Stubbing console.warn', function() { + beforeEach(function() { sinon.stub(console, 'warn'); }); - afterEach(function () { + afterEach(function() { sinon.restore(); }); - it('logs to console by default', function () { + it('logs to console by default', function() { var logger = new Logger(); logger.warn('warning'); expect(console.warn.calledOnceWithExactly('warning')).to.be(true); }); - it('overrides console', function () { + it('overrides console', function() { var customWarn = sinon.stub(); var logger = new Logger(); logger.setMethods({ @@ -31,7 +31,7 @@ describe('Logger', function () { expect(customWarn.calledOnceWithExactly('warning')).to.be(true); }); - it('only overrides if provided with a method', function () { + it('only overrides if provided with a method', function() { var badWarn = 'not a function'; var logger = new Logger(); logger.setMethods({ diff --git a/test/middleware.js b/test/middleware.js index 4af42fbc6..f71ddce5d 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -1,11 +1,9 @@ -var async = require('async'); var Backend = require('../lib/backend'); var expect = require('expect.js'); var util = require('./util'); var types = require('../lib/types'); describe('middleware', function() { - beforeEach(function() { this.backend = new Backend(); }); @@ -22,16 +20,13 @@ describe('middleware', function() { } describe('use', function() { - it('returns itself to allow chaining', function() { var response = this.backend.use('submit', function(request, next) {}); expect(response).equal(this.backend); }); - }); describe('connect', function() { - it('passes the agent on connect', function(done) { var clientId; this.backend.use('connect', function(request, next) { @@ -57,7 +52,6 @@ describe('middleware', function() { done(); }); }); - }); function testReadDoc(expectFidoOnly, expectFidoAndSpot) { @@ -136,7 +130,6 @@ describe('middleware', function() { function expectFidoAndSpot(backend, done) { var doneAfter = util.callAfter(2, done); - var i = 0; backend.use('doc', function(request, next) { doneAfter(); if (doneAfter.called === 1) { @@ -305,5 +298,4 @@ describe('middleware', function() { }); }); }); - }); diff --git a/test/milestone-db.js b/test/milestone-db.js index 1354eb4ee..223423d47 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -5,22 +5,22 @@ var NoOpMilestoneDB = require('../lib/milestone-db/no-op'); var Snapshot = require('../lib/snapshot'); var util = require('./util'); -describe('Base class', function () { +describe('Base class', function() { var db; - beforeEach(function () { + beforeEach(function() { db = new MilestoneDB(); }); - it('calls back with an error when trying to get a snapshot', function (done) { - db.getMilestoneSnapshot('books', '123', 1, function (error) { + it('calls back with an error when trying to get a snapshot', function(done) { + db.getMilestoneSnapshot('books', '123', 1, function(error) { expect(error.code).to.be(5019); done(); }); }); - it('emits an error when trying to get a snapshot', function (done) { - db.on('error', function (error) { + it('emits an error when trying to get a snapshot', function(done) { + db.on('error', function(error) { expect(error.code).to.be(5019); done(); }); @@ -28,15 +28,15 @@ describe('Base class', function () { db.getMilestoneSnapshot('books', '123', 1); }); - it('calls back with an error when trying to save a snapshot', function (done) { - db.saveMilestoneSnapshot('books', {}, function (error) { + it('calls back with an error when trying to save a snapshot', function(done) { + db.saveMilestoneSnapshot('books', {}, function(error) { expect(error.code).to.be(5020); done(); }); }); - it('emits an error when trying to save a snapshot', function (done) { - db.on('error', function (error) { + it('emits an error when trying to save a snapshot', function(done) { + db.on('error', function(error) { expect(error.code).to.be(5020); done(); }); @@ -44,45 +44,45 @@ describe('Base class', function () { db.saveMilestoneSnapshot('books', {}); }); - it('calls back with an error when trying to get a snapshot before a time', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function (error) { + it('calls back with an error when trying to get a snapshot before a time', function(done) { + db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) { expect(error.code).to.be(5021); done(); }); }); - it('calls back with an error when trying to get a snapshot after a time', function (done) { - db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function (error) { + it('calls back with an error when trying to get a snapshot after a time', function(done) { + db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function(error) { expect(error.code).to.be(5022); done(); }); }); }); -describe('NoOpMilestoneDB', function () { +describe('NoOpMilestoneDB', function() { var db; - beforeEach(function () { + beforeEach(function() { db = new NoOpMilestoneDB(); }); - it('does not error when trying to save and fetch a snapshot', function (done) { + it('does not error when trying to save and fetch a snapshot', function(done) { var snapshot = new Snapshot( 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -90,8 +90,8 @@ describe('NoOpMilestoneDB', function () { ]); }); - it('emits an event when saving without a callback', function (done) { - db.on('save', function () { + it('emits an event when saving without a callback', function(done) { + db.on('save', function() { done(); }); @@ -99,51 +99,51 @@ describe('NoOpMilestoneDB', function () { }); }); -module.exports = function (options) { +module.exports = function(options) { var create = options.create; - describe('Milestone Database', function () { + describe('Milestone Database', function() { var db; var backend; - beforeEach(function (done) { - create(function (error, createdDb) { + beforeEach(function(done) { + create(function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - afterEach(function (done) { + afterEach(function(done) { db.close(done); }); - it('can call close() without a callback', function (done) { - create(function (error, db) { + it('can call close() without a callback', function(done) { + create(function(error, db) { if (error) return done(error); db.close(); done(); }); }); - it('stores and fetches a milestone snapshot', function (done) { + it('stores and fetches a milestone snapshot', function(done) { var snapshot = new Snapshot( 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (retrievedSnapshot, next) { + function(retrievedSnapshot, next) { expect(retrievedSnapshot).to.eql(snapshot); next(); }, @@ -151,12 +151,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot before the requested version', function (done) { + it('fetches the most recent snapshot before the requested version', function(done) { var snapshot1 = new Snapshot( 'catcher-in-the-rye', 1, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); @@ -164,7 +164,7 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); @@ -172,24 +172,24 @@ module.exports = function (options) { 'catcher-in-the-rye', 10, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot10, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -197,12 +197,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot even if they are inserted in the wrong order', function (done) { + it('fetches the most recent snapshot even if they are inserted in the wrong order', function(done) { var snapshot1 = new Snapshot( 'catcher-in-the-rye', 1, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); @@ -210,21 +210,21 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -232,12 +232,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot when the version is null', function (done) { + it('fetches the most recent snapshot when the version is null', function(done) { var snapshot1 = new Snapshot( 'catcher-in-the-rye', 1, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); @@ -245,21 +245,21 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -267,62 +267,62 @@ module.exports = function (options) { ]); }); - it('errors when fetching an undefined version', function (done) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function (error) { + it('errors when fetching an undefined version', function(done) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors when fetching version -1', function (done) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function (error) { + it('errors when fetching version -1', function(done) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors when fetching version "foo"', function (done) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function (error) { + it('errors when fetching version "foo"', function(done) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors when fetching a null collection', function (done) { - db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function (error) { + it('errors when fetching a null collection', function(done) { + db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors when fetching a null ID', function (done) { - db.getMilestoneSnapshot('books', null, 1, function (error) { + it('errors when fetching a null ID', function(done) { + db.getMilestoneSnapshot('books', null, 1, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors when saving a null collection', function (done) { + it('errors when saving a null collection', function(done) { var snapshot = new Snapshot( 'catcher-in-the-rye', 1, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); - db.saveMilestoneSnapshot(null, snapshot, function (error) { + db.saveMilestoneSnapshot(null, snapshot, function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns undefined if no snapshot exists', function (done) { + it('returns undefined if no snapshot exists', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -330,16 +330,16 @@ module.exports = function (options) { ]); }); - it('does not store a milestone snapshot on commit', function (done) { + it('does not store a milestone snapshot on commit', function(done) { util.callInSeries([ - function (next) { + function(next) { var doc = backend.connect().get('books', 'catcher-in-the-rye'); - doc.create({ title: 'Catcher in the Rye' }, next); + doc.create({title: 'Catcher in the Rye'}, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -347,16 +347,16 @@ module.exports = function (options) { ]); }); - it('can save without a callback', function (done) { + it('can save without a callback', function(done) { var snapshot = new Snapshot( 'catcher-in-the-rye', 1, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, + {title: 'Catcher in the Rye'}, null ); - db.on('save', function (collection, snapshot) { + db.on('save', function(collection, snapshot) { expect(collection).to.be('books'); expect(snapshot).to.eql(snapshot); done(); @@ -365,14 +365,14 @@ module.exports = function (options) { db.saveMilestoneSnapshot('books', snapshot); }); - it('errors when the snapshot is undefined', function (done) { - db.saveMilestoneSnapshot('books', undefined, function (error) { + it('errors when the snapshot is undefined', function(done) { + db.saveMilestoneSnapshot('books', undefined, function(error) { expect(error).to.be.ok(); done(); }); }); - describe('snapshots with timestamps', function () { + describe('snapshots with timestamps', function() { var snapshot1 = new Snapshot( 'catcher-in-the-rye', 1, @@ -414,28 +414,28 @@ module.exports = function (options) { } ); - beforeEach(function (done) { + beforeEach(function(done) { util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot3, next); }, done ]); }); - describe('fetching a snapshot before or at a time', function () { - it('fetches a snapshot before a given time', function (done) { + describe('fetching a snapshot before or at a time', function() { + it('fetches a snapshot before a given time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2500, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -443,12 +443,12 @@ module.exports = function (options) { ]); }); - it('fetches a snapshot at an exact time', function (done) { + it('fetches a snapshot at an exact time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -456,12 +456,12 @@ module.exports = function (options) { ]); }); - it('fetches the first snapshot for a null timestamp', function (done) { + it('fetches the first snapshot for a null timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot1); next(); }, @@ -469,26 +469,26 @@ module.exports = function (options) { ]); }); - it('returns an error for a string timestamp', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function (error) { + it('returns an error for a string timestamp', function(done) { + db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns an error for a negative timestamp', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', -1, function (error) { + it('returns an error for a negative timestamp', function(done) { + db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', -1, function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns undefined if there are no snapshots before a time', function (done) { + it('returns undefined if there are no snapshots before a time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -496,28 +496,28 @@ module.exports = function (options) { ]); }); - it('errors if no collection is provided', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function (error) { + it('errors if no collection is provided', function(done) { + db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors if no ID is provided', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function (error) { + it('errors if no ID is provided', function(done) { + db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function(error) { expect(error).to.be.ok(); done(); }); }); }); - describe('fetching a snapshot after or at a time', function () { - it('fetches a snapshot after a given time', function (done) { + describe('fetching a snapshot after or at a time', function() { + it('fetches a snapshot after a given time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2500, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot3); next(); }, @@ -525,12 +525,12 @@ module.exports = function (options) { ]); }); - it('fetches a snapshot at an exact time', function (done) { + it('fetches a snapshot at an exact time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -538,12 +538,12 @@ module.exports = function (options) { ]); }); - it('fetches the last snapshot for a null timestamp', function (done) { + it('fetches the last snapshot for a null timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot3); next(); }, @@ -551,26 +551,26 @@ module.exports = function (options) { ]); }); - it('returns an error for a string timestamp', function (done) { - db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function (error) { + it('returns an error for a string timestamp', function(done) { + db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns an error for a negative timestamp', function (done) { - db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', -1, function (error) { + it('returns an error for a negative timestamp', function(done) { + db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', -1, function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns undefined if there are no snapshots after a time', function (done) { + it('returns undefined if there are no snapshots after a time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -578,15 +578,15 @@ module.exports = function (options) { ]); }); - it('errors if no collection is provided', function (done) { - db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function (error) { + it('errors if no collection is provided', function(done) { + db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function(error) { expect(error).to.be.ok(); done(); }); }); - it('errors if no ID is provided', function (done) { - db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function (error) { + it('errors if no ID is provided', function(done) { + db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) { expect(error).to.be.ok(); done(); }); @@ -594,72 +594,72 @@ module.exports = function (options) { }); }); - describe('milestones enabled for every version', function () { - beforeEach(function (done) { - var options = { interval: 1 }; + describe('milestones enabled for every version', function() { + beforeEach(function(done) { + var options = {interval: 1}; - create(options, function (error, createdDb) { + create(options, function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - it('stores a milestone snapshot on commit', function (done) { - db.on('save', function (collection, snapshot) { + it('stores a milestone snapshot on commit', function(done) { + db.on('save', function(collection, snapshot) { expect(collection).to.be('books'); - expect(snapshot.data).to.eql({ title: 'Catcher in the Rye' }); + expect(snapshot.data).to.eql({title: 'Catcher in the Rye'}); done(); }); var doc = backend.connect().get('books', 'catcher-in-the-rye'); - doc.create({ title: 'Catcher in the Rye' }); + doc.create({title: 'Catcher in the Rye'}); }); }); - describe('milestones enabled for every other version', function () { - beforeEach(function (done) { - var options = { interval: 2 }; + describe('milestones enabled for every other version', function() { + beforeEach(function(done) { + var options = {interval: 2}; - create(options, function (error, createdDb) { + create(options, function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - it('only stores even-numbered versions', function (done) { - db.on('save', function (collection, snapshot) { + it('only stores even-numbered versions', function(done) { + db.on('save', function(collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(2); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(2); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(4); next(); }, @@ -670,56 +670,56 @@ module.exports = function (options) { var doc = backend.connect().get('books', 'catcher-in-the-rye'); util.callInSeries([ - function (next) { - doc.create({ title: 'Catcher in the Rye' }, next); + function(next) { + doc.create({title: 'Catcher in the Rye'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next); } ]); }); - it('can have the saving logic overridden in middleware', function (done) { - backend.use('commit', function (request, callback) { + it('can have the saving logic overridden in middleware', function(done) { + backend.use('commit', function(request, callback) { request.saveMilestoneSnapshot = request.snapshot.v >= 3; callback(); }); - db.on('save', function (collection, snapshot) { + db.on('save', function(collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(3); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(4); next(); }, @@ -730,17 +730,17 @@ module.exports = function (options) { var doc = backend.connect().get('books', 'catcher-in-the-rye'); util.callInSeries([ - function (next) { - doc.create({ title: 'Catcher in the Rye' }, next); + function(next) { + doc.create({title: 'Catcher in the Rye'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger' }, next); + function(next) { + doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next); } ]); }); diff --git a/test/ot.js b/test/ot.js index 3f663bac4..b58b9c899 100644 --- a/test/ot.js +++ b/test/ot.js @@ -3,7 +3,6 @@ var ot = require('../lib/ot'); var type = require('../lib/types').defaultType; describe('ot', function() { - describe('checkOp', function() { it('fails if op is not an object', function() { expect(ot.checkOp('hi')).ok(); @@ -28,7 +27,7 @@ describe('ot', function() { }); it('fails if the type is missing', function() { - expect(ot.checkOp({create:{type: 'something that does not exist'}})).ok(); + expect(ot.checkOp({create: {type: 'something that does not exist'}})).ok(); }); it('accepts valid create operations', function() { @@ -37,11 +36,11 @@ describe('ot', function() { }); it('accepts valid delete operations', function() { - expect(ot.checkOp({del:true})).equal(); + expect(ot.checkOp({del: true})).equal(); }); it('accepts valid ops', function() { - expect(ot.checkOp({op:[1,2,3]})).equal(); + expect(ot.checkOp({op: [1, 2, 3]})).equal(); }); }); @@ -106,11 +105,11 @@ describe('ot', function() { describe('op', function() { it('fails if the document does not exist', function() { - expect(ot.apply({v: 6}, {v: 6, op: [1,2,3]})).ok(); + expect(ot.apply({v: 6}, {v: 6, op: [1, 2, 3]})).ok(); }); it('fails if the type is missing', function() { - expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1,2,3]})).ok(); + expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1, 2, 3]})).ok(); }); it('applies the operation to the document data', function() { @@ -238,5 +237,4 @@ describe('ot', function() { expect(op).eql({}); }); }); - }); diff --git a/test/projections.js b/test/projections.js index 3d6f35687..52dc72618 100644 --- a/test/projections.js +++ b/test/projections.js @@ -3,7 +3,6 @@ var projections = require('../lib/projections'); var type = require('../lib/types').defaultType.uri; describe('projection utility methods', function() { - describe('projectSnapshot', function() { function test(fields, snapshot, expected) { projections.projectSnapshot(fields, snapshot); @@ -91,8 +90,8 @@ describe('projection utility methods', function() { ); test( {x: true}, - {type: type, data: {x: [1,2,3]}}, - {type: type, data: {x: [1,2,3]}} + {type: type, data: {x: [1, 2, 3]}}, + {type: type, data: {x: [1, 2, 3]}} ); test( {x: true}, @@ -186,23 +185,23 @@ describe('projection utility methods', function() { it('filters root ops', function() { test( {}, - {op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]}, + {op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]}, {op: [{p: [], od: {}, oi: {}}]} ); test( {x: true}, - {op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]}, + {op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]}, {op: [{p: [], od: {x: 2}, oi: {x: 3}}]} ); test( {x: true}, - {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]}, + {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]}, {op: [{p: [], od: {x: 2}, oi: {}}]} ); test( - {x: true, a:true, z:true}, - {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]}, - {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]} + {x: true, a: true, z: true}, + {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]}, + {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]} ); test( {x: true}, @@ -212,7 +211,7 @@ describe('projection utility methods', function() { // If you make the document something other than an object, it just looks like null. test( {x: true}, - {op: [{p: [], od: {a:2, x: 5}, oi: []}]}, + {op: [{p: [], od: {a: 2, x: 5}, oi: []}]}, {op: [{p: [], od: {x: 5}, oi: null}]} ); }); @@ -359,7 +358,7 @@ describe('projection utility methods', function() { {}, {del: true} ); - expect(projections.isOpAllowed(null, {}, {del:true})).equal(true); + expect(projections.isOpAllowed(null, {}, {del: true})).equal(true); }); it('works with ops', function() { diff --git a/test/pubsub.js b/test/pubsub.js index 48f8abb6b..4bf6cbf1a 100644 --- a/test/pubsub.js +++ b/test/pubsub.js @@ -49,7 +49,6 @@ module.exports = function(create) { pubsub.subscribe('y', function(err, stream) { pubsub.subscribe('y', function(err, stream) { if (err) done(err); - var emitted; stream.on('data', function(data) { expect(data).eql({test: true}); done(); diff --git a/test/util.js b/test/util.js index 5f982ed6e..7eb8aef77 100644 --- a/test/util.js +++ b/test/util.js @@ -52,7 +52,7 @@ exports.callInSeries = function(callbacks, args) { var callback = callbacks.shift(); if (callbacks.length) { - args.push(function () { + args.push(function() { var args = Array.from(arguments); exports.callInSeries(callbacks, args); }); From 8a056199ca8be1345eec9acf686f46e5888067e9 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 4 Jul 2019 15:11:09 +0100 Subject: [PATCH 90/95] Fix indentation linting Large indentation fixes were skipped in the previous commit in order to keep the diff a bit less noisy. This change purely removes the eslint ignore comments, and fixes the indentation in those files. --- test/client/projections.js | 532 +++++----- test/client/query-subscribe.js | 786 +++++++-------- test/client/query.js | 210 ++-- test/client/submit.js | 1738 ++++++++++++++++---------------- test/client/subscribe.js | 942 +++++++++-------- 5 files changed, 2099 insertions(+), 2109 deletions(-) diff --git a/test/client/projections.js b/test/client/projections.js index 471cbe909..77359563e 100644 --- a/test/client/projections.js +++ b/test/client/projections.js @@ -1,334 +1,332 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); var util = require('../util'); module.exports = function() { -describe('client projections', function() { - beforeEach(function(done) { - this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true}); - this.connection = this.backend.connect(); - var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}; - this.connection.get('dogs', 'fido').create(data, done); - }); + describe('client projections', function() { + beforeEach(function(done) { + this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true}); + this.connection = this.backend.connect(); + var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}; + this.connection.get('dogs', 'fido').create(data, done); + }); - ['fetch', 'subscribe'].forEach(function(method) { - it('snapshot ' + method, function(done) { - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs_summary', 'fido'); - fido[method](function(err) { - if (err) return done(err); - expect(fido.data).eql({age: 3, owner: {name: 'jim'}}); - expect(fido.version).eql(1); - done(); + ['fetch', 'subscribe'].forEach(function(method) { + it('snapshot ' + method, function(done) { + var connection2 = this.backend.connect(); + var fido = connection2.get('dogs_summary', 'fido'); + fido[method](function(err) { + if (err) return done(err); + expect(fido.data).eql({age: 3, owner: {name: 'jim'}}); + expect(fido.version).eql(1); + done(); + }); }); }); - }); - ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) { - it('snapshot ' + method, function(done) { - var connection2 = this.backend.connect(); - connection2[method]('dogs_summary', {}, null, function(err, results) { - if (err) return done(err); - expect(results.length).eql(1); - expect(results[0].data).eql({age: 3, owner: {name: 'jim'}}); - expect(results[0].version).eql(1); - done(); + ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) { + it('snapshot ' + method, function(done) { + var connection2 = this.backend.connect(); + connection2[method]('dogs_summary', {}, null, function(err, results) { + if (err) return done(err); + expect(results.length).eql(1); + expect(results[0].data).eql({age: 3, owner: {name: 'jim'}}); + expect(results[0].version).eql(1); + done(); + }); }); }); - }); - function opTests(test) { - it('projected field', function(done) { - test.call(this, - {p: ['age'], na: 1}, - {age: 4, owner: {name: 'jim'}}, - done - ); - }); + function opTests(test) { + it('projected field', function(done) { + test.call(this, + {p: ['age'], na: 1}, + {age: 4, owner: {name: 'jim'}}, + done + ); + }); - it('non-projected field', function(done) { - test.call(this, - {p: ['color'], oi: 'brown', od: 'gold'}, - {age: 3, owner: {name: 'jim'}}, - done - ); - }); + it('non-projected field', function(done) { + test.call(this, + {p: ['color'], oi: 'brown', od: 'gold'}, + {age: 3, owner: {name: 'jim'}}, + done + ); + }); - it('parent field replace', function(done) { - test.call(this, - { - p: [], - oi: {age: 2, color: 'brown', owner: false}, - od: {age: 3, color: 'gold', owner: {name: 'jim'}, - litter: {count: 4}} - }, - {age: 2, owner: false}, - done - ); - }); + it('parent field replace', function(done) { + test.call(this, + { + p: [], + oi: {age: 2, color: 'brown', owner: false}, + od: {age: 3, color: 'gold', owner: {name: 'jim'}, + litter: {count: 4}} + }, + {age: 2, owner: false}, + done + ); + }); - it('parent field set', function(done) { - test.call(this, - {p: [], oi: {age: 2, color: 'brown', owner: false}}, - {age: 2, owner: false}, - done - ); - }); + it('parent field set', function(done) { + test.call(this, + {p: [], oi: {age: 2, color: 'brown', owner: false}}, + {age: 2, owner: false}, + done + ); + }); - it('projected child field', function(done) { - test.call(this, - {p: ['owner', 'sex'], oi: 'male'}, - {age: 3, owner: {name: 'jim', sex: 'male'}}, - done - ); - }); + it('projected child field', function(done) { + test.call(this, + {p: ['owner', 'sex'], oi: 'male'}, + {age: 3, owner: {name: 'jim', sex: 'male'}}, + done + ); + }); - it('non-projected child field', function(done) { - test.call(this, - {p: ['litter', 'count'], na: 1}, - {age: 3, owner: {name: 'jim'}}, - done - ); - }); - } + it('non-projected child field', function(done) { + test.call(this, + {p: ['litter', 'count'], na: 1}, + {age: 3, owner: {name: 'jim'}}, + done + ); + }); + } - describe('op fetch', function() { - function test(op, expected, done) { - var connection = this.connection; - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs_summary', 'fido'); - fido.fetch(function(err) { - if (err) return done(err); - connection.get('dogs', 'fido').submitOp(op, function(err) { + describe('op fetch', function() { + function test(op, expected, done) { + var connection = this.connection; + var connection2 = this.backend.connect(); + var fido = connection2.get('dogs_summary', 'fido'); + fido.fetch(function(err) { if (err) return done(err); - fido.fetch(function(err) { + connection.get('dogs', 'fido').submitOp(op, function(err) { if (err) return done(err); + fido.fetch(function(err) { + if (err) return done(err); + expect(fido.data).eql(expected); + expect(fido.version).eql(2); + done(); + }); + }); + }); + }; + opTests(test); + }); + + describe('op subscribe', function() { + function test(op, expected, done) { + var connection = this.connection; + var connection2 = this.backend.connect(); + var fido = connection2.get('dogs_summary', 'fido'); + fido.subscribe(function(err) { + if (err) return done(err); + fido.on('op', function() { expect(fido.data).eql(expected); expect(fido.version).eql(2); done(); }); + connection.get('dogs', 'fido').submitOp(op); }); - }); - }; - opTests(test); - }); + }; + opTests(test); + }); - describe('op subscribe', function() { - function test(op, expected, done) { - var connection = this.connection; - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs_summary', 'fido'); - fido.subscribe(function(err) { - if (err) return done(err); - fido.on('op', function() { - expect(fido.data).eql(expected); - expect(fido.version).eql(2); - done(); + describe('op fetch query', function() { + function test(op, expected, done) { + var connection = this.connection; + var connection2 = this.backend.connect(); + var fido = connection2.get('dogs_summary', 'fido'); + fido.fetch(function(err) { + if (err) return done(err); + connection.get('dogs', 'fido').submitOp(op, function(err) { + if (err) return done(err); + connection2.createFetchQuery('dogs_summary', {}, null, function(err) { + if (err) return done(err); + expect(fido.data).eql(expected); + expect(fido.version).eql(2); + done(); + }); + }); }); - connection.get('dogs', 'fido').submitOp(op); - }); - }; - opTests(test); - }); + }; + opTests(test); + }); - describe('op fetch query', function() { - function test(op, expected, done) { - var connection = this.connection; - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs_summary', 'fido'); - fido.fetch(function(err) { - if (err) return done(err); - connection.get('dogs', 'fido').submitOp(op, function(err) { + describe('op subscribe query', function() { + function test(op, expected, done) { + var connection = this.connection; + var connection2 = this.backend.connect(); + var fido = connection2.get('dogs_summary', 'fido'); + connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { if (err) return done(err); - connection2.createFetchQuery('dogs_summary', {}, null, function(err) { - if (err) return done(err); + fido.on('op', function() { expect(fido.data).eql(expected); expect(fido.version).eql(2); done(); }); + connection.get('dogs', 'fido').submitOp(op); }); + }; + opTests(test); + }); + + function queryUpdateTests(test) { + it('doc create', function(done) { + test.call(this, + function(connection, callback) { + var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}}; + connection.get('dogs', 'spot').create(data, callback); + }, + function(err, results) { + var sorted = util.sortById(results.slice()); + expect(sorted.length).eql(2); + expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); + expect(util.pluck(sorted, 'data')).eql([ + {age: 3, owner: {name: 'jim'}}, + {age: 5, owner: {name: 'sue'}} + ]); + done(); + } + ); }); - }; - opTests(test); - }); + } - describe('op subscribe query', function() { - function test(op, expected, done) { - var connection = this.connection; - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs_summary', 'fido'); - connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { - if (err) return done(err); - fido.on('op', function() { - expect(fido.data).eql(expected); - expect(fido.version).eql(2); - done(); + describe('subscribe query', function() { + function test(trigger, callback) { + var connection = this.connection; + var connection2 = this.backend.connect(); + var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { + if (err) return callback(err); + query.on('insert', function() { + callback(null, query.results); + }); + trigger(connection); }); - connection.get('dogs', 'fido').submitOp(op); - }); - }; - opTests(test); - }); + } + queryUpdateTests(test); + }); - function queryUpdateTests(test) { - it('doc create', function(done) { - test.call(this, - function(connection, callback) { - var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}}; - connection.get('dogs', 'spot').create(data, callback); - }, - function(err, results) { - var sorted = util.sortById(results.slice()); - expect(sorted.length).eql(2); - expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); - expect(util.pluck(sorted, 'data')).eql([ - {age: 3, owner: {name: 'jim'}}, - {age: 5, owner: {name: 'sue'}} - ]); - done(); - } - ); + describe('fetch query', function() { + function test(trigger, callback) { + var connection = this.connection; + var connection2 = this.backend.connect(); + trigger(connection, function(err) { + if (err) return callback(err); + connection2.createFetchQuery('dogs_summary', {}, null, callback); + }); + } + queryUpdateTests(test); }); - } - describe('subscribe query', function() { - function test(trigger, callback) { - var connection = this.connection; - var connection2 = this.backend.connect(); - var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) { - if (err) return callback(err); - query.on('insert', function() { - callback(null, query.results); + describe('submit on projected doc', function() { + function test(op, expected, done) { + var doc = this.connection.get('dogs', 'fido'); + var projected = this.backend.connect().get('dogs_summary', 'fido'); + projected.fetch(function(err) { + if (err) return done(err); + projected.submitOp(op, function(err) { + if (err) return done(err); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql(expected); + expect(doc.version).equal(2); + done(); + }); + }); }); - trigger(connection); + } + function testError(op, done) { + var doc = this.connection.get('dogs', 'fido'); + var projected = this.backend.connect().get('dogs_summary', 'fido'); + projected.fetch(function(err) { + if (err) return done(err); + projected.submitOp(op, function(err) { + expect(err).ok(); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}); + expect(doc.version).equal(1); + done(); + }); + }); + }); + } + + it('can set on projected field', function(done) { + test.call(this, + {p: ['age'], na: 1}, + {age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}, + done + ); }); - } - queryUpdateTests(test); - }); - describe('fetch query', function() { - function test(trigger, callback) { - var connection = this.connection; - var connection2 = this.backend.connect(); - trigger(connection, function(err) { - if (err) return callback(err); - connection2.createFetchQuery('dogs_summary', {}, null, callback); + it('can set on child of projected field', function(done) { + test.call(this, + {p: ['owner', 'sex'], oi: 'male'}, + {age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}}, + done + ); + }); + + it('cannot set on non-projected field', function(done) { + testError.call(this, + {p: ['color'], od: 'gold', oi: 'tan'}, + done + ); + }); + + it('cannot set on root path of projected doc', function(done) { + testError.call(this, + {p: [], oi: null}, + done + ); }); - } - queryUpdateTests(test); - }); - describe('submit on projected doc', function() { - function test(op, expected, done) { - var doc = this.connection.get('dogs', 'fido'); - var projected = this.backend.connect().get('dogs_summary', 'fido'); - projected.fetch(function(err) { - if (err) return done(err); - projected.submitOp(op, function(err) { + it('can delete on projected doc', function(done) { + var doc = this.connection.get('dogs', 'fido'); + var projected = this.backend.connect().get('dogs_summary', 'fido'); + projected.fetch(function(err) { if (err) return done(err); - doc.fetch(function(err) { + projected.del(function(err) { if (err) return done(err); - expect(doc.data).eql(expected); - expect(doc.version).equal(2); - done(); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql(undefined); + expect(doc.version).equal(2); + done(); + }); }); }); }); - } - function testError(op, done) { - var doc = this.connection.get('dogs', 'fido'); - var projected = this.backend.connect().get('dogs_summary', 'fido'); - projected.fetch(function(err) { - if (err) return done(err); - projected.submitOp(op, function(err) { - expect(err).ok(); + + it('can create a projected doc with only projected fields', function(done) { + var doc = this.connection.get('dogs', 'spot'); + var projected = this.backend.connect().get('dogs_summary', 'spot'); + var data = {age: 5}; + projected.create(data, function(err) { + if (err) return done(err); doc.fetch(function(err) { if (err) return done(err); - expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}); + expect(doc.data).eql({age: 5}); expect(doc.version).equal(1); done(); }); }); }); - } - - it('can set on projected field', function(done) { - test.call(this, - {p: ['age'], na: 1}, - {age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}, - done - ); - }); - - it('can set on child of projected field', function(done) { - test.call(this, - {p: ['owner', 'sex'], oi: 'male'}, - {age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}}, - done - ); - }); - - it('cannot set on non-projected field', function(done) { - testError.call(this, - {p: ['color'], od: 'gold', oi: 'tan'}, - done - ); - }); - - it('cannot set on root path of projected doc', function(done) { - testError.call(this, - {p: [], oi: null}, - done - ); - }); - it('can delete on projected doc', function(done) { - var doc = this.connection.get('dogs', 'fido'); - var projected = this.backend.connect().get('dogs_summary', 'fido'); - projected.fetch(function(err) { - if (err) return done(err); - projected.del(function(err) { - if (err) return done(err); + it('cannot create a projected doc with non-projected fields', function(done) { + var doc = this.connection.get('dogs', 'spot'); + var projected = this.backend.connect().get('dogs_summary', 'spot'); + var data = {age: 5, foo: 'bar'}; + projected.create(data, function(err) { + expect(err).ok(); doc.fetch(function(err) { if (err) return done(err); expect(doc.data).eql(undefined); - expect(doc.version).equal(2); + expect(doc.version).equal(0); done(); }); }); }); }); - - it('can create a projected doc with only projected fields', function(done) { - var doc = this.connection.get('dogs', 'spot'); - var projected = this.backend.connect().get('dogs_summary', 'spot'); - var data = {age: 5}; - projected.create(data, function(err) { - if (err) return done(err); - doc.fetch(function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 5}); - expect(doc.version).equal(1); - done(); - }); - }); - }); - - it('cannot create a projected doc with non-projected fields', function(done) { - var doc = this.connection.get('dogs', 'spot'); - var projected = this.backend.connect().get('dogs_summary', 'spot'); - var data = {age: 5, foo: 'bar'}; - projected.create(data, function(err) { - expect(err).ok(); - doc.fetch(function(err) { - if (err) return done(err); - expect(doc.data).eql(undefined); - expect(doc.version).equal(0); - done(); - }); - }); - }); }); -}); }; diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index d7c685ce7..ddce30d64 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -1,487 +1,485 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); module.exports = function(options) { -var getQuery = options.getQuery; + var getQuery = options.getQuery; -describe('client query subscribe', function() { - before(function() { - if (!getQuery) return this.skip(); - this.matchAllDbQuery = getQuery({query: {}}); - }); - - it('creating a document updates a subscribed query', function(done) { - var connection = this.backend.connect(); - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { - if (err) return done(err); - connection.get('dogs', 'fido').create({age: 3}); - }); - query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['fido']); - expect(util.pluck(docs, 'data')).eql([{age: 3}]); - expect(index).equal(0); - expect(util.pluck(query.results, 'id')).eql(['fido']); - expect(util.pluck(query.results, 'data')).eql([{age: 3}]); - done(); + describe('client query subscribe', function() { + before(function() { + if (!getQuery) return this.skip(); + this.matchAllDbQuery = getQuery({query: {}}); }); - }); - it('creating an additional document updates a subscribed query', function(done) { - var connection = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('creating a document updates a subscribed query', function(done) { + var connection = this.backend.connect(); + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { if (err) return done(err); - connection.get('dogs', 'taco').create({age: 2}); + connection.get('dogs', 'fido').create({age: 3}); }); query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['taco']); - expect(util.pluck(docs, 'data')).eql([{age: 2}]); - expect(query.results[index]).equal(docs[0]); - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']); - expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]); + expect(util.pluck(docs, 'id')).eql(['fido']); + expect(util.pluck(docs, 'data')).eql([{age: 3}]); + expect(index).equal(0); + expect(util.pluck(query.results, 'id')).eql(['fido']); + expect(util.pluck(query.results, 'data')).eql([{age: 3}]); done(); }); }); - }); - it('deleting a document updates a subscribed query', function(done) { - var connection = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('creating an additional document updates a subscribed query', function(done) { + var connection = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - connection.get('dogs', 'fido').del(); - }); - query.on('remove', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['fido']); - expect(util.pluck(docs, 'data')).eql([undefined]); - expect(index).a('number'); - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['spot']); - expect(util.pluck(results, 'data')).eql([{age: 5}]); - done(); + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection.get('dogs', 'taco').create({age: 2}); + }); + query.on('insert', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['taco']); + expect(util.pluck(docs, 'data')).eql([{age: 2}]); + expect(query.results[index]).equal(docs[0]); + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']); + expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]); + done(); + }); }); }); - }); - it('subscribed query does not get updated after destroyed', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('deleting a document updates a subscribed query', function(done) { + var connection = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - query.destroy(function(err) { + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { if (err) return done(err); - connection2.get('dogs', 'taco').create({age: 2}, done); + connection.get('dogs', 'fido').del(); + }); + query.on('remove', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['fido']); + expect(util.pluck(docs, 'data')).eql([undefined]); + expect(index).a('number'); + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['spot']); + expect(util.pluck(results, 'data')).eql([{age: 5}]); + done(); }); - }); - query.on('insert', function() { - done(); }); }); - }); - it('subscribed query does not get updated after connection is disconnected', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('subscribed query does not get updated after destroyed', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - connection.close(); - connection2.get('dogs', 'taco').create({age: 2}, done); - }); - query.on('insert', function() { - done(); + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + query.destroy(function(err) { + if (err) return done(err); + connection2.get('dogs', 'taco').create({age: 2}, done); + }); + }); + query.on('insert', function() { + done(); + }); }); }); - }); - it('subscribed query gets update after reconnecting', function(done) { - var backend = this.backend; - var connection = backend.connect(); - var connection2 = backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('subscribed query does not get updated after connection is disconnected', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - connection.close(); - connection2.get('dogs', 'taco').create({age: 2}); - process.nextTick(function() { - backend.connect(connection); + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection.close(); + connection2.get('dogs', 'taco').create({age: 2}, done); + }); + query.on('insert', function() { + done(); }); - }); - query.on('insert', function() { - done(); }); }); - }); - it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) { - var backend = this.backend; - var connection = backend.connect(); - var connection2 = backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('subscribed query gets update after reconnecting', function(done) { + var backend = this.backend; + var connection = backend.connect(); + var connection2 = backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - connection.close(); - connection2.get('dogs', 'fido').fetch(function(err) { + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { if (err) return done(err); - connection2.get('dogs', 'fido').del(); + connection.close(); connection2.get('dogs', 'taco').create({age: 2}); process.nextTick(function() { backend.connect(connection); }); }); - }); - var wait = 2; - function finish() { - if (--wait) return; - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['spot', 'taco']); - expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]); - done(); - } - query.on('insert', function(docs) { - expect(util.pluck(docs, 'id')).eql(['taco']); - expect(util.pluck(docs, 'data')).eql([{age: 2}]); - finish(); - }); - query.on('remove', function(docs) { - expect(util.pluck(docs, 'id')).eql(['fido']); - expect(util.pluck(docs, 'data')).eql([undefined]); - finish(); + query.on('insert', function() { + done(); + }); }); }); - }); - it('creating an additional document updates a subscribed query', function(done) { - var connection = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) { + var backend = this.backend; + var connection = backend.connect(); + var connection2 = backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { if (err) return done(err); - connection.get('dogs', 'taco').create({age: 2}); + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection.close(); + connection2.get('dogs', 'fido').fetch(function(err) { + if (err) return done(err); + connection2.get('dogs', 'fido').del(); + connection2.get('dogs', 'taco').create({age: 2}); + process.nextTick(function() { + backend.connect(connection); + }); + }); + }); + var wait = 2; + function finish() { + if (--wait) return; + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['spot', 'taco']); + expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]); + done(); + } + query.on('insert', function(docs) { + expect(util.pluck(docs, 'id')).eql(['taco']); + expect(util.pluck(docs, 'data')).eql([{age: 2}]); + finish(); + }); + query.on('remove', function(docs) { + expect(util.pluck(docs, 'id')).eql(['fido']); + expect(util.pluck(docs, 'data')).eql([undefined]); + finish(); + }); }); - query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['taco']); - expect(util.pluck(docs, 'data')).eql([{age: 2}]); - expect(query.results[index]).equal(docs[0]); - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']); - expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]); - done(); + }); + + it('creating an additional document updates a subscribed query', function(done) { + var connection = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection.get('dogs', 'taco').create({age: 2}); + }); + query.on('insert', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['taco']); + expect(util.pluck(docs, 'data')).eql([{age: 2}]); + expect(query.results[index]).equal(docs[0]); + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']); + expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]); + done(); + }); }); }); - }); - it('pollDebounce option reduces subsequent poll interval', function(done) { - var connection = this.backend.connect(); - this.backend.db.canPollDoc = function() { - return false; - }; - var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000}); - var batchSizes = []; - var total = 0; + it('pollDebounce option reduces subsequent poll interval', function(done) { + var connection = this.backend.connect(); + this.backend.db.canPollDoc = function() { + return false; + }; + var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000}); + var batchSizes = []; + var total = 0; - query.on('insert', function(docs) { - batchSizes.push(docs.length); - total += docs.length; - if (total === 1) { + query.on('insert', function(docs) { + batchSizes.push(docs.length); + total += docs.length; + if (total === 1) { // first write received by client. we're debouncing. create 9 // more documents. - for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({}); - } - if (total === 10) { + for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({}); + } + if (total === 10) { // first document is its own batch; then subsequent creates // are debounced until after all other 9 docs are created - expect(batchSizes).eql([1, 9]); - done(); - } - }); + expect(batchSizes).eql([1, 9]); + done(); + } + }); - // create an initial document. this will lead to the 'insert' - // event firing the first time, while sharedb is definitely - // debouncing - connection.get('items', '0').create({}); - }); + // create an initial document. this will lead to the 'insert' + // event firing the first time, while sharedb is definitely + // debouncing + connection.get('items', '0').create({}); + }); - it('db.pollDebounce option reduces subsequent poll interval', function(done) { - var connection = this.backend.connect(); - this.backend.db.canPollDoc = function() { - return false; - }; - this.backend.db.pollDebounce = 1000; - var query = connection.createSubscribeQuery('items', this.matchAllDbQuery); - var batchSizes = []; - var total = 0; + it('db.pollDebounce option reduces subsequent poll interval', function(done) { + var connection = this.backend.connect(); + this.backend.db.canPollDoc = function() { + return false; + }; + this.backend.db.pollDebounce = 1000; + var query = connection.createSubscribeQuery('items', this.matchAllDbQuery); + var batchSizes = []; + var total = 0; - query.on('insert', function(docs) { - batchSizes.push(docs.length); - total += docs.length; - if (total === 1) { + query.on('insert', function(docs) { + batchSizes.push(docs.length); + total += docs.length; + if (total === 1) { // first write received by client. we're debouncing. create 9 // more documents. - for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({}); - } - if (total === 10) { + for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({}); + } + if (total === 10) { // first document is its own batch; then subsequent creates // are debounced until after all other 9 docs are created - expect(batchSizes).eql([1, 9]); - done(); - } - }); - - // create an initial document. this will lead to the 'insert' - // event firing the first time, while sharedb is definitely - // debouncing - connection.get('items', '0').create({}); - }); - - it('pollInterval updates a subscribed query after an unpublished create', function(done) { - var connection = this.backend.connect(); - this.backend.suppressPublish = true; - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) { - if (err) return done(err); - connection.get('dogs', 'fido').create({}); - }); - query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['fido']); - done(); - }); - }); + expect(batchSizes).eql([1, 9]); + done(); + } + }); - it('db.pollInterval updates a subscribed query after an unpublished create', function(done) { - var connection = this.backend.connect(); - this.backend.suppressPublish = true; - this.backend.db.pollInterval = 50; - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { - if (err) return done(err); - connection.get('dogs', 'fido').create({}); + // create an initial document. this will lead to the 'insert' + // event firing the first time, while sharedb is definitely + // debouncing + connection.get('items', '0').create({}); }); - query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['fido']); - done(); - }); - }); - it('pollInterval captures additional unpublished creates', function(done) { - var connection = this.backend.connect(); - this.backend.suppressPublish = true; - var count = 0; - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) { - if (err) return done(err); - connection.get('dogs', count.toString()).create({}); - }); - query.on('insert', function() { - count++; - if (count === 3) return done(); - connection.get('dogs', count.toString()).create({}); + it('pollInterval updates a subscribed query after an unpublished create', function(done) { + var connection = this.backend.connect(); + this.backend.suppressPublish = true; + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) { + if (err) return done(err); + connection.get('dogs', 'fido').create({}); + }); + query.on('insert', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['fido']); + done(); + }); }); - }); - it('query extra is returned to client', function(done) { - var connection = this.backend.connect(); - this.backend.db.query = function(collection, query, fields, options, callback) { - process.nextTick(function() { - callback(null, [], {colors: ['brown', 'gold']}); + it('db.pollInterval updates a subscribed query after an unpublished create', function(done) { + var connection = this.backend.connect(); + this.backend.suppressPublish = true; + this.backend.db.pollInterval = 50; + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { + if (err) return done(err); + connection.get('dogs', 'fido').create({}); + }); + query.on('insert', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['fido']); + done(); }); - }; - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) { - if (err) return done(err); - expect(results).eql([]); - expect(extra).eql({colors: ['brown', 'gold']}); - expect(query.extra).eql({colors: ['brown', 'gold']}); - done(); }); - }); - it('query extra is updated on change', function(done) { - var connection = this.backend.connect(); - this.backend.db.query = function(collection, query, fields, options, callback) { - process.nextTick(function() { - callback(null, [], 1); + it('pollInterval captures additional unpublished creates', function(done) { + var connection = this.backend.connect(); + this.backend.suppressPublish = true; + var count = 0; + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) { + if (err) return done(err); + connection.get('dogs', count.toString()).create({}); }); - }; - this.backend.db.queryPoll = function(collection, query, options, callback) { - process.nextTick(function() { - callback(null, [], 2); + query.on('insert', function() { + count++; + if (count === 3) return done(); + connection.get('dogs', count.toString()).create({}); }); - }; - this.backend.db.canPollDoc = function() { - return false; - }; - var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) { - if (err) return done(err); - expect(extra).eql(1); - expect(query.extra).eql(1); - }); - query.on('extra', function(extra) { - expect(extra).eql(2); - expect(query.extra).eql(2); - done(); }); - connection.get('dogs', 'fido').create({age: 3}); - }); - it('changing a filtered property removes from a subscribed query', function(done) { - var connection = this.backend.connect(); - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 3}, cb); - } - ], function(err) { - if (err) return done(err); - var dbQuery = getQuery({query: {age: 3}}); - var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) { + it('query extra is returned to client', function(done) { + var connection = this.backend.connect(); + this.backend.db.query = function(collection, query, fields, options, callback) { + process.nextTick(function() { + callback(null, [], {colors: ['brown', 'gold']}); + }); + }; + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) { if (err) return done(err); - var sorted = util.sortById(results); - expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); - expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]); - connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2}); - }); - query.on('remove', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['fido']); - expect(util.pluck(docs, 'data')).eql([{age: 5}]); - expect(index).a('number'); - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['spot']); - expect(util.pluck(results, 'data')).eql([{age: 3}]); + expect(results).eql([]); + expect(extra).eql({colors: ['brown', 'gold']}); + expect(query.extra).eql({colors: ['brown', 'gold']}); done(); }); }); - }); - it('changing a filtered property inserts to a subscribed query', function(done) { - var connection = this.backend.connect(); - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var dbQuery = getQuery({query: {age: 3}}); - var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) { + it('query extra is updated on change', function(done) { + var connection = this.backend.connect(); + this.backend.db.query = function(collection, query, fields, options, callback) { + process.nextTick(function() { + callback(null, [], 1); + }); + }; + this.backend.db.queryPoll = function(collection, query, options, callback) { + process.nextTick(function() { + callback(null, [], 2); + }); + }; + this.backend.db.canPollDoc = function() { + return false; + }; + var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) { if (err) return done(err); - var sorted = util.sortById(results); - expect(util.pluck(sorted, 'id')).eql(['fido']); - expect(util.pluck(sorted, 'data')).eql([{age: 3}]); - connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2}); + expect(extra).eql(1); + expect(query.extra).eql(1); }); - query.on('insert', function(docs, index) { - expect(util.pluck(docs, 'id')).eql(['spot']); - expect(util.pluck(docs, 'data')).eql([{age: 3}]); - expect(index).a('number'); - var results = util.sortById(query.results); - expect(util.pluck(results, 'id')).eql(['fido', 'spot']); - expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]); + query.on('extra', function(extra) { + expect(extra).eql(2); + expect(query.extra).eql(2); done(); }); + connection.get('dogs', 'fido').create({age: 3}); }); - }); - it('changing a sorted property moves in a subscribed query', function(done) { - var connection = this.backend.connect(); + it('changing a filtered property removes from a subscribed query', function(done) { + var connection = this.backend.connect(); + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 3}, cb); + } + ], function(err) { + if (err) return done(err); + var dbQuery = getQuery({query: {age: 3}}); + var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) { + if (err) return done(err); + var sorted = util.sortById(results); + expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); + expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]); + connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2}); + }); + query.on('remove', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['fido']); + expect(util.pluck(docs, 'data')).eql([{age: 5}]); + expect(index).a('number'); + var results = util.sortById(query.results); + expect(util.pluck(results, 'id')).eql(['spot']); + expect(util.pluck(results, 'data')).eql([{age: 3}]); + done(); + }); + }); + }); - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - } - ], function(err) { - if (err) return done(err); - var dbQuery = getQuery({query: {}, sort: [['age', 1]]}); - var query = connection.createSubscribeQuery( - 'dogs', - dbQuery, - null, - function(err, results) { + it('changing a filtered property inserts to a subscribed query', function(done) { + var connection = this.backend.connect(); + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + var dbQuery = getQuery({query: {age: 3}}); + var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) { if (err) return done(err); + var sorted = util.sortById(results); + expect(util.pluck(sorted, 'id')).eql(['fido']); + expect(util.pluck(sorted, 'data')).eql([{age: 3}]); + connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2}); + }); + query.on('insert', function(docs, index) { + expect(util.pluck(docs, 'id')).eql(['spot']); + expect(util.pluck(docs, 'data')).eql([{age: 3}]); + expect(index).a('number'); + var results = util.sortById(query.results); expect(util.pluck(results, 'id')).eql(['fido', 'spot']); - expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]); - connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3}); + expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]); + done(); }); + }); + }); - query.on('move', function(docs, from, to) { - expect(docs.length).eql(1); - expect(from).a('number'); - expect(to).a('number'); - expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']); - expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]); - done(); + it('changing a sorted property moves in a subscribed query', function(done) { + var connection = this.backend.connect(); + + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + var dbQuery = getQuery({query: {}, sort: [['age', 1]]}); + var query = connection.createSubscribeQuery( + 'dogs', + dbQuery, + null, + function(err, results) { + if (err) return done(err); + expect(util.pluck(results, 'id')).eql(['fido', 'spot']); + expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]); + connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3}); + }); + + query.on('move', function(docs, from, to) { + expect(docs.length).eql(1); + expect(from).a('number'); + expect(to).a('number'); + expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']); + expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]); + done(); + }); }); }); }); -}); }; diff --git a/test/client/query.js b/test/client/query.js index 423cdd2a3..785c55211 100644 --- a/test/client/query.js +++ b/test/client/query.js @@ -1,96 +1,66 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); module.exports = function(options) { -var getQuery = options.getQuery; + var getQuery = options.getQuery; -describe('client query', function() { - before(function() { - if (!getQuery) return this.skip(); - this.matchAllDbQuery = getQuery({query: {}}); - }); - - ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) { - it(method + ' on an empty collection', function(done) { - var connection = this.backend.connect(); - connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) { - if (err) return done(err); - expect(results).eql([]); - done(); - }); + describe('client query', function() { + before(function() { + if (!getQuery) return this.skip(); + this.matchAllDbQuery = getQuery({query: {}}); }); - it(method + ' on collection with fetched docs', function(done) { - var connection = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); - connection[method]('dogs', matchAllDbQuery, null, function(err, results) { + ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) { + it(method + ' on an empty collection', function(done) { + var connection = this.backend.connect(); + connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) { if (err) return done(err); - var sorted = util.sortById(results); - expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); - expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]); + expect(results).eql([]); done(); }); }); - }); - it(method + ' on collection with unfetched docs', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); - connection2[method]('dogs', matchAllDbQuery, null, function(err, results) { + it(method + ' on collection with fetched docs', function(done) { + var connection = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } + ], function(err) { if (err) return done(err); - var sorted = util.sortById(results); - expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); - expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]); - done(); + connection[method]('dogs', matchAllDbQuery, null, function(err, results) { + if (err) return done(err); + var sorted = util.sortById(results); + expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); + expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]); + done(); + }); }); }); - }); - it(method + ' on collection with one fetched doc', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); - connection2.get('dogs', 'fido').fetch(function(err) { + it(method + ' on collection with unfetched docs', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } + ], function(err) { if (err) return done(err); connection2[method]('dogs', matchAllDbQuery, null, function(err, results) { if (err) return done(err); @@ -101,47 +71,75 @@ describe('client query', function() { }); }); }); - }); - it(method + ' on collection with one fetched doc missing an op', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var matchAllDbQuery = this.matchAllDbQuery; - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); - connection2.get('dogs', 'fido').fetch(function(err) { + it(method + ' on collection with one fetched doc', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } + ], function(err) { if (err) return done(err); - connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) { + connection2.get('dogs', 'fido').fetch(function(err) { if (err) return done(err); - // The results option is meant for making resubscribing more - // efficient and has no effect on query fetching - var options = { - results: [ - connection2.get('dogs', 'fido'), - connection2.get('dogs', 'spot') - ] - }; - connection2[method]('dogs', matchAllDbQuery, options, function(err, results) { + connection2[method]('dogs', matchAllDbQuery, null, function(err, results) { if (err) return done(err); var sorted = util.sortById(results); expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); - expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]); + expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]); done(); }); }); }); }); + + it(method + ' on collection with one fetched doc missing an op', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var matchAllDbQuery = this.matchAllDbQuery; + async.parallel([ + function(cb) { + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); + }, + function(cb) { + connection.get('cats', 'finn').create({age: 2}, cb); + } + ], function(err) { + if (err) return done(err); + connection2.get('dogs', 'fido').fetch(function(err) { + if (err) return done(err); + connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) { + if (err) return done(err); + // The results option is meant for making resubscribing more + // efficient and has no effect on query fetching + var options = { + results: [ + connection2.get('dogs', 'fido'), + connection2.get('dogs', 'spot') + ] + }; + connection2[method]('dogs', matchAllDbQuery, options, function(err, results) { + if (err) return done(err); + var sorted = util.sortById(results); + expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']); + expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]); + done(); + }); + }); + }); + }); + }); }); }); -}); }; diff --git a/test/client/submit.js b/test/client/submit.js index 546f4ec9e..63a5a46ff 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,5 +1,3 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var async = require('async'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -10,23 +8,34 @@ types.register(deserializedType.type2); types.register(numberType.type); module.exports = function() { -describe('client submit', function() { - it('can fetch an uncreated doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - expect(doc.data).equal(undefined); - expect(doc.version).equal(null); - doc.fetch(function(err) { - if (err) return done(err); + describe('client submit', function() { + it('can fetch an uncreated doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); expect(doc.data).equal(undefined); - expect(doc.version).equal(0); - done(); + expect(doc.version).equal(null); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.data).equal(undefined); + expect(doc.version).equal(0); + done(); + }); + }); + + it('can fetch then create a new doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.fetch(function(err) { + if (err) return done(err); + doc.create({age: 3}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc.version).eql(1); + done(); + }); + }); }); - }); - it('can fetch then create a new doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.fetch(function(err) { - if (err) return done(err); + it('can create a new doc without fetching', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { if (err) return done(err); expect(doc.data).eql({age: 3}); @@ -34,638 +43,603 @@ describe('client submit', function() { done(); }); }); - }); - it('can create a new doc without fetching', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 3}); - expect(doc.version).eql(1); - done(); - }); - }); + it('can create then delete then create a doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc.version).eql(1); - it('can create then delete then create a doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 3}); - expect(doc.version).eql(1); + doc.del(null, function(err) { + if (err) return done(err); + expect(doc.data).eql(undefined); + expect(doc.version).eql(2); - doc.del(null, function(err) { - if (err) return done(err); - expect(doc.data).eql(undefined); - expect(doc.version).eql(2); + doc.create({age: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 2}); + expect(doc.version).eql(3); + done(); + }); + }); + }); + }); - doc.create({age: 2}, function(err) { + it('can create then submit an op', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 2}); - expect(doc.version).eql(3); + expect(doc.data).eql({age: 5}); + expect(doc.version).eql(2); done(); }); }); }); - }); - it('can create then submit an op', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 2}, function(err) { + it('can create then submit an op sync', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}); + expect(doc.data).eql({age: 3}); + expect(doc.version).eql(null); + doc.submitOp({p: ['age'], na: 2}); + expect(doc.data).eql({age: 5}); + expect(doc.version).eql(null); + doc.whenNothingPending(done); + }); + + it('submitting an op from a future version fails', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 5}); - expect(doc.version).eql(2); - done(); + doc.version++; + doc.submitOp({p: ['age'], na: 2}, function(err) { + expect(err).ok(); + done(); + }); }); }); - }); - - it('can create then submit an op sync', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}); - expect(doc.data).eql({age: 3}); - expect(doc.version).eql(null); - doc.submitOp({p: ['age'], na: 2}); - expect(doc.data).eql({age: 5}); - expect(doc.version).eql(null); - doc.whenNothingPending(done); - }); - it('submitting an op from a future version fails', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.version++; + it('cannot submit op on an uncreated doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); doc.submitOp({p: ['age'], na: 2}, function(err) { expect(err).ok(); done(); }); }); - }); - - it('cannot submit op on an uncreated doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); - done(); - }); - }); - it('cannot delete an uncreated doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.del(function(err) { - expect(err).ok(); - done(); + it('cannot delete an uncreated doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.del(function(err) { + expect(err).ok(); + done(); + }); }); - }); - it('ops submitted sync get composed', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}); - doc.submitOp({p: ['age'], na: 2}); - doc.submitOp({p: ['age'], na: 2}, function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 7}); - // Version is 1 instead of 3, because the create and ops got composed - expect(doc.version).eql(1); + it('ops submitted sync get composed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}); doc.submitOp({p: ['age'], na: 2}); doc.submitOp({p: ['age'], na: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 11}); - // Ops get composed - expect(doc.version).eql(2); + expect(doc.data).eql({age: 7}); + // Version is 1 instead of 3, because the create and ops got composed + expect(doc.version).eql(1); doc.submitOp({p: ['age'], na: 2}); - doc.del(function(err) { + doc.submitOp({p: ['age'], na: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql(undefined); - // del DOES NOT get composed - expect(doc.version).eql(4); - done(); + expect(doc.data).eql({age: 11}); + // Ops get composed + expect(doc.version).eql(2); + doc.submitOp({p: ['age'], na: 2}); + doc.del(function(err) { + if (err) return done(err); + expect(doc.data).eql(undefined); + // del DOES NOT get composed + expect(doc.version).eql(4); + done(); + }); }); }); }); - }); - it('does not compose ops when doc.preventCompose is true', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.preventCompose = true; - doc.create({age: 3}); - doc.submitOp({p: ['age'], na: 2}); - doc.submitOp({p: ['age'], na: 2}, function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 7}); - // Compare to version in above test - expect(doc.version).eql(3); + it('does not compose ops when doc.preventCompose is true', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.preventCompose = true; + doc.create({age: 3}); doc.submitOp({p: ['age'], na: 2}); doc.submitOp({p: ['age'], na: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 11}); + expect(doc.data).eql({age: 7}); // Compare to version in above test - expect(doc.version).eql(5); - done(); + expect(doc.version).eql(3); + doc.submitOp({p: ['age'], na: 2}); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 11}); + // Compare to version in above test + expect(doc.version).eql(5); + done(); + }); }); }); - }); - it('resumes composing after doc.preventCompose is set back to false', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.preventCompose = true; - doc.create({age: 3}); - doc.submitOp({p: ['age'], na: 2}); - doc.submitOp({p: ['age'], na: 2}, function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 7}); - // Compare to version in above test - expect(doc.version).eql(3); - // Reset back to start composing ops again - doc.preventCompose = false; + it('resumes composing after doc.preventCompose is set back to false', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.preventCompose = true; + doc.create({age: 3}); doc.submitOp({p: ['age'], na: 2}); doc.submitOp({p: ['age'], na: 2}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 11}); + expect(doc.data).eql({age: 7}); // Compare to version in above test - expect(doc.version).eql(4); - done(); + expect(doc.version).eql(3); + // Reset back to start composing ops again + doc.preventCompose = false; + doc.submitOp({p: ['age'], na: 2}); + doc.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 11}); + // Compare to version in above test + expect(doc.version).eql(4); + done(); + }); }); }); - }); - it('can create a new doc then fetch', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.fetch(function(err) { + it('can create a new doc then fetch', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 3}); - expect(doc.version).eql(1); - done(); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc.version).eql(1); + done(); + }); }); }); - }); - it('calling create on the same doc twice fails', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.create({age: 4}, function(err) { - expect(err).ok(); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); - done(); + it('calling create on the same doc twice fails', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.create({age: 4}, function(err) { + expect(err).ok(); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); }); }); - }); - it('trying to create an already created doc without fetching fails and fetches', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.create({age: 4}, function(err) { - expect(err).ok(); - expect(doc2.version).equal(1); - expect(doc2.data).eql({age: 3}); - done(); + it('trying to create an already created doc without fetching fails and fetches', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2.create({age: 4}, function(err) { + expect(err).ok(); + expect(doc2.version).equal(1); + expect(doc2.data).eql({age: 3}); + done(); + }); }); }); - }); - it('server fetches and transforms by already committed op', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('server fetches and transforms by already committed op', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc2.submitOp({p: ['age'], na: 2}, function(err) { + doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - expect(doc2.version).equal(3); - expect(doc2.data).eql({age: 6}); - done(); + doc2.submitOp({p: ['age'], na: 2}, function(err) { + if (err) return done(err); + expect(doc2.version).equal(3); + expect(doc2.data).eql({age: 6}); + done(); + }); }); }); }); }); - }); - it('submit fails if the server is missing ops required for transforming', function(done) { - this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { - callback(null, []); - }; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('submit fails if the server is missing ops required for transforming', function(done) { + this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { + callback(null, []); + }; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc2.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); - done(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + doc2.submitOp({p: ['age'], na: 2}, function(err) { + expect(err).ok(); + done(); + }); }); }); }); }); - }); - it('submit fails if ops returned are not the expected version', function(done) { - var getOpsToSnapshot = this.backend.db.getOpsToSnapshot; - this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { - getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) { - ops[0].v++; - callback(null, ops); - }); - }; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('submit fails if ops returned are not the expected version', function(done) { + var getOpsToSnapshot = this.backend.db.getOpsToSnapshot; + this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { + getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) { + ops[0].v++; + callback(null, ops); + }); + }; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc2.submitOp({p: ['age'], na: 2}, function(err) { - expect(err).ok(); - done(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + doc2.submitOp({p: ['age'], na: 2}, function(err) { + expect(err).ok(); + done(); + }); }); }); }); }); - }); - function delayedReconnect(backend, connection) { + function delayedReconnect(backend, connection) { // Disconnect after the message has sent and before the server will have // had a chance to reply - process.nextTick(function() { - connection.close(); - // Reconnect once the server has a chance to save the op data - setTimeout(function() { - backend.connect(connection); - }, 100); - }); - } - - it('resends create when disconnected before ack', function(done) { - var backend = this.backend; - var doc = backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); - done(); - }); - delayedReconnect(backend, doc.connection); - }); - - it('resent create on top of deleted doc gets proper starting version', function(done) { - var backend = this.backend; - var doc = backend.connect().get('dogs', 'fido'); - doc.create({age: 4}, function(err) { - if (err) return done(err); - doc.del(function(err) { - if (err) return done(err); - - var doc2 = backend.connect().get('dogs', 'fido'); - doc2.create({age: 3}, function(err) { - if (err) return done(err); - expect(doc2.version).equal(3); - expect(doc2.data).eql({age: 3}); - done(); - }); - delayedReconnect(backend, doc2.connection); + process.nextTick(function() { + connection.close(); + // Reconnect once the server has a chance to save the op data + setTimeout(function() { + backend.connect(connection); + }, 100); }); - }); - }); + } - it('resends delete when disconnected before ack', function(done) { - var backend = this.backend; - var doc = backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.del(function(err) { + it('resends create when disconnected before ack', function(done) { + var backend = this.backend; + var doc = backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.version).equal(2); - expect(doc.data).eql(undefined); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); done(); }); delayedReconnect(backend, doc.connection); }); - }); - it('op submitted during inflight create does not compose and gets flushed', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}); - // Submit an op after message is sent but before server has a chance to reply - process.nextTick(function() { - doc.submitOp({p: ['age'], na: 2}, function(err) { + it('resent create on top of deleted doc gets proper starting version', function(done) { + var backend = this.backend; + var doc = backend.connect().get('dogs', 'fido'); + doc.create({age: 4}, function(err) { if (err) return done(err); - expect(doc.version).equal(2); - expect(doc.data).eql({age: 5}); - done(); + doc.del(function(err) { + if (err) return done(err); + + var doc2 = backend.connect().get('dogs', 'fido'); + doc2.create({age: 3}, function(err) { + if (err) return done(err); + expect(doc2.version).equal(3); + expect(doc2.data).eql({age: 3}); + done(); + }); + delayedReconnect(backend, doc2.connection); + }); }); }); - }); - it('can commit then fetch in a new connection to get the same data', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('resends delete when disconnected before ack', function(done) { + var backend = this.backend; + var doc = backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 3}); - expect(doc2.data).eql({age: 3}); - expect(doc.version).eql(1); - expect(doc2.version).eql(1); - expect(doc.data).not.equal(doc2.data); - done(); + doc.del(function(err) { + if (err) return done(err); + expect(doc.version).equal(2); + expect(doc.data).eql(undefined); + done(); + }); + delayedReconnect(backend, doc.connection); }); }); - }); - it('an op submitted concurrently is transformed by the first', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { - if (err) return done(err); - var count = 0; + it('op submitted during inflight create does not compose and gets flushed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}); + // Submit an op after message is sent but before server has a chance to reply + process.nextTick(function() { doc.submitOp({p: ['age'], na: 2}, function(err) { - count++; if (err) return done(err); - if (count === 1) { - expect(doc.data).eql({age: 5}); - expect(doc.version).eql(2); - } else { - expect(doc.data).eql({age: 12}); - expect(doc.version).eql(3); - done(); - } - }); - doc2.submitOp({p: ['age'], na: 7}, function(err) { - count++; - if (err) return done(err); - if (count === 1) { - expect(doc2.data).eql({age: 10}); - expect(doc2.version).eql(2); - } else { - expect(doc2.data).eql({age: 12}); - expect(doc2.version).eql(3); - done(); - } + expect(doc.version).equal(2); + expect(doc.data).eql({age: 5}); + done(); }); }); }); - }); - it('second of two concurrent creates is rejected', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - var count = 0; - doc.create({age: 3}, function(err) { - count++; - if (count === 1) { - if (err) return done(err); - expect(doc.version).eql(1); - expect(doc.data).eql({age: 3}); - } else { - expect(err).ok(); - expect(doc.version).eql(1); - expect(doc.data).eql({age: 5}); - done(); - } - }); - doc2.create({age: 5}, function(err) { - count++; - if (count === 1) { + it('can commit then fetch in a new connection to get the same data', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 5}); - } else { - expect(err).ok(); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); - } + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 3}); + expect(doc2.data).eql({age: 3}); + expect(doc.version).eql(1); + expect(doc2.version).eql(1); + expect(doc.data).not.equal(doc2.data); + done(); + }); + }); }); - }); - it('concurrent delete operations transform', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('an op submitted concurrently is transformed by the first', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - var count = 0; - doc.del(function(err) { - count++; + doc2.fetch(function(err) { if (err) return done(err); - if (count === 1) { - expect(doc.version).eql(2); - expect(doc.data).eql(undefined); - } else { - expect(doc.version).eql(3); - expect(doc.data).eql(undefined); - done(); - } + var count = 0; + doc.submitOp({p: ['age'], na: 2}, function(err) { + count++; + if (err) return done(err); + if (count === 1) { + expect(doc.data).eql({age: 5}); + expect(doc.version).eql(2); + } else { + expect(doc.data).eql({age: 12}); + expect(doc.version).eql(3); + done(); + } + }); + doc2.submitOp({p: ['age'], na: 7}, function(err) { + count++; + if (err) return done(err); + if (count === 1) { + expect(doc2.data).eql({age: 10}); + expect(doc2.version).eql(2); + } else { + expect(doc2.data).eql({age: 12}); + expect(doc2.version).eql(3); + done(); + } + }); }); - doc2.del(function(err) { - count++; + }); + }); + + it('second of two concurrent creates is rejected', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + var count = 0; + doc.create({age: 3}, function(err) { + count++; + if (count === 1) { if (err) return done(err); - if (count === 1) { - expect(doc2.version).eql(2); - expect(doc2.data).eql(undefined); - } else { - expect(doc2.version).eql(3); - expect(doc2.data).eql(undefined); - done(); - } - }); + expect(doc.version).eql(1); + expect(doc.data).eql({age: 3}); + } else { + expect(err).ok(); + expect(doc.version).eql(1); + expect(doc.data).eql({age: 5}); + done(); + } + }); + doc2.create({age: 5}, function(err) { + count++; + if (count === 1) { + if (err) return done(err); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 5}); + } else { + expect(err).ok(); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); + done(); + } }); }); - }); - it('submits retry below the backend.maxSubmitRetries threshold', function(done) { - this.backend.maxSubmitRetries = 10; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('concurrent delete operations transform', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - var count = 0; - var cb = function(err) { - count++; + doc2.fetch(function(err) { if (err) return done(err); - if (count > 1) done(); - }; - doc.submitOp({p: ['age'], na: 2}, cb); - doc2.submitOp({p: ['age'], na: 7}, cb); + var count = 0; + doc.del(function(err) { + count++; + if (err) return done(err); + if (count === 1) { + expect(doc.version).eql(2); + expect(doc.data).eql(undefined); + } else { + expect(doc.version).eql(3); + expect(doc.data).eql(undefined); + done(); + } + }); + doc2.del(function(err) { + count++; + if (err) return done(err); + if (count === 1) { + expect(doc2.version).eql(2); + expect(doc2.data).eql(undefined); + } else { + expect(doc2.version).eql(3); + expect(doc2.data).eql(undefined); + done(); + } + }); + }); }); }); - }); - it('submits fail above the backend.maxSubmitRetries threshold', function(done) { - var backend = this.backend; - this.backend.maxSubmitRetries = 0; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('submits retry below the backend.maxSubmitRetries threshold', function(done) { + this.backend.maxSubmitRetries = 10; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - var docCallback; - var doc2Callback; - // The submit retry happens just after an op is committed. This hook into the middleware - // catches both ops just before they're about to be committed. This ensures that both ops - // are certainly working on the same snapshot (ie one op hasn't been committed before the - // other fetches the snapshot to apply to). By storing the callbacks, we can then - // manually trigger the callbacks, first calling doc, and when we know that's been committed, - // we then commit doc2. - backend.use('commit', function(request, callback) { - if (request.op.op[0].na === 2) docCallback = callback; - if (request.op.op[0].na === 7) doc2Callback = callback; - - // Wait until both ops have been applied to the same snapshot and are about to be committed - if (docCallback && doc2Callback) { - // Trigger the first op's commit and then the second one later, which will cause the - // second op to retry - docCallback(); - } - }); - doc.submitOp({p: ['age'], na: 2}, function(error) { - if (error) return done(error); - // When we know the first op has been committed, we try to commit the second op, which will - // fail because it's working on an out-of-date snapshot. It will retry, but exceed the - // maxSubmitRetries limit of 0 - doc2Callback(); - }); - doc2.submitOp({p: ['age'], na: 7}, function(error) { - expect(error).ok(); - done(); + doc2.fetch(function(err) { + if (err) return done(err); + var count = 0; + var cb = function(err) { + count++; + if (err) return done(err); + if (count > 1) done(); + }; + doc.submitOp({p: ['age'], na: 2}, cb); + doc2.submitOp({p: ['age'], na: 7}, cb); }); }); }); - }); - it('pending delete transforms incoming ops', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('submits fail above the backend.maxSubmitRetries threshold', function(done) { + var backend = this.backend; + this.backend.maxSubmitRetries = 0; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.submitOp({p: ['age'], na: 1}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - async.parallel([ - function(cb) { - doc.del(cb); - }, - function(cb) { - doc.create({age: 5}, cb); + var docCallback; + var doc2Callback; + // The submit retry happens just after an op is committed. This hook into the middleware + // catches both ops just before they're about to be committed. This ensures that both ops + // are certainly working on the same snapshot (ie one op hasn't been committed before the + // other fetches the snapshot to apply to). By storing the callbacks, we can then + // manually trigger the callbacks, first calling doc, and when we know that's been committed, + // we then commit doc2. + backend.use('commit', function(request, callback) { + if (request.op.op[0].na === 2) docCallback = callback; + if (request.op.op[0].na === 7) doc2Callback = callback; + + // Wait until both ops have been applied to the same snapshot and are about to be committed + if (docCallback && doc2Callback) { + // Trigger the first op's commit and then the second one later, which will cause the + // second op to retry + docCallback(); } - ], function(err) { - if (err) return done(err); - expect(doc.version).equal(4); - expect(doc.data).eql({age: 5}); + }); + doc.submitOp({p: ['age'], na: 2}, function(error) { + if (error) return done(error); + // When we know the first op has been committed, we try to commit the second op, which will + // fail because it's working on an out-of-date snapshot. It will retry, but exceed the + // maxSubmitRetries limit of 0 + doc2Callback(); + }); + doc2.submitOp({p: ['age'], na: 7}, function(error) { + expect(error).ok(); done(); }); }); }); }); - }); - it('pending delete transforms incoming delete', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('pending delete transforms incoming ops', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.del(function(err) { + doc2.fetch(function(err) { if (err) return done(err); - async.parallel([ - function(cb) { - doc.del(cb); - }, - function(cb) { - doc.create({age: 5}, cb); - } - ], function(err) { + doc2.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - expect(doc.version).equal(4); - expect(doc.data).eql({age: 5}); - done(); + async.parallel([ + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + expect(doc.version).equal(4); + expect(doc.data).eql({age: 5}); + done(); + }); }); }); }); }); - }); - it('submitting op after delete returns error', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('pending delete transforms incoming delete', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.del(function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err).ok(); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); - done(); + doc2.del(function(err) { + if (err) return done(err); + async.parallel([ + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } + ], function(err) { + if (err) return done(err); + expect(doc.version).equal(4); + expect(doc.data).eql({age: 5}); + done(); + }); }); }); }); }); - }); - it('transforming pending op by server delete returns error', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('submitting op after delete returns error', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.del(function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc.pause(); - doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err.code).to.equal(4017); - expect(doc.version).equal(2); - expect(doc.data).eql(undefined); - done(); + doc2.del(function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 1}, function(err) { + expect(err).ok(); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); }); - doc.fetch(); }); }); }); - }); - it('transforming pending op by server create returns error', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.del(function(err) { + it('transforming pending op by server delete returns error', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); doc2.fetch(function(err) { if (err) return done(err); - doc2.create({age: 5}, function(err) { + doc2.del(function(err) { if (err) return done(err); doc.pause(); - doc.create({age: 9}, function(err) { - expect(err.code).to.equal(4018); - expect(doc.version).equal(3); - expect(doc.data).eql({age: 5}); + doc.submitOp({p: ['age'], na: 1}, function(err) { + expect(err.code).to.equal(4017); + expect(doc.version).equal(2); + expect(doc.data).eql(undefined); done(); }); doc.fetch(); @@ -673,92 +647,117 @@ describe('client submit', function() { }); }); }); - }); - it('second client can create following delete', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.del(function(err) { + it('transforming pending op by server create returns error', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.create({age: 5}, function(err) { + doc.del(function(err) { if (err) return done(err); - expect(doc2.version).eql(3); - expect(doc2.data).eql({age: 5}); - done(); + doc2.fetch(function(err) { + if (err) return done(err); + doc2.create({age: 5}, function(err) { + if (err) return done(err); + doc.pause(); + doc.create({age: 9}, function(err) { + expect(err.code).to.equal(4018); + expect(doc.version).equal(3); + expect(doc.data).eql({age: 5}); + done(); + }); + doc.fetch(); + }); + }); }); }); }); - }); - it('doc.pause() prevents ops from being sent', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.pause(); - doc.create({age: 3}, done); - done(); - }); + it('second client can create following delete', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.del(function(err) { + if (err) return done(err); + doc2.create({age: 5}, function(err) { + if (err) return done(err); + expect(doc2.version).eql(3); + expect(doc2.data).eql({age: 5}); + done(); + }); + }); + }); + }); - it('can call doc.resume() without pausing', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.resume(); - doc.create({age: 3}, done); - }); + it('doc.pause() prevents ops from being sent', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.pause(); + doc.create({age: 3}, done); + done(); + }); - it('doc.resume() resumes sending ops after pause', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.pause(); - doc.create({age: 3}, done); - doc.resume(); - }); + it('can call doc.resume() without pausing', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.resume(); + doc.create({age: 3}, done); + }); - it('pending ops are transformed by ops from other clients', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { - if (err) return done(err); - doc.pause(); - doc.submitOp({p: ['age'], na: 1}); - doc.submitOp({p: ['color'], oi: 'gold'}); - expect(doc.version).equal(1); + it('doc.resume() resumes sending ops after pause', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.pause(); + doc.create({age: 3}, done); + doc.resume(); + }); - doc2.submitOp({p: ['age'], na: 5}); - process.nextTick(function() { - doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) { - if (err) return done(err); - expect(doc2.version).equal(3); + it('pending ops are transformed by ops from other clients', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2.fetch(function(err) { + if (err) return done(err); + doc.pause(); + doc.submitOp({p: ['age'], na: 1}); + doc.submitOp({p: ['color'], oi: 'gold'}); + expect(doc.version).equal(1); - async.parallel([ - function(cb) { - doc.fetch(cb); - }, - function(cb) { - doc2.fetch(cb); - } - ], function(err) { + doc2.submitOp({p: ['age'], na: 5}); + process.nextTick(function() { + doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) { if (err) return done(err); - expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'}); - expect(doc.version).equal(3); - expect(doc.hasPending()).equal(true); - - expect(doc2.data).eql({age: 8, sex: 'female'}); expect(doc2.version).equal(3); - expect(doc2.hasPending()).equal(false); - doc.resume(); - doc.whenNothingPending(function() { - doc2.fetch(function(err) { - if (err) return done(err); - expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'}); - expect(doc.version).equal(4); - expect(doc.hasPending()).equal(false); - - expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'}); - expect(doc2.version).equal(4); - expect(doc2.hasPending()).equal(false); - done(); + async.parallel([ + function(cb) { + doc.fetch(cb); + }, + function(cb) { + doc2.fetch(cb); + } + ], function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'}); + expect(doc.version).equal(3); + expect(doc.hasPending()).equal(true); + + expect(doc2.data).eql({age: 8, sex: 'female'}); + expect(doc2.version).equal(3); + expect(doc2.hasPending()).equal(false); + + doc.resume(); + doc.whenNothingPending(function() { + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'}); + expect(doc.version).equal(4); + expect(doc.hasPending()).equal(false); + + expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'}); + expect(doc2.version).equal(4); + expect(doc2.hasPending()).equal(false); + done(); + }); }); }); }); @@ -766,421 +765,420 @@ describe('client submit', function() { }); }); }); - }); - it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - this.backend.use('doc', function(request, next) { - doc.create({age: 3}); - doc.del(next); - }); - doc.fetch(function(err) { - if (err) return done(err); - expect(doc.version).equal(2); - done(); + it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + this.backend.use('doc', function(request, next) { + doc.create({age: 3}); + doc.del(next); + }); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.version).equal(2); + done(); + }); }); - }); - it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - this.backend.use('doc', function(request, next) { - doc.create({age: 3}, function(err) { - if (err) return done(err); - next(); + it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + this.backend.use('doc', function(request, next) { + doc.create({age: 3}, function(err) { + if (err) return done(err); + next(); + }); + process.nextTick(function() { + doc.pause(); + doc.del(done); + }); }); - process.nextTick(function() { - doc.pause(); - doc.del(done); + doc.fetch(function(err) { + if (err) return done(err); + expect(doc.version).equal(1); + doc.resume(); }); }); - doc.fetch(function(err) { - if (err) return done(err); - expect(doc.version).equal(1); - doc.resume(); - }); - }); - it('snapshot fetch from query does not advance version of doc with pending ops', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({name: 'kido'}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it('snapshot fetch from query does not advance version of doc with pending ops', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({name: 'kido'}, function(err) { if (err) return done(err); - doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - expect(doc2.data).eql({name: 'fkido'}); - doc.connection.createFetchQuery('dogs', {}, null, function(err) { + doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) { if (err) return done(err); - doc.resume(); + expect(doc2.data).eql({name: 'fkido'}); + doc.connection.createFetchQuery('dogs', {}, null, function(err) { + if (err) return done(err); + doc.resume(); + }); }); }); }); - }); - process.nextTick(function() { - doc.pause(); - doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) { - if (err) return done(err); + process.nextTick(function() { doc.pause(); - doc2.fetch(function(err) { + doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) { if (err) return done(err); - expect(doc2.version).equal(3); - expect(doc2.data).eql({name: 'fido'}); - done(); - }); - }); - doc.del(); - }); - }); - - it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) { - this.backend.use('submit', function(request, next) { - next({message: 'Custom error'}); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - expect(err.message).equal('Custom error'); - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - done(); - }); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 3}); - }); - - it('passing an error in submit middleware rejects a create and throws the erorr', function(done) { - this.backend.use('submit', function(request, next) { - next({message: 'Custom error'}); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 3}); - doc.on('error', function(err) { - expect(err.message).equal('Custom error'); - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - done(); - }); - }); - - it('passing an error in submit middleware rejects pending ops after failed create', function(done) { - var submitCount = 0; - this.backend.use('submit', function(request, next) { - submitCount++; - if (submitCount === 1) return next({message: 'Custom error'}); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - async.parallel([ - function(cb) { - doc.create({age: 3}, function(err) { - expect(err.message).equal('Custom error'); - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - cb(); - }); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 3}); - }, - function(cb) { - process.nextTick(function() { - doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err.message).equal('Custom error'); - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - expect(submitCount).equal(1); - cb(); + doc.pause(); + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc2.version).equal(3); + expect(doc2.data).eql({name: 'fido'}); + done(); }); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 4}); }); - } - ], done); - }); - - it('request.rejectedError() soft rejects a create', function(done) { - this.backend.use('submit', function(request, next) { - next(request.rejectedError()); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - done(); - }); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 3}); - }); - - it('request.rejectedError() soft rejects a create without callback', function(done) { - this.backend.use('submit', function(request, next) { - next(request.rejectedError()); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}); - expect(doc.version).equal(null); - expect(doc.data).eql({age: 3}); - doc.whenNothingPending(function() { - expect(doc.version).equal(0); - expect(doc.data).equal(undefined); - done(); + doc.del(); + }); }); - }); - it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) { - this.backend.use('submit', function(request, next) { - if (request.op.op) return next({message: 'Custom error'}); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) { + this.backend.use('submit', function(request, next) { + next({message: 'Custom error'}); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { expect(err.message).equal('Custom error'); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); done(); }); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 3}); }); - }); - it('passing an error in submit middleware rejects an op and emits the erorr', function(done) { - this.backend.use('submit', function(request, next) { - if (request.op.op) return next({message: 'Custom error'}); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); + it('passing an error in submit middleware rejects a create and throws the erorr', function(done) { + this.backend.use('submit', function(request, next) { + next({message: 'Custom error'}); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 3}); doc.on('error', function(err) { expect(err.message).equal('Custom error'); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); done(); }); }); - }); - it('passing an error in submit middleware transforms pending ops after failed op', function(done) { - var submitCount = 0; - this.backend.use('submit', function(request, next) { - submitCount++; - if (submitCount === 2) return next({message: 'Custom error'}); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); + it('passing an error in submit middleware rejects pending ops after failed create', function(done) { + var submitCount = 0; + this.backend.use('submit', function(request, next) { + submitCount++; + if (submitCount === 1) return next({message: 'Custom error'}); + next(); + }); + var doc = this.backend.connect().get('dogs', 'fido'); async.parallel([ function(cb) { - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc.create({age: 3}, function(err) { expect(err.message).equal('Custom error'); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); cb(); }); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 3}); }, function(cb) { process.nextTick(function() { - doc.submitOp({p: ['age'], na: 5}, cb); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 9}); + doc.submitOp({p: ['age'], na: 1}, function(err) { + expect(err.message).equal('Custom error'); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); + expect(submitCount).equal(1); + cb(); + }); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 4}); }); } - ], function(err) { - if (err) return done(err); - expect(doc.version).equal(2); - expect(doc.data).eql({age: 8}); - expect(submitCount).equal(3); - done(); - }); + ], done); }); - }); - it('request.rejectedError() soft rejects an op', function(done) { - this.backend.use('submit', function(request, next) { - if (request.op.op) return next(request.rejectedError()); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + it('request.rejectedError() soft rejects a create', function(done) { + this.backend.use('submit', function(request, next) { + next(request.rejectedError()); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); done(); }); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 3}); }); - }); - it('request.rejectedError() soft rejects an op without callback', function(done) { - this.backend.use('submit', function(request, next) { - if (request.op.op) return next(request.rejectedError()); - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); + it('request.rejectedError() soft rejects a create without callback', function(done) { + this.backend.use('submit', function(request, next) { + next(request.rejectedError()); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}); + expect(doc.version).equal(null); + expect(doc.data).eql({age: 3}); doc.whenNothingPending(function() { - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); done(); }); }); - }); - it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) { - this.backend.use('submit', function(request, next) { - if (request.op) request.op.op = null; - next(); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) { + this.backend.use('submit', function(request, next) { + if (request.op.op) return next({message: 'Custom error'}); + next(); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.version).equal(2); - expect(doc.data).eql({age: 4}); - doc2.fetch(function(err) { - if (err) return done(err); - expect(doc2.version).equal(2); - expect(doc2.data).eql({age: 3}); + doc.submitOp({p: ['age'], na: 1}, function(err) { + expect(err.message).equal('Custom error'); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); done(); }); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); }); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 4}); }); - }); - it('submitting an invalid op message returns error', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc._submit({}, null, function(err) { - expect(err).ok(); - done(); + it('passing an error in submit middleware rejects an op and emits the erorr', function(done) { + this.backend.use('submit', function(request, next) { + if (request.op.op) return next({message: 'Custom error'}); + next(); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 1}); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); + doc.on('error', function(err) { + expect(err.message).equal('Custom error'); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); }); }); - }); - it('allows snapshot and op to be a non-object', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create(5, numberType.type.uri, function(err) { - if (err) return done(err); - expect(doc.data).to.equal(5); - doc.submitOp(2, function(err) { + it('passing an error in submit middleware transforms pending ops after failed op', function(done) { + var submitCount = 0; + this.backend.use('submit', function(request, next) { + submitCount++; + if (submitCount === 2) return next({message: 'Custom error'}); + next(); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.data).to.equal(7); - done(); + async.parallel([ + function(cb) { + doc.submitOp({p: ['age'], na: 1}, function(err) { + expect(err.message).equal('Custom error'); + cb(); + }); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); + }, + function(cb) { + process.nextTick(function() { + doc.submitOp({p: ['age'], na: 5}, cb); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 9}); + }); + } + ], function(err) { + if (err) return done(err); + expect(doc.version).equal(2); + expect(doc.data).eql({age: 8}); + expect(submitCount).equal(3); + done(); + }); }); }); - }); - describe('type.deserialize', function() { - it('can create a new doc', function(done) { + it('request.rejectedError() soft rejects an op', function(done) { + this.backend.use('submit', function(request, next) { + if (request.op.op) return next(request.rejectedError()); + next(); + }); var doc = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type.uri, function(err) { + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); - done(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); }); }); - it('is stored serialized in backend', function(done) { - var db = this.backend.db; + it('request.rejectedError() soft rejects an op without callback', function(done) { + this.backend.use('submit', function(request, next) { + if (request.op.op) return next(request.rejectedError()); + next(); + }); var doc = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type.uri, function(err) { + doc.create({age: 3}, function(err) { if (err) return done(err); - db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) { - if (err) return done(err); - expect(snapshot.data).eql([3]); + doc.submitOp({p: ['age'], na: 1}); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); + doc.whenNothingPending(function() { + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); done(); }); }); }); - it('deserializes on fetch', function(done) { + it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) { + this.backend.use('submit', function(request, next) { + if (request.op) request.op.op = null; + next(); + }); var doc = this.backend.connect().get('dogs', 'fido'); var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type.uri, function(err) { + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.fetch(function(err) { + doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - expect(doc2.data).a(deserializedType.Node); - expect(doc2.data).eql({value: 3, next: null}); - done(); + expect(doc.version).equal(2); + expect(doc.data).eql({age: 4}); + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc2.version).equal(2); + expect(doc2.data).eql({age: 3}); + done(); + }); }); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); }); }); - it('can create then submit an op', function(done) { + it('submitting an invalid op message returns error', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type.uri, function(err) { + doc.create({age: 3}, function(err) { if (err) return done(err); - doc.submitOp({insert: 0, value: 2}, function(err) { - if (err) return done(err); - expect(doc.data).eql({value: 2, next: {value: 3, next: null}}); + doc._submit({}, null, function(err) { + expect(err).ok(); done(); }); }); }); - it('server fetches and transforms by already committed op', function(done) { + it('allows snapshot and op to be a non-object', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type.uri, function(err) { + doc.create(5, numberType.type.uri, function(err) { if (err) return done(err); - doc2.fetch(function(err) { + expect(doc.data).to.equal(5); + doc.submitOp(2, function(err) { + if (err) return done(err); + expect(doc.data).to.equal(7); + done(); + }); + }); + }); + + describe('type.deserialize', function() { + it('can create a new doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { + if (err) return done(err); + expect(doc.data).a(deserializedType.Node); + expect(doc.data).eql({value: 3, next: null}); + done(); + }); + }); + + it('is stored serialized in backend', function(done) { + var db = this.backend.db; + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { + if (err) return done(err); + db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) { + if (err) return done(err); + expect(snapshot.data).eql([3]); + done(); + }); + }); + }); + + it('deserializes on fetch', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { + if (err) return done(err); + doc2.fetch(function(err) { + if (err) return done(err); + expect(doc2.data).a(deserializedType.Node); + expect(doc2.data).eql({value: 3, next: null}); + done(); + }); + }); + }); + + it('can create then submit an op', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { if (err) return done(err); doc.submitOp({insert: 0, value: 2}, function(err) { if (err) return done(err); - doc2.submitOp({insert: 1, value: 4}, function(err) { + expect(doc.data).eql({value: 2, next: {value: 3, next: null}}); + done(); + }); + }); + }); + + it('server fetches and transforms by already committed op', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { + if (err) return done(err); + doc2.fetch(function(err) { + if (err) return done(err); + doc.submitOp({insert: 0, value: 2}, function(err) { if (err) return done(err); - expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}}); - done(); + doc2.submitOp({insert: 1, value: 4}, function(err) { + if (err) return done(err); + expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}}); + done(); + }); }); }); }); }); }); - }); - describe('type.createDeserialized', function() { - it('can create a new doc', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create([3], deserializedType.type2.uri, function(err) { - if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); - done(); + describe('type.createDeserialized', function() { + it('can create a new doc', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type2.uri, function(err) { + if (err) return done(err); + expect(doc.data).a(deserializedType.Node); + expect(doc.data).eql({value: 3, next: null}); + done(); + }); }); - }); - it('can create a new doc from deserialized form', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) { - if (err) return done(err); - expect(doc.data).a(deserializedType.Node); - expect(doc.data).eql({value: 3, next: null}); - done(); + it('can create a new doc from deserialized form', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) { + if (err) return done(err); + expect(doc.data).a(deserializedType.Node); + expect(doc.data).eql({value: 3, next: null}); + done(); + }); }); }); }); -}); }; diff --git a/test/client/subscribe.js b/test/client/subscribe.js index ed45b1b90..083634fb5 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -1,90 +1,116 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); module.exports = function() { -describe('client subscribe', function() { - it('can call bulk without doing any actions', function() { - var connection = this.backend.connect(); - connection.startBulk(); - connection.endBulk(); - }); + describe('client subscribe', function() { + it('can call bulk without doing any actions', function() { + var connection = this.backend.connect(); + connection.startBulk(); + connection.endBulk(); + }); - ['fetch', 'subscribe'].forEach(function(method) { - it(method + ' gets initial data', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2[method](function(err) { + ['fetch', 'subscribe'].forEach(function(method) { + it(method + ' gets initial data', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); + doc2[method](function(err) { + if (err) return done(err); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); + done(); + }); }); }); - }); - it(method + ' twice simultaneously calls back', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - async.parallel([ - function(cb) { - doc2[method](cb); - }, - function(cb) { - doc2[method](cb); - } - ], function(err) { + it(method + ' twice simultaneously calls back', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); + async.parallel([ + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } + ], function(err) { + if (err) return done(err); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); + done(); + }); }); }); - }); - it(method + ' twice in bulk simultaneously calls back', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.connection.startBulk(); + it(method + ' twice in bulk simultaneously calls back', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2.connection.startBulk(); + async.parallel([ + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } + ], function(err) { + if (err) return done(err); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); + done(); + }); + doc2.connection.endBulk(); + }); + }); + + it(method + ' bulk on same collection', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); async.parallel([ function(cb) { - doc2[method](cb); + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); }, function(cb) { - doc2[method](cb); + connection.get('cats', 'finn').create({age: 2}, cb); } ], function(err) { if (err) return done(err); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); + var fido = connection2.get('dogs', 'fido'); + var spot = connection2.get('dogs', 'spot'); + var finn = connection2.get('cats', 'finn'); + connection2.startBulk(); + async.parallel([ + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } + ], function(err) { + if (err) return done(err); + expect(fido.data).eql({age: 3}); + expect(spot.data).eql({age: 5}); + expect(finn.data).eql({age: 2}); + done(); + }); + connection2.endBulk(); }); - doc2.connection.endBulk(); }); - }); - it(method + ' bulk on same collection', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); + it(method + ' bulk on same collection from known version', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); var fido = connection2.get('dogs', 'fido'); var spot = connection2.get('dogs', 'spot'); var finn = connection2.get('cats', 'finn'); @@ -101,71 +127,25 @@ describe('client subscribe', function() { } ], function(err) { if (err) return done(err); - expect(fido.data).eql({age: 3}); - expect(spot.data).eql({age: 5}); - expect(finn.data).eql({age: 2}); - done(); - }); - connection2.endBulk(); - }); - }); + expect(fido.version).equal(0); + expect(spot.version).equal(0); + expect(finn.version).equal(0); + expect(fido.data).equal(undefined); + expect(spot.data).equal(undefined); + expect(finn.data).equal(undefined); - it(method + ' bulk on same collection from known version', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var fido = connection2.get('dogs', 'fido'); - var spot = connection2.get('dogs', 'spot'); - var finn = connection2.get('cats', 'finn'); - connection2.startBulk(); - async.parallel([ - function(cb) { - fido[method](cb); - }, - function(cb) { - spot[method](cb); - }, - function(cb) { - finn[method](cb); - } - ], function(err) { - if (err) return done(err); - expect(fido.version).equal(0); - expect(spot.version).equal(0); - expect(finn.version).equal(0); - expect(fido.data).equal(undefined); - expect(spot.data).equal(undefined); - expect(finn.data).equal(undefined); - - async.parallel([ - function(cb) { - connection.get('dogs', 'fido').create({age: 3}, cb); - }, - function(cb) { - connection.get('dogs', 'spot').create({age: 5}, cb); - }, - function(cb) { - connection.get('cats', 'finn').create({age: 2}, cb); - } - ], function(err) { - if (err) return done(err); - connection2.startBulk(); async.parallel([ function(cb) { - fido[method](cb); + connection.get('dogs', 'fido').create({age: 3}, cb); }, function(cb) { - spot[method](cb); + connection.get('dogs', 'spot').create({age: 5}, cb); }, function(cb) { - finn[method](cb); + connection.get('cats', 'finn').create({age: 2}, cb); } ], function(err) { if (err) return done(err); - expect(fido.data).eql({age: 3}); - expect(spot.data).eql({age: 5}); - expect(finn.data).eql({age: 2}); - - // Test sending a fetch without any new ops being created connection2.startBulk(); async.parallel([ function(cb) { @@ -179,513 +159,531 @@ describe('client subscribe', function() { } ], function(err) { if (err) return done(err); + expect(fido.data).eql({age: 3}); + expect(spot.data).eql({age: 5}); + expect(finn.data).eql({age: 2}); - // Create new ops and test if they are received + // Test sending a fetch without any new ops being created + connection2.startBulk(); async.parallel([ function(cb) { - connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); + fido[method](cb); }, function(cb) { - connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); + spot[method](cb); }, function(cb) { - connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); + finn[method](cb); } ], function(err) { if (err) return done(err); - connection2.startBulk(); + + // Create new ops and test if they are received async.parallel([ function(cb) { - fido[method](cb); + connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); }, function(cb) { - spot[method](cb); + connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); }, function(cb) { - finn[method](cb); + connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); } ], function(err) { if (err) return done(err); - expect(fido.data).eql({age: 4}); - expect(spot.data).eql({age: 6}); - expect(finn.data).eql({age: 3}); - done(); + connection2.startBulk(); + async.parallel([ + function(cb) { + fido[method](cb); + }, + function(cb) { + spot[method](cb); + }, + function(cb) { + finn[method](cb); + } + ], function(err) { + if (err) return done(err); + expect(fido.data).eql({age: 4}); + expect(spot.data).eql({age: 6}); + expect(finn.data).eql({age: 3}); + done(); + }); + connection2.endBulk(); }); - connection2.endBulk(); }); + connection2.endBulk(); }); connection2.endBulk(); }); - connection2.endBulk(); }); + connection2.endBulk(); }); - connection2.endBulk(); - }); - it(method + ' gets new ops', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.fetch(function(err) { + it(method + ' gets new ops', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.fetch(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc2[method](); }); - doc2[method](); }); }); }); - }); - it(method + ' calls back after reconnect', function(done) { - var backend = this.backend; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2[method](function(err) { + it(method + ' calls back after reconnect', function(done) { + var backend = this.backend; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); - }); - doc2.connection.close(); - process.nextTick(function() { - backend.connect(doc2.connection); + doc2[method](function(err) { + if (err) return done(err); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); + done(); + }); + doc2.connection.close(); + process.nextTick(function() { + backend.connect(doc2.connection); + }); }); }); - }); - it(method + ' returns error passed to doc read middleware', function(done) { - this.backend.use('doc', function(request, next) { - next({message: 'Reject doc read'}); - }); - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2[method](function(err) { - expect(err.message).equal('Reject doc read'); - expect(doc2.version).eql(null); - expect(doc2.data).eql(undefined); - done(); + it(method + ' returns error passed to doc read middleware', function(done) { + this.backend.use('doc', function(request, next) { + next({message: 'Reject doc read'}); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2[method](function(err) { + expect(err.message).equal('Reject doc read'); + expect(doc2.version).eql(null); + expect(doc2.data).eql(undefined); + done(); + }); }); }); - }); - it(method + ' emits error passed to doc read middleware', function(done) { - this.backend.use('doc', function(request, next) { - next({message: 'Reject doc read'}); + it(method + ' emits error passed to doc read middleware', function(done) { + this.backend.use('doc', function(request, next) { + next({message: 'Reject doc read'}); + }); + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2[method](); + doc2.on('error', function(err) { + expect(err.message).equal('Reject doc read'); + expect(doc2.version).eql(null); + expect(doc2.data).eql(undefined); + done(); + }); + }); }); - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2[method](); - doc2.on('error', function(err) { - expect(err.message).equal('Reject doc read'); - expect(doc2.version).eql(null); - expect(doc2.data).eql(undefined); - done(); + + it(method + ' will call back when ops are pending', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc.pause(); + doc.submitOp({p: ['age'], na: 1}); + doc[method](done); }); }); - }); - it(method + ' will call back when ops are pending', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); + it(method + ' will not call back when creating the doc is pending', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); doc.pause(); - doc.submitOp({p: ['age'], na: 1}); + doc.create({age: 3}); doc[method](done); + // HACK: Delay done call to keep from closing the db connection too soon + setTimeout(done, 10); }); - }); - - it(method + ' will not call back when creating the doc is pending', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.pause(); - doc.create({age: 3}); - doc[method](done); - // HACK: Delay done call to keep from closing the db connection too soon - setTimeout(done, 10); - }); - - it(method + ' will wait for write when doc is locally created', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.pause(); - var calls = 0; - doc.create({age: 3}, function(err) { - if (err) return done(err); - calls++; - }); - doc[method](function(err) { - if (err) return done(err); - expect(calls).equal(1); - expect(doc.version).equal(1); - expect(doc.data).eql({age: 3}); - done(); - }); - setTimeout(function() { - doc.resume(); - }, 10); - }); - it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc2.create({age: 5}, function(err) { - if (err) return done(err); + it(method + ' will wait for write when doc is locally created', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); doc.pause(); var calls = 0; doc.create({age: 3}, function(err) { - expect(err).ok(); + if (err) return done(err); calls++; }); doc[method](function(err) { if (err) return done(err); expect(calls).equal(1); expect(doc.version).equal(1); - expect(doc.data).eql({age: 5}); + expect(doc.data).eql({age: 3}); done(); }); setTimeout(function() { doc.resume(); }, 10); }); - }); - }); - it('unsubscribe calls back immediately on disconnect', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - doc.unsubscribe(done); - doc.connection.close(); + it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc2.create({age: 5}, function(err) { + if (err) return done(err); + doc.pause(); + var calls = 0; + doc.create({age: 3}, function(err) { + expect(err).ok(); + calls++; + }); + doc[method](function(err) { + if (err) return done(err); + expect(calls).equal(1); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 5}); + done(); + }); + setTimeout(function() { + doc.resume(); + }, 10); + }); + }); }); - }); - it('unsubscribe calls back immediately when already disconnected', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - doc.connection.close(); - doc.unsubscribe(done); + it('unsubscribe calls back immediately on disconnect', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + doc.unsubscribe(done); + doc.connection.close(); + }); }); - }); - it('subscribed client gets create from other client', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc2.subscribe(function(err) { - if (err) return done(err); - doc2.on('create', function(context) { - expect(context).equal(false); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); + it('unsubscribe calls back immediately when already disconnected', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + doc.connection.close(); + doc.unsubscribe(done); }); - doc.create({age: 3}); }); - }); - it('subscribed client gets op from other client', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); + it('subscribed client gets create from other client', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - expect(doc2.version).eql(2); - expect(doc2.data).eql({age: 4}); + doc2.on('create', function(context) { + expect(context).equal(false); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); done(); }); - doc.submitOp({p: ['age'], na: 1}); + doc.create({age: 3}); }); }); - }); - it('disconnecting stops op updates', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('subscribed client gets op from other client', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + expect(doc2.version).eql(2); + expect(doc2.data).eql({age: 4}); + done(); + }); + doc.submitOp({p: ['age'], na: 1}); }); - doc2.connection.close(); - doc.submitOp({p: ['age'], na: 1}, done); }); }); - }); - it('backend.suppressPublish stops op updates', function(done) { - var backend = this.backend; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('disconnecting stops op updates', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc2.connection.close(); + doc.submitOp({p: ['age'], na: 1}, done); }); - backend.suppressPublish = true; - doc.submitOp({p: ['age'], na: 1}, done); }); }); - }); - it('unsubscribe stops op updates', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('backend.suppressPublish stops op updates', function(done) { + var backend = this.backend; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(); - }); - doc2.unsubscribe(function(err) { + doc2.subscribe(function(err) { if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + backend.suppressPublish = true; doc.submitOp({p: ['age'], na: 1}, done); }); }); }); - }); - it('doc destroy stops op updates', function(done) { - var connection1 = this.backend.connect(); - var connection2 = this.backend.connect(); - var doc = connection1.get('dogs', 'fido'); - var doc2 = connection2.get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('unsubscribe stops op updates', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(new Error('Should not get op event')); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc2.unsubscribe(function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 1}, done); + }); }); - doc2.destroy(function(err) { + }); + }); + + it('doc destroy stops op updates', function(done) { + var connection1 = this.backend.connect(); + var connection2 = this.backend.connect(); + var doc = connection1.get('dogs', 'fido'); + var doc2 = connection2.get('dogs', 'fido'); + doc.create({age: 3}, function(err) { + if (err) return done(err); + doc2.subscribe(function(err) { if (err) return done(err); - expect(connection2.getExisting('dogs', 'fido')).equal(undefined); - doc.submitOp({p: ['age'], na: 1}, done); + doc2.on('op', function(op, context) { + done(new Error('Should not get op event')); + }); + doc2.destroy(function(err) { + if (err) return done(err); + expect(connection2.getExisting('dogs', 'fido')).equal(undefined); + doc.submitOp({p: ['age'], na: 1}, done); + }); }); }); }); - }); - it('doc destroy removes doc from connection when doc is not subscribed', function(done) { - var connection = this.backend.connect(); - var doc = connection.get('dogs', 'fido'); - expect(connection.getExisting('dogs', 'fido')).equal(doc); - doc.destroy(function(err) { - if (err) return done(err); - expect(connection.getExisting('dogs', 'fido')).equal(undefined); - done(); + it('doc destroy removes doc from connection when doc is not subscribed', function(done) { + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + expect(connection.getExisting('dogs', 'fido')).equal(doc); + doc.destroy(function(err) { + if (err) return done(err); + expect(connection.getExisting('dogs', 'fido')).equal(undefined); + done(); + }); }); - }); - it('bulk unsubscribe stops op updates', function(done) { - var connection = this.backend.connect(); - var connection2 = this.backend.connect(); - var doc = connection.get('dogs', 'fido'); - var fido = connection2.get('dogs', 'fido'); - var spot = connection2.get('dogs', 'spot'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - async.parallel([ - function(cb) { - fido.subscribe(cb); - }, - function(cb) { - spot.subscribe(cb); - } - ], function(err) { + it('bulk unsubscribe stops op updates', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + var fido = connection2.get('dogs', 'fido'); + var spot = connection2.get('dogs', 'spot'); + doc.create({age: 3}, function(err) { if (err) return done(err); - fido.connection.startBulk(); async.parallel([ function(cb) { - fido.unsubscribe(cb); + fido.subscribe(cb); }, function(cb) { - spot.unsubscribe(cb); + spot.subscribe(cb); } ], function(err) { if (err) return done(err); - fido.on('op', function(op, context) { - done(); + fido.connection.startBulk(); + async.parallel([ + function(cb) { + fido.unsubscribe(cb); + }, + function(cb) { + spot.unsubscribe(cb); + } + ], function(err) { + if (err) return done(err); + fido.on('op', function(op, context) { + done(); + }); + doc.submitOp({p: ['age'], na: 1}, done); }); - doc.submitOp({p: ['age'], na: 1}, done); + fido.connection.endBulk(); }); - fido.connection.endBulk(); }); }); - }); - it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) { - var backend = this.backend; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) { + var backend = this.backend; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - expect(doc2.version).eql(2); - expect(doc2.data).eql({age: 4}); - done(); - }); - - doc2.connection.close(); - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.subscribe(function(err) { if (err) return done(err); - backend.connect(doc2.connection); + doc2.on('op', function(op, context) { + expect(doc2.version).eql(2); + expect(doc2.data).eql({age: 4}); + done(); + }); + + doc2.connection.close(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + backend.connect(doc2.connection); + }); }); }); }); - }); - it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(); - doc2.unsubscribe(); - doc2.subscribe(function(err) { + it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { - done(); + doc2.subscribe(); + doc2.unsubscribe(); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc.submitOp({p: ['age'], na: 1}); }); - doc.submitOp({p: ['age'], na: 1}); }); }); - }); - it('doc fetches ops to catch up if it receives a future op', function(done) { - var backend = this.backend; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('doc fetches ops to catch up if it receives a future op', function(done) { + var backend = this.backend; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + doc.create({age: 3}, function(err) { if (err) return done(err); - var expected = [ - [{p: ['age'], na: 1}], - [{p: ['age'], na: 5}], - ]; - doc2.on('op', function(op, context) { - var item = expected.shift(); - expect(op).eql(item); - if (expected.length) return; - expect(doc2.version).equal(3); - expect(doc2.data).eql({age: 9}); - done(); - }); - backend.suppressPublish = true; - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.subscribe(function(err) { if (err) return done(err); - backend.suppressPublish = false; - doc.submitOp({p: ['age'], na: 5}); + var expected = [ + [{p: ['age'], na: 1}], + [{p: ['age'], na: 5}], + ]; + doc2.on('op', function(op, context) { + var item = expected.shift(); + expect(op).eql(item); + if (expected.length) return; + expect(doc2.version).equal(3); + expect(doc2.data).eql({age: 9}); + done(); + }); + backend.suppressPublish = true; + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + backend.suppressPublish = false; + doc.submitOp({p: ['age'], na: 5}); + }); }); }); }); - }); - it('doc fetches ops to catch up if it receives multiple future ops', function(done) { - var backend = this.backend; - var doc = this.backend.connect().get('dogs', 'fido'); - var doc2 = this.backend.connect().get('dogs', 'fido'); - // Delaying op replies will cause multiple future ops to be received - // before the fetch to catch up completes - backend.use('op', function(request, next) { - setTimeout(next, 10 * Math.random()); - }); - doc.create({age: 3}, function(err) { - if (err) return done(err); - doc2.subscribe(function(err) { + it('doc fetches ops to catch up if it receives multiple future ops', function(done) { + var backend = this.backend; + var doc = this.backend.connect().get('dogs', 'fido'); + var doc2 = this.backend.connect().get('dogs', 'fido'); + // Delaying op replies will cause multiple future ops to be received + // before the fetch to catch up completes + backend.use('op', function(request, next) { + setTimeout(next, 10 * Math.random()); + }); + doc.create({age: 3}, function(err) { if (err) return done(err); - var wait = 4; - doc2.on('op', function(op, context) { - if (--wait) return; - expect(doc2.version).eql(5); - expect(doc2.data).eql({age: 122}); - done(); - }); - backend.suppressPublish = true; - doc.submitOp({p: ['age'], na: 1}, function(err) { + doc2.subscribe(function(err) { if (err) return done(err); - backend.suppressPublish = false; - doc.submitOp({p: ['age'], na: 5}, function(err) { + var wait = 4; + doc2.on('op', function(op, context) { + if (--wait) return; + expect(doc2.version).eql(5); + expect(doc2.data).eql({age: 122}); + done(); + }); + backend.suppressPublish = true; + doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 13}, function(err) { + backend.suppressPublish = false; + doc.submitOp({p: ['age'], na: 5}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 100}); + doc.submitOp({p: ['age'], na: 13}, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 100}); + }); }); }); }); }); }); - }); - - describe('doc.subscribed', function() { - it('is set to false initially', function() { - var doc = this.backend.connect().get('dogs', 'fido'); - expect(doc.subscribed).equal(false); - }); - it('remains false before subscribe call completes', function() { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(); - expect(doc.subscribed).equal(false); - }); - - it('is set to true after subscribe completes', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - expect(doc.subscribed).equal(true); - done(); + describe('doc.subscribed', function() { + it('is set to false initially', function() { + var doc = this.backend.connect().get('dogs', 'fido'); + expect(doc.subscribed).equal(false); }); - }); - it('is not set to true after subscribe completes if already unsubscribed', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); + it('remains false before subscribe call completes', function() { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(); expect(doc.subscribed).equal(false); - done(); }); - doc.unsubscribe(); - }); - it('is set to false sychronously in unsubscribe', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - expect(doc.subscribed).equal(true); + it('is set to true after subscribe completes', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + expect(doc.subscribed).equal(true); + done(); + }); + }); + + it('is not set to true after subscribe completes if already unsubscribed', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + expect(doc.subscribed).equal(false); + done(); + }); doc.unsubscribe(); - expect(doc.subscribed).equal(false); - done(); }); - }); - it('is set to false sychronously on disconnect', function(done) { - var doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - expect(doc.subscribed).equal(true); - doc.connection.close(); - expect(doc.subscribed).equal(false); - done(); + it('is set to false sychronously in unsubscribe', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + expect(doc.subscribed).equal(true); + doc.unsubscribe(); + expect(doc.subscribed).equal(false); + done(); + }); + }); + + it('is set to false sychronously on disconnect', function(done) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(function(err) { + if (err) return done(err); + expect(doc.subscribed).equal(true); + doc.connection.close(); + expect(doc.subscribed).equal(false); + done(); + }); }); }); }); -}); }; From a551de3444dd90cbcb08362e4b4e57fe16929f34 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:29:19 +0100 Subject: [PATCH 91/95] Extend Google's ESLint config This change actively extends the Google ESLint config. We move the config into a `.js` file so that we can add comments about particular rules that we've overridden. This change also fixes some trailing comma and indentation linting errors. --- .eslintrc | 139 ------------------ .eslintrc.js | 43 ++++++ lib/backend.js | 2 +- .../snapshot-timestamp-request.js | 2 +- .../snapshot-version-request.js | 2 +- lib/query-emitter.js | 4 +- package.json | 1 + test/client/snapshot-timestamp-request.js | 4 +- test/client/snapshot-version-request.js | 8 +- test/client/subscribe.js | 2 +- 10 files changed, 56 insertions(+), 151 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0ce02cbc0..000000000 --- a/.eslintrc +++ /dev/null @@ -1,139 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 3 - }, - "rules": { - "no-cond-assign": 0, - "no-irregular-whitespace": 2, - "no-unexpected-multiline": 2, - "curly": [ - 2, - "multi-line" - ], - "guard-for-in": 0, - "no-caller": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-invalid-this": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-new-wrappers": 2, - "no-throw-literal": 2, - "no-with": 2, - "prefer-promise-reject-errors": 2, - "no-unused-vars": [ - 2, - { - "args": "none" - } - ], - "array-bracket-newline": 0, - "array-bracket-spacing": [ - 2, - "never" - ], - "array-element-newline": 0, - "block-spacing": [ - 2, - "never" - ], - "brace-style": 2, - "camelcase": [ - 2, - { - "properties": "never" - } - ], - "comma-spacing": 2, - "comma-style": 2, - "computed-property-spacing": 2, - "eol-last": 2, - "func-call-spacing": 2, - "indent": [ - "error", - 2, - { - "CallExpression": { - "arguments": 1 - }, - "FunctionDeclaration": { - "body": 1, - "parameters": 1 - }, - "FunctionExpression": { - "body": 1, - "parameters": 1 - }, - "MemberExpression": 1, - "ObjectExpression": 1, - "SwitchCase": 1, - "ignoredNodes": [ - "ConditionalExpression" - ] - } - ], - "key-spacing": 2, - "keyword-spacing": 2, - "linebreak-style": 2, - "max-len": [ - 2, - { - "code": 120, - "tabWidth": 2, - "ignoreUrls": true, - } - ], - "new-cap": 2, - "no-array-constructor": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multiple-empty-lines": [ - 2, - { - "max": 2 - } - ], - "no-new-object": 2, - "no-tabs": 2, - "no-trailing-spaces": 2, - "object-curly-spacing": 2, - "one-var": [ - 2, - { - "var": "never", - "let": "never", - "const": "never" - } - ], - "padded-blocks": [ - 2, - "never" - ], - "quote-props": [ - 2, - "as-needed" - ], - "quotes": [ - 2, - "single", - { - "allowTemplateLiterals": true - } - ], - "semi": 2, - "semi-spacing": 2, - "space-before-blocks": 2, - "space-before-function-paren": [ - 2, - { - "asyncArrow": "always", - "anonymous": "never", - "named": "never" - } - ], - "spaced-comment": [ - 2, - "always" - ], - "switch-colon-spacing": 2, - }, -} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..475941fc2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,43 @@ +// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has +// been set, it will still error even if it's not applicable to that version number. Since Google sets these +// rules, we have to turn them off ourselves. +const DISABLED_ES6_OPTIONS = { + 'no-var': 'off', + 'prefer-rest-params': 'off' +}; + +const SHAREDB_RULES = { + // Comma dangle is not supported in ES3 + 'comma-dangle': ['error', 'never'], + // We control our own objects and prototypes, so no need for this check + 'guard-for-in': 'off', + // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have + // to override ESLint's default of 0 indents for this. + 'indent': ['error', 2, { + 'SwitchCase': 1 + }], + // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code + 'max-len': ['error', + { + code: 120, + tabWidth: 2, + ignoreUrls: true, + } + ], + // as-needed quote props are easier to write + 'quote-props': ['error', 'as-needed'], + 'require-jsdoc': 'off', + 'valid-jsdoc': 'off' +}; + +module.exports = { + extends: 'google', + parserOptions: { + ecmaVersion: 3 + }, + rules: Object.assign( + {}, + DISABLED_ES6_OPTIONS, + SHAREDB_RULES + ), +}; diff --git a/lib/backend.js b/lib/backend.js index 1d5c4fe4f..ae11aa450 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -616,7 +616,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac query: query, options: options, db: null, - snapshotProjection: null, + snapshotProjection: null }; var backend = this; backend.trigger(backend.MIDDLEWARE_ACTIONS.query, agent, request, function(err) { diff --git a/lib/client/snapshot-request/snapshot-timestamp-request.js b/lib/client/snapshot-request/snapshot-timestamp-request.js index 0e8af04af..15789137b 100644 --- a/lib/client/snapshot-request/snapshot-timestamp-request.js +++ b/lib/client/snapshot-request/snapshot-timestamp-request.js @@ -21,6 +21,6 @@ SnapshotTimestampRequest.prototype._message = function() { id: this.requestId, c: this.collection, d: this.id, - ts: this.timestamp, + ts: this.timestamp }; }; diff --git a/lib/client/snapshot-request/snapshot-version-request.js b/lib/client/snapshot-request/snapshot-version-request.js index 0b64b3821..d352a676a 100644 --- a/lib/client/snapshot-request/snapshot-version-request.js +++ b/lib/client/snapshot-request/snapshot-version-request.js @@ -21,6 +21,6 @@ SnapshotVersionRequest.prototype._message = function() { id: this.requestId, c: this.collection, d: this.id, - v: this.version, + v: this.version }; }; diff --git a/lib/query-emitter.js b/lib/query-emitter.js index 77b91f8b8..fe5c5b01a 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -21,10 +21,10 @@ function QueryEmitter(request, stream, ids, extra) { this.canPollDoc = this.db.canPollDoc(this.collection, this.query); this.pollDebounce = (typeof this.options.pollDebounce === 'number') ? this.options.pollDebounce : - (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0; + (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0; this.pollInterval = (typeof this.options.pollInterval === 'number') ? this.options.pollInterval : - (typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0; + (typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0; this._polling = false; this._pendingPoll = null; diff --git a/package.json b/package.json index 9e4e5b09c..058190e06 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "coveralls": "^2.11.8", "eslint": "^5.16.0", + "eslint-config-google": "^0.13.0", "expect.js": "^0.3.1", "istanbul": "^0.4.2", "lolex": "^3.0.0", diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 0aeae9f9d..83b24644e 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -340,7 +340,7 @@ describe('SnapshotTimestampRequest', function() { function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); - }, + } ]; backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) { @@ -354,7 +354,7 @@ describe('SnapshotTimestampRequest', function() { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ function(request, callback) { callback({message: 'foo'}); - }, + } ]; backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index 2f959be07..e55fcf2fc 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -278,7 +278,7 @@ describe('SnapshotVersionRequest', function() { function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); - }, + } ]; backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) { @@ -292,7 +292,7 @@ describe('SnapshotVersionRequest', function() { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ function(request, callback) { callback({message: 'foo'}); - }, + } ]; backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { @@ -353,7 +353,7 @@ describe('SnapshotVersionRequest', function() { v: 1, type: 'http://sharejs.org/types/JSONv0', data: { - title: 'Catch 22', + title: 'Catch 22' }, m: null }); @@ -386,7 +386,7 @@ describe('SnapshotVersionRequest', function() { v: 3, type: 'http://sharejs.org/types/JSONv0', data: { - title: 'The Restaurant at the End of the Universe', + title: 'The Restaurant at the End of the Universe' }, m: null }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 083634fb5..0674d5bb0 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -576,7 +576,7 @@ module.exports = function() { if (err) return done(err); var expected = [ [{p: ['age'], na: 1}], - [{p: ['age'], na: 5}], + [{p: ['age'], na: 5}] ]; doc2.on('op', function(op, context) { var item = expected.shift(); From 3dfc93863a0ebc264c379d528326fd0a8737a864 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:30:22 +0100 Subject: [PATCH 92/95] Use `.gitignore` for defining ESLint's ignore pattern This change uses our `.gitignore` file to define which files ESLint should ignore, so that all of our committed JavaScript has a consistent style. --- examples/leaderboard/server/index.js | 19 ++++++++++--------- examples/textarea/client.js | 26 +++++++++++++------------- examples/textarea/server.js | 2 +- package.json | 3 ++- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js index 25410f8bb..2c10932a1 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -1,14 +1,13 @@ -var http = require("http"); -var ShareDB = require("sharedb"); -var connect = require("connect"); +var http = require('http'); +var ShareDB = require('sharedb'); +var connect = require('connect'); var serveStatic = require('serve-static'); var ShareDBMingoMemory = require('sharedb-mingo-memory'); var WebSocketJSONStream = require('@teamwork/websocket-json-stream'); var WebSocket = require('ws'); -var util = require('util'); // Start ShareDB -var share = ShareDB({db: new ShareDBMingoMemory()}); +var share = new ShareDB({db: new ShareDBMingoMemory()}); // Create a WebSocket server var app = connect(); @@ -16,7 +15,7 @@ app.use(serveStatic('.')); var server = http.createServer(app); var wss = new WebSocket.Server({server: server}); server.listen(8080); -console.log("Listening on http://localhost:8080"); +console.log('Listening on http://localhost:8080'); // Connect any incoming WebSocket connection with ShareDB wss.on('connection', function(ws, req) { @@ -27,11 +26,13 @@ wss.on('connection', function(ws, req) { // Create initial documents var connection = share.connect(); connection.createFetchQuery('players', {}, {}, function(err, results) { - if (err) { throw err; } + if (err) { + throw err; + } if (results.length === 0) { - var names = ["Ada Lovelace", "Grace Hopper", "Marie Curie", - "Carl Friedrich Gauss", "Nikola Tesla", "Claude Shannon"]; + var names = ['Ada Lovelace', 'Grace Hopper', 'Marie Curie', + 'Carl Friedrich Gauss', 'Nikola Tesla', 'Claude Shannon']; names.forEach(function(name, index) { var doc = connection.get('players', ''+index); diff --git a/examples/textarea/client.js b/examples/textarea/client.js index dadaef7fa..4cbf55208 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -2,35 +2,35 @@ var sharedb = require('sharedb/lib/client'); var StringBinding = require('sharedb-string-binding'); // Open WebSocket connection to ShareDB server -const WebSocket = require('reconnecting-websocket'); +var WebSocket = require('reconnecting-websocket'); var socket = new WebSocket('ws://' + window.location.host); var connection = new sharedb.Connection(socket); var element = document.querySelector('textarea'); var statusSpan = document.getElementById('status-span'); -statusSpan.innerHTML = "Not Connected" +statusSpan.innerHTML = 'Not Connected'; -element.style.backgroundColor = "gray"; -socket.onopen = function(){ - statusSpan.innerHTML = "Connected" - element.style.backgroundColor = "white"; +element.style.backgroundColor = 'gray'; +socket.onopen = function() { + statusSpan.innerHTML = 'Connected'; + element.style.backgroundColor = 'white'; }; -socket.onclose = function(){ - statusSpan.innerHTML = "Closed" - element.style.backgroundColor = "gray"; +socket.onclose = function() { + statusSpan.innerHTML = 'Closed'; + element.style.backgroundColor = 'gray'; }; socket.onerror = function() { - statusSpan.innerHTML = "Error" - element.style.backgroundColor = "red"; -} + statusSpan.innerHTML = 'Error'; + element.style.backgroundColor = 'red'; +}; // Create local Doc instance mapped to 'examples' collection document with id 'textarea' var doc = connection.get('examples', 'textarea'); doc.subscribe(function(err) { if (err) throw err; - + var binding = new StringBinding(element, doc, ['content']); binding.setup(); }); diff --git a/examples/textarea/server.js b/examples/textarea/server.js index 9d359a771..ee419ba1e 100644 --- a/examples/textarea/server.js +++ b/examples/textarea/server.js @@ -14,7 +14,7 @@ function createDoc(callback) { doc.fetch(function(err) { if (err) throw err; if (doc.type === null) { - doc.create({ content: '' }, callback); + doc.create({content: ''}, callback); return; } callback(); diff --git a/package.json b/package.json index 058190e06..b30da8173 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "scripts": { "test": "./node_modules/.bin/mocha && npm run lint", "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha", - "lint": "./node_modules/.bin/eslint 'lib/**/*.js' 'test/**/*.js'" + "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", + "lint:fix": "npm run lint -- --fix" }, "repository": { "type": "git", From df5a466b43cd951e7fb5abd010bb463f04ecef86 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:40:19 +0100 Subject: [PATCH 93/95] Reset `no-unused-vars` linting rule to default --- .eslintrc.js | 2 + examples/counter/server.js | 2 +- examples/leaderboard/server/index.js | 2 +- examples/rich-text/server.js | 2 +- examples/textarea/server.js | 2 +- lib/db/memory.js | 2 +- test/client/number-type.js | 2 +- test/client/query-subscribe.js | 4 +- test/client/snapshot-timestamp-request.js | 5 +- test/client/snapshot-version-request.js | 5 +- test/client/subscribe.js | 22 +++---- test/db-memory.js | 2 +- test/db.js | 77 +++++++++++++---------- test/middleware.js | 2 +- test/pubsub.js | 5 +- 15 files changed, 77 insertions(+), 59 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 475941fc2..8c5f1133e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,8 @@ const SHAREDB_RULES = { ignoreUrls: true, } ], + // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused variables + 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], // as-needed quote props are easier to write 'quote-props': ['error', 'as-needed'], 'require-jsdoc': 'off', diff --git a/examples/counter/server.js b/examples/counter/server.js index 8817a4584..f6575bfeb 100644 --- a/examples/counter/server.js +++ b/examples/counter/server.js @@ -29,7 +29,7 @@ function startServer() { // Connect any incoming WebSocket connection to ShareDB var wss = new WebSocket.Server({server: server}); - wss.on('connection', function(ws, req) { + wss.on('connection', function(ws) { var stream = new WebSocketJSONStream(ws); backend.listen(stream); }); diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js index 2c10932a1..6aebe997f 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -18,7 +18,7 @@ server.listen(8080); console.log('Listening on http://localhost:8080'); // Connect any incoming WebSocket connection with ShareDB -wss.on('connection', function(ws, req) { +wss.on('connection', function(ws) { var stream = new WebSocketJSONStream(ws); share.listen(stream); }); diff --git a/examples/rich-text/server.js b/examples/rich-text/server.js index 486130f95..f0654cdf8 100644 --- a/examples/rich-text/server.js +++ b/examples/rich-text/server.js @@ -32,7 +32,7 @@ function startServer() { // Connect any incoming WebSocket connection to ShareDB var wss = new WebSocket.Server({server: server}); - wss.on('connection', function(ws, req) { + wss.on('connection', function(ws) { var stream = new WebSocketJSONStream(ws); backend.listen(stream); }); diff --git a/examples/textarea/server.js b/examples/textarea/server.js index ee419ba1e..165c8ea0d 100644 --- a/examples/textarea/server.js +++ b/examples/textarea/server.js @@ -29,7 +29,7 @@ function startServer() { // Connect any incoming WebSocket connection to ShareDB var wss = new WebSocket.Server({server: server}); - wss.on('connection', function(ws, req) { + wss.on('connection', function(ws) { var stream = new WebSocketJSONStream(ws); backend.listen(stream); }); diff --git a/lib/db/memory.js b/lib/db/memory.js index 9c1a47b56..029c1229b 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -122,7 +122,7 @@ MemoryDB.prototype.query = function(collection, query, fields, options, callback // two properties: // - snapshots: array of query result snapshots // - extra: (optional) other types of results, such as counts -MemoryDB.prototype._querySync = function(snapshots, query, options) { +MemoryDB.prototype._querySync = function(snapshots) { return {snapshots: snapshots}; }; diff --git a/test/client/number-type.js b/test/client/number-type.js index d25401ebc..fa95056e4 100644 --- a/test/client/number-type.js +++ b/test/client/number-type.js @@ -18,6 +18,6 @@ function apply(snapshot, op) { return snapshot + op; } -function transform(op1, op2, side) { +function transform(op1) { return op1; } diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index ddce30d64..166c859eb 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -306,7 +306,7 @@ module.exports = function(options) { if (err) return done(err); connection.get('dogs', 'fido').create({}); }); - query.on('insert', function(docs, index) { + query.on('insert', function(docs) { expect(util.pluck(docs, 'id')).eql(['fido']); done(); }); @@ -320,7 +320,7 @@ module.exports = function(options) { if (err) return done(err); connection.get('dogs', 'fido').create({}); }); - query.on('insert', function(docs, index) { + query.on('insert', function(docs) { expect(util.pluck(docs, 'id')).eql(['fido']); done(); }); diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 83b24644e..8d7cc4bf3 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -227,7 +227,8 @@ describe('SnapshotTimestampRequest', function() { it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error, snapshot) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) { + if (error) return done(error); expect(connection.hasPending()).to.be(false); done(); }); @@ -357,7 +358,7 @@ describe('SnapshotTimestampRequest', function() { } ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) { expect(error.message).to.be('foo'); done(); }); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index e55fcf2fc..a39355f38 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -165,7 +165,8 @@ describe('SnapshotVersionRequest', function() { it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', null, function(error, snapshot) { + connection.fetchSnapshot('books', 'don-quixote', null, function(error) { + if (error) return done(error); expect(connection.hasPending()).to.be(false); done(); }); @@ -295,7 +296,7 @@ describe('SnapshotVersionRequest', function() { } ]; - backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) { expect(error.message).to.be('foo'); done(); }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 0674d5bb0..2af398681 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -229,7 +229,7 @@ module.exports = function() { if (err) return done(err); doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2[method](); @@ -396,7 +396,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { expect(doc2.version).eql(2); expect(doc2.data).eql({age: 4}); done(); @@ -413,7 +413,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2.connection.close(); @@ -430,7 +430,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); backend.suppressPublish = true; @@ -446,7 +446,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2.unsubscribe(function(err) { @@ -466,7 +466,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(new Error('Should not get op event')); }); doc2.destroy(function(err) { @@ -516,7 +516,7 @@ module.exports = function() { } ], function(err) { if (err) return done(err); - fido.on('op', function(op, context) { + fido.on('op', function() { done(); }); doc.submitOp({p: ['age'], na: 1}, done); @@ -534,7 +534,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { expect(doc2.version).eql(2); expect(doc2.data).eql({age: 4}); done(); @@ -558,7 +558,7 @@ module.exports = function() { doc2.unsubscribe(); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc.submitOp({p: ['age'], na: 1}); @@ -578,7 +578,7 @@ module.exports = function() { [{p: ['age'], na: 1}], [{p: ['age'], na: 5}] ]; - doc2.on('op', function(op, context) { + doc2.on('op', function(op) { var item = expected.shift(); expect(op).eql(item); if (expected.length) return; @@ -610,7 +610,7 @@ module.exports = function() { doc2.subscribe(function(err) { if (err) return done(err); var wait = 4; - doc2.on('op', function(op, context) { + doc2.on('op', function() { if (--wait) return; expect(doc2.version).eql(5); expect(doc2.data).eql({age: 122}); diff --git a/test/db-memory.js b/test/db-memory.js index b8b8e064e..0f0d01a92 100644 --- a/test/db-memory.js +++ b/test/db-memory.js @@ -64,7 +64,7 @@ function BasicQueryableMemoryDB() { BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype); BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB; -BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query, options) { +BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) { if (query.filter) { snapshots = snapshots.filter(function(snapshot) { for (var queryKey in query.filter) { diff --git a/test/db.js b/test/db.js index 270a2ea92..701bdb7a0 100644 --- a/test/db.js +++ b/test/db.js @@ -238,7 +238,7 @@ module.exports = function(options) { var data = {x: 5, y: 6}; var op = {v: 0, create: {type: 'json0', data: data}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getSnapshot('testcollection', 'test', null, null, function(err, result) { if (err) return done(err); @@ -251,6 +251,7 @@ module.exports = function(options) { it('getSnapshot does not return committed metadata by default', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.getSnapshot('testcollection', 'test', null, null, function(err, result) { if (err) return done(err); expect(result.m).equal(null); @@ -262,6 +263,7 @@ module.exports = function(options) { it('getSnapshot returns metadata when option is true', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.getSnapshot('testcollection', 'test', null, {metadata: true}, function(err, result) { if (err) return done(err); expect(result.m).eql({test: 3}); @@ -273,6 +275,7 @@ module.exports = function(options) { it('getSnapshot returns metadata when fields is {$submit: true}', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, result) { if (err) return done(err); expect(result.m).eql({test: 3}); @@ -287,7 +290,7 @@ module.exports = function(options) { var data = {x: 5, y: 6}; var op = {v: 0, create: {type: 'json0', data: data}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) { if (err) return done(err); @@ -303,6 +306,7 @@ module.exports = function(options) { it('getSnapshotBulk does not return committed metadata by default', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) { if (err) return done(err); expect(resultMap.test.m).equal(null); @@ -314,6 +318,7 @@ module.exports = function(options) { it('getSnapshotBulk returns metadata when option is true', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.getSnapshotBulk('testcollection', ['test2', 'test'], null, {metadata: true}, function(err, resultMap) { if (err) return done(err); expect(resultMap.test.m).eql({test: 3}); @@ -335,7 +340,7 @@ module.exports = function(options) { it('getOps returns 1 committed op', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', 0, null, null, function(err, ops) { if (err) return done(err); @@ -349,9 +354,9 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', 0, null, null, function(err, ops) { if (err) return done(err); @@ -366,9 +371,9 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', null, null, null, function(err, ops) { if (err) return done(err); @@ -383,9 +388,9 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', 1, null, null, function(err, ops) { if (err) return done(err); @@ -400,9 +405,9 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', 0, 1, null, function(err, ops) { if (err) return done(err); @@ -416,7 +421,7 @@ module.exports = function(options) { it('getOps does not return committed metadata by default', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', null, null, null, function(err, ops) { if (err) return done(err); @@ -429,7 +434,7 @@ module.exports = function(options) { it('getOps returns metadata when option is true', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getOps('testcollection', 'test', null, null, {metadata: true}, function(err, ops) { if (err) return done(err); @@ -455,9 +460,9 @@ module.exports = function(options) { it('getOpsBulk returns committed ops', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: 0, test2: 0}, null, null, function(err, opsMap) { if (err) return done(err); @@ -474,9 +479,9 @@ module.exports = function(options) { it('getOpsBulk returns all ops committed from null', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: null, test2: null}, null, null, function(err, opsMap) { if (err) return done(err); @@ -494,13 +499,13 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op1, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: 0, test2: 1}, null, null, function(err, opsMap) { if (err) return done(err); @@ -520,13 +525,13 @@ module.exports = function(options) { var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var op1 = {v: 1, op: [{p: ['x'], na: 1}]}; var db = this.db; - submit(db, 'testcollection', 'test', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op0, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op0, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test', op1, function(err) { if (err) return done(err); - submit(db, 'testcollection', 'test2', op1, function(err, succeeded) { + submit(db, 'testcollection', 'test2', op1, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: 1, test2: 0}, {test2: 1}, null, function(err, opsMap) { if (err) return done(err); @@ -545,7 +550,7 @@ module.exports = function(options) { it('getOpsBulk does not return committed metadata by default', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: null}, null, null, function(err, opsMap) { if (err) return done(err); @@ -558,7 +563,7 @@ module.exports = function(options) { it('getOpsBulk returns metadata when option is true', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getOpsBulk('testcollection', {test: null}, null, {metadata: true}, function(err, opsMap) { if (err) return done(err); @@ -573,7 +578,7 @@ module.exports = function(options) { it('getOpsToSnapshot returns committed op', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) { if (err) return done(err); @@ -589,7 +594,7 @@ module.exports = function(options) { it('getOpsToSnapshot does not return committed metadata by default', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) { if (err) return done(err); @@ -605,7 +610,7 @@ module.exports = function(options) { it('getOpsToSnapshot returns metadata when option is true', function(done) { var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}}; var db = this.db; - submit(db, 'testcollection', 'test', op, function(err, succeeded) { + submit(db, 'testcollection', 'test', op, function(err) { if (err) return done(err); db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) { if (err) return done(err); @@ -623,7 +628,7 @@ module.exports = function(options) { it('query returns data in the collection', function(done) { var snapshot = {v: 1, type: 'json0', data: {x: 5, y: 6}, m: null}; var db = this.db; - db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) { + db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { if (err) return done(err); db.query('testcollection', {x: 5}, null, null, function(err, results) { if (err) return done(err); @@ -645,6 +650,7 @@ module.exports = function(options) { it('query does not return committed metadata by default', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, null, null, function(err, results) { if (err) return done(err); expect(results[0].m).equal(null); @@ -656,6 +662,7 @@ module.exports = function(options) { it('query returns metadata when option is true', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, null, {metadata: true}, function(err, results) { if (err) return done(err); expect(results[0].m).eql({test: 3}); @@ -672,6 +679,7 @@ module.exports = function(options) { var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, {y: true}, null, function(err, results) { if (err) return done(err); expect(results).eql([{type: 'json0', v: 1, data: {y: 6}, id: 'test'}]); @@ -686,6 +694,7 @@ module.exports = function(options) { var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, {}, null, function(err, results) { if (err) return done(err); expect(results).eql([{type: 'json0', v: 1, data: {}, id: 'test'}]); @@ -697,6 +706,7 @@ module.exports = function(options) { it('query does not return committed metadata by default with projection', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, {x: true}, null, function(err, results) { if (err) return done(err); expect(results[0].m).equal(null); @@ -708,6 +718,7 @@ module.exports = function(options) { it('query returns metadata when option is true with projection', function(done) { var db = this.db; commitSnapshotWithMetadata(db, function(err) { + if (err) return done(err); db.query('testcollection', {x: 5}, {x: true}, {metadata: true}, function(err, results) { if (err) return done(err); expect(results[0].m).eql({test: 3}); @@ -721,7 +732,7 @@ module.exports = function(options) { it('returns data in the collection', function(done) { var snapshot = {v: 1, type: 'json0', data: {x: 5, y: 6}}; var db = this.db; - db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) { + db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { if (err) return done(err); db.queryPoll('testcollection', {x: 5}, null, function(err, ids) { if (err) return done(err); @@ -760,6 +771,7 @@ module.exports = function(options) { var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { + if (err) return done(err); db.queryPollDoc('testcollection', 'test', query, null, function(err, result) { if (err) return done(err); expect(result).equal(true); @@ -775,6 +787,7 @@ module.exports = function(options) { var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}}; var db = this.db; db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) { + if (err) return done(err); db.queryPollDoc('testcollection', 'test', query, null, function(err, result) { if (err) return done(err); expect(result).equal(false); diff --git a/test/middleware.js b/test/middleware.js index f71ddce5d..85ceb686e 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -21,7 +21,7 @@ describe('middleware', function() { describe('use', function() { it('returns itself to allow chaining', function() { - var response = this.backend.use('submit', function(request, next) {}); + var response = this.backend.use('submit', function() {}); expect(response).equal(this.backend); }); }); diff --git a/test/pubsub.js b/test/pubsub.js index 4bf6cbf1a..57c193095 100644 --- a/test/pubsub.js +++ b/test/pubsub.js @@ -38,7 +38,7 @@ module.exports = function(create) { it('publish optional callback returns', function(done) { var pubsub = this.pubsub; - pubsub.subscribe('x', function(err, stream) { + pubsub.subscribe('x', function(err) { if (err) done(err); pubsub.publish(['x'], {test: true}, done); }); @@ -46,7 +46,8 @@ module.exports = function(create) { it('can subscribe to a channel twice', function(done) { var pubsub = this.pubsub; - pubsub.subscribe('y', function(err, stream) { + pubsub.subscribe('y', function(err) { + if (err) done(err); pubsub.subscribe('y', function(err, stream) { if (err) done(err); stream.on('data', function(data) { From 40abc1740b8e44fa93a14240b526c11cce8778ea Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:47:58 +0100 Subject: [PATCH 94/95] Review markups This change makes some linting review markups: - pre-comupte long, concatenated strings - only allow one statement per line --- .eslintrc.js | 2 ++ lib/agent.js | 4 +++- lib/backend.js | 17 +++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 8c5f1133e..ab65eaee8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,8 @@ const SHAREDB_RULES = { ], // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused variables 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], + // It's more readable to ensure we only have one statement per line + 'max-statements-per-line': ['error', {max: 1}], // as-needed quote props are easier to write 'quote-props': ['error', 'as-needed'], 'require-jsdoc': 'off', diff --git a/lib/agent.js b/lib/agent.js index db18c4a5d..f548f4d71 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -332,7 +332,9 @@ Agent.prototype._handleMessage = function(request, callback) { }; function getQueryOptions(request) { var results = request.r; - var ids; var fetch; var fetchOps; + var ids; + var fetch; + var fetchOps; if (results) { ids = []; for (var i = 0; i < results.length; i++) { diff --git a/lib/backend.js b/lib/backend.js index ae11aa450..dc3b025ce 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -15,6 +15,13 @@ var SubmitRequest = require('./submit-request'); var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; +var DOC_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. ' + + 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.'; + +var AFFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" middleware action. ' + + 'Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' + + 'disable the "after submit" action and this warning.'; + function Backend(options) { if (!(this instanceof Backend)) return new Backend(options); emitter.EventEmitter.call(this); @@ -94,10 +101,7 @@ Backend.prototype.SNAPSHOT_TYPES = { Backend.prototype._shimDocAction = function() { if (warnDeprecatedDoc) { warnDeprecatedDoc = false; - console.warn([ - 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead.', - 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.' - ].join(' ')); + console.warn(DOC_ACTION_DEPRECATION_WARNING); } var backend = this; @@ -114,10 +118,7 @@ Backend.prototype._shimDocAction = function() { Backend.prototype._shimAfterSubmit = function() { if (warnDeprecatedAfterSubmit) { warnDeprecatedAfterSubmit = false; - console.warn([ - 'DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass', - '`disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.' - ].join(' ')); + console.warn(AFFTER_SUBMIT_ACTION_DEPRECATION_WARNING); } var backend = this; From 14b0d31c7ae1b0be075d46267d6c4b549f427e8f Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 17 Jul 2019 10:02:38 -0700 Subject: [PATCH 95/95] eslint --fix, plus a bit of manual line wrapping to get under 120 chars --- lib/agent.js | 1 - lib/client/doc.js | 6 +- lib/presence/dummy.js | 12 +- lib/presence/index.js | 8 +- lib/presence/stateless.js | 65 +++---- lib/util.js | 2 +- test/client/presence-type.js | 6 +- test/client/presence.js | 346 ++++++++++++++++++----------------- test/client/submit.js | 4 +- 9 files changed, 231 insertions(+), 219 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 519157987..dbd35318c 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -2,7 +2,6 @@ var hat = require('hat'); var util = require('./util'); var types = require('./types'); var logger = require('./logger'); -var ShareDBError = require('./error'); /** * Agent deserializes the wire protocol messages received from the stream and diff --git a/lib/client/doc.js b/lib/client/doc.js index 571fc0e03..cb504c68a 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -359,11 +359,11 @@ Doc.prototype._handleOp = function(err, message) { } }; -Doc.prototype._handlePresence = function (err, presence) { +Doc.prototype._handlePresence = function(err, presence) { this._docPresence.handlePresence(err, presence); }; -Doc.prototype.submitPresence = function (data, callback) { +Doc.prototype.submitPresence = function(data, callback) { this._docPresence.submitPresence(data, callback); }; @@ -611,7 +611,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('op', op.op, source); return; } - + this._docPresence.transformAllPresence(op); if (op.create) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 7eda4b13e..423729967 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -8,11 +8,15 @@ */ var presence = require('./index'); -function noop () {} -function returnEmptyArray () { return []; }; -function returnFalse () { return false; }; +function noop() {} +function returnEmptyArray() { + return []; +}; +function returnFalse() { + return false; +}; -function DocPresence () {} +function DocPresence() {} DocPresence.prototype = Object.create(presence.DocPresence.prototype); Object.assign(DocPresence.prototype, { submitPresence: noop, diff --git a/lib/presence/index.js b/lib/presence/index.js index 2d85e8fc5..cddecd0ad 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,6 +1,6 @@ module.exports = { - DocPresence: function DocPresence () {}, - ConnectionPresence: function ConnectionPresence () {}, - AgentPresence: function AgentPresence () {}, - BackendPresence: function BackendPresence () {} + DocPresence: function DocPresence() {}, + ConnectionPresence: function ConnectionPresence() {}, + AgentPresence: function AgentPresence() {}, + BackendPresence: function BackendPresence() {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 8f1d27782..14c186ed7 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -65,14 +65,15 @@ function DocPresence(doc) { DocPresence.prototype = Object.create(presence.DocPresence.prototype); // Submit presence data to a document. -// This is the only public facing method. +// This is the only public facing method. // All the others are marked as internal with a leading "_". -DocPresence.prototype.submitPresence = function (data, callback) { +DocPresence.prototype.submitPresence = function(data, callback) { if (data != null) { if (!this.doc.type) { var doc = this.doc; return process.nextTick(function() { - var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); + var err = new ShareDBError(4015, + 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id); if (callback) return callback(err); doc.emit('error', err); }); @@ -81,7 +82,8 @@ DocPresence.prototype.submitPresence = function (data, callback) { if (!this.doc.type.createPresence || !this.doc.type.transformPresence) { var doc = this.doc; return process.nextTick(function() { - var err = new ShareDBError(4028, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); + var err = new ShareDBError(4028, + 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); if (callback) return callback(err); doc.emit('error', err); }); @@ -97,7 +99,6 @@ DocPresence.prototype.submitPresence = function (data, callback) { if (callback) { this.pending.push(callback); } - } else if (callback) { process.nextTick(callback); } @@ -105,7 +106,7 @@ DocPresence.prototype.submitPresence = function (data, callback) { process.nextTick(this.doc.flush.bind(this.doc)); }; -DocPresence.prototype.handlePresence = function (err, presence) { +DocPresence.prototype.handlePresence = function(err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -156,10 +157,10 @@ DocPresence.prototype.handlePresence = function (err, presence) { this._processReceivedPresence(src, true); }; - + // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed for src. Otherwise false. -DocPresence.prototype._processReceivedPresence = function (src, emit) { +DocPresence.prototype._processReceivedPresence = function(src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -241,7 +242,7 @@ DocPresence.prototype._processReceivedPresence = function (src, emit) { return this._setPresence(src, data, emit); }; -DocPresence.prototype.processAllReceivedPresence = function () { +DocPresence.prototype.processAllReceivedPresence = function() { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -253,7 +254,7 @@ DocPresence.prototype.processAllReceivedPresence = function () { this._emitPresence(changedSrcList, true); }; -DocPresence.prototype._transformPresence = function (src, op) { +DocPresence.prototype._transformPresence = function(src, op) { var presenceData = this.doc.presence[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -263,8 +264,8 @@ DocPresence.prototype._transformPresence = function (src, op) { } return this._setPresence(src, presenceData); }; - -DocPresence.prototype.transformAllPresence = function (op) { + +DocPresence.prototype.transformAllPresence = function(op) { var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -276,7 +277,7 @@ DocPresence.prototype.transformAllPresence = function (op) { this._emitPresence(changedSrcList, false); }; -DocPresence.prototype.pausePresence = function () { +DocPresence.prototype.pausePresence = function() { if (!this) return; if (this.inflight) { @@ -303,7 +304,7 @@ DocPresence.prototype.pausePresence = function () { // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed. Otherwise false. -DocPresence.prototype._setPresence = function (src, data, emit) { +DocPresence.prototype._setPresence = function(src, data, emit) { if (data == null) { if (this.doc.presence[src] == null) return false; delete this.doc.presence[src]; @@ -314,11 +315,11 @@ DocPresence.prototype._setPresence = function (src, data, emit) { if (isPresenceEqual) return false; this.doc.presence[src] = data; } - if (emit) this._emitPresence([ src ], true); + if (emit) this._emitPresence([src], true); return true; }; -DocPresence.prototype._emitPresence = function (srcList, submitted) { +DocPresence.prototype._emitPresence = function(srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { @@ -327,14 +328,14 @@ DocPresence.prototype._emitPresence = function (srcList, submitted) { } }; -DocPresence.prototype.cacheOp = function (message) { +DocPresence.prototype.cacheOp = function(message) { var op = { src: message.src, time: Date.now(), create: !!message.create, op: message.op, del: !!message.del - } + }; // Remove the old ops. var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; @@ -352,8 +353,8 @@ DocPresence.prototype.cacheOp = function (message) { }; // If there are no pending ops, this method sends the pending presence data, if possible. -DocPresence.prototype.flushPresence = function () { - if(!this.inflight && this.pending) { +DocPresence.prototype.flushPresence = function() { + if (!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; @@ -362,17 +363,17 @@ DocPresence.prototype.flushPresence = function () { } }; -DocPresence.prototype.destroyPresence = function () { +DocPresence.prototype.destroyPresence = function() { this.received = {}; this.clearCachedOps(); }; -DocPresence.prototype.clearCachedOps = function () { +DocPresence.prototype.clearCachedOps = function() { this.cachedOps.length = 0; }; // Reset presence-related properties. -DocPresence.prototype.hardRollbackPresence = function () { +DocPresence.prototype.hardRollbackPresence = function() { this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -391,11 +392,11 @@ DocPresence.prototype.hardRollbackPresence = function () { this._emitPresence(changedSrcList, false); }; -DocPresence.prototype.hasPendingPresence = function () { +DocPresence.prototype.hasPendingPresence = function() { return this.inflight || this.pending; }; -DocPresence.prototype.getPendingPresence = function () { +DocPresence.prototype.getPendingPresence = function() { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); @@ -414,7 +415,7 @@ ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototy ConnectionPresence.prototype.isPresenceMessage = isPresenceMessage; -ConnectionPresence.prototype.handlePresenceMessage = function (err, message) { +ConnectionPresence.prototype.handlePresenceMessage = function(err, message) { var doc = this.connection.getExisting(message.c, message.d); if (doc) doc._handlePresence(err, message); }; @@ -451,7 +452,7 @@ AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); AgentPresence.prototype.isPresenceMessage = isPresenceMessage; -AgentPresence.prototype.processPresenceData = function (data) { +AgentPresence.prototype.processPresenceData = function(data) { if (data.a === 'p') { // Send other clients' presence data if (data.src !== this.agent.clientId) this.agent.send(data); @@ -472,14 +473,14 @@ AgentPresence.prototype.createPresence = function(collection, id, data, version, }; }; -AgentPresence.prototype.subscribeToStream = function (collection, id, stream) { +AgentPresence.prototype.subscribeToStream = function(collection, id, stream) { var agent = this.agent; stream.on('end', function() { agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); }); }; -AgentPresence.prototype.checkRequest = function (request) { +AgentPresence.prototype.checkRequest = function(request) { if (request.a === 'p') { if (typeof request.c !== 'string') return 'Invalid collection'; if (typeof request.d !== 'string') return 'Invalid id'; @@ -510,7 +511,7 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { } this.agent.backend.sendPresence(presence, function(err) { if (err) return callback(err); - callback(null, { seq: presence.seq }); + callback(null, {seq: presence.seq}); }); }; @@ -525,9 +526,9 @@ function BackendPresence(backend) { BackendPresence.prototype = Object.create(presence.BackendPresence.prototype); BackendPresence.prototype.sendPresence = function(presence, callback) { - var channels = [ this.backend.getDocChannel(presence.c, presence.d) ]; + var channels = [this.backend.getDocChannel(presence.c, presence.d)]; this.backend.pubsub.publish(channels, presence, callback); -} +}; module.exports = { diff --git a/lib/util.js b/lib/util.js index d96b5429c..4c4783430 100644 --- a/lib/util.js +++ b/lib/util.js @@ -23,7 +23,7 @@ exports.isValidTimestamp = function(timestamp) { return exports.isValidVersion(timestamp); }; -exports.callEach = function (callbacks, err) { +exports.callEach = function(callbacks, err) { var called = false; for (var i = 0; i < callbacks.length; i++) { var callback = callbacks[i]; diff --git a/test/client/presence-type.js b/test/client/presence-type.js index 6138eae7f..7648d0e2a 100644 --- a/test/client/presence-type.js +++ b/test/client/presence-type.js @@ -48,17 +48,17 @@ function apply(snapshot, op) { function transform(op1, op2, side) { return op1.index < op2.index || (op1.index === op2.index && side === 'left') ? op1 - : { index: op1.index + 1, value: op1.value }; + : {index: op1.index + 1, value: op1.value}; } function createPresence(data) { - return { index: (data && data.index) | 0 }; + return {index: (data && data.index) | 0}; } function transformPresence(presence, op, isOwnOperation) { return presence.index < op.index || (presence.index === op.index && !isOwnOperation) ? presence - : { index: presence.index + 1 }; + : {index: presence.index + 1}; } function comparePresence(presence1, presence2) { diff --git a/test/client/presence.js b/test/client/presence.js index 5cdd46f6f..c5fa39ff1 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -23,7 +23,7 @@ describe('client presence', function() { }); it('should use presence option if provided', function() { - var backend = new Backend({ presence: statelessPresence }); + var backend = new Backend({presence: statelessPresence}); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); expect(doc._docPresence instanceof statelessPresence.DocPresence).to.be(true); @@ -44,12 +44,12 @@ describe('client presence', function() { 'unwrapped-presence' ].forEach(function(typeName) { function p(index) { - return typeName === 'unwrapped-presence' ? index : { index: index }; + return typeName === 'unwrapped-presence' ? index : {index: index}; } describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend({ presence: statelessPresence }); + this.backend = new Backend({presence: statelessPresence}); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); @@ -69,7 +69,7 @@ describe('client presence', function() { this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); @@ -85,14 +85,14 @@ describe('client presence', function() { this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); - this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc.submitOp({index: 0, value: 'a'}, errorHandler(done)); + this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done)); this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b' ]); + expect(this.doc2.data).to.eql(['a', 'b']); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); @@ -107,9 +107,9 @@ describe('client presence', function() { this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b' ]); + expect(this.doc2.data).to.eql(['a', 'b']); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); @@ -119,8 +119,8 @@ describe('client presence', function() { this.doc.submitPresence(p(1), function(err) { if (err) return done(err); this.doc.version -= 2; - this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); - this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc.submitOp({index: 0, value: 'a'}, errorHandler(done)); + this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this)); }.bind(this) ], allDone); @@ -128,22 +128,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (own ops, presence.index < op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), - this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), + this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}), + this.doc.submitOp.bind(this.doc, {index: 2, value: 'c'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'a' ]; + this.doc.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) @@ -152,22 +152,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (own ops, presence.index === op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc.submitOp.bind(this.doc, { index: 1, value: 'c' }), - this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), + this.doc.submitOp.bind(this.doc, {index: 1, value: 'c'}), + this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'a' ]; + this.doc.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -176,22 +176,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (own ops, presence.index > op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc.submitOp.bind(this.doc, { index: 0, value: 'b' }), - this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }), + this.doc.submitOp.bind(this.doc, {index: 0, value: 'b'}), + this.doc.submitOp.bind(this.doc, {index: 0, value: 'a'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'c' ]; + this.doc.data = ['c']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -200,22 +200,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (non-own ops, presence.index < op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), - this.doc2.submitOp.bind(this.doc2, { index: 2, value: 'c' }), + this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}), + this.doc2.submitOp.bind(this.doc2, {index: 2, value: 'c'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'a' ]; + this.doc.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) @@ -224,22 +224,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (non-own ops, presence.index === op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'c' }), - this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), + this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'c'}), + this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'a' ]; + this.doc.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -248,22 +248,22 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (non-own ops, presence.index > op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'b' }), - this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'a' }), + this.doc2.submitOp.bind(this.doc2, {index: 0, value: 'b'}), + this.doc2.submitOp.bind(this.doc2, {index: 0, value: 'a'}), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'c' ]; + this.doc.data = ['c']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -275,14 +275,14 @@ describe('client presence', function() { this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.create.bind(this.doc, [], typeName), - this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }), + this.doc.submitOp.bind(this.doc, {index: 0, value: 'a'}), this.doc.del.bind(this.doc), - this.doc.create.bind(this.doc, [ 'b' ], typeName), + this.doc.create.bind(this.doc, ['b'], typeName), function(done) { this.doc2.once('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'b' ]); + expect(this.doc2.data).to.eql(['b']); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); @@ -291,9 +291,9 @@ describe('client presence', function() { }.bind(this), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'b' ]); + expect(this.doc2.data).to.eql(['b']); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); @@ -307,16 +307,16 @@ describe('client presence', function() { it('handles presence sent for earlier revisions (no cached ops)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), - this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }), - this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }), + this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}), + this.doc.submitOp.bind(this.doc, {index: 2, value: 'c'}), function(done) { this.doc2.once('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); @@ -326,15 +326,15 @@ describe('client presence', function() { function(done) { this.doc2._docPresence.cachedOps = []; this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); - expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); + expect(this.doc2.data).to.eql(['a', 'b', 'c']); expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 1; - this.doc.data = [ 'a' ]; + this.doc.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -351,7 +351,7 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(srcList.sort()).to.eql(['', this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); @@ -372,7 +372,7 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(srcList.sort()).to.eql(['', this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); @@ -385,7 +385,7 @@ describe('client presence', function() { it('transforms presence against local op (presence.index != op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -393,20 +393,20 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection2.id ]); + expect(srcList).to.eql([this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this) ], allDone); }); it('transforms presence against non-local op (presence.index != op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -414,20 +414,20 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection2.id ]); + expect(srcList).to.eql([this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(0)); expect(this.doc.presence[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this) ], allDone); }); it('transforms presence against local op (presence.index == op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), @@ -435,20 +435,20 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ '' ]); + expect(srcList).to.eql(['']); expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(2)); expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this) ], allDone); }); it('transforms presence against non-local op (presence.index == op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), @@ -456,21 +456,21 @@ describe('client presence', function() { setTimeout, function(done) { this.doc.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection2.id ]); + expect(srcList).to.eql([this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence['']).to.eql(p(1)); expect(this.doc.presence[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); - this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); + this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this) ], allDone); }); it('caches local ops', function(allDone) { - var op = { index: 1, value: 'b' }; + var op = {index: 1, value: 'b'}; async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), function(done) { @@ -484,10 +484,10 @@ describe('client presence', function() { }); it('caches non-local ops', function(allDone) { - var op = { index: 1, value: 'b' }; + var op = {index: 1, value: 'b'}; async.series([ this.doc2.subscribe.bind(this.doc2), - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), setTimeout, @@ -503,13 +503,13 @@ describe('client presence', function() { it('expires cached ops', function(allDone) { var clock = lolex.install(); - var op1 = { index: 1, value: 'b' }; - var op2 = { index: 2, value: 'b' }; - var op3 = { index: 3, value: 'b' }; + var op1 = {index: 1, value: 'b'}; + var op2 = {index: 2, value: 'b'}; + var op3 = {index: 3, value: 'b'}; this.doc._docPresence.cachedOpsTimeout = 60; async.series([ // Cache 2 ops. - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.submitOp.bind(this.doc, op1), function(done) { expect(this.doc._docPresence.cachedOps.length).to.equal(2); @@ -519,7 +519,7 @@ describe('client presence', function() { }.bind(this), // Cache another op before the first 2 expire. - function (callback) { + function(callback) { setTimeout(callback, 30); clock.next(); }, @@ -533,7 +533,7 @@ describe('client presence', function() { }.bind(this), // Cache another op after the first 2 expire. - function (callback) { + function(callback) { setTimeout(callback, 31); clock.next(); }, @@ -550,19 +550,19 @@ describe('client presence', function() { it('requests reply presence when sending presence for the first time', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { if (srcList[0] === '') { - expect(srcList).to.eql([ '' ]); + expect(srcList).to.eql(['']); expect(submitted).to.equal(true); expect(this.doc2.presence['']).to.eql(p(1)); expect(this.doc2.presence).to.not.have.key(this.connection.id); } else { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(this.doc2.presence['']).to.eql(p(1)); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); expect(this.doc2._docPresence.requestReply).to.equal(false); @@ -639,12 +639,12 @@ describe('client presence', function() { it('sends presence once, if submitted multiple times synchronously', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); done(); @@ -659,11 +659,11 @@ describe('client presence', function() { it('buffers presence until subscribed', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -682,12 +682,12 @@ describe('client presence', function() { it('buffers presence when disconnected', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -704,12 +704,12 @@ describe('client presence', function() { it('submits presence without a callback', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); @@ -722,7 +722,7 @@ describe('client presence', function() { it('hasPending is true, if there is pending presence', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), function(done) { expect(this.doc.hasPending()).to.equal(false); @@ -743,7 +743,7 @@ describe('client presence', function() { it('hasPending is true, if there is inflight presence', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), function(done) { expect(this.doc.hasPending()).to.equal(false); @@ -770,7 +770,7 @@ describe('client presence', function() { it('receives presence after doc is deleted', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -778,7 +778,7 @@ describe('client presence', function() { function(done) { expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); @@ -794,7 +794,7 @@ describe('client presence', function() { it('clears peer presence on peer disconnection', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -808,7 +808,7 @@ describe('client presence', function() { var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ connectionId ]); + expect(srcList).to.eql([connectionId]); expect(submitted).to.equal(true); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); @@ -821,7 +821,7 @@ describe('client presence', function() { it('clears peer presence on own disconnection', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -835,7 +835,7 @@ describe('client presence', function() { var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ connectionId ]); + expect(srcList).to.eql([connectionId]); expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); @@ -848,7 +848,7 @@ describe('client presence', function() { it('clears peer presence on peer unsubscribe', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -862,7 +862,7 @@ describe('client presence', function() { var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ connectionId ]); + expect(srcList).to.eql([connectionId]); expect(submitted).to.equal(true); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); @@ -875,7 +875,7 @@ describe('client presence', function() { it('clears peer presence on own unsubscribe', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -889,7 +889,7 @@ describe('client presence', function() { var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ connectionId ]); + expect(srcList).to.eql([connectionId]); expect(submitted).to.equal(false); expect(this.doc2.presence).to.not.have.key(connectionId); expect(this.doc2.presence['']).to.eql(p(1)); @@ -902,7 +902,7 @@ describe('client presence', function() { it('pauses inflight and pending presence on disconnect', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), function(done) { var called = 0; @@ -924,7 +924,7 @@ describe('client presence', function() { it('pauses inflight and pending presence on unsubscribe', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), function(done) { var called = 0; @@ -946,7 +946,7 @@ describe('client presence', function() { it('re-synchronizes presence after reconnecting', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -972,7 +972,7 @@ describe('client presence', function() { it('re-synchronizes presence after resubscribing', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -995,76 +995,76 @@ describe('client presence', function() { ], allDone); }); - it('transforms received presence against inflight and pending ops (presence.index < op.index)', function(allDone) { + it('transforms received presence against inflight/pending ops (presence.index < op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); - this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) - this.doc2.submitOp({ index: 2, value: 'c' }, errorHandler(done)) + this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done)); + this.doc2.submitOp({index: 2, value: 'c'}, errorHandler(done)); }.bind(this) ], allDone); }); - it('transforms received presence against inflight and pending ops (presence.index === op.index)', function(allDone) { + it('transforms received presence against inflight/pending ops (presence.index === op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); - this.doc2.submitOp({ index: 1, value: 'c' }, errorHandler(done)) - this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)) + this.doc2.submitOp({index: 1, value: 'c'}, errorHandler(done)); + this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done)); }.bind(this) ], allDone); }); - it('transforms received presence against inflight and pending ops (presence.index > op.index)', function(allDone) { + it('transforms received presence against inflight/pending ops (presence.index > op.index)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); - this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)) - this.doc2.submitOp({ index: 0, value: 'a' }, errorHandler(done)) + this.doc2.submitOp({index: 0, value: 'b'}, errorHandler(done)); + this.doc2.submitOp({index: 0, value: 'a'}, errorHandler(done)); }.bind(this) ], allDone); }); it('transforms received presence against inflight delete', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), setTimeout, function(done) { this.doc2.on('presence', function(srcList, submitted) { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); @@ -1074,14 +1074,14 @@ describe('client presence', function() { this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.del(errorHandler(done)); - this.doc2.create([ 'c' ], typeName, errorHandler(done)); + this.doc2.create(['c'], typeName, errorHandler(done)); }.bind(this) ], allDone); }); it('transforms received presence against a pending delete', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), @@ -1090,7 +1090,7 @@ describe('client presence', function() { var firstCall = true; this.doc2.on('presence', function(srcList, submitted) { if (firstCall) return firstCall = false; - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); // The call to `del` transforms the presence and fires the event. // The call to `submitPresence` does not fire the event because presence is already null. expect(submitted).to.equal(false); @@ -1099,22 +1099,22 @@ describe('client presence', function() { }.bind(this)); this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); - this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); + this.doc2.submitOp({index: 0, value: 'b'}, errorHandler(done)); this.doc2.del(errorHandler(done)); - this.doc2.create([ 'c' ], typeName, errorHandler(done)); + this.doc2.create(['c'], typeName, errorHandler(done)); }.bind(this) ], allDone); }); it('emits the same presence only if comparePresence is not implemented (local presence)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(1)), function(done) { this.doc.on('presence', function(srcList, submitted) { if (typeName === 'wrapped-presence-no-compare') { - expect(srcList).to.eql([ '' ]); + expect(srcList).to.eql(['']); expect(submitted).to.equal(true); expect(this.doc.presence['']).to.eql(p(1)); done(); @@ -1129,7 +1129,7 @@ describe('client presence', function() { it('emits the same presence only if comparePresence is not implemented (non-local presence)', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(1)), @@ -1137,7 +1137,7 @@ describe('client presence', function() { function(done) { this.doc2.on('presence', function(srcList, submitted) { if (typeName === 'wrapped-presence-no-compare') { - expect(srcList).to.eql([ this.connection.id ]); + expect(srcList).to.eql([this.connection.id]); expect(submitted).to.equal(true); expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); @@ -1152,7 +1152,7 @@ describe('client presence', function() { it('returns an error when not subscribed on the server', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), function(done) { this.connection.sendUnsubscribe(this.doc); @@ -1164,14 +1164,14 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4026); done(); - }.bind(this)); + }); }.bind(this) ], allDone); }); it('emits an error when not subscribed on the server and no callback is provided', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), function(done) { this.connection.sendUnsubscribe(this.doc); @@ -1182,7 +1182,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4026); done(); - }.bind(this)); + }); this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -1190,7 +1190,7 @@ describe('client presence', function() { it('returns an error when the server gets an old sequence number', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, @@ -1201,14 +1201,14 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4027); done(); - }.bind(this)); + }); }.bind(this) ], allDone); }); it('emits an error when the server gets an old sequence number and no callback is provided', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, @@ -1217,7 +1217,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4027); done(); - }.bind(this)); + }); this.connection.seq--; this.doc.submitPresence(p(1)); }.bind(this) @@ -1226,7 +1226,7 @@ describe('client presence', function() { it('does not publish presence unnecessarily', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, @@ -1244,14 +1244,14 @@ describe('client presence', function() { expect(err).to.not.be.ok(); } done(); - }.bind(this)); + }); }.bind(this) ], allDone); }); it('does not publish presence unnecessarily when no callback is provided', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, @@ -1265,7 +1265,7 @@ describe('client presence', function() { } else { done(err); } - }.bind(this)); + }); // Decremented sequence number would cause the server to return an error, however, // the message won't be sent to the server at all because the presence data has not changed. this.connection.seq--; @@ -1279,7 +1279,7 @@ describe('client presence', function() { it('returns an error when publishing presence fails', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), setTimeout, function(done) { @@ -1295,14 +1295,14 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(-1); done(); - }.bind(this)); + }); }.bind(this) ], allDone); }); it('emits an error when publishing presence fails and no callback is provided', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'c' ], typeName), + this.doc.create.bind(this.doc, ['c'], typeName), this.doc.subscribe.bind(this.doc), setTimeout, function(done) { @@ -1317,7 +1317,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(-1); done(); - }.bind(this)); + }); this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -1325,7 +1325,7 @@ describe('client presence', function() { it('clears presence on hard rollback and emits an error', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'b', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -1351,7 +1351,7 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(presenceEmitted).to.equal(false); presenceEmitted = true; - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(srcList.sort()).to.eql(['', this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); @@ -1362,7 +1362,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4000); done(); - }.bind(this)); + }); // send an invalid op this.doc._submit({}, null); @@ -1372,7 +1372,7 @@ describe('client presence', function() { it('clears presence on hard rollback and executes all callbacks', function(allDone) { async.series([ - this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName), + this.doc.create.bind(this.doc, ['a', 'b', 'c'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), this.doc.submitPresence.bind(this.doc, p(0)), @@ -1406,7 +1406,7 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(presenceEmitted).to.equal(false); presenceEmitted = true; - expect(srcList.sort()).to.eql([ '', this.connection2.id ]); + expect(srcList.sort()).to.eql(['', this.connection2.id]); expect(submitted).to.equal(false); expect(this.doc.presence).to.not.have.key(''); expect(this.doc.presence).to.not.have.key(this.connection2.id); @@ -1414,7 +1414,7 @@ describe('client presence', function() { this.doc.on('error', done); // send an invalid op - this.doc._submit({ index: 3, value: 'b' }, null, callback); + this.doc._submit({index: 3, value: 'b'}, null, callback); }.bind(this)); }.bind(this)); }.bind(this) @@ -1435,7 +1435,7 @@ describe('client presence', function() { this.doc._docPresence.receivedTimeout = 0; } async.series([ - this.doc.create.bind(this.doc, [ 'a' ], typeName), + this.doc.create.bind(this.doc, ['a'], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { @@ -1443,10 +1443,10 @@ describe('client presence', function() { this.doc2.submitPresence(p(0), done); }.bind(this), setTimeout, - this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), // forces processing of all received presence + this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}), // forces processing of all received presence setTimeout, function(done) { - expect(this.doc.data).to.eql([ 'a', 'b' ]); + expect(this.doc.data).to.eql(['a', 'b']); expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); @@ -1465,16 +1465,24 @@ describe('client presence', function() { }; } - it('ignores an old message (cache not expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(false, false)); - it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(false, true)); - it('processes an old message (cache expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(true, false)); - it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(true, true)); + it('ignores an old message (cache not expired, presence.seq === cachedPresence.seq)', + testReceivedMessageExpiry(false, false)); + + it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)', + testReceivedMessageExpiry(false, true)); + + it('processes an old message (cache expired, presence.seq === cachedPresence.seq)', + testReceivedMessageExpiry(true, false)); + + it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', + testReceivedMessageExpiry(true, true)); it('invokes presence.destroy inside doc.destroy', function(done) { var presence = this.doc._docPresence; presence.cachedOps = ['foo']; - presence.received = { bar: true }; + presence.received = {bar: true}; this.doc.destroy(function(err) { + if (err) return done(err); expect(presence.cachedOps).to.eql([]); expect(presence.received).to.eql({}); done(); diff --git a/test/client/submit.js b/test/client/submit.js index f90ccc2da..592aa9eff 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1097,7 +1097,7 @@ module.exports = function() { done(); }); }); - + it('hasWritePending is false when submimtOp\'s callback is executed', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) { @@ -1109,7 +1109,7 @@ module.exports = function() { }); }); }); - + it('hasWritePending is false when del\'s callback is executed', function(done) { var doc = this.backend.connect().get('dogs', 'fido'); doc.create({age: 3}, function(err) {