sharedb 5.2.2 → 6.0.0

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/backend.js CHANGED
@@ -881,29 +881,38 @@ Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp
881
881
  var from = 0;
882
882
  var to = null;
883
883
 
884
- var shouldGetLatestSnapshot = timestamp === null;
885
- if (shouldGetLatestSnapshot) {
886
- return backend.db.getSnapshot(collection, id, null, null, function(error, snapshot) {
887
- if (error) return callback(error);
888
-
889
- callback(null, snapshot);
890
- });
891
- }
892
-
893
- milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
884
+ // Always fetch the current snapshot first. We request its metadata so that we
885
+ // can read its mtime, which lets us serve the current snapshot directly when
886
+ // the requested timestamp is after it. This avoids replaying ops when they
887
+ // aren't needed, and - crucially - still works when older ops have been
888
+ // deleted/TTLed and the current version can no longer be rebuilt from ops.
889
+ db.getSnapshot(collection, id, null, {metadata: true}, function(error, currentSnapshot) {
894
890
  if (error) return callback(error);
895
- milestoneSnapshot = snapshot;
896
- if (snapshot) from = snapshot.v;
897
891
 
898
- milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
892
+ var mtime = currentSnapshot.m && currentSnapshot.m.mtime;
893
+ var shouldGetLatestSnapshot = timestamp === null || (mtime != null && timestamp > mtime);
894
+ if (shouldGetLatestSnapshot) {
895
+ // Strip the metadata that we only fetched in order to compare the mtime,
896
+ // so that the returned snapshot is consistent with the op-replayed path.
897
+ currentSnapshot.m = null;
898
+ return callback(null, currentSnapshot);
899
+ }
900
+
901
+ milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
899
902
  if (error) return callback(error);
900
- if (snapshot) to = snapshot.v;
903
+ milestoneSnapshot = snapshot;
904
+ if (snapshot) from = snapshot.v;
901
905
 
902
- var options = {metadata: true};
903
- db.getOps(collection, id, from, to, options, function(error, ops) {
906
+ milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
904
907
  if (error) return callback(error);
905
- filterOpsInPlaceBeforeTimestamp(ops, timestamp);
906
- backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback);
908
+ if (snapshot) to = snapshot.v;
909
+
910
+ var options = {metadata: true};
911
+ db.getOps(collection, id, from, to, options, function(error, ops) {
912
+ if (error) return callback(error);
913
+ filterOpsInPlaceBeforeTimestamp(ops, timestamp);
914
+ backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback);
915
+ });
907
916
  });
908
917
  });
909
918
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sharedb",
3
- "version": "5.2.2",
3
+ "version": "6.0.0",
4
4
  "description": "JSON OT database backend",
5
5
  "main": "lib/index.js",
6
6
  "dependencies": {
@@ -11,18 +11,18 @@
11
11
  "ot-json0": "^1.1.0"
12
12
  },
13
13
  "devDependencies": {
14
- "chai": "^4.3.7",
15
- "coveralls": "^3.1.1",
16
- "eslint": "^8.47.0",
14
+ "@eslint/js": "^10.0.1",
15
+ "chai": "^6.2.2",
16
+ "eslint": "^10.1.0",
17
17
  "eslint-config-google": "^0.14.0",
18
- "mocha": "^10.2.0",
19
- "nyc": "^15.1.0",
18
+ "mocha": "^11.7.5",
19
+ "nyc": "^18.0.0",
20
20
  "ot-json0-v2": "https://github.com/ottypes/json0#90a3ae26364c4fa3b19b6df34dad46707a704421",
21
21
  "ot-json1": "^1.0.2",
22
22
  "rich-text": "^4.1.0",
23
23
  "sharedb-legacy": "npm:sharedb@1.1.0",
24
- "sinon": "^15.2.0",
25
- "sinon-chai": "^3.7.0"
24
+ "sinon": "^21.0.3",
25
+ "sinon-chai": "^4.0.1"
26
26
  },
27
27
  "files": [
28
28
  "lib/",
@@ -34,8 +34,8 @@
34
34
  "docs:start": "cd docs && bundle exec jekyll serve --livereload",
35
35
  "test": "mocha",
36
36
  "test-cover": "nyc --temp-dir=coverage -r text -r lcov npm test",
37
- "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'",
38
- "lint:fix": "npm run lint -- --fix"
37
+ "lint": "eslint",
38
+ "lint:fix": "eslint --fix"
39
39
  },
40
40
  "repository": {
41
41
  "type": "git",
@@ -13,6 +13,9 @@ module.exports = function(options) {
13
13
  });
14
14
 
15
15
  afterEach(function() {
16
+ if (sinon.clock) {
17
+ sinon.clock.uninstall();
18
+ }
16
19
  sinon.restore();
17
20
  });
