UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

206 lines (186 loc) 6.08 kB
// 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 };