@kmamal/watcher 0.0.1 → 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 -105
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,137 +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
|
-
setTimeout(() => {
|
|
84
|
-
this._scheduled.delete(fsPath)
|
|
85
|
-
_handleChange(fsPath)
|
|
86
|
-
}, this._throttling)
|
|
87
|
-
}
|
|
146
|
+
const type = isDir ? 'delDir' : 'delFile'
|
|
147
|
+
this.emit('change', type, path)
|
|
148
|
+
}
|
|
88
149
|
|
|
89
|
-
|
|
90
|
-
|
|
150
|
+
_handleChange (fsPath, signal) {
|
|
151
|
+
if (signal.aborted) { return }
|
|
91
152
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const isDir = newStats.isDirectory()
|
|
95
|
-
const newPath = isDir ? `${fsPath}/` : fsPath
|
|
153
|
+
if (this._scheduled.has(fsPath)) { return }
|
|
154
|
+
if (!this._matcher.matchesPath(fsPath)) { return }
|
|
96
155
|
|
|
97
|
-
|
|
98
|
-
|
|
156
|
+
this._scheduled.set(fsPath, setTimeout(() => {
|
|
157
|
+
this._scheduled.delete(fsPath)
|
|
99
158
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
|
105
168
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
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
|
|
124
187
|
}
|
|
125
|
-
}
|
|
126
188
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
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)
|
|
133
194
|
}
|
|
134
|
-
|
|
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
|
+
}
|
|
135
217
|
}
|
|
136
218
|
}
|
|
137
219
|
|