sharedb 4.0.2 → 4.1.0
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 +17 -4
- package/lib/client/doc.js +72 -36
- package/lib/client/presence/presence.js +34 -7
- package/lib/client/presence/remote-doc-presence.js +1 -1
- package/lib/error.js +2 -0
- package/package.json +1 -1
- package/test/client/doc.js +33 -0
- package/test/client/presence/presence.js +56 -0
- package/test/client/submit.js +105 -0
- package/CHANGELOG.md +0 -44
package/lib/agent.js
CHANGED
|
@@ -776,6 +776,7 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
|
|
|
776
776
|
presence: presence,
|
|
777
777
|
collection: presence.c
|
|
778
778
|
};
|
|
779
|
+
var start = Date.now();
|
|
779
780
|
backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
|
|
780
781
|
if (error) return callback(error);
|
|
781
782
|
backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
|
|
@@ -783,6 +784,7 @@ Agent.prototype._broadcastPresence = function(presence, callback) {
|
|
|
783
784
|
var channel = agent._getPresenceChannel(presence.ch);
|
|
784
785
|
agent.backend.pubsub.publish([channel], presence, function(error) {
|
|
785
786
|
if (error) return callback(error);
|
|
787
|
+
backend.emit('timing', 'presence.broadcast', Date.now() - start, context);
|
|
786
788
|
callback(null, presence);
|
|
787
789
|
});
|
|
788
790
|
});
|
|
@@ -807,6 +809,19 @@ Agent.prototype._createPresence = function(request) {
|
|
|
807
809
|
|
|
808
810
|
Agent.prototype._subscribePresence = function(channel, seq, callback) {
|
|
809
811
|
var agent = this;
|
|
812
|
+
|
|
813
|
+
function requestPresence() {
|
|
814
|
+
agent._requestPresence(channel, function(error) {
|
|
815
|
+
callback(error, {ch: channel, seq: seq});
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
var existingStream = agent.subscribedPresences[channel];
|
|
820
|
+
if (existingStream) {
|
|
821
|
+
agent.presenceSubscriptionSeq[channel] = seq;
|
|
822
|
+
return requestPresence();
|
|
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);
|
|
@@ -817,17 +832,15 @@ Agent.prototype._subscribePresence = function(channel, seq, callback) {
|
|
|
817
832
|
agent.presenceSubscriptionSeq[channel] = seq;
|
|
818
833
|
agent.subscribedPresences[channel] = stream;
|
|
819
834
|
agent._subscribeToPresenceStream(channel, stream);
|
|
820
|
-
|
|
821
|
-
callback(error, {ch: channel, seq: seq});
|
|
822
|
-
});
|
|
835
|
+
requestPresence();
|
|
823
836
|
});
|
|
824
837
|
};
|
|
825
838
|
|
|
826
839
|
Agent.prototype._unsubscribePresence = function(channel, seq, callback) {
|
|
827
|
-
if (seq < this.presenceSubscriptionSeq[channel]) return;
|
|
828
840
|
this.presenceSubscriptionSeq[channel] = seq;
|
|
829
841
|
var stream = this.subscribedPresences[channel];
|
|
830
842
|
if (stream) stream.destroy();
|
|
843
|
+
delete this.subscribedPresences[channel];
|
|
831
844
|
callback(null, {ch: channel, seq: seq});
|
|
832
845
|
};
|
|
833
846
|
|
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
|
+
|
|
@@ -28,12 +28,12 @@ function Presence(connection, channel) {
|
|
|
28
28
|
}
|
|
29
29
|
emitter.mixin(Presence);
|
|
30
30
|
|
|
31
|
-
Presence.prototype.subscribe = function(callback) {
|
|
32
|
-
this._sendSubscriptionAction(true, callback);
|
|
31
|
+
Presence.prototype.subscribe = function(options, callback) {
|
|
32
|
+
this._sendSubscriptionAction(true, options, callback);
|
|
33
33
|
};
|
|
34
34
|
|
|
35
|
-
Presence.prototype.unsubscribe = function(callback) {
|
|
36
|
-
this._sendSubscriptionAction(false, callback);
|
|
35
|
+
Presence.prototype.unsubscribe = function(options, callback) {
|
|
36
|
+
this._sendSubscriptionAction(false, options, callback);
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
Presence.prototype.create = function(id) {
|
|
@@ -83,8 +83,21 @@ Presence.prototype.destroy = function(callback) {
|
|
|
83
83
|
});
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
Presence.prototype._sendSubscriptionAction = function(wantSubscribe, callback) {
|
|
87
|
-
|
|
86
|
+
Presence.prototype._sendSubscriptionAction = function(wantSubscribe, options, callback) {
|
|
87
|
+
if (typeof options === 'function') {
|
|
88
|
+
callback = options;
|
|
89
|
+
options = {};
|
|
90
|
+
}
|
|
91
|
+
options = options || {};
|
|
92
|
+
wantSubscribe = !!wantSubscribe;
|
|
93
|
+
if (!options._force && wantSubscribe === this.wantSubscribe) {
|
|
94
|
+
if (!callback) return;
|
|
95
|
+
if (wantSubscribe === this.subscribed) return util.nextTick(callback);
|
|
96
|
+
if (Object.keys(this._subscriptionCallbacksBySeq).length) {
|
|
97
|
+
return this._combineSubscribeCallbackWithLastAdded(callback);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.wantSubscribe = wantSubscribe;
|
|
88
101
|
var action = this.wantSubscribe ? ACTIONS.presenceSubscribe : ACTIONS.presenceUnsubscribe;
|
|
89
102
|
var seq = this.connection._presenceSeq++;
|
|
90
103
|
this._subscriptionCallbacksBySeq[seq] = callback;
|
|
@@ -139,7 +152,10 @@ Presence.prototype._removeRemotePresence = function(id) {
|
|
|
139
152
|
};
|
|
140
153
|
|
|
141
154
|
Presence.prototype._onConnectionStateChanged = function() {
|
|
142
|
-
if (!this.connection.canSend)
|
|
155
|
+
if (!this.connection.canSend) {
|
|
156
|
+
this.subscribed = false;
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
143
159
|
this._resubscribe();
|
|
144
160
|
for (var id in this.localPresences) {
|
|
145
161
|
this.localPresences[id]._sendPending();
|
|
@@ -184,3 +200,14 @@ Presence.prototype._callEachOrEmit = function(callbacks, error) {
|
|
|
184
200
|
var called = util.callEach(callbacks, error);
|
|
185
201
|
if (!called && error) this.emit('error', error);
|
|
186
202
|
};
|
|
203
|
+
|
|
204
|
+
Presence.prototype._combineSubscribeCallbackWithLastAdded = function(callback) {
|
|
205
|
+
var seqs = Object.keys(this._subscriptionCallbacksBySeq);
|
|
206
|
+
var lastSeq = seqs[seqs.length - 1];
|
|
207
|
+
var originalCallback = this._subscriptionCallbacksBySeq[lastSeq];
|
|
208
|
+
if (!originalCallback) return this._subscriptionCallbacksBySeq[lastSeq] = callback;
|
|
209
|
+
this._subscriptionCallbacksBySeq[lastSeq] = function(error) {
|
|
210
|
+
originalCallback(error);
|
|
211
|
+
callback(error);
|
|
212
|
+
};
|
|
213
|
+
};
|
|
@@ -107,7 +107,7 @@ RemoteDocPresence.prototype._catchUpStalePresence = function() {
|
|
|
107
107
|
this._doc.fetch();
|
|
108
108
|
// We're already subscribed, but we send another subscribe message
|
|
109
109
|
// to force presence updates from other clients
|
|
110
|
-
this.presence.subscribe();
|
|
110
|
+
this.presence.subscribe({_force: true});
|
|
111
111
|
return false;
|
|
112
112
|
}
|
|
113
113
|
|
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
|
|
@@ -401,6 +401,62 @@ describe('Presence', function() {
|
|
|
401
401
|
], done);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
+
it('does not send another subscribe request if already subscribed', function(done) {
|
|
405
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
406
|
+
async.series([
|
|
407
|
+
presence1.subscribe.bind(presence1),
|
|
408
|
+
presence1.subscribe.bind(presence1),
|
|
409
|
+
function(next) {
|
|
410
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
411
|
+
next();
|
|
412
|
+
}
|
|
413
|
+
], done);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('only subscribes once when calling multiple in parallel', function(done) {
|
|
417
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
418
|
+
async.series([
|
|
419
|
+
function(next) {
|
|
420
|
+
async.parallel([
|
|
421
|
+
presence1.subscribe.bind(presence1),
|
|
422
|
+
presence1.subscribe.bind(presence1)
|
|
423
|
+
], next);
|
|
424
|
+
},
|
|
425
|
+
function(next) {
|
|
426
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
427
|
+
next();
|
|
428
|
+
}
|
|
429
|
+
], done);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('subscribes once when calling again after no callback', function(done) {
|
|
433
|
+
var sendPresenceAction = sinon.spy(connection1, '_sendPresenceAction');
|
|
434
|
+
presence1.subscribe(); // no callback
|
|
435
|
+
async.series([
|
|
436
|
+
presence1.subscribe.bind(presence1),
|
|
437
|
+
function(next) {
|
|
438
|
+
expect(sendPresenceAction).to.have.been.calledOnce;
|
|
439
|
+
next();
|
|
440
|
+
}
|
|
441
|
+
], done);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('does not leak Streams when subscribing the same presence multiple times', function(done) {
|
|
445
|
+
var streamsCount;
|
|
446
|
+
async.series([
|
|
447
|
+
presence1.subscribe.bind(presence1, {force: true}),
|
|
448
|
+
function(next) {
|
|
449
|
+
streamsCount = backend.pubsub.streamsCount;
|
|
450
|
+
next();
|
|
451
|
+
},
|
|
452
|
+
presence1.subscribe.bind(presence1, {force: true}),
|
|
453
|
+
function(next) {
|
|
454
|
+
expect(backend.pubsub.streamsCount).to.equal(streamsCount);
|
|
455
|
+
next();
|
|
456
|
+
}
|
|
457
|
+
], done);
|
|
458
|
+
});
|
|
459
|
+
|
|
404
460
|
it('throws an error when trying to create a presence with a non-string ID', function() {
|
|
405
461
|
expect(function() {
|
|
406
462
|
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).
|