watchEventSource.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  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 (${this.rootPath}) to all watchers\n`
  88. );
  89. }
  90. for (const w of this.mapWatcherToPath.keys()) {
  91. w.emit("change", type);
  92. }
  93. } else {
  94. const dir = path.dirname(filename);
  95. const watchers = this.mapPathToWatchers.get(dir);
  96. if (recursiveWatcherLogging) {
  97. process.stderr.write(
  98. `[watchpack] dispatch ${type} event in recursive watcher (${
  99. this.rootPath
  100. }) for '${filename}' to ${
  101. watchers ? watchers.size : 0
  102. } watchers\n`
  103. );
  104. }
  105. if (watchers === undefined) return;
  106. for (const w of watchers) {
  107. w.emit("change", type, path.basename(filename));
  108. }
  109. }
  110. });
  111. watcher.on("error", error => {
  112. for (const w of this.mapWatcherToPath.keys()) {
  113. w.emit("error", error);
  114. }
  115. });
  116. } catch (err) {
  117. process.nextTick(() => {
  118. for (const w of this.mapWatcherToPath.keys()) {
  119. w.emit("error", err);
  120. }
  121. });
  122. }
  123. watcherCount++;
  124. if (recursiveWatcherLogging) {
  125. process.stderr.write(
  126. `[watchpack] created recursive watcher at ${rootPath}\n`
  127. );
  128. }
  129. }
  130. add(filePath, watcher) {
  131. underlyingWatcher.set(watcher, this);
  132. const subpath = filePath.slice(this.rootPath.length + 1) || ".";
  133. this.mapWatcherToPath.set(watcher, subpath);
  134. const set = this.mapPathToWatchers.get(subpath);
  135. if (set === undefined) {
  136. const newSet = new Set();
  137. newSet.add(watcher);
  138. this.mapPathToWatchers.set(subpath, newSet);
  139. } else {
  140. set.add(watcher);
  141. }
  142. }
  143. remove(watcher) {
  144. const subpath = this.mapWatcherToPath.get(watcher);
  145. if (!subpath) return;
  146. this.mapWatcherToPath.delete(watcher);
  147. const set = this.mapPathToWatchers.get(subpath);
  148. set.delete(watcher);
  149. if (set.size === 0) {
  150. this.mapPathToWatchers.delete(subpath);
  151. }
  152. if (this.mapWatcherToPath.size === 0) {
  153. recursiveWatchers.delete(this.rootPath);
  154. watcherCount--;
  155. if (this.watcher) this.watcher.close();
  156. if (recursiveWatcherLogging) {
  157. process.stderr.write(
  158. `[watchpack] closed recursive watcher at ${this.rootPath}\n`
  159. );
  160. }
  161. }
  162. }
  163. getWatchers() {
  164. return this.mapWatcherToPath;
  165. }
  166. }
  167. class Watcher extends EventEmitter {
  168. close() {
  169. if (pendingWatchers.has(this)) {
  170. pendingWatchers.delete(this);
  171. return;
  172. }
  173. const watcher = underlyingWatcher.get(this);
  174. watcher.remove(this);
  175. underlyingWatcher.delete(this);
  176. }
  177. }
  178. const createDirectWatcher = filePath => {
  179. const existing = directWatchers.get(filePath);
  180. if (existing !== undefined) return existing;
  181. const w = new DirectWatcher(filePath);
  182. directWatchers.set(filePath, w);
  183. return w;
  184. };
  185. const createRecursiveWatcher = rootPath => {
  186. const existing = recursiveWatchers.get(rootPath);
  187. if (existing !== undefined) return existing;
  188. const w = new RecursiveWatcher(rootPath);
  189. recursiveWatchers.set(rootPath, w);
  190. return w;
  191. };
  192. const execute = () => {
  193. /** @type {Map<string, Watcher[] | Watcher>} */
  194. const map = new Map();
  195. const addWatcher = (watcher, filePath) => {
  196. const entry = map.get(filePath);
  197. if (entry === undefined) {
  198. map.set(filePath, watcher);
  199. } else if (Array.isArray(entry)) {
  200. entry.push(watcher);
  201. } else {
  202. map.set(filePath, [entry, watcher]);
  203. }
  204. };
  205. for (const [watcher, filePath] of pendingWatchers) {
  206. addWatcher(watcher, filePath);
  207. }
  208. pendingWatchers.clear();
  209. // Fast case when we are not reaching the limit
  210. if (!SUPPORTS_RECURSIVE_WATCHING || watcherLimit - watcherCount >= map.size) {
  211. // Create watchers for all entries in the map
  212. for (const [filePath, entry] of map) {
  213. const w = createDirectWatcher(filePath);
  214. if (Array.isArray(entry)) {
  215. for (const item of entry) w.add(item);
  216. } else {
  217. w.add(entry);
  218. }
  219. }
  220. return;
  221. }
  222. // Reconsider existing watchers to improving watch plan
  223. for (const watcher of recursiveWatchers.values()) {
  224. for (const [w, subpath] of watcher.getWatchers()) {
  225. addWatcher(w, path.join(watcher.rootPath, subpath));
  226. }
  227. }
  228. for (const watcher of directWatchers.values()) {
  229. for (const w of watcher.getWatchers()) {
  230. addWatcher(w, watcher.filePath);
  231. }
  232. }
  233. // Merge map entries to keep watcher limit
  234. // Create a 10% buffer to be able to enter fast case more often
  235. const plan = reducePlan(map, watcherLimit * 0.9);
  236. // Update watchers for all entries in the map
  237. for (const [filePath, entry] of plan) {
  238. if (entry.size === 1) {
  239. for (const [watcher, filePath] of entry) {
  240. const w = createDirectWatcher(filePath);
  241. const old = underlyingWatcher.get(watcher);
  242. if (old === w) continue;
  243. w.add(watcher);
  244. if (old !== undefined) old.remove(watcher);
  245. }
  246. } else {
  247. const filePaths = new Set(entry.values());
  248. if (filePaths.size > 1) {
  249. const w = createRecursiveWatcher(filePath);
  250. for (const [watcher, watcherPath] of entry) {
  251. const old = underlyingWatcher.get(watcher);
  252. if (old === w) continue;
  253. w.add(watcherPath, watcher);
  254. if (old !== undefined) old.remove(watcher);
  255. }
  256. } else {
  257. for (const filePath of filePaths) {
  258. const w = createDirectWatcher(filePath);
  259. for (const watcher of entry.keys()) {
  260. const old = underlyingWatcher.get(watcher);
  261. if (old === w) continue;
  262. w.add(watcher);
  263. if (old !== undefined) old.remove(watcher);
  264. }
  265. }
  266. }
  267. }
  268. }
  269. };
  270. exports.watch = filePath => {
  271. const watcher = new Watcher();
  272. // Find an existing watcher
  273. const directWatcher = directWatchers.get(filePath);
  274. if (directWatcher !== undefined) {
  275. directWatcher.add(watcher);
  276. return watcher;
  277. }
  278. let current = filePath;
  279. for (;;) {
  280. const recursiveWatcher = recursiveWatchers.get(current);
  281. if (recursiveWatcher !== undefined) {
  282. recursiveWatcher.add(filePath, watcher);
  283. return watcher;
  284. }
  285. const parent = path.dirname(current);
  286. if (parent === current) break;
  287. current = parent;
  288. }
  289. // Queue up watcher for creation
  290. pendingWatchers.set(watcher, filePath);
  291. if (!isBatch) execute();
  292. return watcher;
  293. };
  294. exports.batch = fn => {
  295. isBatch = true;
  296. try {
  297. fn();
  298. } finally {
  299. isBatch = false;
  300. execute();
  301. }
  302. };
  303. exports.getNumberOfWatchers = () => {
  304. return watcherCount;
  305. };