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 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, callback) {
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(null, {ch: channel, seq: seq});
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, {ch: channel, seq: seq});
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
 
@@ -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
- try {
989
- op.op = op.type.invert(op.op);
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
- // Transform the undo operation by any pending ops.
997
- for (var i = 0; i < this.pendingOps.length; i++) {
998
- var transformErr = transformX(this.pendingOps[i], op);
999
- if (transformErr) return this._hardRollback(transformErr);
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
- // ... and apply it locally, reverting the changes.
1003
- //
1004
- // This operation is applied to look like it comes from a remote source.
1005
- // I'm still not 100% sure about this functionality, because its really a
1006
- // local op. Basically, the problem is that if the client's op is rejected
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
- this._clearInflightOp(err);
1015
- return;
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
- this._hardRollback(err);
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
- if (this.inflightOp) pendingOps.push(this.inflightOp);
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) return doc.emit('error', err);
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
- this.wantSubscribe = !!wantSubscribe;
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) return;
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
- // We're already subscribed, but we send another subscribe message
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "4.0.2",
3
+ "version": "4.1.1",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -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._setState('disconnected');
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
- connection1._setState('connecting');
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._setState('disconnected');
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
- connection2._setState('connecting');
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);
@@ -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).