-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathmetawatch.js
More file actions
110 lines (99 loc) · 3.05 KB
/
Copy pathmetawatch.js
File metadata and controls
110 lines (99 loc) · 3.05 KB
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
'use strict';
const fs = require('node:fs');
const path = require('node:path');
const { EventEmitter } = require('node:events');
const DEFAULT_WATCH_TIMEOUT = 5000;
class DirectoryWatcher extends EventEmitter {
#watchers = new Map();
#timeout = DEFAULT_WATCH_TIMEOUT;
#timer = null;
#queue = new Map();
#closed = false;
constructor(options = {}) {
super();
const { timeout } = options;
if (timeout !== undefined) this.#timeout = timeout;
}
close() {
this.#closed = true;
if (this.#timer) {
clearTimeout(this.#timer);
this.#timer = null;
}
const watchers = this.#watchers.values();
for (const watcher of watchers) if (watcher) watcher.close();
this.#watchers.clear();
this.#queue.clear();
}
#post(eventName, filePath) {
if (this.#timer) clearTimeout(this.#timer);
this.#queue.set(filePath, eventName);
if (this.#timeout === 0) return void this.#sendQueue();
this.#timer = setTimeout(() => {
if (this.#timer) {
clearTimeout(this.#timer);
this.#timer = null;
}
this.#sendQueue();
}, this.#timeout);
}
#sendQueue() {
if (this.#queue.size === 0) return;
const queue = [...this.#queue.entries()];
this.#queue.clear();
this.emit('before', queue);
for (const [filePath, event] of queue) {
this.emit(event, filePath);
}
this.emit('after', queue);
}
#watchDirectory(targetPath) {
if (this.#closed) return;
const existing = this.#watchers.get(targetPath);
if (existing) return;
const watcher = fs.watch(targetPath, (event, fileName) => {
const target = targetPath.endsWith(path.sep + fileName);
const filePath = target ? targetPath : path.join(targetPath, fileName);
fs.stat(filePath, (error, stats) => {
if (error) {
if (error.code === 'ENOENT') {
this.unwatch(filePath);
return void this.#post('delete', filePath);
}
return void this.emit('error', error);
}
if (stats.isDirectory()) this.watch(filePath);
if (filePath !== targetPath) this.#post('change', filePath);
});
});
watcher.on('error', (error) => this.emit('error', error));
this.#watchers.set(targetPath, watcher);
}
watch(targetPath) {
if (this.#closed) return;
const watcher = this.#watchers.get(targetPath);
if (watcher) return;
this.#watchers.set(targetPath, null);
const options = { withFileTypes: true };
fs.readdir(targetPath, options, (error, files) => {
if (error) {
this.#watchers.delete(targetPath);
return void this.emit('error', error);
}
for (const file of files) {
if (file.isDirectory()) {
const dirPath = path.join(targetPath, file.name);
this.watch(dirPath);
}
}
this.#watchDirectory(targetPath);
});
}
unwatch(targetPath) {
const watcher = this.#watchers.get(targetPath);
if (!watcher) return;
this.#watchers.delete(targetPath);
if (watcher) watcher.close();
}
}
module.exports = { DirectoryWatcher };