sharedb 5.0.2 → 5.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/agent.js CHANGED
@@ -48,6 +48,10 @@ function Agent(backend, stream) {
48
48
  // request if the client disconnects ungracefully. This is a
49
49
  // map of channel -> id -> request
50
50
  this.presenceRequests = Object.create(null);
51
+ // Keep track of the latest known Doc version, so that we can avoid fetching
52
+ // ops to transform presence if not needed
53
+ this.latestDocVersionStreams = Object.create(null);
54
+ this.latestDocVersions = Object.create(null);
51
55
 
52
56
  // We need to track this manually to make sure we don't reply to messages
53
57
  // after the stream was closed.
@@ -108,6 +112,12 @@ Agent.prototype._cleanup = function() {
108
112
  emitter.destroy();
109
113
  }
110
114
  this.subscribedQueries = Object.create(null);
115
+
116
+ for (var collection in this.latestDocVersionStreams) {
117
+ var streams = this.latestDocVersionStreams[collection];
118
+ for (var id in streams) streams[id].destroy();
119
+ }
120
+ this.latestDocVersionStreams = Object.create(null);
111
121
  };
112
122
 
113
123
  /**
@@ -115,17 +125,8 @@ Agent.prototype._cleanup = function() {
115
125
  * _sendOp()
116
126
  */
