sharedb 2.1.0 → 2.2.3

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/README.md CHANGED
@@ -1,5 +1,3 @@
1
- _This README is for `sharedb@1.x`. For `sharedb@1.x-beta`, see [the 1.x-beta branch](https://github.com/share/sharedb/tree/1.x-beta). To upgrade, see [the upgrade guide](https://github.com/share/sharedb/wiki/Upgrading-to-sharedb@1.0.0-from-1.0.0-beta)._
2
-
3
1
  # ShareDB
4
2
 
5
3
  [![NPM Version](https://img.shields.io/npm/v/sharedb.svg)](https://npmjs.org/package/sharedb)
@@ -11,8 +9,9 @@ ShareDB is a realtime database backend based on [Operational Transformation
11
9
  documents. It is the realtime backend for the [DerbyJS web application
12
10
  framework](http://derbyjs.com/).
13
11
 
14
- For questions, discussion and announcements, join the [ShareJS mailing
15
- list](https://groups.google.com/forum/?fromgroups#!forum/sharejs) or [check the FAQ](./docs/faq.md).
12
+ For help, questions, discussion and announcements, join the [ShareJS mailing
13
+ list](https://groups.google.com/forum/?fromgroups#!forum/sharejs) or [read the documentation](https://share.github.io/sharedb/
14
+ ).
16
15
 
17
16
  Please report any bugs you find to the [issue
18
17
  tracker](https://github.com/share/sharedb/issues).
package/lib/agent.js CHANGED
@@ -736,17 +736,25 @@ Agent.prototype._src = function() {
736
736
 
737
737
  Agent.prototype._broadcastPresence = function(presence, callback) {
738
738
  var agent = this;
739
+ var backend = this.backend;
739
740
  var requests = this.presenceRequests[presence.ch] || (this.presenceRequests[presence.ch] = {});
740
741
  var previousRequest = requests[presence.id];
741
742
  if (!previousRequest || previousRequest.pv < presence.pv) {
742
743
  this.presenceRequests[presence.ch][presence.id] = presence;
743
744
  }
744
- this.backend.transformPresenceToLatestVersion(this, presence, function(error, presence) {
745
+ var context = {
746
+ presence: presence,
747
+ collection: presence.c
748
+ };
749
+ backend.trigger(backend.MIDDLEWARE_ACTIONS.receivePresence, this, context, function(error) {
745
750
  if (error) return callback(error);
746
- var channel = agent._getPresenceChannel(presence.ch);
747
- agent.backend.pubsub.publish([channel], presence, function(error) {
751
+ backend.transformPresenceToLatestVersion(agent, presence, function(error, presence) {
748
752
  if (error) return callback(error);
749
- callback(null, presence);
753
+ var channel = agent._getPresenceChannel(presence.ch);
754
+ agent.backend.pubsub.publish([channel], presence, function(error) {
755
+ if (error) return callback(error);
756
+ callback(null, presence);
757
+ });
750
758
  });
751
759
  });
752
760
  };
@@ -815,7 +823,9 @@ Agent.prototype._handlePresenceData = function(presence) {
815
823
  var agent = this;
816
824
  backend.trigger(backend.MIDDLEWARE_ACTIONS.sendPresence, this, context, function(error) {
817
825
  if (error) {
818
- return agent.send({a: 'p', ch: presence.ch, id: presence.id, error: getReplyErrorObject(error)});
826
+ if (backend.doNotForwardSendPresenceErrorsToClient) backend.errorHandler(error, {agent: agent});
827
+ else agent.send({a: 'p', ch: presence.ch, id: presence.id, error: getReplyErrorObject(error)});
828
+ return;
819
829
  }
820
830
  agent.send(presence);
821
831
  });
package/lib/backend.js CHANGED
@@ -14,6 +14,7 @@ var StreamSocket = require('./stream-socket');
14
14
  var SubmitRequest = require('./submit-request');
15
15
  var ReadSnapshotsRequest = require('./read-snapshots-request');
16
16
  var util = require('./util');
17
+ var logger = require('./logger');
17
18
 
18
19
  var ERROR_CODE = ShareDBError.CODES;
19
20
 
@@ -34,6 +35,14 @@ function Backend(options) {
34
35
  this.suppressPublish = !!options.suppressPublish;
35
36
  this.maxSubmitRetries = options.maxSubmitRetries || null;
36
37
  this.presenceEnabled = !!options.presence;
38
+ this.doNotForwardSendPresenceErrorsToClient = !!options.doNotForwardSendPresenceErrorsToClient;
39
+ if (this.presenceEnabled && !this.doNotForwardSendPresenceErrorsToClient) {
40
+ logger.warn(
41
+ 'Broadcasting "sendPresence" middleware errors to clients is deprecated ' +
42
+ 'and will be removed in a future release. Disable this behaviour with:\n\n' +
43
+ 'new Backend({doNotForwardSendPresenceErrorsToClient: true})\n\n'
44
+ );
45
+ }
37
46
 
38
47
  // Map from event name to a list of middleware
39
48
  this.middleware = {};
@@ -41,6 +50,13 @@ function Backend(options) {
41
50
  // The number of open agents for monitoring and testing memory leaks
42
51
  this.agentsCount = 0;
43
52
  this.remoteAgentsCount = 0;
53
+
54
+ this.errorHandler = typeof options.errorHandler === 'function' ?
55
+ options.errorHandler :
56
+ // eslint-disable-next-line no-unused-vars
57
+ function(error, context) {
58
+ logger.error(error);
59
+ };
44
60
  }
45
61
  module.exports = Backend;
46
62
  emitter.mixin(Backend);
@@ -68,6 +84,8 @@ Backend.prototype.MIDDLEWARE_ACTIONS = {
68
84
  // by design, changing existing reply properties can cause weird bugs, since
69
85
  // the rest of ShareDB would be unaware of those changes.
70
86
  reply: 'reply',
87
+ // The server received presence information
88
+ receivePresence: 'receivePresence',
71
89
  // About to send presence information to a client
72
90
  sendPresence: 'sendPresence',
73
91
  // An operation is about to be submitted to the database
package/lib/client/doc.js CHANGED
@@ -114,6 +114,10 @@ function Doc(connection, collection, id) {
114
114
  // ops are still received. Should be toggled through the pause() and
115
115
  // resume() methods to correctly flush on resume.
116
116
  this.paused = false;
117
+
118
+ // Internal counter that gets incremented every time doc.data is updated.
119
+ // Used as a cheap way to check if doc.data has changed.
120
+ this._dataStateVersion = 0;
117
121
  }
118
122
  emitter.mixin(Doc);
119
123
 
@@ -156,13 +160,18 @@ Doc.prototype._setType = function(newType) {
156
160
  } else if (newType === null) {
157
161
  this.type = newType;
158
162
  // If we removed the type from the object, also remove its data
159
- this.data = undefined;
163
+ this._setData(undefined);
160
164
  } else {
161
165
  var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Missing type ' + newType);
162
166
  return this.emit('error', err);
163
167
  }
164
168
  };
165
169
 
170
+ Doc.prototype._setData = function(data) {
171
+ this.data = data;
172
+ this._dataStateVersion++;
173
+ };
174
+
166
175
  // Ingest snapshot data. This data must include a version, snapshot and type.
167
176
  // This is used both to ingest data that was exported with a webpage and data
168
177
  // that was received from the server during a fetch.
@@ -218,9 +227,11 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) {
218
227
  this.version = snapshot.v;
219
228
  var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type;
220
229
  this._setType(type);
221
- this.data = (this.type && this.type.deserialize) ?
222
- this.type.deserialize(snapshot.data) :
223
- snapshot.data;
230
+ this._setData(
231
+ (this.type && this.type.deserialize) ?
232
+ this.type.deserialize(snapshot.data) :
233
+ snapshot.data
234
+ );
224
235
  this.emit('load');
225
236
  callback && callback();
226
237
  };
@@ -622,7 +633,7 @@ Doc.prototype._otApply = function(op, source) {
622
633
  }
623
634
  // Apply the individual op component
624
635
  this.emit('before op', componentOp.op, source, op.src);
625
- this.data = this.type.apply(this.data, componentOp.op);
636
+ this._setData(this.type.apply(this.data, componentOp.op));
626
637
  this.emit('op', componentOp.op, source, op.src);
627
638
  }
628
639
  this.emit('op batch', op.op, source);
@@ -635,7 +646,7 @@ Doc.prototype._otApply = function(op, source) {
635
646
  // the snapshot before it gets changed
636
647
  this.emit('before op', op.op, source, op.src);
637
648
  // Apply the operation to the local data, mutating it in place
638
- this.data = this.type.apply(this.data, op.op);
649
+ this._setData(this.type.apply(this.data, op.op));
639
650
  // Emit an 'op' event once the local data includes the changes from the
640
651
  // op. For locally submitted ops, this will be synchronously with
641
652
  // submission and before the server or other clients have received the op.
@@ -648,11 +659,15 @@ Doc.prototype._otApply = function(op, source) {
648
659
 
649
660
  if (op.create) {
650
661
  this._setType(op.create.type);
651
- this.data = (this.type.deserialize) ?
652
- (this.type.createDeserialized) ?
653
- this.type.createDeserialized(op.create.data) :
654
- this.type.deserialize(this.type.create(op.create.data)) :
655
- this.type.create(op.create.data);
662
+ if (this.type.deserialize) {
663
+ if (this.type.createDeserialized) {
664
+ this._setData(this.type.createDeserialized(op.create.data));
665
+ } else {
666
+ this._setData(this.type.deserialize(this.type.create(op.create.data)));
667
+ }
668
+ } else {
669
+ this._setData(this.type.create(op.create.data));
670
+ }
656
671
  this.emit('create', source);
657
672
  return;
658
673
  }
@@ -12,6 +12,7 @@ function LocalDocPresence(presence, presenceId) {
12
12
 
13
13
  this._doc = this.connection.get(this.collection, this.id);
14
14
  this._isSending = false;
15
+ this._docDataVersionByPresenceVersion = {};
15
16
 
16
17
  this._opHandler = this._transformAgainstOp.bind(this);
17
18
  this._createOrDelHandler = this._handleCreateOrDel.bind(this);
@@ -35,6 +36,9 @@ LocalDocPresence.prototype.submit = function(value, callback) {
35
36
  return this._callbackOrEmit(error, callback);
36
37
  };
37
38
 
39
+ // Record the current data state version to check if we need to transform
40
+ // the presence later
41
+ this._docDataVersionByPresenceVersion[this.presenceVersion] = this._doc._dataStateVersion;
38
42
  LocalPresence.prototype.submit.call(this, value, callback);
39
43
  };
40
44
 
@@ -63,6 +67,7 @@ LocalDocPresence.prototype._sendPending = function() {
63
67
  });
64
68
 
65
69
  presence._pendingMessages = [];
70
+ presence._docDataVersionByPresenceVersion = {};
66
71
  });
67
72
  };
68
73
 
@@ -76,9 +81,18 @@ LocalDocPresence.prototype._registerWithDoc = function() {
76
81
 
77
82
  LocalDocPresence.prototype._transformAgainstOp = function(op, source) {
78
83
  var presence = this;
84
+ var docDataVersion = this._doc._dataStateVersion;
85
+
79
86
  this._pendingMessages.forEach(function(message) {
87
+ // Check if the presence needs transforming against the op - this is to check against
88
+ // edge cases where presence is submitted from an 'op' event
89
+ var messageDocDataVersion = presence._docDataVersionByPresenceVersion[message.pv];
90
+ if (messageDocDataVersion >= docDataVersion) return;
80
91
  try {
81
92
  message.p = presence._transformPresence(message.p, op, source);
93
+ // Ensure the presence's data version is kept consistent to deal with "deep" op
94
+ // submissions
95
+ presence._docDataVersionByPresenceVersion[message.pv] = docDataVersion;
82
96
  } catch (error) {
83
97
  var callback = presence._getCallback(message.pv);
84
98
  presence._callbackOrEmit(error, callback);
@@ -103,6 +117,7 @@ LocalDocPresence.prototype._handleCreateOrDel = function() {
103
117
  LocalDocPresence.prototype._handleLoad = function() {
104
118
  this.value = null;
105
119
  this._pendingMessages = [];
120
+ this._docDataVersionByPresenceVersion = {};
106
121
  };
107
122
 
108
123
  LocalDocPresence.prototype._message = function() {
package/lib/ot.js CHANGED
@@ -221,11 +221,18 @@ function normalizeLegacyJson0Ops(snapshot, json0Op) {
221
221
  if (snapshot.type !== types.defaultType.uri) return;
222
222
  var components = json0Op.op;
223
223
  if (!components) return;
224
+ var data = snapshot.data;
225
+
226
+ // type.apply() makes no guarantees about mutating the original data, so
227
+ // we need to clone. However, we only need to apply() if we have multiple
228
+ // components, so avoid cloning if we don't have to.
229
+ if (components.length > 1) data = util.clone(data);
230
+
224
231
  for (var i = 0; i < components.length; i++) {
225
232
  var component = components[i];
226
233
  if (typeof component.lm === 'string') component.lm = +component.lm;
227
234
  var path = component.p;
228
- var element = snapshot.data;
235
+ var element = data;
229
236
  for (var j = 0; j < path.length; j++) {
230
237
  var key = path[j];
231
238
  // https://github.com/ottypes/json0/blob/73db17e86adc5d801951d1a69453b01382e66c7d/lib/json0.js#L21
@@ -234,5 +241,10 @@ function normalizeLegacyJson0Ops(snapshot, json0Op) {
234
241
  else if (element.constructor === Object) path[j] = key.toString();
235
242
  element = element[key];
236
243
  }
244
+
245
+ // Apply to update the snapshot, so we can correctly check the path for
246
+ // the next component. We don't need to do this on the final iteration,
247
+ // since there's no more ops.
248
+ if (i < components.length - 1) data = types.defaultType.apply(data, [component]);
237
249
  }
238
250
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "2.1.0",
3
+ "version": "2.2.3",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
package/test/backend.js CHANGED
@@ -1,29 +1,51 @@
1
1
  var Backend = require('../lib/backend');
2
2
  var expect = require('chai').expect;
3
3
  var sinon = require('sinon');
4
+ var logger = require('../lib/logger');
4
5
 
5
6
  describe('Backend', function() {
6
7
  var backend;
7
- var agent = {
8
- custom: {
9
- foo: 'bar'
10
- }
11
- };
12
- var fetchOptions = {
13
- snapshotOptions: {
14
- fizz: 'buzz'
15
- }
16
- };
17
-
18
- beforeEach(function() {
19
- backend = new Backend();
20
- });
21
8
 
22
9
  afterEach(function(done) {
23
10
  backend.close(done);
24
11
  });
25
12
 
13
+ describe('options', function() {
14
+ describe('errorHandler', function() {
15
+ it('logs by default', function() {
16
+ backend = new Backend();
17
+ var error = new Error('foo');
18
+ sinon.spy(logger, 'error');
19
+ backend.errorHandler(error);
20
+ expect(logger.error.callCount).to.equal(1);
21
+ });
22
+
23
+ it('overrides with another function', function() {
24
+ var handler = sinon.spy();
25
+ backend = new Backend({errorHandler: handler});
26
+ var error = new Error('foo');
27
+ backend.errorHandler(error);
28
+ expect(handler.callCount).to.equal(1);
29
+ });
30
+ });
31
+ });
32
+
26
33
  describe('a simple document', function() {
34
+ var agent = {
35
+ custom: {
36
+ foo: 'bar'
37
+ }
38
+ };
39
+ var fetchOptions = {
40
+ snapshotOptions: {
41
+ fizz: 'buzz'
42
+ }
43
+ };
44
+
45
+ beforeEach(function() {
46
+ backend = new Backend();
47
+ });
48
+
27
49
  beforeEach(function(done) {
28
50
  var doc = backend.connect().get('books', '1984');
29
51
  doc.create({title: '1984'}, function(error) {
@@ -979,4 +979,50 @@ describe('DocPresence', function() {
979
979
  }
980
980
  ], done);
981
981
  });
982
+
983
+ it('does not transform presence submitted in an op event when the presence was created late', function(done) {
984
+ var localPresence1;
985
+
986
+ doc1.on('op', function() {
987
+ localPresence1.submit({index: 7});
988
+ });
989
+
990
+ localPresence1 = presence1.create('presence-1');
991
+
992
+ async.series([
993
+ presence2.subscribe.bind(presence2),
994
+ function(next) {
995
+ doc1.submitOp({index: 5, value: 'ern'});
996
+ presence2.on('receive', function(id, value) {
997
+ expect(value).to.eql({index: 7});
998
+ next();
999
+ });
1000
+ }
1001
+ ], done);
1002
+ });
1003
+
1004
+ it('does transform late-created presence submitted in an op event by a deep second op', function(done) {
1005
+ var localPresence1;
1006
+
1007
+ var submitted = false;
1008
+ doc1.on('op', function() {
1009
+ if (submitted) return;
1010
+ submitted = true;
1011
+ localPresence1.submit({index: 7});
1012
+ doc1.submitOp({index: 5, value: 'akg'});
1013
+ });
1014
+
1015
+ localPresence1 = presence1.create('presence-1');
1016
+
1017
+ async.series([
1018
+ presence2.subscribe.bind(presence2),
1019
+ function(next) {
1020
+ doc1.submitOp({index: 5, value: 'ern'});
1021
+ presence2.on('receive', function(id, value) {
1022
+ expect(value).to.eql({index: 10});
1023
+ next();
1024
+ });
1025
+ }
1026
+ ], done);
1027
+ });
982
1028
  });
@@ -487,4 +487,102 @@ describe('Presence', function() {
487
487
  }
488
488
  ], done);
489
489
  });
490
+
491
+ describe('middleware', function() {
492
+ describe('receivePresence', function() {
493
+ it('provides the presence in the middleware', function(done) {
494
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context) {
495
+ expect(context.presence.p).to.eql({index: 5});
496
+ done();
497
+ });
498
+
499
+ var localPresence1 = presence1.create('presence-1');
500
+ localPresence1.submit({index: 5}, errorHandler(done));
501
+ });
502
+
503
+ it('can mutate the presence in the middleware', function(done) {
504
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
505
+ context.presence.p.index++;
506
+ next();
507
+ });
508
+
509
+ var localPresence1 = presence1.create('presence-1');
510
+
511
+ async.series([
512
+ presence2.subscribe.bind(presence2),
513
+ function(next) {
514
+ presence2.on('receive', function(id, value) {
515
+ expect(value).to.eql({index: 6});
516
+ next();
517
+ });
518
+ localPresence1.submit({index: 5}, errorHandler(done));
519
+ }
520
+ ], done);
521
+ });
522
+
523
+ it('can cancel a presence broadcast by erroring', function(done) {
524
+ backend.use(backend.MIDDLEWARE_ACTIONS.receivePresence, function(context, next) {
525
+ next(new Error('bad!'));
526
+ });
527
+
528
+ var localPresence1 = presence1.create('presence-1');
529
+
530
+ async.series([
531
+ presence2.subscribe.bind(presence2),
532
+ function(next) {
533
+ presence2.on('receive', function() {
534
+ done(new Error('should not have received presence'));
535
+ });
536
+ localPresence1.submit({index: 5}, function(error) {
537
+ expect(error.message).to.contain('bad!');
538
+ next();
539
+ });
540
+ }
541
+ ], done);
542
+ });
543
+ });
544
+
545
+ describe('sendPresence', function() {
546
+ // TODO: This functionality is deprecated
547
+ it('sends an error to a subscribed client', function(done) {
548
+ var localPresence1 = presence1.create('presence-1');
549
+
550
+ backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
551
+ next(new Error('uh-oh!'));
552
+ });
553
+
554
+ async.series([
555
+ presence2.subscribe.bind(presence2),
556
+ function(next) {
557
+ localPresence1.submit({index: 3}, errorHandler(done));
558
+ presence2.once('error', function(error) {
559
+ expect(error.message).to.equal('uh-oh!');
560
+ next();
561
+ });
562
+ }
563
+ ], done);
564
+ });
565
+
566
+ it('emits errors on the server instead of sending to the client', function(done) {
567
+ var localPresence1 = presence1.create('presence-1');
568
+
569
+ backend.doNotForwardSendPresenceErrorsToClient = true;
570
+ backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(context, next) {
571
+ next(new Error('uh-oh!'));
572
+ });
573
+
574
+ async.series([
575
+ presence2.subscribe.bind(presence2),
576
+ function(next) {
577
+ localPresence1.submit({index: 3}, errorHandler(done));
578
+ presence2.on('error', errorHandler(done));
579
+ backend.errorHandler = function(error) {
580
+ expect(error.message).to.equal('uh-oh!');
581
+ next();
582
+ };
583
+ }
584
+ ], done);
585
+ });
586
+ });
587
+ });
490
588
  });
@@ -496,4 +496,50 @@ describe('SnapshotVersionRequest', function() {
496
496
  });
497
497
  });
498
498
  });
499
+
500
+ describe('invalid json0v2 path with multiple components', function() {
501
+ beforeEach(function(done) {
502
+ var doc = backend.connect().get('series', 'his-dark-materials');
503
+ async.series([
504
+ doc.create.bind(doc, [{}]),
505
+ doc.submitOp.bind(doc, [
506
+ {p: ['0', 'title'], oi: ''},
507
+ {p: ['0', 'title', 0], si: 'Northern Lights'}
508
+ ])
509
+ ], done);
510
+ });
511
+
512
+ describe('json0v1', function() {
513
+ it('fetches v2 with json0v1', function(done) {
514
+ backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
515
+ if (error) return done(error);
516
+ expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
517
+ done();
518
+ });
519
+ });
520
+ });
521
+
522
+ describe('json0v2', function() {
523
+ var defaultType;
524
+
525
+ beforeEach(function() {
526
+ defaultType = types.defaultType;
527
+ types.defaultType = json0v2;
528
+ types.register(json0v2);
529
+ });
530
+
531
+ afterEach(function() {
532
+ types.defaultType = defaultType;
533
+ types.register(defaultType);
534
+ });
535
+
536
+ it('fetches v2 with json0v2', function(done) {
537
+ backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
538
+ if (error) return done(error);
539
+ expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
540
+ done();
541
+ });
542
+ });
543
+ });
544
+ });
499
545
  });