watchEventSource.js (8546B)
1 /* 2 MIT License http://www.opensource.org/licenses/mit-license.php 3 Author Tobias Koppers @sokra 4 */ 5 "use strict"; 6 7 const fs = require("fs"); 8 const path = require("path"); 9 const { EventEmitter } = require("events"); 10 const reducePlan = require("./reducePlan"); 11 12 const IS_OSX = require("os").platform() === "darwin"; 13 const IS_WIN = require("os").platform() === "win32"; 14 const SUPPORTS_RECURSIVE_WATCHING = IS_OSX || IS_WIN; 15 16 const watcherLimit = 17 +process.env.WATCHPACK_WATCHER_LIMIT || (IS_OSX ? 2000 : 10000); 18 19 const recursiveWatcherLogging = !!process.env 20 .WATCHPACK_RECURSIVE_WATCHER_LOGGING; 21 22 let isBatch = false; 23 let watcherCount = 0; 24 25 /** @type {Map<Watcher, string>} */ 26 const pendingWatchers = new Map(); 27 28 /** @type {Map<string, RecursiveWatcher>} */ 29 const recursiveWatchers = new Map(); 30 31 /** @type {Map<string, DirectWatcher>} */ 32 const directWatchers = new Map(); 33 34 /** @type {Map<Watcher, RecursiveWatcher | DirectWatcher>} */ 35 const underlyingWatcher = new Map(); 36 37 class DirectWatcher { 38 constructor(filePath) { 39 this.filePath = filePath; 40 this.watchers = new Set(); 41 this.watcher = undefined; 42 try { 43 const watcher = fs.watch(filePath); 44 this.watcher = watcher; 45 watcher.on("change", (type, filename) => { 46 for (const w of this.watchers) { 47 w.emit("change", type, filename); 48 } 49 }); 50 watcher.on("error", error => { 51 for (const w of this.watchers) { 52 w.emit("error", error); 53 } 54 }); 55 } catch (err) { 56 process.nextTick(() => { 57 for (const w of this.watchers) { 58 w.emit("error", err); 59 } 60 }); 61 } 62 watcherCount++; 63 } 64 65 add(watcher) { 66 underlyingWatcher.set(watcher, this); 67 this.watchers.add(watcher); 68 } 69 70 remove(watcher) { 71 this.watchers.delete(watcher); 72 if (this.watchers.size === 0) { 73 directWatchers.delete(this.filePath); 74 watcherCount--; 75 if (this.watcher) this.watcher.close(); 76 } 77 } 78 79 getWatchers() { 80 return this.watchers; 81 } 82 } 83 84 class RecursiveWatcher { 85 constructor(rootPath) { 86 this.rootPath = rootPath; 87 /** @type {Map<Watcher, string>} */ 88 this.mapWatcherToPath = new Map(); 89 /** @type {Map<string, Set<Watcher>>} */ 90 this.mapPathToWatchers = new Map(); 91 this.watcher = undefined; 92 try { 93 const watcher = fs.watch(rootPath, { 94 recursive: true 95 }); 96 this.watcher = watcher; 97 watcher.on("change", (type, filename) => { 98 if (!filename) { 99 if (recursiveWatcherLogging) { 100 process.stderr.write( 101 `[watchpack] dispatch ${type} event in recursive watcher (${ 102 this.rootPath 103 }) to all watchers\n` 104 ); 105 } 106 for (const w of this.mapWatcherToPath.keys()) { 107 w.emit("change", type); 108 } 109 } else { 110 const dir = path.dirname(filename); 111 const watchers = this.mapPathToWatchers.get(dir); 112 if (recursiveWatcherLogging) { 113 process.stderr.write( 114 `[watchpack] dispatch ${type} event in recursive watcher (${ 115 this.rootPath 116 }) for '${filename}' to ${ 117 watchers ? watchers.size : 0 118 } watchers\n` 119 ); 120 } 121 if (watchers === undefined) return; 122 for (const w of watchers) { 123 w.emit("change", type, path.basename(filename)); 124 } 125 } 126 }); 127 watcher.on("error", error => { 128 for (const w of this.mapWatcherToPath.keys()) { 129 w.emit("error", error); 130 } 131 }); 132 } catch (err) { 133 process.nextTick(() => { 134 for (const w of this.mapWatcherToPath.keys()) { 135 w.emit("error", err); 136 } 137 }); 138 } 139 watcherCount++; 140 if (recursiveWatcherLogging) { 141 process.stderr.write( 142 `[watchpack] created recursive watcher at ${rootPath}\n` 143 ); 144 } 145 } 146 147 add(filePath, watcher) { 148 underlyingWatcher.set(watcher, this); 149 const subpath = filePath.slice(this.rootPath.length + 1) || "."; 150 this.mapWatcherToPath.set(watcher, subpath); 151 const set = this.mapPathToWatchers.get(subpath); 152 if (set === undefined) { 153 const newSet = new Set(); 154 newSet.add(watcher); 155 this.mapPathToWatchers.set(subpath, newSet); 156 } else { 157 set.add(watcher); 158 } 159 } 160 161 remove(watcher) { 162 const subpath = this.mapWatcherToPath.get(watcher); 163 if (!subpath) return; 164 this.mapWatcherToPath.delete(watcher); 165 const set = this.mapPathToWatchers.get(subpath); 166 set.delete(watcher); 167 if (set.size === 0) { 168 this.mapPathToWatchers.delete(subpath); 169 } 170 if (this.mapWatcherToPath.size === 0) { 171 recursiveWatchers.delete(this.rootPath); 172 watcherCount--; 173 if (this.watcher) this.watcher.close(); 174 if (recursiveWatcherLogging) { 175 process.stderr.write( 176 `[watchpack] closed recursive watcher at ${this.rootPath}\n` 177 ); 178 } 179 } 180 } 181 182 getWatchers() { 183 return this.mapWatcherToPath; 184 } 185 } 186 187 class Watcher extends EventEmitter { 188 close() { 189 if (pendingWatchers.has(this)) { 190 pendingWatchers.delete(this); 191 return; 192 } 193 const watcher = underlyingWatcher.get(this); 194 watcher.remove(this); 195 underlyingWatcher.delete(this); 196 } 197 } 198 199 const createDirectWatcher = filePath => { 200 const existing = directWatchers.get(filePath); 201 if (existing !== undefined) return existing; 202 const w = new DirectWatcher(filePath); 203 directWatchers.set(filePath, w); 204 return w; 205 }; 206 207 const createRecursiveWatcher = rootPath => { 208 const existing = recursiveWatchers.get(rootPath); 209 if (existing !== undefined) return existing; 210 const w = new RecursiveWatcher(rootPath); 211 recursiveWatchers.set(rootPath, w); 212 return w; 213 }; 214 215 const execute = () => { 216 /** @type {Map<string, Watcher[] | Watcher>} */ 217 const map = new Map(); 218 const addWatcher = (watcher, filePath) => { 219 const entry = map.get(filePath); 220 if (entry === undefined) { 221 map.set(filePath, watcher); 222 } else if (Array.isArray(entry)) { 223 entry.push(watcher); 224 } else { 225 map.set(filePath, [entry, watcher]); 226 } 227 }; 228 for (const [watcher, filePath] of pendingWatchers) { 229 addWatcher(watcher, filePath); 230 } 231 pendingWatchers.clear(); 232 233 // Fast case when we are not reaching the limit 234 if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) { 235 // Create watchers for all entries in the map 236 for (const [filePath, entry] of map) { 237 const w = createDirectWatcher(filePath); 238 if (Array.isArray(entry)) { 239 for (const item of entry) w.add(item); 240 } else { 241 w.add(entry); 242 } 243 } 244 return; 245 } 246 247 // Reconsider existing watchers to improving watch plan 248 for (const watcher of recursiveWatchers.values()) { 249 for (const [w, subpath] of watcher.getWatchers()) { 250 addWatcher(w, path.join(watcher.rootPath, subpath)); 251 } 252 } 253 for (const watcher of directWatchers.values()) { 254 for (const w of watcher.getWatchers()) { 255 addWatcher(w, watcher.filePath); 256 } 257 } 258 259 // Merge map entries to keep watcher limit 260 // Create a 10% buffer to be able to enter fast case more often 261 const plan = reducePlan(map, watcherLimit * 0.9); 262 263 // Update watchers for all entries in the map 264 for (const [filePath, entry] of plan) { 265 if (entry.size === 1) { 266 for (const [watcher, filePath] of entry) { 267 const w = createDirectWatcher(filePath); 268 const old = underlyingWatcher.get(watcher); 269 if (old === w) continue; 270 w.add(watcher); 271 if (old !== undefined) old.remove(watcher); 272 } 273 } else { 274 const filePaths = new Set(entry.values()); 275 if (filePaths.size > 1) { 276 const w = createRecursiveWatcher(filePath); 277 for (const [watcher, watcherPath] of entry) { 278 const old = underlyingWatcher.get(watcher); 279 if (old === w) continue; 280 w.add(watcherPath, watcher); 281 if (old !== undefined) old.remove(watcher); 282 } 283 } else { 284 for (const filePath of filePaths) { 285 const w = createDirectWatcher(filePath); 286 for (const watcher of entry.keys()) { 287 const old = underlyingWatcher.get(watcher); 288 if (old === w) continue; 289 w.add(watcher); 290 if (old !== undefined) old.remove(watcher); 291 } 292 } 293 } 294 } 295 } 296 }; 297 298 exports.watch = filePath => { 299 const watcher = new Watcher(); 300 // Find an existing watcher 301 const directWatcher = directWatchers.get(filePath); 302 if (directWatcher !== undefined) { 303 directWatcher.add(watcher); 304 return watcher; 305 } 306 let current = filePath; 307 for (;;) { 308 const recursiveWatcher = recursiveWatchers.get(current); 309 if (recursiveWatcher !== undefined) { 310 recursiveWatcher.add(filePath, watcher); 311 return watcher; 312 } 313 const parent = path.dirname(current); 314 if (parent === current) break; 315 current = parent; 316 } 317 // Queue up watcher for creation 318 pendingWatchers.set(watcher, filePath); 319 if (!isBatch) execute(); 320 return watcher; 321 }; 322 323 exports.batch = fn => { 324 isBatch = true; 325 try { 326 fn(); 327 } finally { 328 isBatch = false; 329 execute(); 330 } 331 }; 332 333 exports.getNumberOfWatchers = () => { 334 return watcherCount; 335 };