-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathWatcher.js
150 lines (132 loc) · 4.73 KB
/
Watcher.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
const fs = require('fs');
const path = require('path');
const events = require('events');
const { assert } = require('./util');
/** @type {Map<string, DirectoryWatcher>} */
const directoryWatchers = new Map();
/**
* A reference counting singleton nodejs watcher for directories.
* Emits events 'change' and 'rename' with the file name as argument.
*/
class DirectoryWatcher extends events.EventEmitter {
/**
* @param {string} directory
* @param {object} [options] The options to pass to the fs.watch call. See https://nodejs.org/api/fs.html#fs_fs_watch_filename_options_listener
* @returns {DirectoryWatcher}
*/
constructor(directory, options = {}) {
directory = path.normalize(directory);
if (directoryWatchers.has(directory)) {
const watcher = directoryWatchers.get(directory);
watcher.references++;
return watcher;
}
assert(fs.existsSync(directory), `Can not watch a non-existing directory "${directory}".`);
assert(fs.statSync(directory).isDirectory(), `Can only watch directories, but "${directory}" is none.`);
super();
this.setMaxListeners(1000);
directoryWatchers.set(directory, this);
this.directory = directory;
this.watcher = fs.watch(directory, Object.assign({ persistent: false }, options), this.emit.bind(this));
this.references = 1;
}
/**
* Close this watcher.
* @returns void
*/
close() {
this.references--;
if (this.references === 0 && this.watcher) {
this.watcher.close();
this.watcher = null;
directoryWatchers.delete(this.directory);
}
}
}
/**
* A watcher for a single file or a directory, with the possibility to provide a filter method for file names to watch.
*/
class Watcher {
/**
* @param {string|string[]} fileOrDirectory The filename or directory or list of directories to watch
* @param {function(string): boolean} [fileFilter] A filter that will receive a filename and needs to return true if this watcher should be invoked. Will be ignored if the first argument is a file.
* @returns {Watcher}
*/
constructor(fileOrDirectory, fileFilter = null) {
let directories;
if (typeof fileOrDirectory === 'string') {
directories = [fileOrDirectory];
if (!fs.statSync(fileOrDirectory).isDirectory()) {
directories = [path.dirname(fileOrDirectory)];
const filename = path.basename(fileOrDirectory);
fileFilter = changedFilename => changedFilename === filename;
}
} else {
directories = [...new Set(fileOrDirectory.map(path.normalize))];
}
this.watchers = directories.map(dir => new DirectoryWatcher(dir));
if (fileFilter === null) {
fileFilter = () => true;
}
this.fileFilter = fileFilter;
this.onChange = this.onChange.bind(this);
this.onRename = this.onRename.bind(this);
this.watchers.forEach(watcher => {
watcher.on('change', this.onChange);
watcher.on('rename', this.onRename);
});
this.handlers = { change: [], rename: [] };
}
/**
* Register a new handler that is triggered if the fileFilter matches.
* @param {string} eventType
* @param {function(string): void} handler A handler method that should be invoked with the filename as argument
* @api
*/
on(eventType, handler) {
assert(eventType in this.handlers, `Event type ${eventType} is unknown. Only 'change' and 'rename' are supported.`);
this.handlers[eventType].push(handler);
}
/**
* @private
* @param {string} filename
*/
onChange(filename) {
if (this.handlers.change.length === 0) {
return;
}
if (!filename || !this.fileFilter(filename)) {
return;
}
this.handlers.change
.forEach((handler) => handler(filename));
}
/**
* @private
* @param {string} filename
*/
onRename(filename) {
if (this.handlers.rename.length === 0) {
return;
}
if (!filename || !this.fileFilter(filename)) {
return;
}
this.handlers.rename
.forEach((handler) => handler(filename));
}
/**
* Close this watcher and release all handlers.
* @api
*/
close() {
this.watchers.forEach(watcher => {
watcher.removeListener('change', this.onChange);
watcher.removeListener('rename', this.onRename);
watcher.close();
});
this.watchers = [];
this.handlers = { change: [], rename: [] };
}
}
module.exports = Watcher;