1'use strict'; 2 3const { 4 ArrayIsArray, 5 ArrayPrototypeForEach, 6 SafeMap, 7 SafeSet, 8 StringPrototypeStartsWith, 9} = primordials; 10 11const { validateNumber, validateOneOf } = require('internal/validators'); 12const { kEmptyObject } = require('internal/util'); 13const { TIMEOUT_MAX } = require('internal/timers'); 14 15const EventEmitter = require('events'); 16const { watch } = require('fs'); 17const { fileURLToPath } = require('url'); 18const { resolve, dirname } = require('path'); 19const { setTimeout } = require('timers'); 20 21const supportsRecursiveWatching = process.platform === 'win32' || 22 process.platform === 'darwin'; 23 24class FilesWatcher extends EventEmitter { 25 #watchers = new SafeMap(); 26 #filteredFiles = new SafeSet(); 27 #debouncing = new SafeSet(); 28 #depencencyOwners = new SafeMap(); 29 #ownerDependencies = new SafeMap(); 30 #debounce; 31 #mode; 32 #signal; 33 34 constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) { 35 super(); 36 37 validateNumber(debounce, 'options.debounce', 0, TIMEOUT_MAX); 38 validateOneOf(mode, 'options.mode', ['filter', 'all']); 39 this.#debounce = debounce; 40 this.#mode = mode; 41 this.#signal = signal; 42 43 if (signal) { 44 EventEmitter.addAbortListener(signal, () => this.clear()); 45 } 46 } 47 48 #isPathWatched(path) { 49 if (this.#watchers.has(path)) { 50 return true; 51 } 52 53 for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) { 54 if (watcher.recursive && StringPrototypeStartsWith(path, watchedPath)) { 55 return true; 56 } 57 } 58 59 return false; 60 } 61 62 #removeWatchedChildren(path) { 63 for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) { 64 if (path !== watchedPath && StringPrototypeStartsWith(watchedPath, path)) { 65 this.#unwatch(watcher); 66 this.#watchers.delete(watchedPath); 67 } 68 } 69 } 70 71 #unwatch(watcher) { 72 watcher.handle.removeAllListeners(); 73 watcher.handle.close(); 74 } 75 76 #onChange(trigger) { 77 if (this.#debouncing.has(trigger)) { 78 return; 79 } 80 if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) { 81 return; 82 } 83 this.#debouncing.add(trigger); 84 const owners = this.#depencencyOwners.get(trigger); 85 setTimeout(() => { 86 this.#debouncing.delete(trigger); 87 this.emit('changed', { owners }); 88 }, this.#debounce).unref(); 89 } 90 91 get watchedPaths() { 92 return [...this.#watchers.keys()]; 93 } 94 95 watchPath(path, recursive = true) { 96 if (this.#isPathWatched(path)) { 97 return; 98 } 99 const watcher = watch(path, { recursive, signal: this.#signal }); 100 watcher.on('change', (eventType, fileName) => this 101 .#onChange(recursive ? resolve(path, fileName) : path)); 102 this.#watchers.set(path, { handle: watcher, recursive }); 103 if (recursive) { 104 this.#removeWatchedChildren(path); 105 } 106 } 107 108 filterFile(file, owner) { 109 if (!file) return; 110 if (supportsRecursiveWatching) { 111 this.watchPath(dirname(file)); 112 } else { 113 // Having multiple FSWatcher's seems to be slower 114 // than a single recursive FSWatcher 115 this.watchPath(file, false); 116 } 117 this.#filteredFiles.add(file); 118 if (owner) { 119 const owners = this.#depencencyOwners.get(file) ?? new SafeSet(); 120 const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet(); 121 owners.add(owner); 122 dependencies.add(file); 123 this.#depencencyOwners.set(file, owners); 124 this.#ownerDependencies.set(owner, dependencies); 125 } 126 } 127 watchChildProcessModules(child, key = null) { 128 if (this.#mode !== 'filter') { 129 return; 130 } 131 child.on('message', (message) => { 132 try { 133 if (ArrayIsArray(message['watch:require'])) { 134 ArrayPrototypeForEach(message['watch:require'], (file) => this.filterFile(file, key)); 135 } 136 if (ArrayIsArray(message['watch:import'])) { 137 ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key)); 138 } 139 } catch { 140 // Failed watching file. ignore 141 } 142 }); 143 } 144 unfilterFilesOwnedBy(owners) { 145 owners.forEach((owner) => { 146 this.#ownerDependencies.get(owner)?.forEach((dependency) => { 147 this.#filteredFiles.delete(dependency); 148 this.#depencencyOwners.delete(dependency); 149 }); 150 this.#filteredFiles.delete(owner); 151 this.#depencencyOwners.delete(owner); 152 this.#ownerDependencies.delete(owner); 153 }); 154 } 155 clearFileFilters() { 156 this.#filteredFiles.clear(); 157 } 158 clear() { 159 this.#watchers.forEach(this.#unwatch); 160 this.#watchers.clear(); 161 this.#filteredFiles.clear(); 162 this.#depencencyOwners.clear(); 163 this.#ownerDependencies.clear(); 164 } 165} 166 167module.exports = { FilesWatcher }; 168