simple-squiggle

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

watchpack.js (10390B)


      1 /*
      2 	MIT License http://www.opensource.org/licenses/mit-license.php
      3 	Author Tobias Koppers @sokra
      4 */
      5 "use strict";
      6 
      7 const getWatcherManager = require("./getWatcherManager");
      8 const LinkResolver = require("./LinkResolver");
      9 const EventEmitter = require("events").EventEmitter;
     10 const globToRegExp = require("glob-to-regexp");
     11 const watchEventSource = require("./watchEventSource");
     12 
     13 const EMPTY_ARRAY = [];
     14 const EMPTY_OPTIONS = {};
     15 
     16 function addWatchersToSet(watchers, set) {
     17 	for (const ww of watchers) {
     18 		const w = ww.watcher;
     19 		if (!set.has(w.directoryWatcher)) {
     20 			set.add(w.directoryWatcher);
     21 		}
     22 	}
     23 }
     24 
     25 const stringToRegexp = ignored => {
     26 	const source = globToRegExp(ignored, { globstar: true, extended: true })
     27 		.source;
     28 	const matchingStart = source.slice(0, source.length - 1) + "(?:$|\\/)";
     29 	return matchingStart;
     30 };
     31 
     32 const ignoredToFunction = ignored => {
     33 	if (Array.isArray(ignored)) {
     34 		const regexp = new RegExp(ignored.map(i => stringToRegexp(i)).join("|"));
     35 		return x => regexp.test(x.replace(/\\/g, "/"));
     36 	} else if (typeof ignored === "string") {
     37 		const regexp = new RegExp(stringToRegexp(ignored));
     38 		return x => regexp.test(x.replace(/\\/g, "/"));
     39 	} else if (ignored instanceof RegExp) {
     40 		return x => ignored.test(x.replace(/\\/g, "/"));
     41 	} else if (ignored instanceof Function) {
     42 		return ignored;
     43 	} else if (ignored) {
     44 		throw new Error(`Invalid option for 'ignored': ${ignored}`);
     45 	} else {
     46 		return () => false;
     47 	}
     48 };
     49 
     50 const normalizeOptions = options => {
     51 	return {
     52 		followSymlinks: !!options.followSymlinks,
     53 		ignored: ignoredToFunction(options.ignored),
     54 		poll: options.poll
     55 	};
     56 };
     57 
     58 const normalizeCache = new WeakMap();
     59 const cachedNormalizeOptions = options => {
     60 	const cacheEntry = normalizeCache.get(options);
     61 	if (cacheEntry !== undefined) return cacheEntry;
     62 	const normalized = normalizeOptions(options);
     63 	normalizeCache.set(options, normalized);
     64 	return normalized;
     65 };
     66 
     67 class WatchpackFileWatcher {
     68 	constructor(watchpack, watcher, files) {
     69 		this.files = Array.isArray(files) ? files : [files];
     70 		this.watcher = watcher;
     71 		watcher.on("initial-missing", type => {
     72 			for (const file of this.files) {
     73 				if (!watchpack._missing.has(file))
     74 					watchpack._onRemove(file, file, type);
     75 			}
     76 		});
     77 		watcher.on("change", (mtime, type) => {
     78 			for (const file of this.files) {
     79 				watchpack._onChange(file, mtime, file, type);
     80 			}
     81 		});
     82 		watcher.on("remove", type => {
     83 			for (const file of this.files) {
     84 				watchpack._onRemove(file, file, type);
     85 			}
     86 		});
     87 	}
     88 
     89 	update(files) {
     90 		if (!Array.isArray(files)) {
     91 			if (this.files.length !== 1) {
     92 				this.files = [files];
     93 			} else if (this.files[0] !== files) {
     94 				this.files[0] = files;
     95 			}
     96 		} else {
     97 			this.files = files;
     98 		}
     99 	}
    100 
    101 	close() {
    102 		this.watcher.close();
    103 	}
    104 }
    105 
    106 class WatchpackDirectoryWatcher {
    107 	constructor(watchpack, watcher, directories) {
    108 		this.directories = Array.isArray(directories) ? directories : [directories];
    109 		this.watcher = watcher;
    110 		watcher.on("initial-missing", type => {
    111 			for (const item of this.directories) {
    112 				watchpack._onRemove(item, item, type);
    113 			}
    114 		});
    115 		watcher.on("change", (file, mtime, type) => {
    116 			for (const item of this.directories) {
    117 				watchpack._onChange(item, mtime, file, type);
    118 			}
    119 		});
    120 		watcher.on("remove", type => {
    121 			for (const item of this.directories) {
    122 				watchpack._onRemove(item, item, type);
    123 			}
    124 		});
    125 	}
    126 
    127 	update(directories) {
    128 		if (!Array.isArray(directories)) {
    129 			if (this.directories.length !== 1) {
    130 				this.directories = [directories];
    131 			} else if (this.directories[0] !== directories) {
    132 				this.directories[0] = directories;
    133 			}
    134 		} else {
    135 			this.directories = directories;
    136 		}
    137 	}
    138 
    139 	close() {
    140 		this.watcher.close();
    141 	}
    142 }
    143 
    144 class Watchpack extends EventEmitter {
    145 	constructor(options) {
    146 		super();
    147 		if (!options) options = EMPTY_OPTIONS;
    148 		this.options = options;
    149 		this.aggregateTimeout =
    150 			typeof options.aggregateTimeout === "number"
    151 				? options.aggregateTimeout
    152 				: 200;
    153 		this.watcherOptions = cachedNormalizeOptions(options);
    154 		this.watcherManager = getWatcherManager(this.watcherOptions);
    155 		this.fileWatchers = new Map();
    156 		this.directoryWatchers = new Map();
    157 		this._missing = new Set();
    158 		this.startTime = undefined;
    159 		this.paused = false;
    160 		this.aggregatedChanges = new Set();
    161 		this.aggregatedRemovals = new Set();
    162 		this.aggregateTimer = undefined;
    163 		this._onTimeout = this._onTimeout.bind(this);
    164 	}
    165 
    166 	watch(arg1, arg2, arg3) {
    167 		let files, directories, missing, startTime;
    168 		if (!arg2) {
    169 			({
    170 				files = EMPTY_ARRAY,
    171 				directories = EMPTY_ARRAY,
    172 				missing = EMPTY_ARRAY,
    173 				startTime
    174 			} = arg1);
    175 		} else {
    176 			files = arg1;
    177 			directories = arg2;
    178 			missing = EMPTY_ARRAY;
    179 			startTime = arg3;
    180 		}
    181 		this.paused = false;
    182 		const fileWatchers = this.fileWatchers;
    183 		const directoryWatchers = this.directoryWatchers;
    184 		const ignored = this.watcherOptions.ignored;
    185 		const filter = path => !ignored(path);
    186 		const addToMap = (map, key, item) => {
    187 			const list = map.get(key);
    188 			if (list === undefined) {
    189 				map.set(key, item);
    190 			} else if (Array.isArray(list)) {
    191 				list.push(item);
    192 			} else {
    193 				map.set(key, [list, item]);
    194 			}
    195 		};
    196 		const fileWatchersNeeded = new Map();
    197 		const directoryWatchersNeeded = new Map();
    198 		const missingFiles = new Set();
    199 		if (this.watcherOptions.followSymlinks) {
    200 			const resolver = new LinkResolver();
    201 			for (const file of files) {
    202 				if (filter(file)) {
    203 					for (const innerFile of resolver.resolve(file)) {
    204 						if (file === innerFile || filter(innerFile)) {
    205 							addToMap(fileWatchersNeeded, innerFile, file);
    206 						}
    207 					}
    208 				}
    209 			}
    210 			for (const file of missing) {
    211 				if (filter(file)) {
    212 					for (const innerFile of resolver.resolve(file)) {
    213 						if (file === innerFile || filter(innerFile)) {
    214 							missingFiles.add(file);
    215 							addToMap(fileWatchersNeeded, innerFile, file);
    216 						}
    217 					}
    218 				}
    219 			}
    220 			for (const dir of directories) {
    221 				if (filter(dir)) {
    222 					let first = true;
    223 					for (const innerItem of resolver.resolve(dir)) {
    224 						if (filter(innerItem)) {
    225 							addToMap(
    226 								first ? directoryWatchersNeeded : fileWatchersNeeded,
    227 								innerItem,
    228 								dir
    229 							);
    230 						}
    231 						first = false;
    232 					}
    233 				}
    234 			}
    235 		} else {
    236 			for (const file of files) {
    237 				if (filter(file)) {
    238 					addToMap(fileWatchersNeeded, file, file);
    239 				}
    240 			}
    241 			for (const file of missing) {
    242 				if (filter(file)) {
    243 					missingFiles.add(file);
    244 					addToMap(fileWatchersNeeded, file, file);
    245 				}
    246 			}
    247 			for (const dir of directories) {
    248 				if (filter(dir)) {
    249 					addToMap(directoryWatchersNeeded, dir, dir);
    250 				}
    251 			}
    252 		}
    253 		// Close unneeded old watchers
    254 		// and update existing watchers
    255 		for (const [key, w] of fileWatchers) {
    256 			const needed = fileWatchersNeeded.get(key);
    257 			if (needed === undefined) {
    258 				w.close();
    259 				fileWatchers.delete(key);
    260 			} else {
    261 				w.update(needed);
    262 				fileWatchersNeeded.delete(key);
    263 			}
    264 		}
    265 		for (const [key, w] of directoryWatchers) {
    266 			const needed = directoryWatchersNeeded.get(key);
    267 			if (needed === undefined) {
    268 				w.close();
    269 				directoryWatchers.delete(key);
    270 			} else {
    271 				w.update(needed);
    272 				directoryWatchersNeeded.delete(key);
    273 			}
    274 		}
    275 		// Create new watchers and install handlers on these watchers
    276 		watchEventSource.batch(() => {
    277 			for (const [key, files] of fileWatchersNeeded) {
    278 				const watcher = this.watcherManager.watchFile(key, startTime);
    279 				if (watcher) {
    280 					fileWatchers.set(key, new WatchpackFileWatcher(this, watcher, files));
    281 				}
    282 			}
    283 			for (const [key, directories] of directoryWatchersNeeded) {
    284 				const watcher = this.watcherManager.watchDirectory(key, startTime);
    285 				if (watcher) {
    286 					directoryWatchers.set(
    287 						key,
    288 						new WatchpackDirectoryWatcher(this, watcher, directories)
    289 					);
    290 				}
    291 			}
    292 		});
    293 		this._missing = missingFiles;
    294 		this.startTime = startTime;
    295 	}
    296 
    297 	close() {
    298 		this.paused = true;
    299 		if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
    300 		for (const w of this.fileWatchers.values()) w.close();
    301 		for (const w of this.directoryWatchers.values()) w.close();
    302 		this.fileWatchers.clear();
    303 		this.directoryWatchers.clear();
    304 	}
    305 
    306 	pause() {
    307 		this.paused = true;
    308 		if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
    309 	}
    310 
    311 	getTimes() {
    312 		const directoryWatchers = new Set();
    313 		addWatchersToSet(this.fileWatchers.values(), directoryWatchers);
    314 		addWatchersToSet(this.directoryWatchers.values(), directoryWatchers);
    315 		const obj = Object.create(null);
    316 		for (const w of directoryWatchers) {
    317 			const times = w.getTimes();
    318 			for (const file of Object.keys(times)) obj[file] = times[file];
    319 		}
    320 		return obj;
    321 	}
    322 
    323 	getTimeInfoEntries() {
    324 		const map = new Map();
    325 		this.collectTimeInfoEntries(map, map);
    326 		return map;
    327 	}
    328 
    329 	collectTimeInfoEntries(fileTimestamps, directoryTimestamps) {
    330 		const allWatchers = new Set();
    331 		addWatchersToSet(this.fileWatchers.values(), allWatchers);
    332 		addWatchersToSet(this.directoryWatchers.values(), allWatchers);
    333 		const safeTime = { value: 0 };
    334 		for (const w of allWatchers) {
    335 			w.collectTimeInfoEntries(fileTimestamps, directoryTimestamps, safeTime);
    336 		}
    337 	}
    338 
    339 	getAggregated() {
    340 		if (this.aggregateTimer) {
    341 			clearTimeout(this.aggregateTimer);
    342 			this.aggregateTimer = undefined;
    343 		}
    344 		const changes = this.aggregatedChanges;
    345 		const removals = this.aggregatedRemovals;
    346 		this.aggregatedChanges = new Set();
    347 		this.aggregatedRemovals = new Set();
    348 		return { changes, removals };
    349 	}
    350 
    351 	_onChange(item, mtime, file, type) {
    352 		file = file || item;
    353 		if (!this.paused) {
    354 			this.emit("change", file, mtime, type);
    355 			if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
    356 			this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
    357 		}
    358 		this.aggregatedRemovals.delete(item);
    359 		this.aggregatedChanges.add(item);
    360 	}
    361 
    362 	_onRemove(item, file, type) {
    363 		file = file || item;
    364 		if (!this.paused) {
    365 			this.emit("remove", file, type);
    366 			if (this.aggregateTimer) clearTimeout(this.aggregateTimer);
    367 			this.aggregateTimer = setTimeout(this._onTimeout, this.aggregateTimeout);
    368 		}
    369 		this.aggregatedChanges.delete(item);
    370 		this.aggregatedRemovals.add(item);
    371 	}
    372 
    373 	_onTimeout() {
    374 		this.aggregateTimer = undefined;
    375 		const changes = this.aggregatedChanges;
    376 		const removals = this.aggregatedRemovals;
    377 		this.aggregatedChanges = new Set();
    378 		this.aggregatedRemovals = new Set();
    379 		this.emit("aggregated", changes, removals);
    380 	}
    381 }
    382 
    383 module.exports = Watchpack;