• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1'use strict';
2
3const {
4  ArrayIsArray,
5  ArrayPrototypeForEach,
6  SafeMap,
7  SafeSet,
8  StringPrototypeStartsWith,
9} = primordials;
10
11const { validateNumber, validateOneOf } = require('internal/validators');
12const { kEmptyObject } = require('internal/util');
13const { TIMEOUT_MAX } = require('internal/timers');
14
15const EventEmitter = require('events');
16const { watch } = require('fs');
17const { fileURLToPath } = require('url');
18const { resolve, dirname } = require('path');
19const { setTimeout } = require('timers');
20
21const supportsRecursiveWatching = process.platform === 'win32' ||
22  process.platform === 'darwin';
23
24class FilesWatcher extends EventEmitter {
25  #watchers = new SafeMap();
26  #filteredFiles = new SafeSet();
27  #throttling = new SafeSet();
28  #depencencyOwners = new SafeMap();
29  #ownerDependencies = new SafeMap();
30  #throttle;
31  #mode;
32  #signal;
33
34  constructor({ throttle = 500, mode = 'filter', signal } = kEmptyObject) {
35    super();
36
37    validateNumber(throttle, 'options.throttle', 0, TIMEOUT_MAX);
38    validateOneOf(mode, 'options.mode', ['filter', 'all']);
39    this.#throttle = throttle;
40    this.#mode = mode;
41    this.#signal = signal;
42
43    if (signal) {
44      EventEmitter.addAbortListener(signal, () => this.clear());
45    }
46  }
47
48  #isPathWatched(path) {
49    if (this.#watchers.has(path)) {
50      return true;
51    }
52
53    for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
54      if (watcher.recursive && StringPrototypeStartsWith(path, watchedPath)) {
55        return true;
56      }
57    }
58
59    return false;
60  }
61
62  #removeWatchedChildren(path) {
63    for (const { 0: watchedPath, 1: watcher } of this.#watchers.entries()) {
64      if (path !== watchedPath && StringPrototypeStartsWith(watchedPath, path)) {
65        this.#unwatch(watcher);
66        this.#watchers.delete(watchedPath);
67      }
68    }
69  }
70
71  #unwatch(watcher) {
72    watcher.handle.removeAllListeners();
73    watcher.handle.close();
74  }
75
76  #onChange(trigger) {
77    if (this.#throttling.has(trigger)) {
78      return;
79    }
80    if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) {
81      return;
82    }
83    this.#throttling.add(trigger);
84    const owners = this.#depencencyOwners.get(trigger);
85    this.emit('changed', { owners });
86    setTimeout(() => this.#throttling.delete(trigger), this.#throttle).unref();
87  }
88
89  get watchedPaths() {
90    return [...this.#watchers.keys()];
91  }
92
93  watchPath(path, recursive = true) {
94    if (this.#isPathWatched(path)) {
95      return;
96    }
97    const watcher = watch(path, { recursive, signal: this.#signal });
98    watcher.on('change', (eventType, fileName) => this
99      .#onChange(recursive ? resolve(path, fileName) : path));
100    this.#watchers.set(path, { handle: watcher, recursive });
101    if (recursive) {
102      this.#removeWatchedChildren(path);
103    }
104  }
105
106  filterFile(file, owner) {
107    if (!file) return;
108    if (supportsRecursiveWatching) {
109      this.watchPath(dirname(file));
110    } else {
111      // Having multiple FSWatcher's seems to be slower
112      // than a single recursive FSWatcher
113      this.watchPath(file, false);
114    }
115    this.#filteredFiles.add(file);
116    if (owner) {
117      const owners = this.#depencencyOwners.get(file) ?? new SafeSet();
118      const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet();
119      owners.add(owner);
120      dependencies.add(file);
121      this.#depencencyOwners.set(file, owners);
122      this.#ownerDependencies.set(owner, dependencies);
123    }
124  }
125  watchChildProcessModules(child, key = null) {
126    if (this.#mode !== 'filter') {
127      return;
128    }
129    child.on('message', (message) => {
130      try {
131        if (ArrayIsArray(message['watch:require'])) {
132          ArrayPrototypeForEach(message['watch:require'], (file) => this.filterFile(file, key));
133        }
134        if (ArrayIsArray(message['watch:import'])) {
135          ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key));
136        }
137      } catch {
138        // Failed watching file. ignore
139      }
140    });
141  }
142  unfilterFilesOwnedBy(owners) {
143    owners.forEach((owner) => {
144      this.#ownerDependencies.get(owner)?.forEach((dependency) => {
145        this.#filteredFiles.delete(dependency);
146        this.#depencencyOwners.delete(dependency);
147      });
148      this.#filteredFiles.delete(owner);
149      this.#depencencyOwners.delete(owner);
150      this.#ownerDependencies.delete(owner);
151    });
152  }
153  clearFileFilters() {
154    this.#filteredFiles.clear();
155  }
156  clear() {
157    this.#watchers.forEach(this.#unwatch);
158    this.#watchers.clear();
159    this.#filteredFiles.clear();
160    this.#depencencyOwners.clear();
161    this.#ownerDependencies.clear();
162  }
163}
164
165module.exports = { FilesWatcher };
166