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  #debouncing = new SafeSet();
28  #depencencyOwners = new SafeMap();
29  #ownerDependencies = new SafeMap();
30  #debounce;
31  #mode;
32  #signal;
33
34  constructor({ debounce = 200, mode = 'filter', signal } = kEmptyObject) {
35    super();
36
37    validateNumber(debounce, 'options.debounce', 0, TIMEOUT_MAX);
38    validateOneOf(mode, 'options.mode', ['filter', 'all']);
39    this.#debounce = debounce;
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.#debouncing.has(trigger)) {
78      return;
79    }
80    if (this.#mode === 'filter' && !this.#filteredFiles.has(trigger)) {
81      return;
82    }
83    this.#debouncing.add(trigger);
84    const owners = this.#depencencyOwners.get(trigger);
85    setTimeout(() => {
86      this.#debouncing.delete(trigger);
87      this.emit('changed', { owners });
88    }, this.#debounce).unref();
89  }
90
91  get watchedPaths() {
92    return [...this.#watchers.keys()];
93  }
94
95  watchPath(path, recursive = true) {
96    if (this.#isPathWatched(path)) {
97      return;
98    }
99    const watcher = watch(path, { recursive, signal: this.#signal });
100    watcher.on('change', (eventType, fileName) => this
101      .#onChange(recursive ? resolve(path, fileName) : path));
102    this.#watchers.set(path, { handle: watcher, recursive });
103    if (recursive) {
104      this.#removeWatchedChildren(path);
105    }
106  }
107
108  filterFile(file, owner) {
109    if (!file) return;
110    if (supportsRecursiveWatching) {
111      this.watchPath(dirname(file));
112    } else {
113      // Having multiple FSWatcher's seems to be slower
114      // than a single recursive FSWatcher
115      this.watchPath(file, false);
116    }
117    this.#filteredFiles.add(file);
118    if (owner) {
119      const owners = this.#depencencyOwners.get(file) ?? new SafeSet();
120      const dependencies = this.#ownerDependencies.get(file) ?? new SafeSet();
121      owners.add(owner);
122      dependencies.add(file);
123      this.#depencencyOwners.set(file, owners);
124      this.#ownerDependencies.set(owner, dependencies);
125    }
126  }
127  watchChildProcessModules(child, key = null) {
128    if (this.#mode !== 'filter') {
129      return;
130    }
131    child.on('message', (message) => {
132      try {
133        if (ArrayIsArray(message['watch:require'])) {
134          ArrayPrototypeForEach(message['watch:require'], (file) => this.filterFile(file, key));
135        }
136        if (ArrayIsArray(message['watch:import'])) {
137          ArrayPrototypeForEach(message['watch:import'], (file) => this.filterFile(fileURLToPath(file), key));
138        }
139      } catch {
140        // Failed watching file. ignore
141      }
142    });
143  }
144  unfilterFilesOwnedBy(owners) {
145    owners.forEach((owner) => {
146      this.#ownerDependencies.get(owner)?.forEach((dependency) => {
147        this.#filteredFiles.delete(dependency);
148        this.#depencencyOwners.delete(dependency);
149      });
150      this.#filteredFiles.delete(owner);
151      this.#depencencyOwners.delete(owner);
152      this.#ownerDependencies.delete(owner);
153    });
154  }
155  clearFileFilters() {
156    this.#filteredFiles.clear();
157  }
158  clear() {
159    this.#watchers.forEach(this.#unwatch);
160    this.#watchers.clear();
161    this.#filteredFiles.clear();
162    this.#depencencyOwners.clear();
163    this.#ownerDependencies.clear();
164  }
165}
166
167module.exports = { FilesWatcher };
168