watchEventSource.js 8.3 KB

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