@kmamal/watcher 0.0.2 → 0.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -4
- package/package.json +11 -10
- package/src/index.js +187 -106
package/README.md
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
1
|
# @kmamal/watcher
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@kmamal/watcher)
|
|
4
|
-
[](https://libraries.io/npm/@kmamal%2Fwatcher)
|
|
5
|
-
[](https://bundlephobia.com/package/@kmamal/watcher)
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
package/package.json
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
2
|
+
"version": "0.0.5",
|
|
3
3
|
"name": "@kmamal/watcher",
|
|
4
4
|
"description": "watcher",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
|
-
"url": "git@github.com:kmamal/watcher.git"
|
|
7
|
+
"url": "git+ssh://git@github.com:kmamal/watcher.git"
|
|
8
8
|
},
|
|
9
9
|
"license": "MIT",
|
|
10
|
-
"main": "./src/index.js",
|
|
11
|
-
"exports": {
|
|
12
|
-
".": "./src/index.js"
|
|
13
|
-
},
|
|
14
10
|
"scripts": {
|
|
15
11
|
"test": "npx @kmamal/testing",
|
|
16
12
|
"update-exports": "node ./scripts/update-exports.mjs"
|
|
17
13
|
},
|
|
18
14
|
"dependencies": {
|
|
19
|
-
"@kmamal/
|
|
20
|
-
"@kmamal/
|
|
15
|
+
"@kmamal/async": "0.0.20",
|
|
16
|
+
"@kmamal/globs": "0.0.4",
|
|
17
|
+
"@kmamal/util": "0.2.32"
|
|
21
18
|
},
|
|
22
19
|
"devDependencies": {
|
|
23
|
-
"@kmamal/testing": "
|
|
24
|
-
}
|
|
20
|
+
"@kmamal/testing": "0.0.33"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": "./src/index.js"
|
|
24
|
+
},
|
|
25
|
+
"main": "./src/index.js"
|
|
25
26
|
}
|
package/src/index.js
CHANGED
|
@@ -1,138 +1,219 @@
|
|
|
1
|
-
const Fs = require('fs')
|
|
2
|
-
const Path = require('path')
|
|
3
|
-
const { EventEmitter } = require('events')
|
|
1
|
+
const Fs = require('node:fs')
|
|
2
|
+
const Path = require('node:path')
|
|
3
|
+
const { EventEmitter } = require('node:events')
|
|
4
|
+
|
|
4
5
|
const { Matcher } = require('@kmamal/globs/matcher')
|
|
5
6
|
const { comm } = require('@kmamal/util/array/comm')
|
|
7
|
+
const { AbortableOpener } = require('@kmamal/async/opener')
|
|
6
8
|
|
|
7
9
|
class Watcher extends EventEmitter {
|
|
8
|
-
constructor (
|
|
10
|
+
constructor () {
|
|
9
11
|
super()
|
|
10
|
-
this._cwd =
|
|
11
|
-
this._throttling =
|
|
12
|
+
this._cwd = null
|
|
13
|
+
this._throttling = null
|
|
14
|
+
|
|
15
|
+
this._matcher = null
|
|
16
|
+
this._entries = new Map()
|
|
17
|
+
this._scheduled = new Map()
|
|
18
|
+
this._running = new Map()
|
|
12
19
|
|
|
13
|
-
this.
|
|
14
|
-
this.
|
|
15
|
-
|
|
20
|
+
this._opener = new AbortableOpener()
|
|
21
|
+
this._ac = null
|
|
22
|
+
}
|
|
16
23
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const isDir = props?.isDir ?? stats.isDirectory()
|
|
21
|
-
const path = props?.path ?? (isDir ? `${fsPath}/` : fsPath)
|
|
24
|
+
async open (glob, options) {
|
|
25
|
+
return await this._opener.open(async (ac) => {
|
|
26
|
+
this._ac = ac
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
this._cwd = options?.cwd ? Path.resolve(options.cwd) : process.cwd()
|
|
29
|
+
this._throttling = options?.throttling ?? 100
|
|
30
|
+
|
|
31
|
+
this._matcher = new Matcher(glob, options)
|
|
32
|
+
|
|
33
|
+
const fileIterator = this._matcher.getFiles({ includeDirs: true })
|
|
34
|
+
for await (const { path, stats } of fileIterator) {
|
|
35
|
+
if (ac.signal.aborted) { break }
|
|
36
|
+
|
|
37
|
+
const isDir = stats.isDirectory()
|
|
38
|
+
const fsPath = isDir ? path.slice(0, -1) : path
|
|
39
|
+
await this._addEntry(fsPath, { stats, path, isDir, exhaustive: true }, ac.signal)
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
if (isDir) {
|
|
29
|
-
contents = await Fs.promises.readdir(fsPath)
|
|
30
|
-
contents.sort()
|
|
41
|
+
if (ac.signal.aborted) { break }
|
|
31
42
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async close () {
|
|
47
|
+
return await this._opener.close(() => {
|
|
48
|
+
this._ac.abort()
|
|
49
|
+
this._ac = null
|
|
50
|
+
|
|
51
|
+
for (const entry of this._entries.values()) { entry.watcher.close() }
|
|
52
|
+
for (const timeout of this._scheduled.values()) { clearTimeout(timeout) }
|
|
53
|
+
|
|
54
|
+
this._cwd = null
|
|
55
|
+
this._throttling = null
|
|
56
|
+
|
|
57
|
+
this._matcher = null
|
|
58
|
+
this._entries.clear()
|
|
59
|
+
this._scheduled.clear()
|
|
60
|
+
this._running.clear()
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async _addEntry (fsPath, props, signal) {
|
|
65
|
+
const prev = this._running.get(fsPath) ?? Promise.resolve()
|
|
66
|
+
const next = prev
|
|
67
|
+
.then(() => signal.aborted ? undefined : this._doAddEntry(fsPath, props, signal))
|
|
68
|
+
.finally(() => {
|
|
69
|
+
if (this._running.get(fsPath) === next) { this._running.delete(fsPath) }
|
|
37
70
|
})
|
|
71
|
+
this._running.set(fsPath, next)
|
|
72
|
+
await next
|
|
73
|
+
}
|
|
38
74
|
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
async _doAddEntry (fsPath, props, signal) {
|
|
76
|
+
if (this._entries.has(fsPath)) { return }
|
|
77
|
+
const stats = props?.stats ?? await Fs.promises.stat(fsPath)
|
|
78
|
+
const isDir = props?.isDir ?? stats.isDirectory()
|
|
79
|
+
const path = props?.path ?? (isDir ? `${fsPath}/` : fsPath)
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
if (signal.aborted) { return }
|
|
82
|
+
|
|
83
|
+
let contents
|
|
84
|
+
if (isDir) {
|
|
85
|
+
contents = await Fs.promises.readdir(fsPath)
|
|
86
|
+
if (signal.aborted) { return }
|
|
87
|
+
contents.sort()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const watcher = Fs.watch(fsPath, (event, relPath) => {
|
|
91
|
+
if (!relPath) {
|
|
92
|
+
console.warn(`Watcher: ${event} event on ${fsPath} with no relPath`)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
this._handleChange(isDir ? Path.join(fsPath, relPath) : fsPath, signal)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
this._entries.set(fsPath, {
|
|
99
|
+
watcher,
|
|
100
|
+
ino: stats.ino,
|
|
101
|
+
mtimeMs: stats.mtimeMs,
|
|
102
|
+
contents,
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const type = isDir ? 'addDir' : 'addFile'
|
|
106
|
+
this.emit('change', type, path, stats)
|
|
107
|
+
|
|
108
|
+
if (isDir && !props?.exhaustive) {
|
|
109
|
+
const fileIterator = this._matcher.getFiles({
|
|
110
|
+
cwd: fsPath,
|
|
111
|
+
includeDirs: true,
|
|
112
|
+
})
|
|
113
|
+
for await (const entry of fileIterator) {
|
|
114
|
+
if (signal.aborted) { return }
|
|
115
|
+
|
|
116
|
+
const { path: childPath, stats: childStats } = entry
|
|
117
|
+
const childIsDir = childStats.isDirectory()
|
|
118
|
+
const childFsPath = childIsDir ? childPath.slice(0, -1) : childPath
|
|
119
|
+
|
|
120
|
+
await this._addEntry(childFsPath, {
|
|
121
|
+
stats: childStats,
|
|
122
|
+
path: childPath,
|
|
123
|
+
isDir: childIsDir,
|
|
124
|
+
exhaustive: true,
|
|
125
|
+
}, signal)
|
|
126
|
+
if (signal.aborted) { return }
|
|
58
127
|
}
|
|
59
128
|
}
|
|
129
|
+
}
|
|
60
130
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
131
|
+
_removeEntry (fsPath, props, signal) {
|
|
132
|
+
const entry = props?.entry ?? this._entries.get(fsPath)
|
|
133
|
+
if (!entry) { return }
|
|
134
|
+
const isDir = props?.isDir ?? Boolean(entry.contents)
|
|
135
|
+
const path = props?.path ?? (isDir ? `${fsPath}/` : fsPath)
|
|
66
136
|
|
|
67
|
-
|
|
68
|
-
|
|
137
|
+
entry.watcher.close()
|
|
138
|
+
this._entries.delete(fsPath)
|
|
69
139
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
}
|
|
140
|
+
if (isDir && !props?.exhaustive) {
|
|
141
|
+
for (const name of entry.contents) {
|
|
142
|
+
this._removeEntry(Path.join(fsPath, name), undefined, signal)
|
|
74
143
|
}
|
|
75
|
-
|
|
76
|
-
const type = isDir ? 'delDir' : 'delFile'
|
|
77
|
-
this.emit('change', type, path)
|
|
78
144
|
}
|
|
79
145
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
this._scheduled.add(fsPath)
|
|
84
|
-
setTimeout(() => {
|
|
85
|
-
this._scheduled.delete(fsPath)
|
|
86
|
-
_handleChange(fsPath)
|
|
87
|
-
}, this._throttling)
|
|
88
|
-
}
|
|
146
|
+
const type = isDir ? 'delDir' : 'delFile'
|
|
147
|
+
this.emit('change', type, path)
|
|
148
|
+
}
|
|
89
149
|
|
|
90
|
-
|
|
91
|
-
|
|
150
|
+
_handleChange (fsPath, signal) {
|
|
151
|
+
if (signal.aborted) { return }
|
|
92
152
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const isDir = newStats.isDirectory()
|
|
96
|
-
const newPath = isDir ? `${fsPath}/` : fsPath
|
|
153
|
+
if (this._scheduled.has(fsPath)) { return }
|
|
154
|
+
if (!this._matcher.matchesPath(fsPath)) { return }
|
|
97
155
|
|
|
98
|
-
|
|
99
|
-
|
|
156
|
+
this._scheduled.set(fsPath, setTimeout(() => {
|
|
157
|
+
this._scheduled.delete(fsPath)
|
|
100
158
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
159
|
+
const prev = this._running.get(fsPath) ?? Promise.resolve()
|
|
160
|
+
const next = prev
|
|
161
|
+
.then(() => signal.aborted ? undefined : this._doHandleChange(fsPath, signal))
|
|
162
|
+
.finally(() => {
|
|
163
|
+
if (this._running.get(fsPath) === next) { this._running.delete(fsPath) }
|
|
164
|
+
})
|
|
165
|
+
this._running.set(fsPath, next)
|
|
166
|
+
}, this._throttling))
|
|
167
|
+
}
|
|
106
168
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
169
|
+
async _doHandleChange (fsPath, signal) {
|
|
170
|
+
if (signal.aborted) { return }
|
|
171
|
+
const entry = this._entries.get(fsPath)
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const newStats = await Fs.promises.stat(fsPath)
|
|
175
|
+
if (signal.aborted) { return }
|
|
176
|
+
|
|
177
|
+
const isDir = newStats.isDirectory()
|
|
178
|
+
const newPath = isDir ? `${fsPath}/` : fsPath
|
|
179
|
+
|
|
180
|
+
const wasDir = Boolean(entry?.contents)
|
|
181
|
+
const oldPath = wasDir ? `${fsPath}/` : fsPath
|
|
182
|
+
|
|
183
|
+
if (!entry || wasDir !== isDir || entry.ino !== newStats.ino) {
|
|
184
|
+
if (entry) { this._removeEntry(fsPath, { entry, path: oldPath, isDir: wasDir }, signal) }
|
|
185
|
+
await this._doAddEntry(fsPath, { stats: newStats, path: newPath, isDir }, signal)
|
|
186
|
+
return
|
|
125
187
|
}
|
|
126
|
-
}
|
|
127
188
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await addEntry(fsPath, { stats, path, isDir, exhaustive: true })
|
|
189
|
+
if (entry.mtimeMs >= newStats.mtimeMs) { return }
|
|
190
|
+
entry.mtimeMs = newStats.mtimeMs
|
|
191
|
+
|
|
192
|
+
if (!isDir) {
|
|
193
|
+
this.emit('change', 'change', newPath, newStats)
|
|
134
194
|
}
|
|
135
|
-
|
|
195
|
+
else {
|
|
196
|
+
const newContents = await Fs.promises.readdir(fsPath)
|
|
197
|
+
if (signal.aborted) { return }
|
|
198
|
+
|
|
199
|
+
newContents.sort()
|
|
200
|
+
|
|
201
|
+
const { a, b } = comm(entry.contents, newContents)
|
|
202
|
+
for (const name of a) {
|
|
203
|
+
this._removeEntry(Path.join(fsPath, name), undefined, signal)
|
|
204
|
+
}
|
|
205
|
+
for (const name of b) {
|
|
206
|
+
await this._addEntry(Path.join(fsPath, name), undefined, signal)
|
|
207
|
+
if (signal.aborted) { return }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
entry.contents = newContents
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
if (error.code !== 'ENOENT') { throw error }
|
|
215
|
+
if (entry) { this._removeEntry(fsPath, { entry }, signal) }
|
|
216
|
+
}
|
|
136
217
|
}
|
|
137
218
|
}
|
|
138
219
|
|