18
21
 
@@ -517,7 +520,7 @@ function commonTests(options) {
517
520
  });
518
521
 
519
522
  it('pollDebounce option reduces subsequent poll interval', function(done) {
520
- var clock = sinon.useFakeTimers();
523
+ var clock = util.useFakeTimers();
521
524
  var connection = this.backend.connect();
522
525
  this.backend.db.canPollDoc = function() {
523
526
  return false;
@@ -539,7 +542,7 @@ function commonTests(options) {
539
542
  connection.get('items', i.toString()).on('error', done).create({}, function(err) {
540
543
  if (err) return done(err);
541
544
  counter++;
542
- if (counter === 9) clock.tickAsync(10000);
545
+ if (counter === 9) clock.tick(10000);
543
546
  });
544
547
  }
545
548
  }
@@ -561,12 +564,12 @@ function commonTests(options) {
561
564
  // event firing the first time, while sharedb is definitely
562
565
  // debouncing
563
566
  connection.get('items', '0').on('error', done).create({}, function() {
564
- clock.tickAsync(3000);
567
+ clock.tick(3000);
565
568
  });
566
569
  });
567
570
 
568
571
  it('db.pollDebounce option reduces subsequent poll interval', function(done) {
569
- var clock = sinon.useFakeTimers();
572
+ var clock = util.useFakeTimers();
570
573
  var connection = this.backend.connect();
571
574
  this.backend.db.canPollDoc = function() {
572
575
  return false;
@@ -589,7 +592,7 @@ function commonTests(options) {
589
592
  connection.get('items', i.toString()).on('error', done).create({}, function(err) {
590
593
  if (err) return done(err);
591
594
  counter++;
592
- if (counter === 9) clock.tickAsync(10000);
595
+ if (counter === 9) clock.tick(10000);
593
596
  });
594
597
  }
595
598
  }
@@ -611,12 +614,12 @@ function commonTests(options) {
611
614
  // event firing the first time, while sharedb is definitely
612
615
  // debouncing
613
616
  connection.get('items', '0').on('error', done).create({}, function() {
614
- clock.tickAsync(3000);
617
+ clock.tick(3000);
615
618
  });
616
619
  });
617
620
 
618
621
  it('pollInterval updates a subscribed query after an unpublished create', function(done) {
619
- var clock = sinon.useFakeTimers();
622
+ var clock = util.useFakeTimers();
620
623
  var connection = this.backend.connect();
621
624
  this.backend.suppressPublish = true;
622
625
  var query = connection.createSubscribeQuery(
@@ -626,7 +629,7 @@ function commonTests(options) {
626
629
  function(err) {
627
630
  if (err) return done(err);
628
631
  connection.get('dogs', 'fido').on('error', done).create({}, function() {
629
- clock.tickAsync(51);
632
+ clock.tick(51);
630
633
  });
631
634
  }
632
635
  );
@@ -638,7 +641,7 @@ function commonTests(options) {
638
641
  });
639
642
 
640
643
  it('db.pollInterval updates a subscribed query after an unpublished create', function(done) {
641
- var clock = sinon.useFakeTimers();
644
+ var clock = util.useFakeTimers();
642
645
  var connection = this.backend.connect();
643
646
  this.backend.suppressPublish = true;
644
647
  this.backend.db.pollDebounce = 0;
@@ -646,7 +649,7 @@ function commonTests(options) {
646
649
  var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
647
650
  if (err) return done(err);
648
651
  connection.get('dogs', 'fido').on('error', done).create({}, function() {
649
- clock.tickAsync(51);
652
+ clock.tick(51);
650
653
  });
651
654
  });
652
655
  query.on('error', done);
@@ -657,7 +660,7 @@ function commonTests(options) {
657
660
  });
658
661
 
659
662
  it('pollInterval captures additional unpublished creates', function(done) {
660
- var clock = sinon.useFakeTimers();
663
+ var clock = util.useFakeTimers();
661
664
  var connection = this.backend.connect();
662
665
  this.backend.suppressPublish = true;
663
666
  var count = 0;
@@ -667,7 +670,7 @@ function commonTests(options) {
667
670
  var doc = connection.get('dogs', count.toString()).on('error', done);
668
671
  doc.create({}, function(e) {
669
672
  if (e) return done(e);
670
- clock.tickAsync(2000);
673
+ clock.tick(2000);
671
674
  });
672
675
  });
673
676
  query.on('error', done);
@@ -677,10 +680,10 @@ function commonTests(options) {
677
680
  var doc = connection.get('dogs', count.toString()).on('error', done);
678
681
  doc.create({}, function(e) {
679
682
  if (e) return done(e);
680
- clock.tickAsync(10000);
683
+ clock.tick(10000);
681
684
  });
682
685
  });
683
- clock.tickAsync(1);
686
+ clock.tick(1);
684
687
  });
685
688
 
686
689
  it('query extra is returned to client', function(done) {
@@ -2,6 +2,7 @@ var Backend = require('../../lib/backend');
2
2
  var expect = require('chai').expect;
3
3
  var MemoryDb = require('../../lib/db/memory');
4
4
  var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
5
+ var util = require('../util');
5
6
  var sinon = require('sinon');
6
7
  var async = require('async');
7
8
 
@@ -17,12 +18,12 @@ describe('SnapshotTimestampRequest', function() {
17
18
  var ONE_DAY = 1000 * 60 * 60 * 24;
18
19
 
19
20
  beforeEach(function() {
20
- clock = sinon.useFakeTimers(day1);
21
+ clock = util.useFakeTimers(day1);
21
22
  backend = new Backend();
22
23
  });
23
24
 
24
25
  afterEach(function(done) {
25
- clock.uninstall();
26
+ sinon.restore();
26
27
  backend.close(done);
27
28
  });
28
29
 
@@ -138,6 +139,21 @@ describe('SnapshotTimestampRequest', function() {
138
139
  ], done);
139
140
  });
140
141
 
142
+ it('fetches the current snapshot when ops are missing and the timestamp is after the latest op', function(done) {
143
+ var connection = backend.connect();
144
+ async.series([
145
+ // Simulate ops having been deleted/TTLed so the snapshot can't be rebuilt from ops.
146
+ backend.db.deleteOps.bind(backend.db, 'books', 'time-machine', null, null, null),
147
+ function(next) {
148
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', day4, function(error, snapshot) {
149
+ if (error) return next(error);
150
+ expect(snapshot).to.eql(v3);
151
+ next();
152
+ });
153
+ }
154
+ ], done);
155
+ });
156
+
141
157
  it('fetches the most recent version when not specifying a timestamp', function(done) {
142
158
  var connection = backend.connect();
143
159
  async.waterfall([
package/test/setup.js CHANGED
@@ -3,7 +3,7 @@ var sinon = require('sinon');
3
3
  var sinonChai = require('sinon-chai');
4
4
  var chai = require('chai');
5
5
 
6
- chai.use(sinonChai);
6
+ chai.use(sinonChai.default);
7
7
 
8
8
  if (process.env.LOGGING !== 'true') {
9
9
  // Silence the logger for tests by setting all its methods to no-ops
@@ -15,5 +15,8 @@ if (process.env.LOGGING !== 'true') {
15
15
  }
16
16
 
17
17
  afterEach(function() {
18
+ if (sinon.clock) {
19
+ sinon.clock.uninstall();
20
+ }
18
21
  sinon.restore();
19
22
  });
package/test/util.js CHANGED
@@ -1,3 +1,4 @@
1
+ var sinon = require('sinon');
1
2
 
2
3
  exports.sortById = function(docs) {
3
4
  return docs.slice().sort(function(a, b) {
@@ -15,6 +16,25 @@ exports.pluck = function(docs, key) {
15
16
  return values;
16
17
  };
17
18
 
19
+ /**
20
+ * @param {Parameters<typeof sinon.useFakeTimers>[0]} config
21
+ * @see {@link sinon.useFakeTimers}
22
+ * @see {@link https://github.com/sinonjs/fake-timers#clocksettickmodemode}
23
+ */
24
+ exports.useFakeTimers = function(config) {
25
+ if (config == null) {
26
+ config = {};
27
+ } else if (typeof config !== 'object') {
28
+ // Number or Date
29
+ config = {now: config};
30
+ }
31
+ if (config.toFake == null) {
32
+ config.toFake = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date'];
33
+ }
34
+ var clock = sinon.useFakeTimers(config);
35
+ return clock;
36
+ };
37
+
18
38
  // Wrap a done function to call back only after a specified number of calls.
19
39
  // For example, `var callbackAfter = callAfter(1, callback)` means that if
20
40
  // `callbackAfter` is called once, it won't call back. If it is called twice
package/test/.jshintrc DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "node": true,
3
- "laxcomma": true,
4
- "eqnull": true,
5
- "eqeqeq": true,
6
- "indent": 2,
7
- "newcap": true,
8
- "quotmark": "single",
9
- "undef": true,
10
- "trailing": true,
11
- "shadow": true,
12
- "expr": true,
13
- "boss": true,
14
- "globals": {
15
- "describe": false,
16
- "it": false,
17
- "before": false,
18
- "after": false,
19
- "beforeEach": false,
20
- "afterEach": false
21
- }
22
- }