sharedb 7.0.1 → 7.0.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/lib/agent.js CHANGED
@@ -188,7 +188,9 @@ Agent.prototype._subscribeToPresenceStream = function(channel, stream) {
188
188
  stream.on('data', function(data) {
189
189
  if (data.error) {
190
190
  logger.error('Presence subscription stream error', channel, data.error);
191
+ return;
191
192
  }
193
+ if (!isPresenceOrRequestFor(channel, data)) return;
192
194
  agent._handlePresenceData(data);
193
195
  });
194
196
 
@@ -199,6 +201,12 @@ Agent.prototype._subscribeToPresenceStream = function(channel, stream) {
199
201
  });
200
202
  };
201
203
 
204
+ function isPresenceOrRequestFor(channel, data) {
205
+ var isPresence = data.a === ACTIONS.presence;
206
+ var isPresenceRequest = !!data.r;
207
+ return (isPresence || isPresenceRequest) && data.ch === channel;
208
+ }
209
+
202
210
  Agent.prototype._retractPresences = function(channel) {
203
211
  var pendingBroadcasts = this._pendingPresenceBroadcasts;
204
212
  for (var i = 0; i < pendingBroadcasts.length; i++) {
@@ -486,6 +494,7 @@ Agent.prototype._checkRequest = function(request) {
486
494
  if (request.a === ACTIONS.presence) {
487
495
  if (isInvalidName(request.id)) return 'Invalid presence ID';
488
496
  if (isInvalidVersion(request.pv)) return 'Invalid presence version';
497
+ if (isDocPresence(request) && !util.isInteger(request.v)) return 'Invalid version';
489
498
  }
490
499
  } else if (
491
500
  request.a === ACTIONS.bulkFetch ||
@@ -945,17 +954,8 @@ Agent.prototype._broadcastPresence = function(presence, supersededBroadcasts, ca
945
954
  requests[presence.id] = presence;
946
955
  }
947
956
 
948
- var transformer = function(agent, presence, callback) {
949
- callback(null, presence);
950
- };
951
-
952
- var latestDocVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
953
- var presenceIsUpToDate = presence.v === latestDocVersion;
954
- if (!presenceIsUpToDate) {
955
- transformer = backend.transformPresenceToLatestVersion.bind(backend);
956
- }
957
-
958
- transformer(agent, presence, function(error, transformedPresence) {
957
+ var trackedVersion = util.dig(agent.latestDocVersions, presence.c, presence.d);
958
+ backend._transformPresenceToLatestVersion(agent, presence, trackedVersion, function(error, transformedPresence) {
959
959
  if (error) {
960
960
  agent._withdrawPresenceRequest(requests, presence, previousRequest);
961
961
  return callback(error);
@@ -1016,6 +1016,7 @@ Agent.prototype._subscribeDocVersion = function(collection, id, callback) {
1016
1016
  versions[id] = snapshot.v;
1017
1017
 
1018
1018
  agent._subscribeMapToStream(agent.latestDocVersionStreams, collection, id, stream, function(op) {
1019
+ if (agent._isOpFromCollidingChannel(collection, id, op)) return;
1019
1020
  // op.v behind snapshot.v by 1
1020
1021
  latestDocVersions[collection][id] = op.v + 1;
1021
1022
  });
package/lib/backend.js CHANGED
@@ -809,10 +809,9 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback)
809
809
 
810
810
  this._fetchSnapshot(collection, id, version, function(error, snapshot) {
811
811
  if (error) return callback(error);
812
- var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
813
812
  var snapshots = [snapshot];
814
813
  var snapshotType = backend.SNAPSHOT_TYPES.byVersion;
815
- backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
814
+ backend._sanitizeSnapshots(agent, projection, collection, snapshots, snapshotType, function(error) {
816
815
  if (error) return callback(error);
817
816
  backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);
818
817
  callback(null, snapshot);
@@ -875,10 +874,9 @@ Backend.prototype.fetchSnapshotByTimestamp = function(agent, index, id, timestam
875
874
 
876
875
  this._fetchSnapshotByTimestamp(collection, id, timestamp, function(error, snapshot) {
877
876
  if (error) return callback(error);
878
- var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
879
877
  var snapshots = [snapshot];
880
878
  var snapshotType = backend.SNAPSHOT_TYPES.byTimestamp;
881
- backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
879
+ backend._sanitizeSnapshots(agent, projection, collection, snapshots, snapshotType, function(error) {
882
880
  if (error) return callback(error);
883
881
  backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);
884
882
  callback(null, snapshot);
@@ -938,18 +936,47 @@ Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, ca
938
936
  };
939
937
 
940
938
  Backend.prototype.transformPresenceToLatestVersion = function(agent, presence, callback) {
939
+ this._transformPresenceToLatestVersion(agent, presence, null, callback);
940
+ };
941
+
942
+ Backend.prototype._transformPresenceToLatestVersion = function(agent, presence, trackedVersion, callback) {
941
943
  if (!presence.c || !presence.d) return callback(null, presence);
942
- this.getOps(agent, presence.c, presence.d, presence.v, null, function(error, ops) {
944
+ var backend = this;
945
+ this._getDocVersionForPresence(agent, presence, trackedVersion, function(error, docVersion) {
943
946
  if (error) return callback(error);
944
- for (var i = 0; i < ops.length; i++) {
945
- var op = ops[i];
946
- var isOwnOp = op.src === presence.src;
947
- var transformError = ot.transformPresence(presence, op, isOwnOp);
948
- if (transformError) {
949
- return callback(transformError);
950
- }
947
+ var leaving = presence.p === null;
948
+ if (leaving) presence.v = docVersion;
949
+ if (presence.v > docVersion) {
950
+ return callback(new ShareDBError(
951
+ ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT,
952
+ 'Presence version newer than current snapshot'
953
+ ));
951
954
  }
952
- callback(null, presence);
955
+ if (presence.v === docVersion) return callback(null, presence);
956
+ backend.getOps(agent, presence.c, presence.d, presence.v, null, function(error, ops) {
957
+ if (error) return callback(error);
958
+ for (var i = 0; i < ops.length; i++) {
959
+ var op = ops[i];
960
+ var isOwnOp = op.src === presence.src;
961
+ var transformError = ot.transformPresence(presence, op, isOwnOp);
962
+ if (transformError) {
963
+ return callback(transformError);
964
+ }
965
+ }
966
+ callback(null, presence);
967
+ });
968
+ });
969
+ };
970
+
971
+ Backend.prototype._getDocVersionForPresence = function(agent, presence, trackedVersion, callback) {
972
+ var leaving = presence.p === null;
973
+ var presenceIsWithinTrackedVersion = trackedVersion != null && presence.v <= trackedVersion;
974
+ if (!leaving && presenceIsWithinTrackedVersion) return callback(null, trackedVersion);
975
+ var projection = this.projections[presence.c];
976
+ var collection = (projection) ? projection.target : presence.c;
977
+ this.db.getSnapshot(collection, presence.d, {}, {agentCustom: agent.custom}, function(error, snapshot) {
978
+ if (error) return callback(error);
979
+ callback(null, snapshot.v);
953
980
  });
954
981
  };
955
982
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "7.0.1",
3
+ "version": "7.0.3",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
package/test/agent.js CHANGED
@@ -341,6 +341,64 @@ describe('Agent', function() {
341
341
  });
342
342
  });
343
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
+ });
344
402
  });
345
403
 
346
404
  describe('cleanup', function() {
@@ -585,6 +585,327 @@ describe('DocPresence', function() {
585
585
  connection1.send(message);
586
586
  });
587
587
 
588
+ it('rejects a presence message with a non-integer version', function(done) {
589
+ var localPresence1 = presence1.create('presence-1');
590
+ localPresence1.on('error', function(error) {
591
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
592
+ done();
593
+ });
594
+
595
+ var message = localPresence1._message();
596
+ message.v = 1.5;
597
+ message.t = presenceTestType.type.uri;
598
+ connection1.send(message);
599
+ });
600
+
601
+ it('rejects a presence message without a version', function(done) {
602
+ var localPresence1 = presence1.create('presence-1');
603
+ localPresence1.on('error', function(error) {
604
+ expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
605
+ done();
606
+ });
607
+
608
+ var message = localPresence1._message();
609
+ message.t = presenceTestType.type.uri;
610
+ connection1.send(message);
611
+ });
612
+
613
+ [
614
+ {description: 'without doc coordinates', c: undefined, d: undefined},
615
+ {description: 'for another doc', c: 'books', d: 'subtle-knife'}
616
+ ].forEach(function(test) {
617
+ it('ignores remote presence on its channel ' + test.description, function(done) {
618
+ sinon.spy(doc2, 'fetch');
619
+
620
+ [doc2.version, doc2.version + 1].forEach(function(version, index) {
621
+ var message = presence1.create('presence-' + index)._message();
622
+ message.c = test.c;
623
+ message.d = test.d;
624
+ message.p = {index: 1};
625
+ message.v = version;
626
+ message.t = presenceTestType.type.uri;
627
+ connection2.handleMessage(message);
628
+ });
629
+
630
+ doc2.whenNothingPending(function() {
631
+ expect(presence2.remotePresences).to.be.empty;
632
+ expect(presence2._remotePresenceInstances).to.be.empty;
633
+ expect(doc2.fetch).not.to.have.been.called;
634
+ done();
635
+ });
636
+ });
637
+ });
638
+
639
+ it('rejects presence whose version is ahead of the doc', function(done) {
640
+ var localPresence1 = presence1.create('presence-1');
641
+ presence2.on('receive', function() {
642
+ done(new Error('Should not broadcast presence from the future'));
643
+ });
644
+
645
+ async.series([
646
+ presence2.subscribe.bind(presence2),
647
+ function(next) {
648
+ localPresence1.on('error', function(error) {
649
+ expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
650
+ expect(connection1.agent.presenceRequests[presence1.channel]['presence-1'].p).to.be.null;
651
+ next();
652
+ });
653
+
654
+ var message = localPresence1._message();
655
+ message.p = {index: 1};
656
+ message.v = doc1.version + 1;
657
+ message.t = presenceTestType.type.uri;
658
+ connection1.send(message);
659
+ }
660
+ ], done);
661
+ });
662
+
663
+ it('rejects presence ahead of a tracked doc', function(done) {
664
+ var localPresence1 = presence1.create('presence-1');
665
+
666
+ async.series([
667
+ localPresence1.submit.bind(localPresence1, {index: 1}),
668
+ function(next) {
669
+ localPresence1.on('error', function(error) {
670
+ expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
671
+ next();
672
+ });
673
+
674
+ var message = localPresence1._message();
675
+ message.p = {index: 1};
676
+ message.v = doc1.version + 1;
677
+ message.t = presenceTestType.type.uri;
678
+ connection1.send(message);
679
+ }
680
+ ], done);
681
+ });
682
+
683
+ it('rejects presence ahead of a tracked doc after ops to a doc on the same channel', function(done) {
684
+ var notesDoc1 = connection1.get('notes', 'private.alice');
685
+ var diaryDoc1 = connection1.get('notes.private', 'alice');
686
+ var diaryDoc2 = connection2.get('notes.private', 'alice');
687
+ var localNotesPresence1 = connection1.getDocPresence('notes', 'private.alice').create('notes-presence');
688
+
689
+ async.series([
690
+ notesDoc1.create.bind(notesDoc1, 'Hello', presenceTestType.type.name),
691
+ localNotesPresence1.submit.bind(localNotesPresence1, {index: 1}),
692
+ diaryDoc2.create.bind(diaryDoc2, 'Dear diary', presenceTestType.type.name),
693
+ diaryDoc1.subscribe.bind(diaryDoc1),
694
+ function(next) {
695
+ diaryDoc1.once('op', function() {
696
+ next();
697
+ });
698
+ diaryDoc2.submitOp({index: 0, value: 'Oh, '}, errorHandler(done));
699
+ },
700
+ function(next) {
701
+ localNotesPresence1.on('error', function(error) {
702
+ expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
703
+ next();
704
+ });
705
+
706
+ expect(diaryDoc2.version).to.be.above(notesDoc1.version);
707
+ var message = localNotesPresence1._message();
708
+ message.p = {index: 1};
709
+ message.v = diaryDoc2.version;
710
+ message.t = presenceTestType.type.uri;
711
+ connection1.send(message);
712
+ }
713
+ ], done);
714
+ });
715
+
716
+ it('does not read the snapshot for presence after its own client\'s op', function(done) {
717
+ var localPresence1 = presence1.create('presence-1');
718
+
719
+ async.series([
720
+ presence2.subscribe.bind(presence2),
721
+ function(next) {
722
+ localPresence1.submit({index: 1}, errorHandler(done));
723
+ presence2.once('receive', function() {
724
+ next();
725
+ });
726
+ },
727
+ function(next) {
728
+ doc2.once('op', function() {
729
+ next();
730
+ });
731
+ doc1.submitOp({index: 0, value: 'The '}, errorHandler(done));
732
+ },
733
+ function(next) {
734
+ sinon.spy(backend.db, 'getSnapshot');
735
+ localPresence1.submit({index: 6}, errorHandler(done));
736
+ presence2.once('receive', function(id, presence) {
737
+ expect(presence).to.eql({index: 6});
738
+ expect(backend.db.getSnapshot).not.to.have.been.called;
739
+ next();
740
+ });
741
+ }
742
+ ], done);
743
+ });
744
+
745
+ it('does not read the snapshot for presence at the tracked doc version', function(done) {
746
+ var localPresence1 = presence1.create('presence-1');
747
+
748
+ async.series([
749
+ presence2.subscribe.bind(presence2),
750
+ function(next) {
751
+ localPresence1.submit({index: 1}, errorHandler(done));
752
+ presence2.once('receive', function() {
753
+ next();
754
+ });
755
+ },
756
+ function(next) {
757
+ sinon.spy(backend.db, 'getSnapshot');
758
+ localPresence1.submit({index: 2}, errorHandler(done));
759
+ presence2.once('receive', function(id, presence) {
760
+ expect(presence).to.eql({index: 2});
761
+ expect(backend.db.getSnapshot).not.to.have.been.called;
762
+ next();
763
+ });
764
+ }
765
+ ], done);
766
+ });
767
+
768
+ it('clamps a leave ahead of the doc to the doc version', function(done) {
769
+ var localPresence1 = presence1.create('presence-1');
770
+ localPresence1.on('error', done);
771
+ sinon.spy(doc2, 'fetch');
772
+
773
+ async.series([
774
+ presence2.subscribe.bind(presence2),
775
+ function(next) {
776
+ presence2.once('receive', function(id, presence) {
777
+ expect(id).to.equal('presence-1');
778
+ expect(presence).to.be.null;
779
+ expect(connection1.agent.presenceRequests[presence1.channel]['presence-1'].v).to.equal(doc1.version);
780
+ expect(doc2.fetch).not.to.have.been.called;
781
+ next();
782
+ });
783
+
784
+ var leave = localPresence1._message();
785
+ leave.p = null;
786
+ leave.v = doc1.version + 1;
787
+ leave.t = presenceTestType.type.uri;
788
+ connection1.send(leave);
789
+ }
790
+ ], done);
791
+ });
792
+
793
+ it('broadcasts null presence on disconnect after rejecting presence ahead of the doc', function(done) {
794
+ var localPresence1 = presence1.create('presence-1');
795
+
796
+ async.series([
797
+ presence1.subscribe.bind(presence1),
798
+ presence2.subscribe.bind(presence2),
799
+ function(next) {
800
+ localPresence1.submit({index: 1}, errorHandler(done));
801
+ presence2.once('receive', function() {
802
+ next();
803
+ });
804
+ },
805
+ function(next) {
806
+ localPresence1.once('error', function(error) {
807
+ expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
808
+ next();
809
+ });
810
+
811
+ var message = localPresence1._message();
812
+ message.v = doc1.version + 1;
813
+ message.t = presenceTestType.type.uri;
814
+ connection1.send(message);
815
+ },
816
+ function(next) {
817
+ presence2.once('receive', function(id, presence) {
818
+ expect(id).to.equal('presence-1');
819
+ expect(presence).to.be.null;
820
+ next();
821
+ });
822
+ connection1.close();
823
+ }
824
+ ], done);
825
+ });
826
+
827
+ it('accepts up-to-date presence when the agent\'s tracked doc version lags behind', function(done) {
828
+ var localPresence1 = presence1.create('presence-1');
829
+
830
+ async.series([
831
+ presence2.subscribe.bind(presence2),
832
+ function(next) {
833
+ localPresence1.submit({index: 1}, errorHandler(done));
834
+ presence2.once('receive', function() {
835
+ next();
836
+ });
837
+ },
838
+ function(next) {
839
+ doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
840
+ doc2.once('op', function() {
841
+ next();
842
+ });
843
+ },
844
+ function(next) {
845
+ connection1.agent.latestDocVersions.books['northern-lights'] = 1;
846
+ localPresence1.submit({index: 12}, errorHandler(done));
847
+ presence2.once('receive', function(id, presence) {
848
+ expect(doc2.version).to.eql(2);
849
+ expect(presence).to.eql({index: 12});
850
+ next();
851
+ });
852
+ }
853
+ ], done);
854
+ });
855
+
856
+ it('transforms presence ahead of the agent\'s lagging tracked doc version by later ops', function(done) {
857
+ var localPresence1 = presence1.create('presence-1');
858
+
859
+ async.series([
860
+ presence2.subscribe.bind(presence2),
861
+ function(next) {
862
+ localPresence1.submit({index: 1}, errorHandler(done));
863
+ presence2.once('receive', function() {
864
+ next();
865
+ });
866
+ },
867
+ function(next) {
868
+ doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
869
+ doc2.once('op', function() {
870
+ next();
871
+ });
872
+ },
873
+ function(next) {
874
+ doc2.submitOp({index: 0, value: 'The '}, errorHandler(done));
875
+ doc1.once('op', function() {
876
+ next();
877
+ });
878
+ },
879
+ function(next) {
880
+ connection1.agent.latestDocVersions.books['northern-lights'] = 1;
881
+ presence2.once('receive', function(id, presence) {
882
+ expect(presence).to.eql({index: 16});
883
+ next();
884
+ });
885
+
886
+ var message = localPresence1._message();
887
+ message.p = {index: 12};
888
+ message.v = 2;
889
+ message.t = presenceTestType.type.uri;
890
+ connection1.send(message);
891
+ }
892
+ ], done);
893
+ });
894
+
895
+ it('rejects presence ahead of the doc when transformed without a tracked version', function(done) {
896
+ var presence = {
897
+ c: 'books',
898
+ d: 'northern-lights',
899
+ p: {index: 1},
900
+ v: doc1.version + 1,
901
+ t: presenceTestType.type.uri
902
+ };
903
+ backend.transformPresenceToLatestVersion(connection1.agent, presence, function(error) {
904
+ expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
905
+ done();
906
+ });
907
+ });
908
+
588
909
  it('rejects a presence message without an ID', function(done) {
589
910
  var localPresence1 = presence1.create('presence-1');
590
911
  // Have to catch the error on the Presence instance, because obviously
@@ -682,6 +1003,43 @@ describe('DocPresence', function() {
682
1003
  });
683
1004
  });
684
1005
 
1006
+ it('clears presence when a client that has lost read access disconnects', function(done) {
1007
+ var agent1 = connection1.agent;
1008
+ var canRead = true;
1009
+ [backend.MIDDLEWARE_ACTIONS.readSnapshots, backend.MIDDLEWARE_ACTIONS.op].forEach(function(action) {
1010
+ backend.use(action, function(context, next) {
1011
+ if (canRead || context.agent !== agent1) return next();
1012
+ next(new Error('Forbidden'));
1013
+ });
1014
+ });
1015
+ var localPresence1 = presence1.create('presence-1');
1016
+
1017
+ async.series([
1018
+ presence2.subscribe.bind(presence2),
1019
+ function(next) {
1020
+ localPresence1.submit({index: 1}, errorHandler(done));
1021
+ presence2.once('receive', function() {
1022
+ next();
1023
+ });
1024
+ },
1025
+ function(next) {
1026
+ doc2.submitOp({index: 5, value: 'ern'}, errorHandler(done));
1027
+ doc1.once('op', function() {
1028
+ next();
1029
+ });
1030
+ },
1031
+ function(next) {
1032
+ canRead = false;
1033
+ presence2.once('receive', function(id, presence) {
1034
+ expect(id).to.equal('presence-1');
1035
+ expect(presence).to.be.null;
1036
+ next();
1037
+ });
1038
+ connection1.close();
1039
+ }
1040
+ ], done);
1041
+ });
1042
+
685
1043
  it('ignores untyped presence on its channel', function(done) {
686
1044
  var connection3 = backend.connect();
687
1045
  var untypedPresence3 = connection3.getPresence('books.northern-lights');
@@ -26,6 +26,26 @@ module.exports = function(options) {
26
26
  });
27
27
  });
28
28
 
29
+ [null, 1].forEach(function(version) {
30
+ it('snapshot fetchSnapshot at version ' + version, function(done) {
31
+ this.backend.connect().fetchSnapshot('dogs_summary', 'fido', version, function(err, snapshot) {
32
+ if (err) return done(err);
33
+ expect(snapshot.data).eql({age: 3, owner: {name: 'jim'}});
34
+ expect(snapshot.v).eql(1);
35
+ done();
36
+ });
37
+ });
38
+ });
39
+
40
+ it('snapshot fetchSnapshotByTimestamp', function(done) {
41
+ this.backend.connect().fetchSnapshotByTimestamp('dogs_summary', 'fido', null, function(err, snapshot) {
42
+ if (err) return done(err);
43
+ expect(snapshot.data).eql({age: 3, owner: {name: 'jim'}});
44
+ expect(snapshot.v).eql(1);
45
+ done();
46
+ });
47
+ });
48
+
29
49
  if (getQuery) {
30
50
  ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
31
51
  it('snapshot ' + method, function(done) {
@@ -411,6 +411,23 @@ describe('SnapshotTimestampRequest', function() {
411
411
  done();
412
412
  });
413
413
  });
414
+
415
+ describe('when the database projects snapshots', function() {
416
+ beforeEach(function() {
417
+ backend.db.projectsSnapshots = true;
418
+ });
419
+
420
+ [null, day2].forEach(function(timestamp) {
421
+ it('applies the projection to a snapshot at timestamp ' + timestamp, function(done) {
422
+ var connection = backend.connect();
423
+ connection.fetchSnapshotByTimestamp('bookTitles', 'time-machine', timestamp, function(error, snapshot) {
424
+ if (error) return done(error);
425
+ expect(snapshot.data).to.eql({title: 'The Time Machine'});
426
+ done();
427
+ });
428
+ });
429
+ });
430
+ });
414
431
  });
415
432
  });
416
433
 
@@ -351,6 +351,22 @@ describe('SnapshotVersionRequest', function() {
351
351
  done();
352
352
  });
353
353
  });
354
+
355
+ describe('when the database projects snapshots', function() {
356
+ beforeEach(function() {
357
+ backend.db.projectsSnapshots = true;
358
+ });
359
+
360
+ [null, 2].forEach(function(version) {
361
+ it('applies the projection to a snapshot at version ' + version, function(done) {
362
+ backend.connect().fetchSnapshot('bookTitles', 'don-quixote', version, function(error, snapshot) {
363
+ if (error) return done(error);
364
+ expect(snapshot.data).to.eql({title: 'Don Quixote'});
365
+ done();
366
+ });
367
+ });
368
+ });
369
+ });
354
370
  });
355
371
  });
356
372