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 +197 -99
- package/lib/backend.js +40 -9
- package/lib/client/presence/doc-presence.js +6 -0
- package/lib/client/presence/presence.js +4 -1
- package/lib/pubsub/index.js +21 -9
- package/lib/query-emitter.js +6 -6
- package/lib/submit-request.js +8 -1
- package/lib/util.js +10 -0
- package/package.json +1 -1
- package/test/agent.js +415 -0
- package/test/client/presence/doc-presence.js +861 -18
- package/test/client/presence/presence.js +582 -1
- package/test/client/query-subscribe.js +25 -0
- package/test/middleware.js +150 -0
- package/test/pubsub-memory.js +75 -0
- package/test/pubsub.js +32 -0
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
|
|
49
|
-
//
|
|
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
|
-
|
|
97
|
-
|
|
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
|
|
107
|
-
this.
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
|
|
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
|
|
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 =
|
|
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)
|
|
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
|
|
865
|
-
|
|
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
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
var
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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,
|
|
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
|
-
|
|
946
|
+
var backend = this;
|
|
947
|
+
this._getDocVersionForPresence(agent, presence, trackedVersion, function(error, docVersion) {
|
|
941
948
|
if (error) return callback(error);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
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);
|
package/lib/pubsub/index.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
146
|
-
//
|
|
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
|
-
|
|
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
|
};
|
package/lib/query-emitter.js
CHANGED
|
@@ -197,7 +197,9 @@ QueryEmitter.prototype.queryPoll = function(callback) {
|
|
|
197
197
|
clearTimeout(this._pollIntervalId);
|
|
198
198
|
|
|
199
199
|
var start = Date.now();
|
|
200
|
-
|
|
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
|
|
310
|
-
//
|
|
311
|
-
//
|
|
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 =
|