sharedb 7.0.0 → 7.0.2

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.
@@ -267,8 +267,15 @@ SubmitRequest.prototype.retry = function(callback) {
267
267
  return callback(this.maxRetriesError());
268
268
  }
269
269
  this._resetFixups();
270
+ // Unset the failed attempt's results, which the next attempt replaces
271
+ this.snapshot = null;
272
+ this.channels = null;
270
273
  this.backend.emit('timing', 'submit.retry', Date.now() - this.start, this);
271
- this.submit(callback);
274
+ var request = this;
275
+ this.backend.trigger(this.backend.MIDDLEWARE_ACTIONS.retry, this.agent, this, function(err) {
276
+ if (err) return callback(err);
277
+ request.submit(callback);
278
+ });
272
279
  };
273
280
 
274
281
  // The fixups must be undone before the op is transformed forward again
package/lib/util.js CHANGED
@@ -105,6 +105,16 @@ exports.clone = function(obj) {
105
105
  return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj));
106
106
  };
107
107
 
108
+ exports.shallowCopy = function(object) {
109
+ var out = {};
110
+ for (var key in object) {
111
+ if (exports.hasOwn(object, key)) {
112
+ out[key] = object[key];
113
+ }
114
+ }
115
+ return out;
116
+ };
117
+
108
118
  // A null prototype makes '__proto__' an ordinary own key, so it can be in the map
109
119
  var objectProtoPropNames = Object.create(null);
