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 +6 -0
- package/lib/agent.js +15 -5
- package/lib/backend.js +18 -0
- package/lib/client/doc.js +17 -1
- package/lib/db/memory.js +1 -4
- package/lib/util.js +5 -0
- package/package.json +4 -3
- package/test/backend.js +36 -14
- package/test/client/doc.js +66 -0
- package/test/client/presence/presence.js +98 -0
- package/test/projections.js +1 -4
package/CHANGELOG.md
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sharedb",
|
|
3
|
-
"version": "
|
|
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": "^
|
|
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) {
|
package/test/client/doc.js
CHANGED
|
@@ -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
|
});
|
package/test/projections.js
CHANGED
|
@@ -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
|
-
}
|