sharedb 7.0.0 → 7.0.2

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
@@ -1,3 +1,4 @@
1
+ var async = require('async');
1
2
  var hat = require('hat');
2
3
  var ShareDBError = require('./error');
3
4
  var logger = require('./logger');
@@ -5,6 +6,7 @@ var ACTIONS = require('./message-actions').ACTIONS;
5
6
  var types = require('./types');
6
7
  var util = require('./util');
7
8
  var protocol = require('./protocol');
9
+ var DocPresence = require('./client/presence/doc-presence');
8
10
 
9
11
  var ERROR_CODE = ShareDBError.CODES;
10
12
 
@@ -45,14 +47,26 @@ function Agent(backend, stream) {
45
47
  this.presenceSubscriptionSeq = Object.create(null);
46
48
  // Keep track of the last request that has been sent by each local presence
47
49
  // belonging to this agent. This is used to generate a new disconnection
48
- // request if the client disconnects ungracefully. This is a
49
- // map of channel -> id -> request
50
+ // request if the client disconnects ungracefully, and a presence that's
51
+ // been replaced here by the time it would be published isn't sent. This
52
+ // is a map of channel -> id -> request
50
53
  this.presenceRequests = Object.create(null);
51
54
  // Keep track of the latest known Doc version, so that we can avoid fetching
52
55
  // ops to transform presence if not needed
53
56
  this.latestDocVersionStreams = Object.create(null);
54
57
  this.latestDocVersions = Object.create(null);
55
58
 
59
+ // Every doc, doc version and presence stream this agent holds, so that
60
+ // _cleanup() destroys them even if they've gone missing from their own map.
61
+ // Map of key -> stream
62
+ this._streams = Object.create(null);
63
+ this._nextStreamKey = 1;
64
+ // Presence broadcasts still in their doc version lookup or receivePresence
65
+ // middleware. _retractPresences() flags them to go out as a leave, and a
66
+ // leave from the client that gets through middleware flags its own older
67
+ // ones to be dropped
68
+ this._pendingPresenceBroadcasts = [];
69
+
56
70
  // We need to track this manually to make sure we don't reply to messages
57
71
  // after the stream was closed.
58
72
  this.closed = false;
@@ -93,20 +107,16 @@ Agent.prototype._cleanup = function() {
93
107
  this.backend.agentsCount--;
94
108
  if (!this.stream.isServer) this.backend.remoteAgentsCount--;
95
109
 
96
- // Clean up doc subscription streams
97
- for (var collection in this.subscribedDocs) {
98
- var docs = this.subscribedDocs[collection];
99
- for (var id in docs) {
100
- var stream = docs[id];
101
- stream.destroy();
102
- }
110
+ for (var channel in this.presenceRequests) {
111
+ this._retractPresences(channel);
103
112
  }
104
- this.subscribedDocs = Object.create(null);
105
113
 
106
- for (var channel in this.subscribedPresences) {
107
- this.subscribedPresences[channel].destroy();
114
+ for (var key in this._streams) {
115
+ this._streams[key].destroy();
108
116
  }
117
+ this.subscribedDocs = Object.create(null);
109
118
  this.subscribedPresences = Object.create(null);
119
+ this.latestDocVersionStreams = Object.create(null);
110
120
 
111
121
  // Clean up query subscription streams
112
122
  for (var id in this.subscribedQueries) {
@@ -114,12 +124,16 @@ Agent.prototype._cleanup = function() {
114
124
  emitter.destroy();
115
125
  }
116
126
  this.subscribedQueries = Object.create(null);
127
+ };
117
128
 
118
- for (var collection in this.latestDocVersionStreams) {
119
- var streams = this.latestDocVersionStreams[collection];
120
- for (var id in streams) streams[id].destroy();
121
- }
122
- this.latestDocVersionStreams = Object.create(null);
129
+ Agent.prototype._trackStream = function(stream) {
130
+ if (stream.open === false) return;
131
+ var streams = this._streams;
132
+ var key = this._nextStreamKey++;
133
+ streams[key] = stream;
134
+ stream.once('close', function() {
135
+ delete streams[key];
136
+ });
123
137
  };
124
138
 
125
139
  /**
@@ -142,6 +156,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
142
156
 
143
157
  Agent.prototype._subscribeMapToStream = function(map, collection, id, stream, dataHandler) {
144
158
  if (this.closed) return stream.destroy();
159
+ this._trackStream(stream);
145
160
 
146
161
  var streams = map[collection] || (map[collection] = Object.create(null));
147
162
 
@@ -163,29 +178,57 @@ Agent.prototype._subscribeMapToStream = function(map, collection, id, stream, da
163
178
 
164
179
  Agent.prototype._subscribeToPresenceStream = function(channel, stream) {
165
180
  if (this.closed) return stream.destroy();
181
+ this._trackStream(stream);
166
182
  var agent = this;
167
183
 
184
+ var previous = this.subscribedPresences[channel];
185
+ if (previous) previous.destroy();
186
+ this.subscribedPresences[channel] = stream;
187
+
168
188
  stream.on('data', function(data) {
169
189
  if (data.error) {
170
190
  logger.error('Presence subscription stream error', channel, data.error);
191
+ return;
171
192
  }
193
+ if (!isPresenceOrRequestFor(channel, data)) return;
172
194
  agent._handlePresenceData(data);
173
195
  });
174
196
 
175
197
  stream.on('end', function() {
176
- var requests = agent.presenceRequests[channel] || {};
177
- for (var id in requests) {
178
- var request = agent.presenceRequests[channel][id];
179
- request.seq++;
180
- request.p = null;
181
- agent._broadcastPresence(request, function(error) {
182
- if (error) logger.error('Error broadcasting disconnect presence', channel, error);
183
- });
184
- }
185
198
  if (agent.subscribedPresences[channel] === stream) {
186
199
  delete agent.subscribedPresences[channel];
187
200
  }
188
- delete agent.presenceRequests[channel];
201
+ });
202
+ };
203
+
204
+ function isPresenceOrRequestFor(channel, data) {
205
+ var isPresence = data.a === ACTIONS.presence;
206
+ var isPresenceRequest = !!data.r;
207
+ return (isPresence || isPresenceRequest) && data.ch === channel;
208
+ }
209
+
210
+ Agent.prototype._retractPresences = function(channel) {
211
+ var pendingBroadcasts = this._pendingPresenceBroadcasts;
212
+ for (var i = 0; i < pendingBroadcasts.length; i++) {
213
+ if (pendingBroadcasts[i].channel === channel) pendingBroadcasts[i].retracted = true;
214
+ }
215
+
216
+ var requests = this.presenceRequests[channel];
217
+ for (var id in requests) {
218
+ var request = requests[id];
219
+ var alreadyLeft = request.p === null;
220
+ if (alreadyLeft) this._unsubscribeDocVersion(request.c, request.d, function() {});
221
+ else this._broadcastLeave(request);
222
+ }
223
+ delete this.presenceRequests[channel];
224
+ };
225
+
226
+ Agent.prototype._broadcastLeave = function(presence, callback) {
227
+ var agent = this;
228
+ presence.p = null;
229
+ this._broadcastPresence(presence, [], function(error, result) {
230
+ if (error) logger.error('Error broadcasting disconnect presence', presence.ch, presence.id, agent._src(), error);
231
+ if (callback) callback(error, result);
189
232
  });
190
233
  };
191
234
 
@@ -245,7 +288,7 @@ Agent.prototype._onOp = function(collection, id, op) {
245
288
  // before calling into projection and middleware code
246
289
  var agent = this;
247
290
  util.nextTick(function() {
248
- var copy = shallowCopy(op);
291
+ var copy = util.shallowCopy(op);
249
292
  agent.backend.sanitizeOp(agent, collection, id, copy, function(err) {
250
293
  if (err) {
251
294
  logger.error('Error sanitizing op emitted from subscription', collection, id, copy, err);
@@ -451,6 +494,7 @@ Agent.prototype._checkRequest = function(request) {
451
494
  if (request.a === ACTIONS.presence) {
452
495
  if (isInvalidName(request.id)) return 'Invalid presence ID';
453
496
  if (isInvalidVersion(request.pv)) return 'Invalid presence version';
497
+ if (isDocPresence(request) && !util.isInteger(request.v)) return 'Invalid version';
454
498
  }
455
499
  } else if (
456
500
  request.a === ACTIONS.bulkFetch ||
@@ -466,8 +510,19 @@ Agent.prototype._checkRequest = function(request) {
466
510
  if (request.c != null && isInvalidName(request.c)) return 'Invalid collection';
467
511
  if (request.ch != null && isInvalidName(request.ch)) return 'Invalid presence channel';
468
512
  if (request.seq != null && typeof request.seq !== 'number') return 'Invalid seq';
513
+ if (request.a === ACTIONS.presence && isDocPresence(request) && !matchesDocPresenceChannel(request)) {
514
+ return 'Presence collection and doc ID do not match channel';
515
+ }
469
516
  };
470
517
 
518
+ function isDocPresence(request) {
519
+ return request.c != null || request.d != null;
520
+ }
521
+
522
+ function matchesDocPresenceChannel(request) {
523
+ return !!request.c && !!request.d && request.ch === DocPresence.channel(request.c, request.d);
524
+ }
525
+
471
526
  // Handle an incoming message from the client
472
527
  Agent.prototype._handleMessage = function(request, callback) {
473
528
  try {
@@ -522,7 +577,8 @@ Agent.prototype._handleMessage = function(request, callback) {
522
577
  message: 'Type does not support presence: ' + presence.t
523
578
  });
524
579
  }
525
- return this._broadcastPresence(presence, callback);
580
+ var olderBroadcasts = presence.p === null ? this._olderPendingPresenceBroadcasts(presence) : [];
581
+ return this._broadcastPresence(presence, olderBroadcasts, callback);
526
582
  case ACTIONS.presenceSubscribe:
527
583
  if (!this.backend.presenceEnabled) return;
528
584
  return this._subscribePresence(request.ch, request.seq, callback);
@@ -564,6 +620,7 @@ function getQueryOptions(request) {
564
620
  }
565
621
  }
566
622
  var options = request.o || {};
623
+ delete options.fields;
567
624
  delete options.pollInterval;
568
625
  delete options.pollDebounce;
569
626
  options.ids = ids;
@@ -588,42 +645,50 @@ Agent.prototype._querySubscribe = function(queryId, collection, query, options,
588
645
  // Subscribe to a query. The client is sent the query results and its
589
646
  // notified whenever there's a change
590
647
  var agent = this;
591
- var wait = 1;
592
- var message;
593
- function finish(err) {
648
+ this.backend.querySubscribe(this, collection, query, options, function(err, emitter, results, extra) {
594
649
  if (err) return callback(err);
595
- if (--wait) return;
596
- callback(null, message);
597
- }
598
- if (options.fetch) {
599
- wait++;
600
- this.backend.fetchBulk(this, collection, options.fetch, function(err, snapshotMap) {
601
- if (err) return finish(err);
602
- message = getMapResult(snapshotMap);
603
- finish();
650
+ agent._fetchKnownResults(collection, options, function(err, message) {
651
+ if (err) {
652
+ emitter.destroy();
653
+ return callback(err);
654
+ }
655
+ if (agent.closed) return emitter.destroy();
656
+
657
+ agent._subscribeToQuery(emitter, queryId, collection, query);
658
+ if (results) return callback(null, {data: getResultsData(results), extra: extra});
659
+ // No results are returned when ids are passed in as an option. Instead,
660
+ // want to re-poll the entire query once we've established listeners to
661
+ // emit any diff in results
662
+ emitter.queryPoll(function(err) {
663
+ if (err) {
664
+ emitter.destroy();
665
+ if (agent.subscribedQueries[queryId] === emitter) delete agent.subscribedQueries[queryId];
666
+ }
667
+ callback(err, message);
668
+ });
604
669
  });
605
- }
606
- if (options.fetchOps) {
607
- wait++;
608
- this._fetchBulkOps(collection, options.fetchOps, finish);
609
- }
610
- this.backend.querySubscribe(this, collection, query, options, function(err, emitter, results, extra) {
611
- if (err) return finish(err);
612
- if (agent.closed) return emitter.destroy();
613
-
614
- agent._subscribeToQuery(emitter, queryId, collection, query);
615
- // No results are returned when ids are passed in as an option. Instead,
616
- // want to re-poll the entire query once we've established listeners to
617
- // emit any diff in results
618
- if (!results) {
619
- emitter.queryPoll(finish);
620
- return;
670
+ });
671
+ };
672
+
673
+ Agent.prototype._fetchKnownResults = function(collection, options, callback) {
674
+ var agent = this;
675
+ async.parallel({
676
+ snapshots: function(cb) {
677
+ if (!options.fetch) return cb();
678
+ agent.backend.fetchBulk(agent, collection, options.fetch, function(err, snapshotMap) {
679
+ if (err) return cb(err);
680
+ if (hasRejectedSnapshot(snapshotMap)) return cb(new ShareDBError(ERROR_CODE.ERR_SNAPSHOT_READS_REJECTED));
681
+ cb(null, getMapResult(snapshotMap));
682
+ });
683
+ },
684
+ ops: function(cb) {
685
+ if (!options.fetchOps) return cb();
686
+ agent.backend.getOpsBulk(agent, collection, options.fetchOps, null, cb);
621
687
  }
622
- message = {
623
- data: getResultsData(results),
624
- extra: extra
625
- };
626
- finish();
688
+ }, function(err, results) {
689
+ if (err) return callback(err);
690
+ if (results.ops) agent._sendOpsBulk(collection, results.ops);
691
+ callback(null, results.snapshots);
627
692
  });
628
693
  };
629
694
 
@@ -662,6 +727,13 @@ function getMapResult(snapshotMap) {
662
727
  return {data: data};
663
728
  }
664
729
 
730
+ function hasRejectedSnapshot(snapshotMap) {
731
+ for (var id in snapshotMap) {
732
+ if (snapshotMap[id].error) return true;
733
+ }
734
+ return false;
735
+ }
736
+
665
737
  function getSnapshotData(snapshot) {
666
738
  var data = {
667
739
  v: snapshot.v,
@@ -843,53 +915,92 @@ Agent.prototype._src = function() {
843
915
  return this.src || this.clientId;
844
916
  };
845
917
 
846
- Agent.prototype._broadcastPresence = function(presence, callback) {
918
+ Agent.prototype._broadcastPresence = function(presence, supersededBroadcasts, callback) {
847
919
  var agent = this;
848
920
  var backend = this.backend;
849
- var presenceRequests = this.presenceRequests;
921
+ var leaving = presence.p === null;
922
+ var pendingBroadcast = this._addPendingPresenceBroadcast(presence);
850
923
  var context = {
851
924
  presence: presence,
852
925
  collection: presence.c
853
926
  };
854
927
  var start = Date.now();
855
928
 
856
- var subscriptionUpdater = presence.p === null ?
929
+ var subscriptionUpdater = leaving ?
857
930
  this._unsubscribeDocVersion.bind(this) :
858
931
  this._subscribeDocVersion.bind(this);
859
932
 
860
933
  subscriptionUpdater(presence.c, presence.d, function(error) {
861
- if (error) return callback(error);
934
+ if (error) {
935
+ agent._removePendingPresenceBroadcast(pendingBroadcast);
936
+ return callback(error);
937
+ }
862
938
  backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, agent, context, function(error) {
939
+ agent._removePendingPresenceBroadcast(pendingBroadcast);
863
940
  if (error) return callback(error);
864
- var requests = presenceRequests[presence.ch] || (presenceRequests[presence.ch] = Object.create(null));
865
- var previousRequest = requests[presence.id];
866
- if (!previousRequest || previousRequest.pv < presence.pv) {
867
- presenceRequests[presence.ch][presence.id] = presence;
941
+ for (var i = 0; i < supersededBroadcasts.length; i++) {
942
+ supersededBroadcasts[i].superseded = true;
868
943
  }
869
-
870
- var transformer = function(agent, presence, callback) {
871
- callback(null, presence);
872
- };
873
-
874
- var latestDocVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
875
- var presenceIsUpToDate = presence.v === latestDocVersion;
876
- if (!presenceIsUpToDate) {
877
- transformer = backend.transformPresenceToLatestVersion.bind(backend);
944
+ if (pendingBroadcast.superseded && !leaving) {
945
+ return agent._unsubscribeDocVersion(presence.c, presence.d, function() {
946
+ callback(null, presence);
947
+ });
948
+ }
949
+ var clientHasLeft = agent.closed || pendingBroadcast.retracted;
950
+ if (clientHasLeft && !leaving) return agent._broadcastLeave(presence, callback);
951
+ var requests = agent.presenceRequests[presence.ch] || (agent.presenceRequests[presence.ch] = Object.create(null));
952
+ var previousRequest = requests[presence.id];
953
+ if (!previousRequest || previousRequest.p === null || previousRequest.pv < presence.pv) {
954
+ requests[presence.id] = presence;
878
955
  }
879
956
 
880
- transformer(agent, presence, function(error, presence) {
881
- if (error) return callback(error);
882
- var channel = agent._getPresenceChannel(presence.ch);
883
- agent.backend.pubsub.publish([channel], presence, function(error) {
957
+ var trackedVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
958
+ backend._transformPresenceToLatestVersion(agent, presence, trackedVersion, function(error, transformedPresence) {
959
+ if (error) {
960
+ agent._withdrawPresenceRequest(requests, presence, previousRequest);
961
+ return callback(error);
962
+ }
963
+ var replaced = util.dig(agent.presenceRequests, presence.ch, presence.id) !== presence;
964
+ if (replaced && !leaving) return callback(null, transformedPresence);
965
+ var channel = agent._getPresenceChannel(transformedPresence.ch);
966
+ agent.backend.pubsub.publish([channel], transformedPresence, function(error) {
884
967
  if (error) return callback(error);
885
968
  backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
886
- callback(null, presence);
969
+ callback(null, transformedPresence);
887
970
  });
888
971
  });
889
972
  });
890
973
  });
891
974
  };
892
975
 
976
+ Agent.prototype._withdrawPresenceRequest = function(requests, presence, previousRequest) {
977
+ if (requests[presence.id] !== presence) return;
978
+ if (previousRequest) requests[presence.id] = previousRequest;
979
+ else presence.p = null;
980
+ };
981
+
982
+ Agent.prototype._addPendingPresenceBroadcast = function(presence) {
983
+ var broadcast = {channel: presence.ch, id: presence.id, pv: presence.pv, retracted: false, superseded: false};
984
+ this._pendingPresenceBroadcasts.push(broadcast);
985
+ return broadcast;
986
+ };
987
+
988
+ Agent.prototype._removePendingPresenceBroadcast = function(broadcast) {
989
+ var index = this._pendingPresenceBroadcasts.indexOf(broadcast);
990
+ if (index !== -1) this._pendingPresenceBroadcasts.splice(index, 1);
991
+ };
992
+
993
+ Agent.prototype._olderPendingPresenceBroadcasts = function(presence) {
994
+ var olderBroadcasts = [];
995
+ var pendingBroadcasts = this._pendingPresenceBroadcasts;
996
+ for (var i = 0; i < pendingBroadcasts.length; i++) {
997
+ var broadcast = pendingBroadcasts[i];
998
+ var isOlder = broadcast.channel === presence.ch && broadcast.id === presence.id && broadcast.pv < presence.pv;
999
+ if (isOlder) olderBroadcasts.push(broadcast);
1000
+ }
1001
+ return olderBroadcasts;
1002
+ };
1003
+
893
1004
  Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
894
1005
  if (!collection || !id) return callback();
895
1006
 
@@ -905,6 +1016,7 @@ Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
905
1016
  versions[id] = snapshot.v;
906
1017
 
907
1018
  agent._subscribeMapToStream(agent.latestDocVersionStreams, collection, id, stream, function(op) {
1019
+ if (agent._isOpFromCollidingChannel(collection, id, op)) return;
908
1020
  // op.v behind snapshot.v by 1
909
1021
  latestDocVersions[collection][id] = op.v + 1;
910
1022
  });
@@ -957,11 +1069,6 @@ Agent.prototype._subscribePresence = function(channel, seq, cb) {
957
1069
  return callback();
958
1070
  }
959
1071
  agent.presenceSubscriptionSeq[channel] = seq;
960
- if (agent.subscribedPresences[channel]) {
961
- stream.destroy();
962
- return callback();
963
- }
964
- agent.subscribedPresences[channel] = stream;
965
1072
  agent._subscribeToPresenceStream(channel, stream);
966
1073
  agent._requestPresence(channel, function(error) {
967
1074
  callback(error);
@@ -974,6 +1081,7 @@ Agent.prototype._unsubscribePresence = function(channel, seq, callback) {
974
1081
  var stream = this.subscribedPresences[channel];
975
1082
  if (stream) stream.destroy();
976
1083
  delete this.subscribedPresences[channel];
1084
+ this._retractPresences(channel);
977
1085
  callback(null, {ch: channel, seq: seq});
978
1086
  };
979
1087
 
@@ -1037,16 +1145,6 @@ function createClientOp(request, clientId) {
1037
1145
  undefined;
1038
1146
  }
1039
1147
 
1040
- function shallowCopy(object) {
1041
- var out = {};
1042
- for (var key in object) {
1043
- if (util.hasOwn(object, key)) {
1044
- out[key] = object[key];
1045
- }
1046
- }
1047
- return out;
1048
- }
1049
-
1050
1148
  function CreateOp(src, seq, v, create, x, c, d, m) {
1051
1149
  this.src = src;
1052
1150
  this.seq = seq;
package/lib/backend.js CHANGED
@@ -89,6 +89,8 @@ Backend.prototype.MIDDLEWARE_ACTIONS = {
89
89
  receivePresence: 'receivePresence',
90
90
  // About to send presence information to a client
91
91
  sendPresence: 'sendPresence',
92
+ // An operation lost the race to commit and is about to be retried against a newer snapshot
93
+ retry: 'retry',
92
94
  // An operation is about to be submitted to the database
93
95
  submit: 'submit'
94
96
  };
@@ -936,18 +938,47 @@ Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, ca
936
938
  };
937
939
 
938
940
  Backend.prototype.transformPresenceToLatestVersion = function(agent, presence, callback) {
941
+ this._transformPresenceToLatestVersion(agent, presence, null, callback);
942
+ };
943
+
944
+ Backend.prototype._transformPresenceToLatestVersion = function(agent, presence, trackedVersion, callback) {
939
945
  if (!presence.c || !presence.d) return callback(null, presence);
940
- this.getOps(agent, presence.c, presence.d, presence.v, null, function(error, ops) {
946
+ var backend = this;
947
+ this._getDocVersionForPresence(agent, presence, trackedVersion, function(error, docVersion) {
941
948
  if (error) return callback(error);
942
- for (var i = 0; i < ops.length; i++) {
943
- var op = ops[i];
944
- var isOwnOp = op.src === presence.src;
945
- var transformError = ot.transformPresence(presence, op, isOwnOp);
946
- if (transformError) {
947
- return callback(transformError);
948
- }
949
+ var leaving = presence.p === null;
950
+ if (leaving) presence.v = docVersion;
951
+ if (presence.v > docVersion) {
952
+ return callback(new ShareDBError(
953
+ ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
954
+ 'Presence version newer than current snapshot'
955
+ ));
949
956
  }
950
- callback(null, presence);
957
+ if (presence.v === docVersion) return callback(null, presence);
958
+ backend.getOps(agent, presence.c, presence.d, presence.v, null, function(error, ops) {
959
+ if (error) return callback(error);
960
+ for (var i = 0; i < ops.length; i++) {
961
+ var op = ops[i];
962
+ var isOwnOp = op.src === presence.src;
963
+ var transformError = ot.transformPresence(presence, op, isOwnOp);
964
+ if (transformError) {
965
+ return callback(transformError);
966
+ }
967
+ }
968
+ callback(null, presence);
969
+ });
970
+ });
971
+ };
972
+
973
+ Backend.prototype._getDocVersionForPresence = function(agent, presence, trackedVersion, callback) {
974
+ var leaving = presence.p === null;
975
+ var presenceIsWithinTrackedVersion = trackedVersion != null && presence.v <= trackedVersion;
976
+ if (!leaving && presenceIsWithinTrackedVersion) return callback(null, trackedVersion);
977
+ var projection = this.projections[presence.c];
978
+ var collection = (projection) ? projection.target : presence.c;
979
+ this.db.getSnapshot(collection, presence.d, {}, {agentCustom: agent.custom}, function(error, snapshot) {
980
+ if (error) return callback(error);
981
+ callback(null, snapshot.v);
951
982
  });
952
983
  };
953
984
 
@@ -17,6 +17,12 @@ DocPresence.channel = function(collection, id) {
17
17
  return collection + '.' + id;
18
18
  };
19
19
 
20
+ DocPresence.prototype._receiveUpdate = function(error, message) {
21
+ var isForThisDoc = message.c === this.collection && message.d === this.id;
22
+ if (!error && !isForThisDoc) return;
23
+ Presence.prototype._receiveUpdate.call(this, error, message);
24
+ };
25
+
20
26
  DocPresence.prototype._createLocalPresence = function(id) {
21
27
  return new LocalDocPresence(this, id);
22
28
  };
@@ -120,10 +120,13 @@ Presence.prototype._handleUnsubscribe = function(error, seq) {
120
120
  };
121
121
 
122
122
  Presence.prototype._receiveUpdate = function(error, message) {
123
+ var isErrorForOwnRequest = !!error && message.pv != null;
124
+ var isFromThisConnection = message.src === this.connection.id;
123
125
  var localPresence = util.dig(this.localPresences, message.id);
124
- if (localPresence) return localPresence._ack(error, message.pv);
126
+ if (localPresence && (isErrorForOwnRequest || isFromThisConnection)) return localPresence._ack(error, message.pv);
125
127
 
126
128
  if (error) return this.emit('error', error);
129
+ if (localPresence) return;
127
130
  var presence = this;
128
131
  var remotePresence = util.digOrCreate(this._remotePresenceInstances, message.id, function() {
129
132
  return presence._createRemotePresence(message.id);
@@ -14,13 +14,12 @@ function PubSub(options) {
14
14
  this.streamsCount = 0;
15
15
  // Maps channel -> id -> stream
16
16
  this.streams = Object.create(null);
17
- // State for tracking subscriptions. We track this.subscribed separately from
18
- // the streams, since the stream gets added synchronously, and the subscribe
19
- // isn't complete until the callback returns from Redis
20
17
  // Maps channel -> true
21
18
  this.subscribed = Object.create(null);
22
19
  // Maps channel -> callbacks waiting on _subscribe
23
20
  this._subscribing = Object.create(null);
21
+ // Maps channel -> true while _unsubscribe is in flight
22
+ this._unsubscribing = Object.create(null);
24
23
 
25
24
  var pubsub = this;
26
25
  this._defaultCallback = function(err) {
@@ -70,10 +69,9 @@ PubSub.prototype.subscribe = function(channel, callback) {
70
69
  channel = this.prefix + ' ' + channel;
71
70
  }
72
71
 
73
- var pubsub = this;
74
72
  if (this.subscribed[channel]) {
73
+ var stream = this._createStream(channel);
75
74
  util.nextTick(function() {
76
- var stream = pubsub._createStream(channel);
77
75
  callback(null, stream);
78
76
  });
79
77
  return;
@@ -82,7 +80,14 @@ PubSub.prototype.subscribe = function(channel, callback) {
82
80
  this._subscribing[channel].push(callback);
83
81
  return;
84
82
  }
85
- var callbacks = this._subscribing[channel] = [callback];
83
+ this._subscribing[channel] = [callback];
84
+ if (this._unsubscribing[channel]) return;
85
+ this._startSubscribe(channel);
86
+ };
87
+
88
+ PubSub.prototype._startSubscribe = function(channel) {
89
+ var pubsub = this;
90
+ var callbacks = this._subscribing[channel];
86
91
  this._subscribe(channel, function(err) {
87
92
  delete pubsub._subscribing[channel];
88
93
  if (err) return util.callEach(callbacks, err);
@@ -142,9 +147,16 @@ PubSub.prototype._removeStream = function(channel, stream) {
142
147
  delete this.streams[channel];
143
148
  // Synchronously clear subscribed state. We won't actually be unsubscribed
144
149
  // until some unknown time in the future. If subscribe is called in this
145
- // period, we want to send a subscription message and wait for it to
146
- // complete before we can count on being subscribed again
150
+ // period, it waits for the unsubscribe to complete before calling
151
+ // _subscribe, since an adapter may drop a subscription made while an
152
+ // unsubscribe from the same channel is still pending
147
153
  delete this.subscribed[channel];
148
154
 
149
- this._unsubscribe(channel, this._defaultCallback);
155
+ var pubsub = this;
156
+ this._unsubscribing[channel] = true;
157
+ this._unsubscribe(channel, function(err) {
158
+ delete pubsub._unsubscribing[channel];
159
+ if (pubsub._subscribing[channel]) pubsub._startSubscribe(channel);
160
+ pubsub._defaultCallback(err);
161
+ });
150
162
  };
@@ -197,7 +197,9 @@ QueryEmitter.prototype.queryPoll = function(callback) {
197
197
  clearTimeout(this._pollIntervalId);
198
198
 
199
199
  var start = Date.now();
200
- this.db.queryPoll(this.collection, this.query, this.options, function(err, ids, extra) {
200
+ var pollOptions = util.shallowCopy(this.options);
201
+ pollOptions.fields = this.fields;
202
+ this.db.queryPoll(this.collection, this.query, pollOptions, function(err, ids, extra) {
201
203
  if (err) return emitter._finishPoll(err, callback, pending);
202
204
  emitter._emitTiming('queryEmitter.poll', start);
203
205
 
@@ -306,11 +308,9 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) {
306
308
  });
307
309
  };
308
310
 
309
- // Clients must assign each of these functions synchronously after constructing
310
- // an instance of QueryEmitter. The instance is subscribed to an op stream at
311
- // construction time, and does not buffer emitted events. Diff events assume
312
- // all messages are received and applied in order, so it is critical that none
313
- // are dropped.
311
+ // Clients must assign each of these functions before calling _open(). The op
312
+ // streams buffer emitted events until then. Diff events assume all messages
313
+ // are received and applied in order, so it is critical that none are dropped.
314
314
  QueryEmitter.prototype.onError =
315
315
  QueryEmitter.prototype.onDiff =
316
316
  QueryEmitter.prototype.onExtra =