diff --git a/lib/internal/watch_mode/files_watcher.js b/lib/internal/watch_mode/files_watcher.js index 372a7c1bd4caa8..fda52e88b8d8ad 100644 --- a/lib/internal/watch_mode/files_watcher.js +++ b/lib/internal/watch_mode/files_watcher.js @@ -19,7 +19,7 @@ const { addAbortListener } = require('internal/events/abort_listener'); const { watch } = require('fs'); const { fileURLToPath } = require('internal/url'); const { resolve, dirname } = require('path'); -const { setTimeout } = require('timers'); +const { setTimeout, clearTimeout } = require('timers'); const supportsRecursiveWatching = process.platform === 'win32' || process.platform === 'darwin'; @@ -27,9 +27,10 @@ const supportsRecursiveWatching = process.platform === 'win32' || class FilesWatcher extends EventEmitter { #watchers = new SafeMap(); #filteredFiles = new SafeSet(); - #debouncing = new SafeSet(); #depencencyOwners = new SafeMap(); #ownerDependencies = new SafeMap(); + #debounceOwners = new SafeSet(); + #debounceTimer; #debounce; #mode; #signal; @@ -80,17 +81,20 @@ class FilesWatcher extends EventEmitter { } #onChange(trigger) { - if (this.#debouncing.has(trigger)) { - return; - } if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) { return; } - this.#debouncing.add(trigger); const owners = this.#depencencyOwners.get(trigger); - setTimeout(() => { - this.#debouncing.delete(trigger); - this.emit('changed', { owners }); + if (owners) { + for (const owner of owners) { + this.#debounceOwners.add(owner); + } + } + clearTimeout(this.#debounceTimer); + this.#debounceTimer = setTimeout(() => { + this.#debounceTimer = null; + this.emit('changed', { owners: this.#debounceOwners }); + this.#debounceOwners.clear(); }, this.#debounce).unref(); } diff --git a/test/parallel/test-watch-mode-files_watcher.mjs b/test/parallel/test-watch-mode-files_watcher.mjs index fec26892771fcf..fb358371750d48 100644 --- a/test/parallel/test-watch-mode-files_watcher.mjs +++ b/test/parallel/test-watch-mode-files_watcher.mjs @@ -70,6 +70,29 @@ describe('watch mode file watcher', () => { assert.ok(changesCount < 5); }); + it('should debounce changes on multiple files', async () => { + const files = []; + for (let i = 0; i < 10; i++) { + const file = tmpdir.resolve(`file-debounced-${i}`); + writeFileSync(file, 'written'); + watcher.filterFile(file); + files.push(file); + } + + files.forEach((file) => writeFileSync(file, '1')); + files.forEach((file) => writeFileSync(file, '2')); + files.forEach((file) => writeFileSync(file, '3')); + files.forEach((file) => writeFileSync(file, '4')); + + await setTimeout(200); // debounce * 2 + files.forEach((file) => writeFileSync(file, '5')); + const changed = once(watcher, 'changed'); + files.forEach((file) => writeFileSync(file, 'after')); + await changed; + // Unfortunately testing that changesCount === 2 is flaky + assert.ok(changesCount < 5); + }); + it('should ignore files in watched directory if they are not filtered', { skip: !supportsRecursiveWatching }, async () => { watcher.on('changed', common.mustNotCall());