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 #throttling = new SafeSet(); 28 #depencencyOwners = new SafeMap(); 29 #ownerDependencies = new SafeMap(); 30 #throttle; 31 #mode; 32 #signal; 33 34 constructor({ throttle = 500, mode = 'filter', signal } = kEmptyObject) { 35 super(); 36 37 validateNumber(throttle, 'options.throttle', 0, TIMEOUT_MAX); 38 validateOneOf(mode, 'options.mode', ['filter', 'all']); 39 this.#throttle = throttle; 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.#throttling.has(trigger)) { 78 return; 79 } 80 if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) { 81 return; 82 } 83 this.#throttling.add(trigger); 84 const owners = this.#depencencyOwners.get(trigger); 85 this.emit('changed', { owners }); 86 setTimeout(() => this.#throttling.delete(trigger), this.#throttle).unref(); 87 } 88 89 get watchedPaths() { 90 return [...this.#watchers.keys()]; 91 } 92 93 watchPath(path, recursive = true) { 94 if (this.#isPathWatched(path)) { 95 return; 96 } 97 const watcher = watch(path, { recursive, signal: this.#signal }); 98 watcher.on('change', (eventType, fileName) => this 99 .#onChange(recursive ? resolve(path, fileName) : path)); 100 this.#watchers.set(path, { handle: watcher, recursive }); 101 if (recursive) { 102 this.#removeWatchedChildren(path); 103 } 104 } 105 106 filterFile(file, owner) { 107 if (!file) return; 108 if (supportsRecursiveWatching) { 109 this.watchPath(dirname(file)); 110 } else { 111 // Having multiple FSWatcher's seems to be slower 112 // than a single recursive FSWatcher 113 this.watchPath(file, false); 114 } 115 this.#filteredFiles.add(file); 116 if (owner) { 117 const owners = this.#depencencyOwners.get(file) ?? new SafeSet(); 118 const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet(); 119 owners.add(owner); 120 dependencies.add(file); 121 this.#depencencyOwners.set(file, owners); 122 this.#ownerDependencies.set(owner, dependencies); 123 } 124 } 125 watchChildProcessModules(child, key = null) { 126 if (this.#mode !== 'filter') { 127 return; 128 } 129 child.on('message', (message) => { 130 try { 131 if (ArrayIsArray(message['watch:require'])) { 132 ArrayPrototypeForEach(message['watch:require'], (file) => this.filterFile(file, key)); 133 } 134 if (ArrayIsArray(message['watch:import'])) { 135 ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key)); 136 } 137 } catch { 138 // Failed watching file. ignore 139 } 140 }); 141 } 142 unfilterFilesOwnedBy(owners) { 143 owners.forEach((owner) => { 144 this.#ownerDependencies.get(owner)?.forEach((dependency) => { 145 this.#filteredFiles.delete(dependency); 146 this.#depencencyOwners.delete(dependency); 147 }); 148 this.#filteredFiles.delete(owner); 149 this.#depencencyOwners.delete(owner); 150 this.#ownerDependencies.delete(owner); 151 }); 152 } 153 clearFileFilters() { 154 this.#filteredFiles.clear(); 155 } 156 clear() { 157 this.#watchers.forEach(this.#unwatch); 158 this.#watchers.clear(); 159 this.#filteredFiles.clear(); 160 this.#depencencyOwners.clear(); 161 this.#ownerDependencies.clear(); 162 } 163} 164 165module.exports = { FilesWatcher }; 166