sharedb 1.9.2 → 2.2.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/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## v2.0
2
+
3
+ ### Breaking changes
4
+
5
+ * Drop Node.js v10 support
6
+
1
7
  ## v1.0-beta
2
8
 
3
9
  ### Breaking changes
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
@@ -3,6 +3,7 @@ var logger = require('../logger');
3
3
  var ShareDBError = require('../error');
4
4
  var types = require('../types');
5
5
  var util = require('../util');
6
+ var clone = util.clone;
6
7
  var deepEqual = require('fast-deep-equal');
7
8
 
8
9
  var ERROR_CODE = ShareDBError.CODES;
@@ -919,6 +920,15 @@ Doc.prototype.resume = function() {
919
920
  this.flush();
920
921
  };
921
922
 
923
+ // Create a snapshot that can be serialized, deserialized, and passed into `Doc.ingestSnapshot`.
924
+ Doc.prototype.toSnapshot = function() {
925
+ return {
926
+ v: this.version,
927
+ data: clone(this.data),
928
+ type: this.type.uri
929
+ };
930
+ };
931
+
922
932
  // *** Receiving operations
923
933
 
924
934
  // This is called when the server acknowledges an operation from the client.
@@ -948,7 +958,13 @@ Doc.prototype._rollback = function(err) {
948
958
  var op = this.inflightOp;
949
959
 
950
960
  if ('op' in op && op.type.invert) {
951
- op.op = op.type.invert(op.op);
961
+ try {
962
+ op.op = op.type.invert(op.op);
963
+ } catch (error) {
964
+ // If the op doesn't support `.invert()`, we just reload the doc
965
+ // instead of trying to locally revert it.
966
+ return this._hardRollback(err);
967
+ }
952
968
 
953
969
  // Transform the undo operation by any pending ops.
954
970
  for (var i = 0; i < this.pendingOps.length; i++) {
package/lib/db/memory.js CHANGED
@@ -1,6 +1,7 @@
1
1
  var DB = require('./index');
2
2
  var Snapshot = require('../snapshot');
3
3
  var util = require('../util');
4
+ var clone = util.clone;
4
5
 
5
6
  // In-memory ShareDB database
6
7
  //
@@ -172,7 +173,3 @@ MemoryDB.prototype._getVersionSync = function(collection, id) {
172
173
  var collectionOps = this.ops[collection];
173
174
  return (collectionOps && collectionOps[id] && collectionOps[id].length) || 0;
174
175
  };
175
-
176
- function clone(obj) {
177
- return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
178
- }
package/lib/util.js CHANGED
@@ -96,3 +96,8 @@ exports.nextTick = function(callback) {
96
96
  callback.apply(null, args);
97
97
  });
98
98
  };
99
+
100
+ exports.clone = function(obj) {
101
+ return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
102
+ };
103
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "1.9.2",
3
+ "version": "2.2.1",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -13,13 +13,14 @@
13
13
  "devDependencies": {
14
14
  "chai": "^4.2.0",
15
15
  "coveralls": "^3.0.7",
16
- "eslint": "^6.5.1",
16
+ "eslint": "^7.32.0",
17
17
  "eslint-config-google": "^0.14.0",
18
- "ot-json0-v2": "ottypes/json0",
19
18
  "lolex": "^5.1.1",
20
19
  "mocha": "^8.2.1",
21
20
  "nyc": "^14.1.1",
21
+ "ot-json0-v2": "ottypes/json0",
22
22
  "ot-json1": "^0.3.0",
23
+ "rich-text": "^4.1.0",
23
24
  "sharedb-legacy": "npm:sharedb@=1.1.0",
24
25
  "sinon": "^7.5.0"
25
26
  },
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) {
@@ -1,6 +1,9 @@
1
1
  var Backend = require('../../lib/backend');
2
2
  var expect = require('chai').expect;
3
3
  var async = require('async');
4
+ var json0 = require('ot-json0').type;
5
+ var richText = require('rich-text').type;
6
+ var ShareDBError = require('../../lib/error');
4
7
  var errorHandler = require('../util').errorHandler;
5
8
 
6
9
  describe('Doc', function() {
@@ -407,6 +410,43 @@ describe('Doc', function() {
407
410
  ], done);
408
411
  });
409
412
 
413
+ it('rolls the doc back even if the op is not invertible', function(done) {
414
+ var backend = this.backend;
415
+
416
+ async.series([
417
+ function(next) {
418
+ // Register the rich text type, which can't be inverted
419
+ json0.registerSubtype(richText);
420
+
421
+ var validOp = {p: ['richName'], oi: {ops: [{insert: 'Scooby\n'}]}};
422
+ doc.submitOp(validOp, function(error) {
423
+ expect(error).to.not.exist;
424
+ next();
425
+ });
426
+ },
427
+ function(next) {
428
+ // Make the server reject this insertion
429
+ backend.use('submit', function(_context, backendNext) {
430
+ backendNext(new ShareDBError(ShareDBError.CODES.ERR_UNKNOWN_ERROR, 'Custom unknown error'));
431
+ });
432
+ var nonInvertibleOp = {p: ['richName'], t: 'rich-text', o: [{insert: 'e'}]};
433
+
434
+ // The server error should get all the way back to our handler
435
+ doc.submitOp(nonInvertibleOp, function(error) {
436
+ expect(error.message).to.eql('Custom unknown error');
437
+ next();
438
+ });
439
+ },
440
+ doc.whenNothingPending.bind(doc),
441
+ function(next) {
442
+ // The doc should have been reverted successfully
443
+ expect(doc.data).to.eql({name: 'Scooby', richName: {ops: [{insert: 'Scooby\n'}]}});
444
+ next();
445
+ }
446
+ ], done);
447
+ });
448
+
449
+
410
450
  it('rescues an irreversible op collision', function(done) {
411
451
  // This test case attempts to reconstruct the following corner case, with
412
452
  // two independent references to the same document. We submit two simultaneous, but
@@ -475,4 +515,30 @@ describe('Doc', function() {
475
515
  ], done);
476
516
  });
477
517
  });
518
+
519
+ describe('toSnapshot', function() {
520
+ var doc;
521
+ beforeEach(function(done) {
522
+ doc = this.connection.get('dogs', 'scooby');
523
+ doc.create({name: 'Scooby'}, done);
524
+ });
525
+
526
+ it('generates a snapshot', function() {
527
+ expect(doc.toSnapshot()).to.eql({
528
+ v: 1,
529
+ data: {name: 'Scooby'},
530
+ type: 'http://sharejs.org/types/JSONv0'
531
+ });
532
+ });
533
+
534
+ it('clones snapshot data to guard against mutation', function() {
535
+ var snapshot = doc.toSnapshot();
536
+ doc.data.name = 'Shaggy';
537
+ expect(snapshot).to.eql({
538
+ v: 1,
539
+ data: {name: 'Scooby'},
540
+ type: 'http://sharejs.org/types/JSONv0'
541
+ });
542
+ });
543
+ });
478
544
  });
@@ -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
  });
@@ -1,6 +1,7 @@
1
1
  var expect = require('chai').expect;
2
2
  var projections = require('../lib/projections');
3
3
  var type = require('../lib/types').defaultType.uri;
4
+ var clone = require('../lib/util').clone;
4
5
 
5
6
  describe('projection utility methods', function() {
6
7
  describe('projectSnapshot', function() {
@@ -385,7 +386,3 @@ describe('projection utility methods', function() {
385
386
  });
386
387
  });
387
388
  });
388
-
389
- function clone(obj) {
390
- return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
391
- }