117
127
  Agent.prototype._subscribeToStream = function(collection, id, stream) {
118
- if (this.closed) return stream.destroy();
119
-
120
- var streams = this.subscribedDocs[collection] || (this.subscribedDocs[collection] = Object.create(null));
121
-
122
- // If already subscribed to this document, destroy the previously subscribed stream
123
- var previous = streams[id];
124
- if (previous) previous.destroy();
125
- streams[id] = stream;
126
-
127
128
  var agent = this;
128
- stream.on('data', function(data) {
129
+ this._subscribeMapToStream(this.subscribedDocs, collection, id, stream, function(data) {
129
130
  if (data.error) {
130
131
  // Log then silently ignore errors in a subscription stream, since these
131
132
  // may not be the client's fault, and they were not the result of a
@@ -135,13 +136,26 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
135
136
  }
136
137
  agent._onOp(collection, id, data);
137
138
  });
139
+ };
140
+
141
+ Agent.prototype._subscribeMapToStream = function(map, collection, id, stream, dataHandler) {
142
+ if (this.closed) return stream.destroy();
143
+
144
+ var streams = map[collection] || (map[collection] = Object.create(null));
145
+
146
+ // If already subscribed to this document, destroy the previously subscribed stream
147
+ var previous = streams[id];
148
+ if (previous) previous.destroy();
149
+ streams[id] = stream;
150
+
151
+ stream.on('data', dataHandler);
138
152
  stream.on('end', function() {
139
153
  // The op stream is done sending, so release its reference
140
- var streams = agent.subscribedDocs[collection];
154
+ var streams = map[collection];
141
155
  if (!streams || streams[id] !== stream) return;
142
156
  delete streams[id];
143
157
  if (util.hasKeys(streams)) return;
144
- delete agent.subscribedDocs[collection];
158
+ delete map[collection];
145
159
  });
146
160
  };
147
161
 
@@ -794,25 +808,74 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
794
808
  collection: presence.c
795
809
  };
796
810
  var start = Date.now();
797
- backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
811
+
812
+ var subscriptionUpdater = presence.p === null ?
813
+ this._unsubscribeDocVersion.bind(this) :
814
+ this._subscribeDocVersion.bind(this);
815
+
816
+ subscriptionUpdater(presence.c, presence.d, function(error) {
798
817
  if (error) return callback(error);
799
- var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
800
- var previousRequest = requests[presence.id];
801
- if (!previousRequest || previousRequest.pv < presence.pv) {
802
- presenceRequests[presence.ch][presence.id] = presence;
803
- }
804
- backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
818
+ backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, agent, context, function(error) {
805
819
  if (error) return callback(error);
806
- var channel = agent._getPresenceChannel(presence.ch);
807
- agent.backend.pubsub.publish([channel], presence, function(error) {
808
- if (error) return callback(error);
809
- backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
820
+ var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
821
+ var previousRequest = requests[presence.id];
822
+ if (!previousRequest || previousRequest.pv < presence.pv) {
823
+ presenceRequests[presence.ch][presence.id] = presence;
824
+ }
825
+
826
+ var transformer = function(agent, presence, callback) {
810
827
  callback(null, presence);
828
+ };
829
+
830
+ var latestDocVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
831
+ var presenceIsUpToDate = presence.v === latestDocVersion;
832
+ if (!presenceIsUpToDate) {
833
+ transformer = backend.transformPresenceToLatestVersion.bind(backend);
834
+ }
835
+
836
+ transformer(agent, presence, function(error, presence) {
837
+ if (error) return callback(error);
838
+ var channel = agent._getPresenceChannel(presence.ch);
839
+ agent.backend.pubsub.publish([channel], presence, function(error) {
840
+ if (error) return callback(error);
841
+ backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
842
+ callback(null, presence);
843
+ });
811
844
  });
812
845
  });
813
846
  });
814
847
  };
815
848
 
849
+ Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
850
+ if (!collection || !id) return callback();
851
+
852
+ var latestDocVersions = this.latestDocVersions;
853
+ var isSubscribed = util.dig(latestDocVersions, collection, id) !== undefined;
854
+ if (isSubscribed) return callback();
855
+
856
+ var agent = this;
857
+ this.backend.subscribe(this, collection, id, null, function(error, stream, snapshot) {
858
+ if (error) return callback(error);
859
+
860
+ var versions = latestDocVersions[collection] || (latestDocVersions[collection] = Object.create(null));
861
+ versions[id] = snapshot.v;
862
+
863
+ agent._subscribeMapToStream(agent.latestDocVersionStreams, collection, id, stream, function(op) {
864
+ // op.v behind snapshot.v by 1
865
+ latestDocVersions[collection][id] = op.v + 1;
866
+ });
867
+
868
+ callback();
869
+ });
870
+ };
871
+
872
+ Agent.prototype._unsubscribeDocVersion = function(collection, id, callback) {
873
+ var stream = util.dig(this.latestDocVersionStreams, collection, id);
874
+ if (stream) stream.destroy();
875
+ util.digAndRemove(this.latestDocVersions, collection, id);
876
+ util.nextTick(callback);
877
+ };
878
+
816
879
  Agent.prototype._createPresence = function(request) {
817
880
  return {
818
881
  a: ACTIONS.presence,
package/lib/client/doc.js CHANGED
@@ -295,7 +295,7 @@ Doc.prototype._handleFetch = function(error, snapshot) {
295
295
  var callbacks = this.pendingFetch;
296
296
  this.pendingFetch = [];
297
297
  var callback = this.inflightFetch.shift();
298
- if (callback) callbacks.push(callback);
298
+ if (callback) callbacks.unshift(callback);
299
299
  if (callbacks.length) {
300
300
  callback = function(error) {
301
301
  util.callEach(callbacks, error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "5.0.2",
3
+ "version": "5.0.4",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -101,6 +101,27 @@ describe('Doc', function() {
101
101
  doc.fetch(finish);
102
102
  doc.fetch(finish);
103
103
  });
104
+ it('callbacks called in correct order when fetching and applying ops in quick succession', function(done) {
105
+ var connection = this.connection;
106
+ var doc = connection.get('dogs', 'fido');
107
+ doc.create({name: 'fido'});
108
+ var order = '';
109
+ doc.fetch(function() {
110
+ order += 'A';
111
+ });
112
+ doc.submitOp([{p: ['snacks'], oi: true}]);
113
+ doc.fetch(function() {
114
+ order += 'B';
115
+ });
116
+ doc.submitOp([{p: ['color'], oi: 'gray'}]);
117
+ doc.fetch(function() {
118
+ order += 'C';
119
+ });
120
+ doc.whenNothingPending(function() {
121
+ expect(order).to.eql('ABC');
122
+ done();
123
+ });
124
+ });
104
125
  });
105
126
 
106
127
  describe('when connection closed', function() {
@@ -5,6 +5,7 @@ var types = require('../../../lib/types');
5
5
  var presenceTestType = require('./presence-test-type');
6
6
  var errorHandler = require('../../util').errorHandler;
7
7
  var PresencePauser = require('./presence-pauser');
8
+ var sinon = require('sinon');
8
9
  types.register(presenceTestType.type);
9
10
 
10
11
  describe('DocPresence', function() {
@@ -297,6 +298,23 @@ describe('DocPresence', function() {
297
298
  ], done);
298
299
  });
299
300
 
301
+ it('does not call getOps() when presence is already up-to-date', function(done) {
302
+ var localPresence1 = presence1.create('presence-1');
303
+
304
+ async.series([
305
+ doc1.fetch.bind(doc1), // Ensure up-to-date
306
+ function(next) {
307
+ sinon.spy(Backend.prototype, 'getOps');
308
+ next();
309
+ },
310
+ localPresence1.submit.bind(localPresence1, {index: 1}),
311
+ function(next) {
312
+ expect(Backend.prototype.getOps).not.to.have.been.called;
313
+ next();
314
+ }
315
+ ], done);
316
+ });
317
+
300
318
  // This test case attempts to force us into a tight race condition corner case:
301
319
  // 1. doc1 sends presence, as well as submits an op
302
320
  // 2. doc2 receives the op first, followed by the presence, which is now out-of-date