110
120
  Object.getOwnPropertyNames(Object.prototype).forEach(function(prop) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "7.0.0",
3
+ "version": "7.0.2",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
package/test/agent.js CHANGED
@@ -8,6 +8,11 @@ var ACTIONS = require('../lib/message-actions').ACTIONS;
8
8
  var Connection = require('../lib/client/connection');
9
9
  var protocol = require('../lib/protocol');
10
10
  var LegacyConnection = require('sharedb-legacy/lib/client').Connection;
11
+ var async = require('async');
12
+ var types = require('../lib/types');
13
+ var presenceTestType = require('./client/presence/presence-test-type');
14
+ var util = require('./util');
15
+ types.register(presenceTestType.type);
11
16
 
12
17
  describe('Agent', function() {
13
18
  var backend;
@@ -144,6 +149,20 @@ describe('Agent', function() {
144
149
  });
145
150
  });
146
151
 
152
+ [unconvertible, ['dogs'], '__proto__'].forEach(function(badCollection) {
153
+ it('rejects doc presence with collection ' + JSON.stringify(badCollection), function(done) {
154
+ var message = {a: ACTIONS.presence, ch: 'dogs.fido', c: badCollection, d: 'fido', id: 'rex', pv: 0, v: 0};
155
+ expectRejected(message, 'Invalid collection', done);
156
+ });
157
+ });
158
+
159
+ [unconvertible, '__proto__'].forEach(function(badChannel) {
160
+ it('rejects doc presence with channel ' + JSON.stringify(badChannel), function(done) {
161
+ var message = {a: ACTIONS.presence, ch: badChannel, c: 'dogs', d: 'fido', id: 'rex', pv: 0, v: 0};
162
+ expectRejected(message, 'Invalid presence channel', done);
163
+ });
164
+ });
165
+
147
166
  [unconvertible, true].forEach(function(badProtocol) {
148
167
  it('rejects a handshake with protocol ' + JSON.stringify(badProtocol), function(done) {
149
168
  expectRejected({a: ACTIONS.handshake, protocol: badProtocol, protocolMinor: 2}, 'Invalid protocol', done);
@@ -322,5 +341,401 @@ describe('Agent', function() {
322
341
  });
323
342
  });
324
343
  });
344
+
345
+ describe('with presence', function() {
346
+ beforeEach(function(done) {
347
+ backend.close(function(error) {
348
+ if (error) return done(error);
349
+ backend = new Backend({presence: true});
350
+ writer = backend.connect();
351
+ subscriber = backend.connect();
352
+ done();
353
+ });
354
+ });
355
+
356
+ function expectOnlyPresence(presence, callback) {
357
+ var messages = [];
358
+ subscriber.on('receive', function(request) {
359
+ messages.push(request.data);
360
+ });
361
+ presence.once('receive', function() {
362
+ expect(messages).to.have.length(1);
363
+ expect(messages[0].a).to.equal(ACTIONS.presence);
364
+ callback();
365
+ });
366
+ }
367
+
368
+ it('does not deliver ops from a collection whose channel matches the presence channel', function(done) {
369
+ var presence = subscriber.getPresence('secrets');
370
+ presence.subscribe(function(error) {
371
+ if (error) return done(error);
372
+ expectOnlyPresence(presence, done);
373
+ writer.get('$presence.secrets', 'alice').create({pin: '1234'}, function(error) {
374
+ if (error) return done(error);
375
+ writer.getPresence('secrets').create('writer').submit({name: 'Writer'}, function(error) {
376
+ if (error) done(error);
377
+ });
378
+ });
379
+ });
380
+ });
381
+
382
+ it('does not deliver ops from a doc whose channel matches the doc presence channel', function(done) {
383
+ var writerDoc = writer.get('notes', 'alice');
384
+ var subscriberDoc = subscriber.get('notes', 'alice');
385
+ var presence = subscriber.getDocPresence('notes', 'alice');
386
+ async.series([
387
+ writerDoc.create.bind(writerDoc, 'Hello', presenceTestType.type.name),
388
+ subscriberDoc.fetch.bind(subscriberDoc),
389
+ presence.subscribe.bind(presence),
390
+ function(next) {
391
+ expectOnlyPresence(presence, next);
392
+ writer.get('$presence', 'notes.alice').create({pin: '1234'}, function(error) {
393
+ if (error) return next(error);
394
+ writer.getDocPresence('notes', 'alice').create('writer').submit({index: 1}, function(error) {
395
+ if (error) next(error);
396
+ });
397
+ });
398
+ }
399
+ ], done);
400
+ });
401
+ });
402
+ });
403
+
404
+ describe('cleanup', function() {
405
+ var connection;
406
+
407
+ beforeEach(function(done) {
408
+ backend.close(function(error) {
409
+ if (error) return done(error);
410
+ backend = new Backend({presence: true});
411
+ connection = backend.connect();
412
+ connection.get('books', 'northern-lights').create('North Lights', presenceTestType.type.name, done);
413
+ });
414
+ });
415
+
416
+ function closeConnection(callback) {
417
+ connection.agent.stream.once('end', callback);
418
+ connection.close();
419
+ }
420
+
421
+ function opensStreams(count, step) {
422
+ return function(next) {
423
+ var streamsCount = backend.pubsub.streamsCount;
424
+ step(function(error) {
425
+ if (error) return next(error);
426
+ expect(backend.pubsub.streamsCount - streamsCount).to.equal(count);
427
+ next();
428
+ });
429
+ };
430
+ }
431
+
432
+ it('destroys a stream that is missing from its subscription map', function(done) {
433
+ connection.get('dogs', 'fido').subscribe(function(error) {
434
+ if (error) return done(error);
435
+ delete connection.agent.subscribedDocs.dogs;
436
+ closeConnection(function() {
437
+ expect(backend.pubsub.streamsCount).to.equal(0);
438
+ done();
439
+ });
440
+ });
441
+ });
442
+
443
+ it('stops tracking streams once they close', function(done) {
444
+ var doc = connection.get('dogs', 'fido');
445
+ var presence = connection.getDocPresence('books', 'northern-lights');
446
+ var localPresence = presence.create('presence-1');
447
+ async.series([
448
+ doc.subscribe.bind(doc),
449
+ doc.unsubscribe.bind(doc),
450
+ presence.subscribe.bind(presence),
451
+ presence.unsubscribe.bind(presence),
452
+ localPresence.submit.bind(localPresence, {index: 0}),
453
+ localPresence.destroy.bind(localPresence),
454
+ function(next) {
455
+ expect(connection.agent._streams).to.be.empty;
456
+ next();
457
+ }
458
+ ], done);
459
+ });
460
+
461
+ it('empties its stream maps as soon as the connection closes', function(done) {
462
+ var presence = connection.getDocPresence('books', 'northern-lights');
463
+ var agent = connection.agent;
464
+ async.series([
465
+ function(next) {
466
+ connection.get('dogs', 'fido').subscribe(next);
467
+ },
468
+ presence.subscribe.bind(presence),
469
+ function(next) {
470
+ presence.create('presence-1').submit({index: 0}, next);
471
+ },
472
+ function(next) {
473
+ agent.stream.once('end', function() {
474
+ expect(agent.subscribedDocs).to.be.empty;
475
+ expect(agent.subscribedPresences).to.be.empty;
476
+ expect(agent.latestDocVersionStreams).to.be.empty;
477
+ next();
478
+ });
479
+ connection.close();
480
+ }
481
+ ], done);
482
+ });
483
+
484
+ it('does not track a stream that closed before reaching the agent', function(done) {
485
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(context, next) {
486
+ backend.pubsub.close();
487
+ next();
488
+ });
489
+ connection.get('dogs', 'fido').subscribe(function(error) {
490
+ if (error) return done(error);
491
+ expect(connection.agent._streams).to.be.empty;
492
+ done();
493
+ });
494
+ });
495
+
496
+ it('destroys every pub/sub stream when the connection closes', function(done) {
497
+ var presence = connection.getDocPresence('books', 'northern-lights');
498
+ async.series([
499
+ opensStreams(1, function(next) {
500
+ connection.get('dogs', 'fido').subscribe(next);
501
+ }),
502
+ opensStreams(2, function(next) {
503
+ connection.startBulk();
504
+ connection.get('dogs', 'rex').subscribe();
505
+ connection.get('dogs', 'spot').subscribe(next);
506
+ connection.endBulk();
507
+ }),
508
+ opensStreams(1, presence.subscribe.bind(presence)),
509
+ opensStreams(1, function(next) {
510
+ presence.create('presence-1').submit({index: 0}, next);
511
+ }),
512
+ opensStreams(1, function(next) {
513
+ connection.createSubscribeQuery('dogs', {}, null, next);
514
+ }),
515
+ closeConnection,
516
+ function(next) {
517
+ expect(backend.pubsub.streamsCount).to.equal(0);
518
+ next();
519
+ }
520
+ ], done);
521
+ });
522
+ });
523
+
524
+ describe('query options', function() {
525
+ var connection;
526
+
527
+ beforeEach(function() {
528
+ connection = backend.connect();
529
+ });
530
+
531
+ [ACTIONS.queryFetch, ACTIONS.querySubscribe].forEach(function(action) {
532
+ it('does not pass a client\'s fields to ' + action + ' middleware', function(done) {
533
+ var queryOptions;
534
+ backend.use('query', function(context, next) {
535
+ queryOptions = context.options;
536
+ next();
537
+ });
538
+ connection.on('receive', function(request) {
539
+ if (request.data.a !== action) return;
540
+ expect(request.data).not.to.have.property('error');
541
+ expect(queryOptions).not.to.have.property('fields');
542
+ done();
543
+ });
544
+ connection.send({a: action, id: 1, c: 'dogs', q: {}, o: {fields: {secret: true}}});
545
+ });
546
+ });
547
+ });
548
+
549
+ describe('query subscribe with known results', function() {
550
+ var connection;
551
+
552
+ beforeEach(function(done) {
553
+ connection = backend.connect();
554
+ connection.get('dogs', 'fido').create({name: 'Fido'}, done);
555
+ });
556
+
557
+ function resubscribe(knownResults, callback) {
558
+ connection.on('receive', function(request) {
559
+ if (request.data.a === ACTIONS.querySubscribe) callback(request.data);
560
+ });
561
+ connection.send({
562
+ a: ACTIONS.querySubscribe,
563
+ id: 1,
564
+ c: 'dogs',
565
+ q: {},
566
+ r: knownResults
567
+ });
568
+ }
569
+
570
+ it('does not read known results when the query middleware rejects it', function(done) {
571
+ backend.use('query', function(context, next) {
572
+ next(new Error('Forbidden'));
573
+ });
574
+ sinon.spy(backend.db, 'getOpsBulk');
575
+ sinon.spy(backend.db, 'getSnapshotBulk');
576
+ resubscribe([['fido', 0], ['spot', null]], function(reply) {
577
+ expect(reply.error).to.have.property('message', 'Forbidden');
578
+ expect(backend.db.getOpsBulk).not.to.have.been.called;
579
+ expect(backend.db.getSnapshotBulk).not.to.have.been.called;
580
+ done();
581
+ });
582
+ });
583
+
584
+ it('does not stay subscribed if fetching known results fails', function(done) {
585
+ backend.use('op', function(context, next) {
586
+ next(new Error('Forbidden'));
587
+ });
588
+ resubscribe([['fido', 0], ['spot', null]], function(reply) {
589
+ expect(reply.error).to.have.property('message', 'Forbidden');
590
+ expect(connection.agent.subscribedQueries).to.be.empty;
591
+ expect(backend.pubsub.streamsCount).to.equal(0);
592
+ done();
593
+ });
594
+ });
595
+
596
+ it('does not stay subscribed if one of its known snapshots is rejected', function(done) {
597
+ backend.use('readSnapshots', function(context, next) {
598
+ context.snapshots.forEach(function(snapshot) {
599
+ if (snapshot.id === 'spot') context.rejectSnapshotRead(snapshot, new Error('Forbidden'));
600
+ });
601
+ next();
602
+ });
603
+ resubscribe([['fido', null], ['spot', null]], function(reply) {
604
+ expect(reply.error).to.have.property('code', 'ERR_SNAPSHOT_READS_REJECTED');
605
+ expect(connection.agent.subscribedQueries).to.be.empty;
606
+ expect(backend.pubsub.streamsCount).to.equal(0);
607
+ done();
608
+ });
609
+ });
610
+
611
+ it('does not send known results\' ops if reading their snapshots fails', function(done) {
612
+ backend.use('readSnapshots', function(context, next) {
613
+ next(new Error('Forbidden'));
614
+ });
615
+ var sentOps = [];
616
+ backend.on('send', function(agent, message) {
617
+ if (message.a === ACTIONS.op) sentOps.push(message);
618
+ });
619
+ var getOpsBulk = backend.getOpsBulk;
620
+ var finishGettingOps;
621
+ sinon.stub(backend, 'getOpsBulk').callsFake(function(agent, collection, fromMap, toMap, callback) {
622
+ finishGettingOps = function() {
623
+ getOpsBulk.call(backend, agent, collection, fromMap, toMap, function(error, opsMap) {
624
+ callback(error, opsMap);
625
+ expect(sentOps).to.be.empty;
626
+ done();
627
+ });
628
+ };
629
+ });
630
+ resubscribe([['fido', 0], ['spot', null]], function(reply) {
631
+ expect(reply.error).to.have.property('message', 'Forbidden');
632
+ finishGettingOps();
633
+ });
634
+ });
635
+
636
+ it('does not stay subscribed if re-polling the query fails', function(done) {
637
+ var clock = util.useFakeTimers();
638
+ backend.db.pollInterval = 50;
639
+ sinon.stub(backend.db, 'queryPoll').yieldsAsync(new Error('Forbidden'));
640
+ resubscribe([['fido', 0], ['spot', null]], function(reply) {
641
+ expect(reply.error).to.have.property('message', 'Forbidden');
642
+ expect(connection.agent.subscribedQueries).to.be.empty;
643
+ expect(backend.pubsub.streamsCount).to.equal(0);
644
+ clock.tick(1000);
645
+ expect(backend.db.queryPoll).to.have.been.calledOnce;
646
+ done();
647
+ });
648
+ });
649
+
650
+ [
651
+ {collection: 'dog_names', fields: {name: true}},
652
+ {collection: 'dogs', fields: undefined}
653
+ ].forEach(function(test) {
654
+ it('re-polls ' + test.collection + ' with its projection\'s fields, not the client\'s', function(done) {
655
+ backend.addProjection('dog_names', 'dogs', {name: true});
656
+ sinon.spy(backend.db, 'queryPoll');
657
+ connection.on('receive', function(request) {
658
+ if (request.data.a !== ACTIONS.querySubscribe) return;
659
+ expect(request.data).not.to.have.property('error');
660
+ var pollOptions = backend.db.queryPoll.firstCall.args[2];
661
+ expect(pollOptions.fields).to.eql(test.fields);
662
+ done();
663
+ });
664
+ connection.send({
665
+ a: ACTIONS.querySubscribe,
666
+ id: 1,
667
+ c: test.collection,
668
+ q: {},
669
+ o: {fields: {secret: true}},
670
+ r: [['fido', 0]]
671
+ });
672
+ });
673
+ });
674
+
675
+ it('re-polls each query with its own fields without writing them to shared options', function(done) {
676
+ backend.addProjection('dog_names', 'dogs', {name: true});
677
+ sinon.spy(backend.db, 'queryPoll');
678
+ var options = {ids: ['fido']};
679
+ async.eachSeries(['dog_names', 'dogs'], function(index, next) {
680
+ backend.querySubscribe(connection.agent, index, {}, options, function(err, emitter) {
681
+ if (err) return next(err);
682
+ emitter.queryPoll(function(err) {
683
+ emitter.destroy();
684
+ next(err);
685
+ });
686
+ });
687
+ }, function(err) {
688
+ if (err) return done(err);
689
+ var pollFields = backend.db.queryPoll.args.map(function(args) {
690
+ return args[2].fields;
691
+ });
692
+ expect(pollFields).to.eql([{name: true}, undefined]);
693
+ expect(options).not.to.have.property('fields');
694
+ done();
695
+ });
696
+ });
697
+
698
+ it('keeps a newer subscription with the same id if re-polling the query fails', function(done) {
699
+ var failRepoll;
700
+ sinon.stub(backend.db, 'queryPoll').callsFake(function(collection, query, options, callback) {
701
+ failRepoll = callback.bind(null, new Error('Forbidden'));
702
+ connection.send({a: ACTIONS.querySubscribe, id: 1, c: 'dogs', q: {}});
703
+ });
704
+ var replies = [];
705
+ resubscribe([['fido', 0]], function(reply) {
706
+ replies.push(reply);
707
+ if (replies.length === 1) return failRepoll();
708
+ expect(replies[0]).not.to.have.property('error');
709
+ expect(replies[1].error).to.have.property('message', 'Forbidden');
710
+ expect(connection.agent.subscribedQueries).to.have.property('1');
711
+ done();
712
+ });
713
+ });
714
+
715
+ it('delivers an op committed while fetching known results', function(done) {
716
+ var writer = backend.connect().get('dogs', 'fido');
717
+ connection.createSubscribeQuery('dogs', {}, null, function(error, results) {
718
+ if (error) return done(error);
719
+ var fido = results[0];
720
+ fido.on('op', function() {
721
+ expect(fido.data).to.eql({name: 'Rex'});
722
+ done();
723
+ });
724
+ var getOpsBulk = backend.db.getOpsBulk;
725
+ sinon.stub(backend.db, 'getOpsBulk').callsFake(function(collection, fromMap, toMap, options, callback) {
726
+ backend.db.getOpsBulk.restore();
727
+ getOpsBulk.call(backend.db, collection, fromMap, toMap, options, function(error, opsMap) {
728
+ writer.fetch(function(error) {
729
+ if (error) return done(error);
730
+ writer.submitOp({p: ['name'], od: 'Fido', oi: 'Rex'}, function(error) {
731
+ callback(error, opsMap);
732
+ });
733
+ });
734
+ });
735
+ });
736
+ connection.close();
737
+ backend.connect(connection);
738
+ });
739
+ });
325
740
  });
326
741
  });