sharedb 4.0.2 → 4.1.1
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 +19 -4
- package/lib/client/connection.js +4 -0
- package/lib/client/doc.js +72 -36
- package/lib/client/presence/presence.js +28 -2
- package/lib/client/presence/remote-doc-presence.js +1 -3
- package/lib/error.js +2 -0
- package/package.json +1 -1
- package/test/client/doc.js +33 -0
- package/test/client/presence/doc-presence.js +4 -10
- package/test/client/presence/presence.js +63 -4
- package/test/client/submit.js +105 -0
- package/CHANGELOG.md +0 -44
package/lib/agent.js
CHANGED
|
@@ -453,6 +453,8 @@ Agent.prototype._handleMessage = function(request, callback) {
|
|
|
453
453
|
return this._subscribePresence(request.ch, request.seq, callback);
|
|
454
454
|
case ACTIONS.presenceUnsubscribe:
|
|
455
455
|
return this._unsubscribePresence(request.ch, request.seq, callback);
|
|
456
|
+
case ACTIONS.presenceRequest:
|
|
457
|
+
return this._requestPresence(request.ch, callback);
|
|
456
458
|
case ACTIONS.pingPong:
|
|
457
459
|
return this._pingPong(callback);
|
|
458
460
|
default:
|
|
@@ -776,6 +778,7 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
|
|
|
776
778
|
presence: presence,
|
|
777
779
|
collection: presence.c
|
|
778
780
|
};
|
|
781
|
+
var start = Date.now();
|
|
779
782
|
backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
|
|
780
783
|
if (error) return callback(error);
|
|
781
784
|
backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
|
|
@@ -783,6 +786,7 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
|
|
|
783
786
|
var channel = agent._getPresenceChannel(presence.ch);
|
|
784
787
|
agent.backend.pubsub.publish([channel], presence, function(error) {
|
|
785
788
|
if (error) return callback(error);
|
|
789
|
+
backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
|
|
786
790
|
callback(null, presence);
|
|
787
791
|
});
|
|
788
792
|
});
|
|
@@ -805,29 +809,40 @@ Agent.prototype._createPresence = function(request) {
|
|
|
805
809
|
};
|
|
806
810
|
};
|
|
807
811
|
|
|
808
|
-
Agent.prototype._subscribePresence = function(channel, seq,
|
|
812
|
+
Agent.prototype._subscribePresence = function(channel, seq, cb) {
|
|
809
813
|
var agent = this;
|
|
814
|
+
|
|
815
|
+
function callback(error) {
|
|
816
|
+
cb(error, {ch: channel, seq: seq});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
var existingStream = agent.subscribedPresences[channel];
|
|
820
|
+
if (existingStream) {
|
|
821
|
+
agent.presenceSubscriptionSeq[channel] = seq;
|
|
822
|
+
return callback();
|
|
823
|
+
}
|
|
824
|
+
|
|
810
825
|
var presenceChannel = this._getPresenceChannel(channel);
|
|
811
826
|
this.backend.pubsub.subscribe(presenceChannel, function(error, stream) {
|
|
812
827
|
if (error) return callback(error);
|
|
813
828
|
if (seq < agent.presenceSubscriptionSeq[channel]) {
|
|
814
829
|
stream.destroy();
|
|
815
|
-
return callback(
|
|
830
|
+
return callback();
|
|
816
831
|
}
|
|
817
832
|
agent.presenceSubscriptionSeq[channel] = seq;
|
|
818
833
|
agent.subscribedPresences[channel] = stream;
|
|
819
834
|
agent._subscribeToPresenceStream(channel, stream);
|
|
820
835
|
agent._requestPresence(channel, function(error) {
|
|
821
|
-
callback(error
|
|
836
|
+
callback(error);
|
|
822
837
|
});
|
|
823
838
|
});
|
|
824
839
|
};
|
|
825
840
|
|
|
826
841
|
Agent.prototype._unsubscribePresence = function(channel, seq, callback) {
|
|
827
|
-
if (seq < this.presenceSubscriptionSeq[channel]) return;
|
|
828
842
|
this.presenceSubscriptionSeq[channel] = seq;
|
|
829
843
|
var stream = this.subscribedPresences[channel];
|
|
830
844
|
if (stream) stream.destroy();
|
|
845
|
+
delete this.subscribedPresences[channel];
|
|
831
846
|
callback(null, {ch: channel, seq: seq});
|
|
832
847
|
};
|
|
833
848
|
|
package/lib/client/connection.js
CHANGED
|
@@ -809,6 +809,10 @@ Connection.prototype._addPresence = function(presence) {
|
|
|
809
809
|
});
|
|
810
810
|
};
|
|
811
811
|
|
|
812
|
+
Connection.prototype._requestRemotePresence = function(channel) {
|
|
813
|
+
this.send({a: ACTIONS.presenceRequest, ch: channel});
|
|
814
|
+
};
|
|
815
|
+
|
|
812
816
|
Connection.prototype._handlePresenceSubscribe = function(error, message) {
|
|
813
817
|
var presence = util.dig(this._presences, message.ch);
|
|
814
818
|
if (presence) presence._handleSubscribe(error, message.seq);
|
package/lib/client/doc.js
CHANGED
|
@@ -329,10 +329,6 @@ Doc.prototype._handleSubscribe = function(error, snapshot) {
|
|
|
329
329
|
Doc.prototype._handleOp = function(err, message) {
|
|
330
330
|
if (err) {
|
|
331
331
|
if (this.inflightOp) {
|
|
332
|
-
// The server has rejected submission of the current operation. If we get
|
|
333
|
-
// an "Op submit rejected" error, this was done intentionally
|
|
334
|
-
// and we should roll back but not return an error to the user.
|
|
335
|
-
if (err.code === ERROR_CODE.ERR_OP_SUBMIT_REJECTED) err = null;
|
|
336
332
|
return this._rollback(err);
|
|
337
333
|
}
|
|
338
334
|
return this.emit('error', err);
|
|
@@ -984,38 +980,44 @@ Doc.prototype._rollback = function(err) {
|
|
|
984
980
|
// working state, then call back
|
|
985
981
|
var op = this.inflightOp;
|
|
986
982
|
|
|
987
|
-
if ('op' in op && op.type.invert) {
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
} catch (error) {
|
|
991
|
-
// If the op doesn't support `.invert()`, we just reload the doc
|
|
992
|
-
// instead of trying to locally revert it.
|
|
993
|
-
return this._hardRollback(err);
|
|
994
|
-
}
|
|
983
|
+
if (!('op' in op && op.type.invert)) {
|
|
984
|
+
return this._hardRollback(err);
|
|
985
|
+
}
|
|
995
986
|
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
987
|
+
try {
|
|
988
|
+
op.op = op.type.invert(op.op);
|
|
989
|
+
} catch (error) {
|
|
990
|
+
// If the op doesn't support `.invert()`, we just reload the doc
|
|
991
|
+
// instead of trying to locally revert it.
|
|
992
|
+
return this._hardRollback(err);
|
|
993
|
+
}
|
|
1001
994
|
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
// by the server, the editor window should update to reflect the undo.
|
|
1008
|
-
try {
|
|
1009
|
-
this._otApply(op, false);
|
|
1010
|
-
} catch (error) {
|
|
1011
|
-
return this._hardRollback(error);
|
|
1012
|
-
}
|
|
995
|
+
// Transform the undo operation by any pending ops.
|
|
996
|
+
for (var i = 0; i < this.pendingOps.length; i++) {
|
|
997
|
+
var transformErr = transformX(this.pendingOps[i], op);
|
|
998
|
+
if (transformErr) return this._hardRollback(transformErr);
|
|
999
|
+
}
|
|
1013
1000
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1001
|
+
// ... and apply it locally, reverting the changes.
|
|
1002
|
+
//
|
|
1003
|
+
// This operation is applied to look like it comes from a remote source.
|
|
1004
|
+
// I'm still not 100% sure about this functionality, because its really a
|
|
1005
|
+
// local op. Basically, the problem is that if the client's op is rejected
|
|
1006
|
+
// by the server, the editor window should update to reflect the undo.
|
|
1007
|
+
try {
|
|
1008
|
+
this._otApply(op, false);
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
return this._hardRollback(error);
|
|
1016
1011
|
}
|
|
1017
1012
|
|
|
1018
|
-
|
|
1013
|
+
// The server has rejected submission of the current operation. If we get
|
|
1014
|
+
// an "Op submit rejected" error, this was done intentionally
|
|
1015
|
+
// and we should roll back but not return an error to the user.
|
|
1016
|
+
if (err.code === ERROR_CODE.ERR_OP_SUBMIT_REJECTED) {
|
|
1017
|
+
return this._clearInflightOp(null);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
this._clearInflightOp(err);
|
|
1019
1021
|
};
|
|
1020
1022
|
|
|
1021
1023
|
Doc.prototype._hardRollback = function(err) {
|
|
@@ -1024,9 +1026,8 @@ Doc.prototype._hardRollback = function(err) {
|
|
|
1024
1026
|
// to hit a condition where we have no inflight op, but we do have pending
|
|
1025
1027
|
// ops. This can happen when an invalid op is submitted, which causes us
|
|
1026
1028
|
// to hard rollback before the pending op was flushed.
|
|
1027
|
-
var pendingOps =
|
|
1028
|
-
|
|
1029
|
-
pendingOps = pendingOps.concat(this.pendingOps);
|
|
1029
|
+
var pendingOps = this.pendingOps;
|
|
1030
|
+
var inflightOp = this.inflightOp;
|
|
1030
1031
|
|
|
1031
1032
|
// Cancel all pending ops and reset if we can't invert
|
|
1032
1033
|
this._setType(null);
|
|
@@ -1036,17 +1037,50 @@ Doc.prototype._hardRollback = function(err) {
|
|
|
1036
1037
|
|
|
1037
1038
|
// Fetch the latest version from the server to get us back into a working state
|
|
1038
1039
|
var doc = this;
|
|
1039
|
-
this._fetch({force: true}, function() {
|
|
1040
|
+
this._fetch({force: true}, function(fetchError) {
|
|
1040
1041
|
// We want to check that no errors are swallowed, so we check that:
|
|
1041
1042
|
// - there are callbacks to call, and
|
|
1042
1043
|
// - that every single pending op called a callback
|
|
1043
1044
|
// If there are no ops queued, or one of them didn't handle the error,
|
|
1044
1045
|
// then we emit the error.
|
|
1046
|
+
|
|
1047
|
+
if (fetchError) {
|
|
1048
|
+
// This is critical error as it means that our doc is not in usable state
|
|
1049
|
+
// anymore, we should throw doc error.
|
|
1050
|
+
logger.error('Hard rollback doc fetch failed.', fetchError, inflightOp);
|
|
1051
|
+
|
|
1052
|
+
doc.emit('error', new ShareDBError(
|
|
1053
|
+
ERROR_CODE.ERR_HARD_ROLLBACK_FETCH_FAILED,
|
|
1054
|
+
'Hard rollback fetch failed: ' + fetchError.message
|
|
1055
|
+
));
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
if (err.code === ERROR_CODE.ERR_OP_SUBMIT_REJECTED) {
|
|
1059
|
+
/**
|
|
1060
|
+
* Handle special case of ERR_OP_SUBMIT_REJECTED
|
|
1061
|
+
* This ensures that we resolve the main op callback and reject
|
|
1062
|
+
* all the pending ops. This is hard rollback so all the pending ops will be
|
|
1063
|
+
* discarded. This will ensure that the user is at least informed about it.
|
|
1064
|
+
* more info: https://github.com/share/sharedb/pull/626
|
|
1065
|
+
*/
|
|
1066
|
+
if (inflightOp) {
|
|
1067
|
+
util.callEach(inflightOp.callbacks);
|
|
1068
|
+
inflightOp = null;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if (!pendingOps.length) return;
|
|
1072
|
+
err = new ShareDBError(
|
|
1073
|
+
ERROR_CODE.ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED,
|
|
1074
|
+
'Discarding pending op because of hard rollback during ERR_OP_SUBMIT_REJECTED'
|
|
1075
|
+
);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
if (inflightOp) pendingOps.unshift(inflightOp);
|
|
1045
1079
|
var allOpsHadCallbacks = !!pendingOps.length;
|
|
1046
1080
|
for (var i = 0; i < pendingOps.length; i++) {
|
|
1047
1081
|
allOpsHadCallbacks = util.callEach(pendingOps[i].callbacks, err) && allOpsHadCallbacks;
|
|
1048
1082
|
}
|
|
1049
|
-
if (err && !allOpsHadCallbacks)
|
|
1083
|
+
if (err && !allOpsHadCallbacks) doc.emit('error', err);
|
|
1050
1084
|
});
|
|
1051
1085
|
};
|
|
1052
1086
|
|
|
@@ -1062,3 +1096,5 @@ Doc.prototype._clearInflightOp = function(err) {
|
|
|
1062
1096
|
|
|
1063
1097
|
if (err && !called) return this.emit('error', err);
|
|
1064
1098
|
};
|
|
1099
|
+
|
|
1100
|
+
|
|
@@ -84,7 +84,15 @@ Presence.prototype.destroy = function(callback) {
|
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
Presence.prototype._sendSubscriptionAction = function(wantSubscribe, callback) {
|
|
87
|
-
|
|
87
|
+
wantSubscribe = !!wantSubscribe;
|
|
88
|
+
if (wantSubscribe === this.wantSubscribe) {
|
|
89
|
+
if (!callback) return;
|
|
90
|
+
if (wantSubscribe === this.subscribed) return util.nextTick(callback);
|
|
91
|
+
if (Object.keys(this._subscriptionCallbacksBySeq).length) {
|
|
92
|
+
return this._combineSubscribeCallbackWithLastAdded(callback);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
this.wantSubscribe = wantSubscribe;
|
|
88
96
|
var action = this.wantSubscribe ? ACTIONS.presenceSubscribe : ACTIONS.presenceUnsubscribe;
|
|
89
97
|
var seq = this.connection._presenceSeq++;
|
|
90
98
|
this._subscriptionCallbacksBySeq[seq] = callback;
|
|
@@ -93,6 +101,10 @@ Presence.prototype._sendSubscriptionAction = function(wantSubscribe, callback) {
|
|
|
93
101
|
}
|
|
94
102
|
};
|
|
95
103
|
|
|
104
|
+
Presence.prototype._requestRemotePresence = function() {
|
|
105
|
+
this.connection._requestRemotePresence(this.channel);
|
|
106
|
+
};
|
|
107
|
+
|
|
96
108
|
Presence.prototype._handleSubscribe = function(error, seq) {
|
|
97
109
|
if (this.wantSubscribe) this.subscribed = true;
|
|
98
110
|
var callback = this._subscriptionCallback(seq);
|
|
@@ -139,7 +151,10 @@ Presence.prototype._removeRemotePresence = function(id) {
|
|
|
139
151
|
};
|
|
140
152
|
|
|
141
153
|
Presence.prototype._onConnectionStateChanged = function() {
|
|
142
|
-
if (!this.connection.canSend)
|
|
154
|
+
if (!this.connection.canSend) {
|
|
155
|
+
this.subscribed = false;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
143
158
|
this._resubscribe();
|
|
144
159
|
for (var id in this.localPresences) {
|
|
145
160
|
this.localPresences[id]._sendPending();
|
|
@@ -184,3 +199,14 @@ Presence.prototype._callEachOrEmit = function(callbacks, error) {
|
|
|
184
199
|
var called = util.callEach(callbacks, error);
|
|
185
200
|
if (!called && error) this.emit('error', error);
|
|
186
201
|
};
|
|
202
|
+
|
|
203
|
+
Presence.prototype._combineSubscribeCallbackWithLastAdded = function(callback) {
|
|
204
|
+
var seqs = Object.keys(this._subscriptionCallbacksBySeq);
|
|
205
|
+
var lastSeq = seqs[seqs.length - 1];
|
|
206
|
+
var originalCallback = this._subscriptionCallbacksBySeq[lastSeq];
|
|
207
|
+
if (!originalCallback) return this._subscriptionCallbacksBySeq[lastSeq] = callback;
|
|
208
|
+
this._subscriptionCallbacksBySeq[lastSeq] = function(error) {
|
|
209
|
+
originalCallback(error);
|
|
210
|
+
callback(error);
|
|
211
|
+
};
|
|
212
|
+
};
|
|
@@ -105,9 +105,7 @@ RemoteDocPresence.prototype._catchUpStalePresence = function() {
|
|
|
105
105
|
if (!this._opCache) {
|
|
106
106
|
this._startCachingOps();
|
|
107
107
|
this._doc.fetch();
|
|
108
|
-
|
|
109
|
-
// to force presence updates from other clients
|
|
110
|
-
this.presence.subscribe();
|
|
108
|
+
this.presence._requestRemotePresence();
|
|
111
109
|
return false;
|
|
112
110
|
}
|
|
113
111
|
|
package/lib/error.js
CHANGED
|
@@ -38,6 +38,8 @@ ShareDBError.CODES = {
|
|
|
38
38
|
ERR_OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED',
|
|
39
39
|
ERR_OP_NOT_ALLOWED_IN_PROJECTION: 'ERR_OP_NOT_ALLOWED_IN_PROJECTION',
|
|
40
40
|
ERR_OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED',
|
|
41
|
+
ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED: 'ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED',
|
|
42
|
+
ERR_HARD_ROLLBACK_FETCH_FAILED: 'ERR_HARD_ROLLBACK_FETCH_FAILED',
|
|
41
43
|
ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM',
|
|
42
44
|
ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM',
|
|
43
45
|
ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT: 'ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT',
|
package/package.json
CHANGED
package/test/client/doc.js
CHANGED
|
@@ -6,6 +6,7 @@ var richText = require('rich-text').type;
|
|
|
6
6
|
var ShareDBError = require('../../lib/error');
|
|
7
7
|
var errorHandler = require('../util').errorHandler;
|
|
8
8
|
var sinon = require('sinon');
|
|
9
|
+
var types = require('../../lib/types');
|
|
9
10
|
|
|
10
11
|
describe('Doc', function() {
|
|
11
12
|
beforeEach(function() {
|
|
@@ -514,6 +515,38 @@ describe('Doc', function() {
|
|
|
514
515
|
], done);
|
|
515
516
|
});
|
|
516
517
|
|
|
518
|
+
it('throws an error when hard rollback fetch failed', function(done) {
|
|
519
|
+
var backend = this.backend;
|
|
520
|
+
doc = this.connection.get('dogs', 'scrappy');
|
|
521
|
+
types.register(richText);
|
|
522
|
+
|
|
523
|
+
async.series([
|
|
524
|
+
doc.create.bind(doc, {ops: [{insert: 'Scrappy'}]}, 'rich-text'),
|
|
525
|
+
function(next) {
|
|
526
|
+
backend.use('reply', function(replyContext, cb) {
|
|
527
|
+
if (replyContext.request.a !== 'f') return cb();
|
|
528
|
+
cb({code: 'FETCH_ERROR'});
|
|
529
|
+
});
|
|
530
|
+
backend.use('submit', function(_context, cb) {
|
|
531
|
+
cb(new ShareDBError('SUBMIT_ERROR'));
|
|
532
|
+
});
|
|
533
|
+
var nonInvertibleOp = [{insert: 'e'}];
|
|
534
|
+
|
|
535
|
+
var count = 0;
|
|
536
|
+
function expectError(code) {
|
|
537
|
+
count++;
|
|
538
|
+
return function(error) {
|
|
539
|
+
expect(error.code).to.equal(code);
|
|
540
|
+
count--;
|
|
541
|
+
if (!count) next();
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
doc.on('error', expectError('ERR_HARD_ROLLBACK_FETCH_FAILED'));
|
|
546
|
+
doc.submitOp(nonInvertibleOp, expectError('SUBMIT_ERROR'));
|
|
547
|
+
}
|
|
548
|
+
], done);
|
|
549
|
+
});
|
|
517
550
|
|
|
518
551
|
it('rescues an irreversible op collision', function(done) {
|
|
519
552
|
// This test case attempts to reconstruct the following corner case, with
|
|
@@ -645,20 +645,14 @@ describe('DocPresence', function() {
|
|
|
645
645
|
presence1.subscribe.bind(presence1),
|
|
646
646
|
presence2.subscribe.bind(presence2),
|
|
647
647
|
function(next) {
|
|
648
|
-
connection1.
|
|
649
|
-
next();
|
|
650
|
-
},
|
|
651
|
-
function(next) {
|
|
652
|
-
localPresence2.submit({index: 0}, errorHandler(done));
|
|
653
|
-
// We've not _actually_ disconnected the connection, so this
|
|
654
|
-
// event will still fire.
|
|
655
|
-
presence1.once('receive', function() {
|
|
648
|
+
connection1.once('closed', function() {
|
|
656
649
|
next();
|
|
657
650
|
});
|
|
651
|
+
connection1.close();
|
|
658
652
|
},
|
|
653
|
+
localPresence2.submit.bind(localPresence2, {index: 0}),
|
|
659
654
|
function(next) {
|
|
660
|
-
|
|
661
|
-
connection1._setState('connected');
|
|
655
|
+
backend.connect(connection1);
|
|
662
656
|
presence1.once('receive', function(id, presence) {
|
|
663
657
|
expect(presence).to.eql({index: 0});
|
|
664
658
|
next();
|
|
@@ -344,9 +344,11 @@ describe('Presence', function() {
|
|
|
344
344
|
presence1.subscribe.bind(presence1),
|
|
345
345
|
presence2.subscribe.bind(presence2),
|
|
346
346
|
function(next) {
|
|
347
|
-
connection2.
|
|
347
|
+
connection2.once('closed', function() {
|
|
348
|
+
next();
|
|
349
|
+
});
|
|
350
|
+
connection2.close();
|
|
348
351
|
expect(connection2.canSend).to.be.false;
|
|
349
|
-
next();
|
|
350
352
|
},
|
|
351
353
|
localPresence1.submit.bind(localPresence1, {index: 1}),
|
|
352
354
|
function(next) {
|
|
@@ -355,8 +357,7 @@ describe('Presence', function() {
|
|
|
355
357
|
expect(presence).to.eql({index: 1});
|
|
356
358
|
next();
|
|
357
359
|
});
|
|
358
|
-
|
|
359
|
-
connection2._setState('connected');
|
|
360
|
+
backend.connect(connection2);
|
|
360
361
|
}
|
|
361
362
|
], done);
|
|
362
363
|
});
|
|
@@ -401,6 +402,64 @@ describe('Presence', function() {
|
|
|
401
402
|
], done);
|
|
402
403
|
});
|
|
403
404
|
|
|
405
|
+
it('does not send another subscribe request if already subscribed', function(done) {
|
|
406
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
407
|
+
async.series([
|
|
408
|
+
presence1.subscribe.bind(presence1),
|
|
409
|
+
presence1.subscribe.bind(presence1),
|
|
410
|
+
function(next) {
|
|
411
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
412
|
+
next();
|
|
413
|
+
}
|
|
414
|
+
], done);
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it('only subscribes once when calling multiple in parallel', function(done) {
|
|
418
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
419
|
+
async.series([
|
|
420
|
+
function(next) {
|
|
421
|
+
async.parallel([
|
|
422
|
+
presence1.subscribe.bind(presence1),
|
|
423
|
+
presence1.subscribe.bind(presence1)
|
|
424
|
+
], next);
|
|
425
|
+
},
|
|
426
|
+
function(next) {
|
|
427
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
428
|
+
next();
|
|
429
|
+
}
|
|
430
|
+
], done);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('subscribes once when calling again after no callback', function(done) {
|
|
434
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
435
|
+
presence1.subscribe(); // no callback
|
|
436
|
+
async.series([
|
|
437
|
+
presence1.subscribe.bind(presence1),
|
|
438
|
+
function(next) {
|
|
439
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
440
|
+
next();
|
|
441
|
+
}
|
|
442
|
+
], done);
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('does not leak Streams when subscribing the same presence multiple times', function(done) {
|
|
446
|
+
var streamsCount;
|
|
447
|
+
async.series([
|
|
448
|
+
presence1.subscribe.bind(presence1),
|
|
449
|
+
function(next) {
|
|
450
|
+
streamsCount = backend.pubsub.streamsCount;
|
|
451
|
+
// Trick it into sending a duplicate request
|
|
452
|
+
presence1.wantSubscribe = false;
|
|
453
|
+
next();
|
|
454
|
+
},
|
|
455
|
+
presence1.subscribe.bind(presence1),
|
|
456
|
+
function(next) {
|
|
457
|
+
expect(backend.pubsub.streamsCount).to.equal(streamsCount);
|
|
458
|
+
next();
|
|
459
|
+
}
|
|
460
|
+
], done);
|
|
461
|
+
});
|
|
462
|
+
|
|
404
463
|
it('throws an error when trying to create a presence with a non-string ID', function() {
|
|
405
464
|
expect(function() {
|
|
406
465
|
presence1.create(123);
|
package/test/client/submit.js
CHANGED
|
@@ -5,9 +5,11 @@ var types = require('../../lib/types');
|
|
|
5
5
|
var deserializedType = require('./deserialized-type');
|
|
6
6
|
var numberType = require('./number-type');
|
|
7
7
|
var errorHandler = require('../util').errorHandler;
|
|
8
|
+
var richText = require('rich-text');
|
|
8
9
|
types.register(deserializedType.type);
|
|
9
10
|
types.register(deserializedType.type2);
|
|
10
11
|
types.register(numberType.type);
|
|
12
|
+
types.register(richText.type);
|
|
11
13
|
|
|
12
14
|
module.exports = function() {
|
|
13
15
|
describe('client submit', function() {
|
|
@@ -919,6 +921,27 @@ module.exports = function() {
|
|
|
919
921
|
], done);
|
|
920
922
|
});
|
|
921
923
|
|
|
924
|
+
|
|
925
|
+
it('request.rejectedError() soft rejects main op and throws for pending ops on hard rollback', function(done) {
|
|
926
|
+
this.backend.use('submit', function(request, next) {
|
|
927
|
+
if (request.op.create) {
|
|
928
|
+
next(request.rejectedError());
|
|
929
|
+
}
|
|
930
|
+
});
|
|
931
|
+
|
|
932
|
+
var connection = this.backend.connect();
|
|
933
|
+
var doc = connection.get('dogs', 'fido');
|
|
934
|
+
doc.preventCompose = true;
|
|
935
|
+
|
|
936
|
+
doc.create({age: 3}, function(error) {
|
|
937
|
+
if (error) done(error);
|
|
938
|
+
});
|
|
939
|
+
doc.submitOp({p: ['age'], na: 1}, function(err) {
|
|
940
|
+
expect(err.code).to.be.equal('ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED');
|
|
941
|
+
done();
|
|
942
|
+
});
|
|
943
|
+
});
|
|
944
|
+
|
|
922
945
|
it('request.rejectedError() soft rejects a create', function(done) {
|
|
923
946
|
this.backend.use('submit', function(request, next) {
|
|
924
947
|
next(request.rejectedError());
|
|
@@ -949,6 +972,29 @@ module.exports = function() {
|
|
|
949
972
|
});
|
|
950
973
|
});
|
|
951
974
|
|
|
975
|
+
it(
|
|
976
|
+
'request.rejectedError() soft rejects main op and throws for pending ops on hard rollback without callback',
|
|
977
|
+
function(done) {
|
|
978
|
+
this.backend.use('submit', function(request, next) {
|
|
979
|
+
if (request.op.create) {
|
|
980
|
+
next(request.rejectedError());
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
var connection = this.backend.connect();
|
|
985
|
+
var doc = connection.get('dogs', 'fido');
|
|
986
|
+
doc.preventCompose = true;
|
|
987
|
+
|
|
988
|
+
doc.create({age: 3});
|
|
989
|
+
doc.submitOp({p: ['age'], na: 1});
|
|
990
|
+
|
|
991
|
+
doc.on('error', function(err) {
|
|
992
|
+
expect(err.code).to.be.equal('ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED');
|
|
993
|
+
done();
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
);
|
|
997
|
+
|
|
952
998
|
it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) {
|
|
953
999
|
this.backend.use('submit', function(request, next) {
|
|
954
1000
|
if ('op' in request.op) return next({message: 'Custom error'});
|
|
@@ -1043,6 +1089,65 @@ module.exports = function() {
|
|
|
1043
1089
|
});
|
|
1044
1090
|
});
|
|
1045
1091
|
|
|
1092
|
+
it('request.rejectedError() soft rejects main op and pending ops for invertible type', function(done) {
|
|
1093
|
+
var rejectedOnce = false;
|
|
1094
|
+
this.backend.use('submit', function(request, next) {
|
|
1095
|
+
if ('op' in request.op && !rejectedOnce) {
|
|
1096
|
+
rejectedOnce = true;
|
|
1097
|
+
return next(request.rejectedError());
|
|
1098
|
+
}
|
|
1099
|
+
next();
|
|
1100
|
+
});
|
|
1101
|
+
var doc = this.backend.connect().get('dogs', 'fido');
|
|
1102
|
+
doc.preventCompose = true;
|
|
1103
|
+
|
|
1104
|
+
doc.create({age: 3}, function(err) {
|
|
1105
|
+
if (err) return done(err);
|
|
1106
|
+
doc.submitOp({p: ['age'], na: 1}, function(err) {
|
|
1107
|
+
if (err) return done(err);
|
|
1108
|
+
});
|
|
1109
|
+
doc.submitOp({p: ['age'], na: 3}, function(err) {
|
|
1110
|
+
if (err) return done(err);
|
|
1111
|
+
expect(doc.version).equal(2);
|
|
1112
|
+
expect(doc.data).eql({age: 6});
|
|
1113
|
+
done();
|
|
1114
|
+
});
|
|
1115
|
+
expect(doc.version).equal(1);
|
|
1116
|
+
expect(doc.data).eql({age: 7});
|
|
1117
|
+
});
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
it(
|
|
1121
|
+
'request.rejectedError() soft rejects main op and throws for pending ops for non invertible type',
|
|
1122
|
+
function(done) {
|
|
1123
|
+
var rejectedOnce = false;
|
|
1124
|
+
this.backend.use('submit', function(request, next) {
|
|
1125
|
+
if ('op' in request.op && !rejectedOnce) {
|
|
1126
|
+
rejectedOnce = true;
|
|
1127
|
+
return next(request.rejectedError());
|
|
1128
|
+
}
|
|
1129
|
+
next();
|
|
1130
|
+
});
|
|
1131
|
+
var doc = this.backend.connect().get('dogs', 'fido');
|
|
1132
|
+
doc.preventCompose = true;
|
|
1133
|
+
|
|
1134
|
+
doc.create({ops: [{insert: 'Scrappy'}]}, 'rich-text', function(err) {
|
|
1135
|
+
if (err) return done(err);
|
|
1136
|
+
|
|
1137
|
+
var nonInvertibleOp = [{insert: 'a'}];
|
|
1138
|
+
doc.submitOp(nonInvertibleOp, function(err) {
|
|
1139
|
+
if (err) return done(err);
|
|
1140
|
+
});
|
|
1141
|
+
doc.submitOp([{insert: 'b'}], function(err) {
|
|
1142
|
+
expect(err.code).to.be.equal('ERR_PENDING_OP_REMOVED_BY_OP_SUBMIT_REJECTED');
|
|
1143
|
+
done();
|
|
1144
|
+
});
|
|
1145
|
+
expect(doc.version).equal(1);
|
|
1146
|
+
expect(doc.data.ops).eql([{insert: 'baScrappy'}]);
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
);
|
|
1150
|
+
|
|
1046
1151
|
it('request.rejectedError() soft rejects an op without callback', function(done) {
|
|
1047
1152
|
this.backend.use('submit', function(request, next) {
|
|
1048
1153
|
if ('op' in request.op) return next(request.rejectedError());
|
package/CHANGELOG.md
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
## v2.0
|
|
2
|
-
|
|
3
|
-
### Breaking changes
|
|
4
|
-
|
|
5
|
-
* Drop Node.js v10 support
|
|
6
|
-
|
|
7
|
-
## v1.0-beta
|
|
8
|
-
|
|
9
|
-
### Breaking changes
|
|
10
|
-
|
|
11
|
-
* Add options argument to all public database adapter methods that read
|
|
12
|
-
or write from snapshots or ops.
|
|
13
|
-
|
|
14
|
-
* DB methods that get snapshots or ops no longer return metadata unless
|
|
15
|
-
`{metadata: true}` option is passed.
|
|
16
|
-
|
|
17
|
-
* Replace `source` argument with `options` in doc methods. Use `options.source`
|
|
18
|
-
instead.
|
|
19
|
-
|
|
20
|
-
* Backend streams now write objects intead of strings.
|
|
21
|
-
|
|
22
|
-
* MemoryDB.prototype._querySync now returns `{snapshots: ..., extra: ...}`
|
|
23
|
-
instead of just an array of snapshots.
|
|
24
|
-
|
|
25
|
-
### Non-breaking changes
|
|
26
|
-
|
|
27
|
-
* Add options argument to backend.submit.
|
|
28
|
-
|
|
29
|
-
* Add error codes to all errors.
|
|
30
|
-
|
|
31
|
-
* Add `'updated'` event on queries which fires on all query result changes.
|
|
32
|
-
|
|
33
|
-
* In clients, wrap errors in Error objects to they get passed through event
|
|
34
|
-
emitters.
|
|
35
|
-
|
|
36
|
-
* Sanitize stack traces when sending errors to client, but log them on the
|
|
37
|
-
server.
|
|
38
|
-
|
|
39
|
-
## v0.11.37
|
|
40
|
-
|
|
41
|
-
Beginning of changelog.
|
|
42
|
-
|
|
43
|
-
If you're upgrading from ShareJS 0.7 or earlier,
|
|
44
|
-
take a look at the [ShareJS upgrade guide](docs/upgrading-from-sharejs.md).
|