From 2ff593ceb355585fddb26f98304d96bafbba5d5c Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Mon, 30 Mar 2026 15:48:46 -0700 Subject: [PATCH 1/8] Upgrade dev dependencies (eslint, mocha, chai, nyc, sinon) --- eslint.config.js | 87 +++++++++++++++++++++++ package.json | 18 ++--- test/client/query-subscribe.js | 11 +-- test/client/snapshot-timestamp-request.js | 5 +- test/setup.js | 5 +- test/util.js | 12 ++++ 6 files changed, 121 insertions(+), 17 deletions(-) create mode 100644 eslint.config.js diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..9cfb747e5 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,87 @@ +const js = require("@eslint/js"); +const { + defineConfig, + globalIgnores, +} = require("eslint/config"); +const eslintConfigGoogle = require('eslint-config-google'); + +// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has +// been set, it will still error even if it's not applicable to that version number. Since Google sets these +// rules, we have to turn them off ourselves. +var DISABLED_ES6_OPTIONS = { + 'no-var': 'off', + 'prefer-rest-params': 'off' +}; + +var SHAREDB_RULES = { + // Comma dangle is not supported in ES3 + 'comma-dangle': ['error', 'never'], + // We control our own objects and prototypes, so no need for this check + 'guard-for-in': 'off', + // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have + // to override ESLint's default of 0 indents for this. + indent: ['error', 2, { + SwitchCase: 1 + }], + 'linebreak-style': 'off', + // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code + 'max-len': ['error', + { + code: 120, + tabWidth: 2, + ignoreUrls: true + } + ], + // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused + // variables + 'no-unused-vars': ['error', { + vars: 'all', + args: 'after-used', + // This can be removed once the minimum ES version is ES2019 or newer, and catch statements + // are updated to use optional catch binding. + caughtErrors: 'none', + }], + // It's more readable to ensure we only have one statement per line + 'max-statements-per-line': ['error', { max: 1 }], + // ES3 doesn't support spread + 'prefer-spread': 'off', + // as-needed quote props are easier to write + 'quote-props': ['error', 'as-needed'], + 'require-jsdoc': 'off', + 'valid-jsdoc': 'off', +}; + +module.exports = defineConfig([ + { + extends: [eslintConfigGoogle], + ignores: ['eslint.config.js'], + + languageOptions: { + ecmaVersion: 3, + sourceType: "commonjs", + + parserOptions: { + allowReserved: true, + }, + }, + + rules: Object.assign({}, DISABLED_ES6_OPTIONS, SHAREDB_RULES), + }, + globalIgnores(["docs/"]), + { + files: ["examples/counter-json1-vite/*.js"], + + languageOptions: { + ecmaVersion: 2015, + sourceType: "module", + + parserOptions: { + allowReserved: false, + }, + }, + + rules: { + quotes: ["error", "single"], + }, + } +]); diff --git a/package.json b/package.json index dd93b9422..74425271a 100644 --- a/package.json +++ b/package.json @@ -11,18 +11,18 @@ "ot-json0": "^1.1.0" }, "devDependencies": { - "chai": "^4.3.7", - "coveralls": "^3.1.1", - "eslint": "^8.47.0", + "@eslint/js": "^10.0.1", + "chai": "^6.2.2", + "eslint": "^10.1.0", "eslint-config-google": "^0.14.0", - "mocha": "^10.2.0", - "nyc": "^15.1.0", + "mocha": "^11.7.5", + "nyc": "^18.0.0", "ot-json0-v2": "https://github.com/ottypes/json0#90a3ae26364c4fa3b19b6df34dad46707a704421", "ot-json1": "^1.0.2", "rich-text": "^4.1.0", "sharedb-legacy": "npm:sharedb@1.1.0", - "sinon": "^15.2.0", - "sinon-chai": "^3.7.0" + "sinon": "^21.0.3", + "sinon-chai": "^4.0.1" }, "files": [ "lib/", @@ -34,8 +34,8 @@ "docs:start": "cd docs && bundle exec jekyll serve --livereload", "test": "mocha", "test-cover": "nyc --temp-dir=coverage -r text -r lcov npm test", - "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", - "lint:fix": "npm run lint -- --fix" + "lint": "eslint", + "lint:fix": "eslint --fix" }, "repository": { "type": "git", diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 4cf690ff9..99d96ae20 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -517,7 +517,8 @@ function commonTests(options) { }); it('pollDebounce option reduces subsequent poll interval', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); + clock.setTickMode({mode: 'nextAsync'}); var connection = this.backend.connect(); this.backend.db.canPollDoc = function() { return false; @@ -566,7 +567,7 @@ function commonTests(options) { }); it('db.pollDebounce option reduces subsequent poll interval', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.db.canPollDoc = function() { return false; @@ -616,7 +617,7 @@ function commonTests(options) { }); it('pollInterval updates a subscribed query after an unpublished create', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; var query = connection.createSubscribeQuery( @@ -638,7 +639,7 @@ function commonTests(options) { }); it('db.pollInterval updates a subscribed query after an unpublished create', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; this.backend.db.pollDebounce = 0; @@ -657,7 +658,7 @@ function commonTests(options) { }); it('pollInterval captures additional unpublished creates', function(done) { - var clock = sinon.useFakeTimers(); + var clock = util.useFakeTimers(); var connection = this.backend.connect(); this.backend.suppressPublish = true; var count = 0; diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 5ba12ee8d..1beaaf4fd 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -2,6 +2,7 @@ var Backend = require('../../lib/backend'); var expect = require('chai').expect; var MemoryDb = require('../../lib/db/memory'); var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); +var util = require('../util'); var sinon = require('sinon'); var async = require('async'); @@ -17,12 +18,12 @@ describe('SnapshotTimestampRequest', function() { var ONE_DAY = 1000 * 60 * 60 * 24; beforeEach(function() { - clock = sinon.useFakeTimers(day1); + clock = util.useFakeTimers(day1); backend = new Backend(); }); afterEach(function(done) { - clock.uninstall(); + sinon.restore(); backend.close(done); }); diff --git a/test/setup.js b/test/setup.js index d00aeee38..4d3aca185 100644 --- a/test/setup.js +++ b/test/setup.js @@ -3,7 +3,7 @@ var sinon = require('sinon'); var sinonChai = require('sinon-chai'); var chai = require('chai'); -chai.use(sinonChai); +chai.use(sinonChai.default); if (process.env.LOGGING !== 'true') { // Silence the logger for tests by setting all its methods to no-ops @@ -15,5 +15,8 @@ if (process.env.LOGGING !== 'true') { } afterEach(function() { + if (sinon.clock) { + sinon.clock.uninstall(); + } sinon.restore(); }); diff --git a/test/util.js b/test/util.js index 17fa6500f..812fb7571 100644 --- a/test/util.js +++ b/test/util.js @@ -1,3 +1,4 @@ +var sinon = require('sinon'); exports.sortById = function(docs) { return docs.slice().sort(function(a, b) { @@ -15,6 +16,17 @@ exports.pluck = function(docs, key) { return values; }; +/** + * @param {Parameters[0]} config + * @see {@link sinon.useFakeTimers} + * @see {@link https://github.com/sinonjs/fake-timers#clocksettickmodemode} + */ +exports.useFakeTimers = function(config) { + var clock = sinon.useFakeTimers(config); + clock.setTickMode({mode: 'nextAsync'}); + return clock; +}; + // Wrap a done function to call back only after a specified number of calls. // For example, `var callbackAfter = callAfter(1, callback)` means that if // `callbackAfter` is called once, it won't call back. If it is called twice From cda814a8898656f6a3c3b2c16e311ae154c0ab5c Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Thu, 9 Apr 2026 13:40:11 -0700 Subject: [PATCH 2/8] Remove old .eslintrc.js --- .eslintrc.js | 67 ---------------------------------------------------- 1 file changed, 67 deletions(-) delete mode 100644 .eslintrc.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index c83d81a4d..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,67 +0,0 @@ -// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has -// been set, it will still error even if it's not applicable to that version number. Since Google sets these -// rules, we have to turn them off ourselves. -var DISABLED_ES6_OPTIONS = { - 'no-var': 'off', - 'prefer-rest-params': 'off' -}; - -var SHAREDB_RULES = { - // Comma dangle is not supported in ES3 - 'comma-dangle': ['error', 'never'], - // We control our own objects and prototypes, so no need for this check - 'guard-for-in': 'off', - // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have - // to override ESLint's default of 0 indents for this. - indent: ['error', 2, { - SwitchCase: 1 - }], - // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code - 'max-len': ['error', - { - code: 120, - tabWidth: 2, - ignoreUrls: true - } - ], - // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused - // variables - 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}], - // It's more readable to ensure we only have one statement per line - 'max-statements-per-line': ['error', {max: 1}], - // ES3 doesn't support spread - 'prefer-spread': 'off', - // as-needed quote props are easier to write - 'quote-props': ['error', 'as-needed'], - 'require-jsdoc': 'off', - 'valid-jsdoc': 'off' -}; - -module.exports = { - extends: 'google', - parserOptions: { - ecmaVersion: 3, - allowReserved: true - }, - rules: Object.assign( - {}, - DISABLED_ES6_OPTIONS, - SHAREDB_RULES - ), - ignorePatterns: [ - '/docs/' - ], - overrides: [ - { - files: ['examples/counter-json1-vite/*.js'], - parserOptions: { - ecmaVersion: 6, - sourceType: 'module', - allowReserved: false - }, - rules: { - quotes: ['error', 'single'] - } - } - ] -}; From 662f0370744d5612540bddaf1ca3e2900ec49fe4 Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:37:08 -0700 Subject: [PATCH 3/8] (tests) Remove extraneous clock.setTickMode --- test/client/query-subscribe.js | 1 - 1 file changed, 1 deletion(-) diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 99d96ae20..29f4f7df6 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -518,7 +518,6 @@ function commonTests(options) { it('pollDebounce option reduces subsequent poll interval', function(done) { var clock = util.useFakeTimers(); - clock.setTickMode({mode: 'nextAsync'}); var connection = this.backend.connect(); this.backend.db.canPollDoc = function() { return false; From 3594275227dc46d01a995c95a604aebd40228280 Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:48:05 -0700 Subject: [PATCH 4/8] Remove Node 18 from CI, add Node 24 Node 18 has been EoL for a year. Also, dev deps sinon 19+, chai 5+, and sinon-chai 4+ require at least Node 20, since require(esm) was only backported as far back as Node 20 - https://joyeecheung.github.io/blog/2025/12/30/require-esm-in-node-js-from-experiment-to-stability/\#Backporting-require-esm-to-v22-and-v20-LTS --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f0bea60da..6a45c29b1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,9 +15,9 @@ jobs: strategy: matrix: node: - - 18 - 20 - 22 + - 24 services: mongodb: image: mongo:4.4 From 7d2ab03637d1caa277d88a58f40c5b7c109acadc Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:07:24 -0700 Subject: [PATCH 5/8] Add sinon clock uninstall to query-subscribe tests, since they are transcluded from sharedb-mongo tests --- test/client/query-subscribe.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 29f4f7df6..673299528 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -13,6 +13,9 @@ module.exports = function(options) { }); afterEach(function() { + if (sinon.clock) { + sinon.clock.uninstall(); + } sinon.restore(); }); From fd4cb7b497be3faedde7a3957c1a173d7665d17d Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:43:21 -0700 Subject: [PATCH 6/8] Switch test fake timers to use toFake list, and use clock.tick() instead of clock.tickAsync() Using setTickMode({mode: "nextAsync"}) together with clock.tickAsync() can trigger a fake-timers bug that causes Mocha to hang after test suite completion (https://github.com/sinonjs/fake-timers/issues/564) --- test/client/query-subscribe.js | 18 +++++++++--------- test/util.js | 10 +++++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 673299528..848db99ca 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -542,7 +542,7 @@ function commonTests(options) { connection.get('items', i.toString()).on('error', done).create({}, function(err) { if (err) return done(err); counter++; - if (counter === 9) clock.tickAsync(10000); + if (counter === 9) clock.tick(10000); }); } } @@ -564,7 +564,7 @@ function commonTests(options) { // event firing the first time, while sharedb is definitely // debouncing connection.get('items', '0').on('error', done).create({}, function() { - clock.tickAsync(3000); + clock.tick(3000); }); }); @@ -592,7 +592,7 @@ function commonTests(options) { connection.get('items', i.toString()).on('error', done).create({}, function(err) { if (err) return done(err); counter++; - if (counter === 9) clock.tickAsync(10000); + if (counter === 9) clock.tick(10000); }); } } @@ -614,7 +614,7 @@ function commonTests(options) { // event firing the first time, while sharedb is definitely // debouncing connection.get('items', '0').on('error', done).create({}, function() { - clock.tickAsync(3000); + clock.tick(3000); }); }); @@ -629,7 +629,7 @@ function commonTests(options) { function(err) { if (err) return done(err); connection.get('dogs', 'fido').on('error', done).create({}, function() { - clock.tickAsync(51); + clock.tick(51); }); } ); @@ -649,7 +649,7 @@ function commonTests(options) { var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) { if (err) return done(err); connection.get('dogs', 'fido').on('error', done).create({}, function() { - clock.tickAsync(51); + clock.tick(51); }); }); query.on('error', done); @@ -670,7 +670,7 @@ function commonTests(options) { var doc = connection.get('dogs', count.toString()).on('error', done); doc.create({}, function(e) { if (e) return done(e); - clock.tickAsync(2000); + clock.tick(2000); }); }); query.on('error', done); @@ -680,10 +680,10 @@ function commonTests(options) { var doc = connection.get('dogs', count.toString()).on('error', done); doc.create({}, function(e) { if (e) return done(e); - clock.tickAsync(10000); + clock.tick(10000); }); }); - clock.tickAsync(1); + clock.tick(1); }); it('query extra is returned to client', function(done) { diff --git a/test/util.js b/test/util.js index 812fb7571..8ba19c04b 100644 --- a/test/util.js +++ b/test/util.js @@ -22,8 +22,16 @@ exports.pluck = function(docs, key) { * @see {@link https://github.com/sinonjs/fake-timers#clocksettickmodemode} */ exports.useFakeTimers = function(config) { + if (config == null) { + config = {}; + } else if (typeof config !== 'object') { + // Number or Date + config = {now: config}; + } + if (config.toFake == null) { + config.toFake = ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"]; + } var clock = sinon.useFakeTimers(config); - clock.setTickMode({mode: 'nextAsync'}); return clock; }; From bd49a6645d0c5407e81f944ab3b4723673a467f3 Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:44:39 -0700 Subject: [PATCH 7/8] Remove very old test/.jshintrc file, which was left over after migration to eslint --- test/.jshintrc | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 test/.jshintrc diff --git a/test/.jshintrc b/test/.jshintrc deleted file mode 100644 index 1cb88cb41..000000000 --- a/test/.jshintrc +++ /dev/null @@ -1,22 +0,0 @@ -{ - "node": true, - "laxcomma": true, - "eqnull": true, - "eqeqeq": true, - "indent": 2, - "newcap": true, - "quotmark": "single", - "undef": true, - "trailing": true, - "shadow": true, - "expr": true, - "boss": true, - "globals": { - "describe": false, - "it": false, - "before": false, - "after": false, - "beforeEach": false, - "afterEach": false - } -} From 4e88e2717fa1168a87820c5f8669b76981b2e64a Mon Sep 17 00:00:00 2001 From: Eric Hwang <6915076+ericyhwang@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:03:41 -0700 Subject: [PATCH 8/8] Lint fix --- test/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/util.js b/test/util.js index 8ba19c04b..996c5c42b 100644 --- a/test/util.js +++ b/test/util.js @@ -29,7 +29,7 @@ exports.useFakeTimers = function(config) { config = {now: config}; } if (config.toFake == null) { - config.toFake = ["setTimeout", "clearTimeout", "setInterval", "clearInterval", "Date"]; + config.toFake = ['setTimeout', 'clearTimeout', 'setInterval', 'clearInterval', 'Date']; } var clock = sinon.useFakeTimers(config); return clock;