simple-squiggle

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

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 };