simple-squiggle

A restricted subset of Squiggle
Log | Files | Refs | README

DirectoryWatcher.js (20474B)


      1 /*
      2 	MIT License http://www.opensource.org/licenses/mit-license.php
      3 	Author Tobias Koppers @sokra
      4 */
      5 "use strict";
      6 
      7 const EventEmitter = require("events").EventEmitter;
      8 const fs = require("graceful-fs");
      9 const path = require("path");
     10 
     11 const watchEventSource = require("./watchEventSource");
     12 
     13 const EXISTANCE_ONLY_TIME_ENTRY = Object.freeze({});
     14 
     15 let FS_ACCURACY = 1000;
     16 
     17 const IS_OSX = require("os").platform() === "darwin";
     18 const WATCHPACK_POLLING = process.env.WATCHPACK_POLLING;
     19 const FORCE_POLLING =
     20 	`${+WATCHPACK_POLLING}` === WATCHPACK_POLLING
     21 		? +WATCHPACK_POLLING
     22 		: !!WATCHPACK_POLLING && WATCHPACK_POLLING !== "false";
     23 
     24 function withoutCase(str) {
     25 	return str.toLowerCase();
     26 }
     27 
     28 function needCalls(times, callback) {
     29 	return function() {
     30 		if (--times === 0) {
     31 			return callback();
     32 		}
     33 	};
     34 }
     35 
     36 class Watcher extends EventEmitter {
     37 	constructor(directoryWatcher, filePath, startTime) {
     38 		super();
     39 		this.directoryWatcher = directoryWatcher;
     40 		this.path = filePath;
     41 		this.startTime = startTime && +startTime;
     42 	}
     43 
     44 	checkStartTime(mtime, initial) {
     45 		const startTime = this.startTime;
     46 		if (typeof startTime !== "number") return !initial;
     47 		return startTime <= mtime;
     48 	}
     49 
     50 	close() {
     51 		this.emit("closed");
     52 	}
     53 }
     54 
     55 class DirectoryWatcher extends EventEmitter {
     56 	constructor(watcherManager, directoryPath, options) {
     57 		super();
     58 		if (FORCE_POLLING) {
     59 			options.poll = FORCE_POLLING;
     60 		}
     61 		this.watcherManager = watcherManager;
     62 		this.options = options;
     63 		this.path = directoryPath;
     64 		// safeTime is the point in time after which reading is safe to be unchanged
     65 		// timestamp is a value that should be compared with another timestamp (mtime)
     66 		/** @type {Map<string, { safeTime: number, timestamp: number }} */
     67 		this.files = new Map();
     68 		/** @type {Map<string, number>} */
     69 		this.filesWithoutCase = new Map();
     70 		this.directories = new Map();
     71 		this.lastWatchEvent = 0;
     72 		this.initialScan = true;
     73 		this.ignored = options.ignored || (() => false);
     74 		this.nestedWatching = false;
     75 		this.polledWatching =
     76 			typeof options.poll === "number"
     77 				? options.poll
     78 				: options.poll
     79 				? 5007
     80 				: false;
     81 		this.timeout = undefined;
     82 		this.initialScanRemoved = new Set();
     83 		this.initialScanFinished = undefined;
     84 		/** @type {Map<string, Set<Watcher>>} */
     85 		this.watchers = new Map();
     86 		this.parentWatcher = null;
     87 		this.refs = 0;
     88 		this._activeEvents = new Map();
     89 		this.closed = false;
     90 		this.scanning = false;
     91 		this.scanAgain = false;
     92 		this.scanAgainInitial = false;
     93 
     94 		this.createWatcher();
     95 		this.doScan(true);
     96 	}
     97 
     98 	createWatcher() {
     99 		try {
    100 			if (this.polledWatching) {
    101 				this.watcher = {
    102 					close: () => {
    103 						if (this.timeout) {
    104 							clearTimeout(this.timeout);
    105 							this.timeout = undefined;
    106 						}
    107 					}
    108 				};
    109 			} else {
    110 				if (IS_OSX) {
    111 					this.watchInParentDirectory();
    112 				}
    113 				this.watcher = watchEventSource.watch(this.path);
    114 				this.watcher.on("change", this.onWatchEvent.bind(this));
    115 				this.watcher.on("error", this.onWatcherError.bind(this));
    116 			}
    117 		} catch (err) {
    118 			this.onWatcherError(err);
    119 		}
    120 	}
    121 
    122 	forEachWatcher(path, fn) {
    123 		const watchers = this.watchers.get(withoutCase(path));
    124 		if (watchers !== undefined) {
    125 			for (const w of watchers) {
    126 				fn(w);
    127 			}
    128 		}
    129 	}
    130 
    131 	setMissing(itemPath, initial, type) {
    132 		if (this.initialScan) {
    133 			this.initialScanRemoved.add(itemPath);
    134 		}
    135 
    136 		const oldDirectory = this.directories.get(itemPath);
    137 		if (oldDirectory) {
    138 			if (this.nestedWatching) oldDirectory.close();
    139 			this.directories.delete(itemPath);
    140 
    141 			this.forEachWatcher(itemPath, w => w.emit("remove", type));
    142 			if (!initial) {
    143 				this.forEachWatcher(this.path, w =>
    144 					w.emit("change", itemPath, null, type, initial)
    145 				);
    146 			}
    147 		}
    148 
    149 		const oldFile = this.files.get(itemPath);
    150 		if (oldFile) {
    151 			this.files.delete(itemPath);
    152 			const key = withoutCase(itemPath);
    153 			const count = this.filesWithoutCase.get(key) - 1;
    154 			if (count <= 0) {
    155 				this.filesWithoutCase.delete(key);
    156 				this.forEachWatcher(itemPath, w => w.emit("remove", type));
    157 			} else {
    158 				this.filesWithoutCase.set(key, count);
    159 			}
    160 
    161 			if (!initial) {
    162 				this.forEachWatcher(this.path, w =>
    163 					w.emit("change", itemPath, null, type, initial)
    164 				);
    165 			}
    166 		}
    167 	}
    168 
    169 	setFileTime(filePath, mtime, initial, ignoreWhenEqual, type) {
    170 		const now = Date.now();
    171 
    172 		if (this.ignored(filePath)) return;
    173 
    174 		const old = this.files.get(filePath);
    175 
    176 		let safeTime, accuracy;
    177 		if (initial) {
    178 			safeTime = Math.min(now, mtime) + FS_ACCURACY;
    179 			accuracy = FS_ACCURACY;
    180 		} else {
    181 			safeTime = now;
    182 			accuracy = 0;
    183 
    184 			if (old && old.timestamp === mtime && mtime + FS_ACCURACY < now - 1000) {
    185 				// We are sure that mtime is untouched
    186 				// This can be caused by some file attribute change
    187 				// e. g. when access time has been changed
    188 				// but the file content is untouched
    189 				return;
    190 			}
    191 		}
    192 
    193 		if (ignoreWhenEqual && old && old.timestamp === mtime) return;
    194 
    195 		this.files.set(filePath, {
    196 			safeTime,
    197 			accuracy,
    198 			timestamp: mtime
    199 		});
    200 
    201 		if (!old) {
    202 			const key = withoutCase(filePath);
    203 			const count = this.filesWithoutCase.get(key);
    204 			this.filesWithoutCase.set(key, (count || 0) + 1);
    205 			if (count !== undefined) {
    206 				// There is already a file with case-insensitive-equal name
    207 				// On a case-insensitive filesystem we may miss the renaming
    208 				// when only casing is changed.
    209 				// To be sure that our information is correct
    210 				// we trigger a rescan here
    211 				this.doScan(false);
    212 			}
    213 
    214 			this.forEachWatcher(filePath, w => {
    215 				if (!initial || w.checkStartTime(safeTime, initial)) {
    216 					w.emit("change", mtime, type);
    217 				}
    218 			});
    219 		} else if (!initial) {
    220 			this.forEachWatcher(filePath, w => w.emit("change", mtime, type));
    221 		}
    222 		this.forEachWatcher(this.path, w => {
    223 			if (!initial || w.checkStartTime(safeTime, initial)) {
    224 				w.emit("change", filePath, safeTime, type, initial);
    225 			}
    226 		});
    227 	}
    228 
    229 	setDirectory(directoryPath, birthtime, initial, type) {
    230 		if (this.ignored(directoryPath)) return;
    231 		if (directoryPath === this.path) {
    232 			if (!initial) {
    233 				this.forEachWatcher(this.path, w =>
    234 					w.emit("change", directoryPath, birthtime, type, initial)
    235 				);
    236 			}
    237 		} else {
    238 			const old = this.directories.get(directoryPath);
    239 			if (!old) {
    240 				const now = Date.now();
    241 
    242 				if (this.nestedWatching) {
    243 					this.createNestedWatcher(directoryPath);
    244 				} else {
    245 					this.directories.set(directoryPath, true);
    246 				}
    247 
    248 				let safeTime;
    249 				if (initial) {
    250 					safeTime = Math.min(now, birthtime) + FS_ACCURACY;
    251 				} else {
    252 					safeTime = now;
    253 				}
    254 
    255 				this.forEachWatcher(directoryPath, w => {
    256 					if (!initial || w.checkStartTime(safeTime, false)) {
    257 						w.emit("change", birthtime, type);
    258 					}
    259 				});
    260 				this.forEachWatcher(this.path, w => {
    261 					if (!initial || w.checkStartTime(safeTime, initial)) {
    262 						w.emit("change", directoryPath, safeTime, type, initial);
    263 					}
    264 				});
    265 			}
    266 		}
    267 	}
    268 
    269 	createNestedWatcher(directoryPath) {
    270 		const watcher = this.watcherManager.watchDirectory(directoryPath, 1);
    271 		watcher.on("change", (filePath, mtime, type, initial) => {
    272 			this.forEachWatcher(this.path, w => {
    273 				if (!initial || w.checkStartTime(mtime, initial)) {
    274 					w.emit("change", filePath, mtime, type, initial);
    275 				}
    276 			});
    277 		});
    278 		this.directories.set(directoryPath, watcher);
    279 	}
    280 
    281 	setNestedWatching(flag) {
    282 		if (this.nestedWatching !== !!flag) {
    283 			this.nestedWatching = !!flag;
    284 			if (this.nestedWatching) {
    285 				for (const directory of this.directories.keys()) {
    286 					this.createNestedWatcher(directory);
    287 				}
    288 			} else {
    289 				for (const [directory, watcher] of this.directories) {
    290 					watcher.close();
    291 					this.directories.set(directory, true);
    292 				}
    293 			}
    294 		}
    295 	}
    296 
    297 	watch(filePath, startTime) {
    298 		const key = withoutCase(filePath);
    299 		let watchers = this.watchers.get(key);
    300 		if (watchers === undefined) {
    301 			watchers = new Set();
    302 			this.watchers.set(key, watchers);
    303 		}
    304 		this.refs++;
    305 		const watcher = new Watcher(this, filePath, startTime);
    306 		watcher.on("closed", () => {
    307 			if (--this.refs <= 0) {
    308 				this.close();
    309 				return;
    310 			}
    311 			watchers.delete(watcher);
    312 			if (watchers.size === 0) {
    313 				this.watchers.delete(key);
    314 				if (this.path === filePath) this.setNestedWatching(false);
    315 			}
    316 		});
    317 		watchers.add(watcher);
    318 		let safeTime;
    319 		if (filePath === this.path) {
    320 			this.setNestedWatching(true);
    321 			safeTime = this.lastWatchEvent;
    322 			for (const entry of this.files.values()) {
    323 				fixupEntryAccuracy(entry);
    324 				safeTime = Math.max(safeTime, entry.safeTime);
    325 			}
    326 		} else {
    327 			const entry = this.files.get(filePath);
    328 			if (entry) {
    329 				fixupEntryAccuracy(entry);
    330 				safeTime = entry.safeTime;
    331 			} else {
    332 				safeTime = 0;
    333 			}
    334 		}
    335 		if (safeTime) {
    336 			if (safeTime >= startTime) {
    337 				process.nextTick(() => {
    338 					if (this.closed) return;
    339 					if (filePath === this.path) {
    340 						watcher.emit(
    341 							"change",
    342 							filePath,
    343 							safeTime,
    344 							"watch (outdated on attach)",
    345 							true
    346 						);
    347 					} else {
    348 						watcher.emit(
    349 							"change",
    350 							safeTime,
    351 							"watch (outdated on attach)",
    352 							true
    353 						);
    354 					}
    355 				});
    356 			}
    357 		} else if (this.initialScan) {
    358 			if (this.initialScanRemoved.has(filePath)) {
    359 				process.nextTick(() => {
    360 					if (this.closed) return;
    361 					watcher.emit("remove");
    362 				});
    363 			}
    364 		} else if (
    365 			!this.directories.has(filePath) &&
    366 			watcher.checkStartTime(this.initialScanFinished, false)
    367 		) {
    368 			process.nextTick(() => {
    369 				if (this.closed) return;
    370 				watcher.emit("initial-missing", "watch (missing on attach)");
    371 			});
    372 		}
    373 		return watcher;
    374 	}
    375 
    376 	onWatchEvent(eventType, filename) {
    377 		if (this.closed) return;
    378 		if (!filename) {
    379 			// In some cases no filename is provided
    380 			// This seem to happen on windows
    381 			// So some event happened but we don't know which file is affected
    382 			// We have to do a full scan of the directory
    383 			this.doScan(false);
    384 			return;
    385 		}
    386 
    387 		const filePath = path.join(this.path, filename);
    388 		if (this.ignored(filePath)) return;
    389 
    390 		if (this._activeEvents.get(filename) === undefined) {
    391 			this._activeEvents.set(filename, false);
    392 			const checkStats = () => {
    393 				if (this.closed) return;
    394 				this._activeEvents.set(filename, false);
    395 				fs.lstat(filePath, (err, stats) => {
    396 					if (this.closed) return;
    397 					if (this._activeEvents.get(filename) === true) {
    398 						process.nextTick(checkStats);
    399 						return;
    400 					}
    401 					this._activeEvents.delete(filename);
    402 					// ENOENT happens when the file/directory doesn't exist
    403 					// EPERM happens when the containing directory doesn't exist
    404 					if (err) {
    405 						if (
    406 							err.code !== "ENOENT" &&
    407 							err.code !== "EPERM" &&
    408 							err.code !== "EBUSY"
    409 						) {
    410 							this.onStatsError(err);
    411 						} else {
    412 							if (filename === path.basename(this.path)) {
    413 								// This may indicate that the directory itself was removed
    414 								if (!fs.existsSync(this.path)) {
    415 									this.onDirectoryRemoved("stat failed");
    416 								}
    417 							}
    418 						}
    419 					}
    420 					this.lastWatchEvent = Date.now();
    421 					if (!stats) {
    422 						this.setMissing(filePath, false, eventType);
    423 					} else if (stats.isDirectory()) {
    424 						this.setDirectory(
    425 							filePath,
    426 							+stats.birthtime || 1,
    427 							false,
    428 							eventType
    429 						);
    430 					} else if (stats.isFile() || stats.isSymbolicLink()) {
    431 						if (stats.mtime) {
    432 							ensureFsAccuracy(stats.mtime);
    433 						}
    434 						this.setFileTime(
    435 							filePath,
    436 							+stats.mtime || +stats.ctime || 1,
    437 							false,
    438 							false,
    439 							eventType
    440 						);
    441 					}
    442 				});
    443 			};
    444 			process.nextTick(checkStats);
    445 		} else {
    446 			this._activeEvents.set(filename, true);
    447 		}
    448 	}
    449 
    450 	onWatcherError(err) {
    451 		if (this.closed) return;
    452 		if (err) {
    453 			if (err.code !== "EPERM" && err.code !== "ENOENT") {
    454 				console.error("Watchpack Error (watcher): " + err);
    455 			}
    456 			this.onDirectoryRemoved("watch error");
    457 		}
    458 	}
    459 
    460 	onStatsError(err) {
    461 		if (err) {
    462 			console.error("Watchpack Error (stats): " + err);
    463 		}
    464 	}
    465 
    466 	onScanError(err) {
    467 		if (err) {
    468 			console.error("Watchpack Error (initial scan): " + err);
    469 		}
    470 		this.onScanFinished();
    471 	}
    472 
    473 	onScanFinished() {
    474 		if (this.polledWatching) {
    475 			this.timeout = setTimeout(() => {
    476 				if (this.closed) return;
    477 				this.doScan(false);
    478 			}, this.polledWatching);
    479 		}
    480 	}
    481 
    482 	onDirectoryRemoved(reason) {
    483 		if (this.watcher) {
    484 			this.watcher.close();
    485 			this.watcher = null;
    486 		}
    487 		this.watchInParentDirectory();
    488 		const type = `directory-removed (${reason})`;
    489 		for (const directory of this.directories.keys()) {
    490 			this.setMissing(directory, null, type);
    491 		}
    492 		for (const file of this.files.keys()) {
    493 			this.setMissing(file, null, type);
    494 		}
    495 	}
    496 
    497 	watchInParentDirectory() {
    498 		if (!this.parentWatcher) {
    499 			const parentDir = path.dirname(this.path);
    500 			// avoid watching in the root directory
    501 			// removing directories in the root directory is not supported
    502 			if (path.dirname(parentDir) === parentDir) return;
    503 
    504 			this.parentWatcher = this.watcherManager.watchFile(this.path, 1);
    505 			this.parentWatcher.on("change", (mtime, type) => {
    506 				if (this.closed) return;
    507 
    508 				// On non-osx platforms we don't need this watcher to detect
    509 				// directory removal, as an EPERM error indicates that
    510 				if ((!IS_OSX || this.polledWatching) && this.parentWatcher) {
    511 					this.parentWatcher.close();
    512 					this.parentWatcher = null;
    513 				}
    514 				// Try to create the watcher when parent directory is found
    515 				if (!this.watcher) {
    516 					this.createWatcher();
    517 					this.doScan(false);
    518 
    519 					// directory was created so we emit an event
    520 					this.forEachWatcher(this.path, w =>
    521 						w.emit("change", this.path, mtime, type, false)
    522 					);
    523 				}
    524 			});
    525 			this.parentWatcher.on("remove", () => {
    526 				this.onDirectoryRemoved("parent directory removed");
    527 			});
    528 		}
    529 	}
    530 
    531 	doScan(initial) {
    532 		if (this.scanning) {
    533 			if (this.scanAgain) {
    534 				if (!initial) this.scanAgainInitial = false;
    535 			} else {
    536 				this.scanAgain = true;
    537 				this.scanAgainInitial = initial;
    538 			}
    539 			return;
    540 		}
    541 		this.scanning = true;
    542 		if (this.timeout) {
    543 			clearTimeout(this.timeout);
    544 			this.timeout = undefined;
    545 		}
    546 		process.nextTick(() => {
    547 			if (this.closed) return;
    548 			fs.readdir(this.path, (err, items) => {
    549 				if (this.closed) return;
    550 				if (err) {
    551 					if (err.code === "ENOENT" || err.code === "EPERM") {
    552 						this.onDirectoryRemoved("scan readdir failed");
    553 					} else {
    554 						this.onScanError(err);
    555 					}
    556 					this.initialScan = false;
    557 					this.initialScanFinished = Date.now();
    558 					if (initial) {
    559 						for (const watchers of this.watchers.values()) {
    560 							for (const watcher of watchers) {
    561 								if (watcher.checkStartTime(this.initialScanFinished, false)) {
    562 									watcher.emit(
    563 										"initial-missing",
    564 										"scan (parent directory missing in initial scan)"
    565 									);
    566 								}
    567 							}
    568 						}
    569 					}
    570 					if (this.scanAgain) {
    571 						this.scanAgain = false;
    572 						this.doScan(this.scanAgainInitial);
    573 					} else {
    574 						this.scanning = false;
    575 					}
    576 					return;
    577 				}
    578 				const itemPaths = new Set(
    579 					items.map(item => path.join(this.path, item.normalize("NFC")))
    580 				);
    581 				for (const file of this.files.keys()) {
    582 					if (!itemPaths.has(file)) {
    583 						this.setMissing(file, initial, "scan (missing)");
    584 					}
    585 				}
    586 				for (const directory of this.directories.keys()) {
    587 					if (!itemPaths.has(directory)) {
    588 						this.setMissing(directory, initial, "scan (missing)");
    589 					}
    590 				}
    591 				if (this.scanAgain) {
    592 					// Early repeat of scan
    593 					this.scanAgain = false;
    594 					this.doScan(initial);
    595 					return;
    596 				}
    597 				const itemFinished = needCalls(itemPaths.size + 1, () => {
    598 					if (this.closed) return;
    599 					this.initialScan = false;
    600 					this.initialScanRemoved = null;
    601 					this.initialScanFinished = Date.now();
    602 					if (initial) {
    603 						const missingWatchers = new Map(this.watchers);
    604 						missingWatchers.delete(withoutCase(this.path));
    605 						for (const item of itemPaths) {
    606 							missingWatchers.delete(withoutCase(item));
    607 						}
    608 						for (const watchers of missingWatchers.values()) {
    609 							for (const watcher of watchers) {
    610 								if (watcher.checkStartTime(this.initialScanFinished, false)) {
    611 									watcher.emit(
    612 										"initial-missing",
    613 										"scan (missing in initial scan)"
    614 									);
    615 								}
    616 							}
    617 						}
    618 					}
    619 					if (this.scanAgain) {
    620 						this.scanAgain = false;
    621 						this.doScan(this.scanAgainInitial);
    622 					} else {
    623 						this.scanning = false;
    624 						this.onScanFinished();
    625 					}
    626 				});
    627 				for (const itemPath of itemPaths) {
    628 					fs.lstat(itemPath, (err2, stats) => {
    629 						if (this.closed) return;
    630 						if (err2) {
    631 							if (
    632 								err2.code === "ENOENT" ||
    633 								err2.code === "EPERM" ||
    634 								err2.code === "EACCES" ||
    635 								err2.code === "EBUSY"
    636 							) {
    637 								this.setMissing(itemPath, initial, "scan (" + err2.code + ")");
    638 							} else {
    639 								this.onScanError(err2);
    640 							}
    641 							itemFinished();
    642 							return;
    643 						}
    644 						if (stats.isFile() || stats.isSymbolicLink()) {
    645 							if (stats.mtime) {
    646 								ensureFsAccuracy(stats.mtime);
    647 							}
    648 							this.setFileTime(
    649 								itemPath,
    650 								+stats.mtime || +stats.ctime || 1,
    651 								initial,
    652 								true,
    653 								"scan (file)"
    654 							);
    655 						} else if (stats.isDirectory()) {
    656 							if (!initial || !this.directories.has(itemPath))
    657 								this.setDirectory(
    658 									itemPath,
    659 									+stats.birthtime || 1,
    660 									initial,
    661 									"scan (dir)"
    662 								);
    663 						}
    664 						itemFinished();
    665 					});
    666 				}
    667 				itemFinished();
    668 			});
    669 		});
    670 	}
    671 
    672 	getTimes() {
    673 		const obj = Object.create(null);
    674 		let safeTime = this.lastWatchEvent;
    675 		for (const [file, entry] of this.files) {
    676 			fixupEntryAccuracy(entry);
    677 			safeTime = Math.max(safeTime, entry.safeTime);
    678 			obj[file] = Math.max(entry.safeTime, entry.timestamp);
    679 		}
    680 		if (this.nestedWatching) {
    681 			for (const w of this.directories.values()) {
    682 				const times = w.directoryWatcher.getTimes();
    683 				for (const file of Object.keys(times)) {
    684 					const time = times[file];
    685 					safeTime = Math.max(safeTime, time);
    686 					obj[file] = time;
    687 				}
    688 			}
    689 			obj[this.path] = safeTime;
    690 		}
    691 		if (!this.initialScan) {
    692 			for (const watchers of this.watchers.values()) {
    693 				for (const watcher of watchers) {
    694 					const path = watcher.path;
    695 					if (!Object.prototype.hasOwnProperty.call(obj, path)) {
    696 						obj[path] = null;
    697 					}
    698 				}
    699 			}
    700 		}
    701 		return obj;
    702 	}
    703 
    704 	collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
    705 		let safeTime = this.lastWatchEvent;
    706 		for (const [file, entry] of this.files) {
    707 			fixupEntryAccuracy(entry);
    708 			safeTime = Math.max(safeTime, entry.safeTime);
    709 			fileTimestamps.set(file, entry);
    710 		}
    711 		if (this.nestedWatching) {
    712 			for (const w of this.directories.values()) {
    713 				safeTime = Math.max(
    714 					safeTime,
    715 					w.directoryWatcher.collectTimeInfoEntries(
    716 						fileTimestamps,
    717 						directoryTimestamps
    718 					)
    719 				);
    720 			}
    721 			fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
    722 			directoryTimestamps.set(this.path, {
    723 				safeTime
    724 			});
    725 		} else {
    726 			for (const dir of this.directories.keys()) {
    727 				// No additional info about this directory
    728 				// but maybe another DirectoryWatcher has info
    729 				fileTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
    730 				if (!directoryTimestamps.has(dir))
    731 					directoryTimestamps.set(dir, EXISTANCE_ONLY_TIME_ENTRY);
    732 			}
    733 			fileTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
    734 			directoryTimestamps.set(this.path, EXISTANCE_ONLY_TIME_ENTRY);
    735 		}
    736 		if (!this.initialScan) {
    737 			for (const watchers of this.watchers.values()) {
    738 				for (const watcher of watchers) {
    739 					const path = watcher.path;
    740 					if (!fileTimestamps.has(path)) {
    741 						fileTimestamps.set(path, null);
    742 					}
    743 				}
    744 			}
    745 		}
    746 		return safeTime;
    747 	}
    748 
    749 	close() {
    750 		this.closed = true;
    751 		this.initialScan = false;
    752 		if (this.watcher) {
    753 			this.watcher.close();
    754 			this.watcher = null;
    755 		}
    756 		if (this.nestedWatching) {
    757 			for (const w of this.directories.values()) {
    758 				w.close();
    759 			}
    760 			this.directories.clear();
    761 		}
    762 		if (this.parentWatcher) {
    763 			this.parentWatcher.close();
    764 			this.parentWatcher = null;
    765 		}
    766 		this.emit("closed");
    767 	}
    768 }
    769 
    770 module.exports = DirectoryWatcher;
    771 module.exports.EXISTANCE_ONLY_TIME_ENTRY = EXISTANCE_ONLY_TIME_ENTRY;
    772 
    773 function fixupEntryAccuracy(entry) {
    774 	if (entry.accuracy > FS_ACCURACY) {
    775 		entry.safeTime = entry.safeTime - entry.accuracy + FS_ACCURACY;
    776 		entry.accuracy = FS_ACCURACY;
    777 	}
    778 }
    779 
    780 function ensureFsAccuracy(mtime) {
    781 	if (!mtime) return;
    782 	if (FS_ACCURACY > 1 && mtime % 1 !== 0) FS_ACCURACY = 1;
    783 	else if (FS_ACCURACY > 10 && mtime % 10 !== 0) FS_ACCURACY = 10;
    784 	else if (FS_ACCURACY > 100 && mtime % 100 !== 0) FS_ACCURACY = 100;
    785 }