node/lib/internal/watch_mode/files_watcher.js
Deokjin Kim b3fc9173ed
lib: use internal fileURLToPath
For internal usage, `internal/url` should be used.

Refs: https://github.com/nodejs/node/pull/49553
PR-URL: https://github.com/nodejs/node/pull/49558
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Raz Luvaton <rluvaton@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: LiviaMedeiros <livia@cirno.name>
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Minwoo Jung <nodecorelab@gmail.com>
2023-09-10 14:57:37 +00:00

168 lines
4.6 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
ArrayPrototypeForEach,
SafeMap,
SafeSet,
StringPrototypeStartsWith,
} = primordials;
const { validateNumber, validateOneOf } = require('internal/validators');
const { kEmptyObject } = require('internal/util');
const { TIMEOUT_MAX } = require('internal/timers');
const EventEmitter = require('events');
const { watch } = require('fs');
const { fileURLToPath } = require('internal/url');
const { resolve, dirname } = require('path');
const { setTimeout } = require('timers');
const supportsRecursiveWatching = process.platform === 'win32' ||
process.platform === 'darwin';
class FilesWatcher extends EventEmitter {
#watchers = new SafeMap();
#filteredFiles = new SafeSet();
#debouncing = new SafeSet();
#depencencyOwners = new SafeMap();
#ownerDependencies = new SafeMap();
#debounce;
#mode;
#signal;
constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) {
super();
validateNumber(debounce, 'options.debounce', 0, TIMEOUT_MAX);
validateOneOf(mode, 'options.mode', ['filter', 'all']);
this.#debounce = debounce;
this.#mode = mode;
this.#signal = signal;
if (signal) {
EventEmitter.addAbortListener(signal, () => this.clear());
}
}
#isPathWatched(path) {
if (this.#watchers.has(path)) {
return true;
}
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
if (watcher.recursive && StringPrototypeStartsWith(path, watchedPath)) {
return true;
}
}
return false;
}
#removeWatchedChildren(path) {
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
if (path !== watchedPath && StringPrototypeStartsWith(watchedPath, path)) {
this.#unwatch(watcher);
this.#watchers.delete(watchedPath);
}
}
}
#unwatch(watcher) {
watcher.handle.removeAllListeners();
watcher.handle.close();
}
#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 });
}, this.#debounce).unref();
}
get watchedPaths() {
return [...this.#watchers.keys()];
}
watchPath(path, recursive = true) {
if (this.#isPathWatched(path)) {
return;
}
const watcher = watch(path, { recursive, signal: this.#signal });
watcher.on('change', (eventType, fileName) => this
.#onChange(recursive ? resolve(path, fileName) : path));
this.#watchers.set(path, { handle: watcher, recursive });
if (recursive) {
this.#removeWatchedChildren(path);
}
}
filterFile(file, owner) {
if (!file) return;
if (supportsRecursiveWatching) {
this.watchPath(dirname(file));
} else {
// Having multiple FSWatcher's seems to be slower
// than a single recursive FSWatcher
this.watchPath(file, false);
}
this.#filteredFiles.add(file);
if (owner) {
const owners = this.#depencencyOwners.get(file) ?? new SafeSet();
const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet();
owners.add(owner);
dependencies.add(file);
this.#depencencyOwners.set(file, owners);
this.#ownerDependencies.set(owner, dependencies);
}
}
watchChildProcessModules(child, key = null) {
if (this.#mode !== 'filter') {
return;
}
child.on('message', (message) => {
try {
if (ArrayIsArray(message['watch:require'])) {
ArrayPrototypeForEach(message['watch:require'], (file) => this.filterFile(file, key));
}
if (ArrayIsArray(message['watch:import'])) {
ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key));
}
} catch {
// Failed watching file. ignore
}
});
}
unfilterFilesOwnedBy(owners) {
owners.forEach((owner) => {
this.#ownerDependencies.get(owner)?.forEach((dependency) => {
this.#filteredFiles.delete(dependency);
this.#depencencyOwners.delete(dependency);
});
this.#filteredFiles.delete(owner);
this.#depencencyOwners.delete(owner);
this.#ownerDependencies.delete(owner);
});
}
clearFileFilters() {
this.#filteredFiles.clear();
}
clear() {
this.#watchers.forEach(this.#unwatch);
this.#watchers.clear();
this.#filteredFiles.clear();
this.#depencencyOwners.clear();
this.#ownerDependencies.clear();
}
}
module.exports = { FilesWatcher };