nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
206 lines (186 loc) • 6.08 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/watch_mode/files_watcher.js
import { validateNumber, validateOneOf } from "nstdlib/lib/internal/validators";
import { kEmptyObject } from "nstdlib/lib/internal/util";
import { TIMEOUT_MAX } from "nstdlib/lib/internal/timers";
import EventEmitter from "nstdlib/lib/events";
import { addAbortListener } from "nstdlib/lib/internal/events/abort_listener";
import { watch } from "nstdlib/lib/fs";
import { fileURLToPath } from "nstdlib/lib/internal/url";
import { resolve, dirname } from "nstdlib/lib/path";
import { setTimeout, clearTimeout } from "nstdlib/lib/timers";
const supportsRecursiveWatching =
process.platform === "win32" || process.platform === "darwin";
class FilesWatcher extends EventEmitter {
#watchers = new Map();
#filteredFiles = new Set();
#dependencyOwners = new Map();
#ownerDependencies = new Map();
#debounceOwners = new Set();
#debounceTimer;
#debounce;
#mode;
#signal;
#passthroughIPC = false;
#ipcHandlers = new WeakMap();
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 &&
String.prototype.startsWith.call(path, watchedPath)
) {
return true;
}
}
return false;
}
#removeWatchedChildren(path) {
for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
if (
path !== watchedPath &&
String.prototype.startsWith.call(watchedPath, path)
) {
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 Set();
const dependencies = this.#ownerDependencies.get(file) ?? new Set();
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);
}
if (this.#mode !== "filter") {
return;
}
child.on("message", (message) => {
try {
if (Array.isArray(message["watch:require"])) {
Array.prototype.forEach.call(message["watch:require"], (file) =>
this.filterFile(file, key),
);
}
if (Array.isArray(message["watch:import"])) {
Array.prototype.forEach.call(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.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();
}
}
export { FilesWatcher };