@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.
Files changed (3) hide show
  1. package/README.md +0 -4
  2. package/package.json +11 -10
  3. package/src/index.js +187 -105
package/README.md CHANGED
@@ -1,6 +1,2 @@
1
1
  # @kmamal/watcher
2
2
 
3
- [![Package](https://img.shields.io/npm/v/%2540kmamal%252Fwatcher)](https://www.npmjs.com/package/@kmamal/watcher)
4
- [![Dependencies](https://img.shields.io/librariesio/release/npm/@kmamal/watcher)](https://libraries.io/npm/@kmamal%2Fwatcher)
5
- [![Count](https://badgen.net/bundlephobia/dependency-count/@kmamal/watcher)](https://bundlephobia.com/package/@kmamal/watcher)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
package/package.json CHANGED
@@ -1,25 +1,26 @@
1
1
  {
2
- "version": "0.0.1",
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/globs": "^0.0.1",
20
- "@kmamal/util": "^0.1.13"
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": "^0.0.24"
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 (glob, options) {
10
+ constructor () {
9
11
  super()
10
- this._cwd = options?.cwd ? Path.resolve(options.cwd) : process.cwd()
11
- this._throttling = options?.throttling ?? 100
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._matcher = new Matcher(glob, options)
14
- this._cache = new Map()
15
- this._scheduled = new Set()
20
+ this._opener = new AbortableOpener()
21
+ this._ac = null
22
+ }
16
23
 
17
- const addEntry = async (fsPath, props) => {
18
- if (this._cache.has(fsPath)) { return }
19
- const stats = props?.stats ?? await Fs.promises.stats()
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
- const watcher = Fs.watch(fsPath, (event, relPath) => {
24
- handleChange(isDir ? Path.join(fsPath, relPath) : fsPath)
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
- let contents
28
- if (isDir) {
29
- contents = await Fs.promises.readdir(fsPath)
30
- contents.sort()
41
+ if (ac.signal.aborted) { break }
31
42
  }
32
- this._cache.set(fsPath, {
33
- watcher,
34
- ino: stats.ino,
35
- mtimeMs: stats.mtimeMs,
36
- contents,
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
- const type = isDir ? 'addDir' : 'addFile'
40
- this.emit('change', type, path, stats)
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
- if (isDir && !props?.exhaustive) {
43
- const fileIterator = this._matcher.getFiles({
44
- cwd: fsPath,
45
- includeDirs: true,
46
- })
47
- for await (const entry of fileIterator) {
48
- const { path: childPath, stats: childStats } = entry
49
- const childIsDir = childStats.isDirectory()
50
- const childFsPath = childIsDir ? childPath.slice(0, -1) : childPath
51
- await addEntry(childFsPath, {
52
- stats: childStats,
53
- path: childPath,
54
- isDir: childIsDir,
55
- exhaustive: true,
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
- const removeEntry = (fsPath, props) => {
62
- const entry = props?.entry ?? this._cache.get(fsPath)
63
- if (!entry) { return }
64
- const isDir = props?.isDir ?? Boolean(entry.contents)
65
- const path = props?.isDir ?? (isDir ? `${fsPath}/` : fsPath)
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
- entry.watcher.close()
68
- this._cache.delete(fsPath)
137
+ entry.watcher.close()
138
+ this._entries.delete(fsPath)
69
139
 
70
- if (isDir && !props?.exhaustive) {
71
- for (const name of entry.contents) {
72
- removeEntry(Path.join(fsPath, name))
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 handleChange = (fsPath) => {
81
- if (this._scheduled.has(fsPath)) { return }
82
- this._scheduled.add(fsPath)
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
- const _handleChange = async (fsPath) => {
90
- let entry = this._cache.get(fsPath)
150
+ _handleChange (fsPath, signal) {
151
+ if (signal.aborted) { return }
91
152
 
92
- try {
93
- const newStats = await Fs.promises.stat(fsPath)
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
- const wasDir = Boolean(entry?.contents)
98
- const oldPath = wasDir ? `${fsPath}/` : fsPath
156
+ this._scheduled.set(fsPath, setTimeout(() => {
157
+ this._scheduled.delete(fsPath)
99
158
 
100
- if (!entry || wasDir !== isDir || entry.ino !== newStats.ino) {
101
- if (entry) { removeEntry(fsPath, { entry, path: oldPath, isDir: wasDir }) }
102
- entry = addEntry(fsPath, { stats: newStats, path: newPath, isDir })
103
- return
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
- if (entry.mtimeMs >= newStats.mtimeMs) { return }
107
-
108
- if (!isDir) {
109
- this.emit('change', 'change', newPath, newStats)
110
- } else {
111
- const newContents = await Fs.promises.readdir(fsPath)
112
- newContents.sort()
113
-
114
- const { a, b } = comm(entry.contents, newContents)
115
- for (const name of a) {
116
- removeEntry(Path.join(fsPath, name))
117
- }
118
- for (const name of b) {
119
- addEntry(Path.join(fsPath, name))
120
- }
121
- }
122
- } catch (error) {
123
- if (entry) { removeEntry(fsPath, { entry }) }
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
- ;(async () => {
128
- const fileIterator = this._matcher.getFiles({ includeDirs: true })
129
- for await (const { path, stats } of fileIterator) {
130
- const isDir = stats.isDirectory()
131
- const fsPath = isDir ? path.slice(0, -1) : path
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