From abcea41edd5069c3bd2b439fc91def4e07f59b6a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 11 Apr 2019 19:35:23 -0700 Subject: [PATCH 01/53] 1.0.0-beta.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 668ecc7fc..76dc3f878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.21", + "version": "1.0.0-beta.22", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From f43b75281afb325cc2188b48e4561aeaed6f56ab Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:09:47 +0530 Subject: [PATCH 02/53] 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 03/53] 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 04/53] 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 05/53] 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 06/53] 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 07/53] 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 08/53] 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 09/53] 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 10/53] 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 11/53] 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 12/53] 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 13/53] 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 14/53] 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 15/53] 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 16/53] 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 17/53] 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 18/53] 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 19/53] 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 20/53] 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 21/53] 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 22/53] 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 23/53] 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 24/53] 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 25/53] 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 26/53] 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 27/53] 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 28/53] 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 29/53] 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 30/53] 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 31/53] 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 32/53] 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 33/53] 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 34/53] 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 35/53] 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 36/53] 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 37/53] 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 38/53] 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 39/53] 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 40/53] 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 41/53] 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 42/53] 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 43/53] 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 44/53] 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 45/53] 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 46/53] 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 47/53] 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 48/53] 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 49/53] 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 50/53] 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 51/53] 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 52/53] 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 53/53] 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');