node/lib/internal/watch_mode/files_watcher.js

214 lines
6.2 KiB
JavaScript

'use strict';
const {
ArrayIsArray,
ArrayPrototypeForEach,
Boolean,
SafeMap,
SafeSet,
SafeWeakMap,
StringPrototypeEndsWith,
StringPrototypeStartsWith,
} = primordials;
const { validateNumber, validateOneOf } = require('internal/validators');
const { kEmptyObject } = require('internal/util');
const { TIMEOUT_MAX } = require('internal/timers');
const EventEmitter = require('events');
const { addAbortListener } = require('internal/events/abort_listener');
const { watch } = require('fs');
const { fileURLToPath } = require('internal/url');
const { resolve, dirname, sep } = require('path');
const { setTimeout, clearTimeout } = require('timers');
const supportsRecursiveWatching = process.platform === 'win32' ||
process.platform === 'darwin';
const isParentPath = (parentCandidate, childCandidate) => {
const parent = resolve(parentCandidate);
const child = resolve(childCandidate);
const normalizedParent = StringPrototypeEndsWith(parent, sep) ? parent : parent + sep;
return StringPrototypeStartsWith(child, normalizedParent);
};
class FilesWatcher extends EventEmitter {
#watchers = new SafeMap();
#filteredFiles = new SafeSet();
#dependencyOwners = new SafeMap();
#ownerDependencies = new SafeMap();
#debounceOwners = new SafeSet();
#debounceTimer;
#debounce;
#mode;
#signal;
#passthroughIPC = false;
#ipcHandlers = new SafeWeakMap();
constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) {
super({ __proto__: null, captureRejections: true });
validateNumber(debounce, 'options.debounce', 0, TIMEOUT_MAX);
validateOneOf(mode, 'options.mode', ['filter', 'all']);
this.#debounce = debounce;
this.#mode = mode;
this.#signal = signal;
this.#passthroughIPC = Boolean(process.send);
if (signal) {
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 && isParentPath(watchedPath, path)) {
return true;
}
}
return false;
}
#removeWatchedChildren(path) {
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
if (path !== watchedPath && isParentPath(path, watchedPath)) {
this.#unwatch(watcher);
this.#watchers.delete(watchedPath);
}
}
}
#unwatch(watcher) {
watcher.handle.removeAllListeners();
watcher.handle.close();
}
#onChange(trigger, eventType) {
if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) {
return;
}
const owners = this.#dependencyOwners.get(trigger);
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, eventType });
this.#debounceOwners.clear();
}, 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) => {
// `fileName` can be `null` if it cannot be determined. See
// https://github.com/nodejs/node/pull/49891#issuecomment-1744673430.
this.#onChange(recursive ? resolve(path, fileName ?? '') : path, eventType);
});
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.#dependencyOwners.get(file) ?? new SafeSet();
const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet();
owners.add(owner);
dependencies.add(file);
this.#dependencyOwners.set(file, owners);
this.#ownerDependencies.set(owner, dependencies);
}
}
#setupIPC(child) {
const handlers = {
__proto__: null,
parentToChild: (message) => child.send(message),
childToParent: (message) => process.send(message),
};
this.#ipcHandlers.set(child, handlers);
process.on('message', handlers.parentToChild);
child.on('message', handlers.childToParent);
}
destroyIPC(child) {
const handlers = this.#ipcHandlers.get(child);
if (this.#passthroughIPC && handlers !== undefined) {
process.off('message', handlers.parentToChild);
child.off('message', handlers.childToParent);
}
}
watchChildProcessModules(child, key = null) {
if (this.#passthroughIPC) {
this.#setupIPC(child);
}
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.#dependencyOwners.get(dependency)?.delete(owner);
if (this.#dependencyOwners.get(dependency)?.size === 0) {
this.#dependencyOwners.delete(dependency);
}
});
this.#filteredFiles.delete(owner);
this.#dependencyOwners.delete(owner);
this.#ownerDependencies.delete(owner);
});
}
clearFileFilters() {
this.#filteredFiles.clear();
}
clear() {
this.#watchers.forEach(this.#unwatch);
this.#watchers.clear();
this.#filteredFiles.clear();
this.#dependencyOwners.clear();
this.#ownerDependencies.clear();
}
}
module.exports = { FilesWatcher };