@@ -25,14 +25,20 @@ const fs = require('fs');
25
25
const platform = require ( 'os' ) . platform ( ) ;
26
26
const path = require ( 'path' ) ;
27
27
28
- const DEFAULT_DELAY = common . DEFAULT_DELAY ;
29
28
const CHANGE_EVENT = common . CHANGE_EVENT ;
30
29
const DELETE_EVENT = common . DELETE_EVENT ;
31
30
const ADD_EVENT = common . ADD_EVENT ;
32
31
const ALL_EVENT = common . ALL_EVENT ;
33
32
33
+ /**
34
+ * This setting delays all events. It suppresses 'change' events that
35
+ * immediately follow an 'add', and debounces successive 'change' events to
36
+ * only emit the latest.
37
+ */
38
+ const DEBOUNCE_MS = 100 ;
39
+
34
40
module . exports = class NodeWatcher extends EventEmitter {
35
- _changeTimers : { [ key : string ] : TimeoutID , __proto__ : null } ;
41
+ _changeTimers : Map < string , TimeoutID > = new Map ( ) ;
36
42
_dirRegistry : {
37
43
[ directory : string ] : { [ file : string ] : true , __proto__ : null } ,
38
44
__proto__ : null ,
@@ -52,14 +58,15 @@ module.exports = class NodeWatcher extends EventEmitter {
52
58
common . assignOptions ( this , opts ) ;
53
59
54
60
this . watched = Object . create ( null ) ;
55
- this . _changeTimers = Object . create ( null ) ;
56
61
this . _dirRegistry = Object . create ( null ) ;
57
62
this . root = path . resolve ( dir ) ;
58
63
59
64
this . _watchdir ( this . root ) ;
60
65
common . recReaddir (
61
66
this . root ,
62
- this . _watchdir ,
67
+ dir => {
68
+ this . _watchdir ( dir ) ;
69
+ } ,
63
70
filename => {
64
71
this . _register ( filename ) ;
65
72
} ,
@@ -82,21 +89,27 @@ module.exports = class NodeWatcher extends EventEmitter {
82
89
* filename => true
83
90
* }
84
91
* }
92
+ *
93
+ * Return false if ignored or already registered.
85
94
*/
86
95
_register ( filepath : string ) : boolean {
96
+ const dir = path . dirname ( filepath ) ;
97
+ const filename = path . basename ( filepath ) ;
98
+ if ( this . _dirRegistry [ dir ] && this . _dirRegistry [ dir ] [ filename ] ) {
99
+ return false ;
100
+ }
101
+
87
102
const relativePath = path . relative ( this . root , filepath ) ;
88
103
if (
89
104
! common . isFileIncluded ( this . globs , this . dot , this . doIgnore , relativePath )
90
105
) {
91
106
return false ;
92
107
}
93
108
94
- const dir = path . dirname ( filepath ) ;
95
109
if ( ! this . _dirRegistry [ dir ] ) {
96
110
this . _dirRegistry [ dir ] = Object . create ( null ) ;
97
111
}
98
112
99
- const filename = path . basename ( filepath ) ;
100
113
this . _dirRegistry [ dir ] [ filename ] = true ;
101
114
102
115
return true ;
@@ -146,11 +159,10 @@ module.exports = class NodeWatcher extends EventEmitter {
146
159
/**
147
160
* Watch a directory.
148
161
*/
149
- _watchdir: string => void = ( dir : string ) => {
162
+ _watchdir: string => boolean = ( dir : string ) => {
150
163
if ( this . watched [ dir ] ) {
151
- return ;
164
+ return false ;
152
165
}
153
-
154
166
const watcher = fs . watch ( dir , { persistent : true } , ( event , filename ) =>
155
167
this . _normalizeChange ( dir , event , filename ) ,
156
168
) ;
@@ -161,6 +173,7 @@ module.exports = class NodeWatcher extends EventEmitter {
161
173
if ( this . root !== dir ) {
162
174
this . _register ( dir ) ;
163
175
}
176
+ return true ;
164
177
} ;
165
178
166
179
/**
@@ -249,24 +262,43 @@ module.exports = class NodeWatcher extends EventEmitter {
249
262
if ( error && error . code !== 'ENOENT' ) {
250
263
this . emit ( 'error' , error ) ;
251
264
} else if ( ! error && stat . isDirectory ( ) ) {
252
- // win32 emits usless change events on dirs.
253
- if ( event !== 'change' ) {
254
- this . _watchdir ( fullPath ) ;
255
- if (
256
- stat &&
257
- common . isFileIncluded (
258
- this . globs ,
259
- this . dot ,
260
- this . doIgnore ,
261
- relativePath ,
262
- )
263
- ) {
264
- this . _emitEvent ( ADD_EVENT , relativePath , {
265
- modifiedTime : stat . mtime . getTime ( ) ,
266
- size : stat . size ,
267
- type : 'd' ,
268
- } ) ;
269
- }
265
+ if ( event === 'change' ) {
266
+ // win32 emits usless change events on dirs.
267
+ return ;
268
+ }
269
+ if (
270
+ stat &&
271
+ common . isFileIncluded (
272
+ this . globs ,
273
+ this . dot ,
274
+ this . doIgnore ,
275
+ relativePath ,
276
+ )
277
+ ) {
278
+ common . recReaddir (
279
+ path . resolve ( this . root , relativePath ) ,
280
+ ( dir , stats ) => {
281
+ if ( this . _watchdir ( dir ) ) {
282
+ this . _emitEvent ( ADD_EVENT , path . relative ( this . root , dir ) , {
283
+ modifiedTime : stats . mtime . getTime ( ) ,
284
+ size : stats . size ,
285
+ type : 'd' ,
286
+ } ) ;
287
+ }
288
+ } ,
289
+ ( file , stats ) => {
290
+ if ( this . _register ( file ) ) {
291
+ this . _emitEvent ( ADD_EVENT , path . relative ( this . root , file ) , {
292
+ modifiedTime : stats . mtime . getTime ( ) ,
293
+ size : stats . size ,
294
+ type : 'f' ,
295
+ } ) ;
296
+ }
297
+ } ,
298
+ function endCallback ( ) { } ,
299
+ this . _checkedEmitError ,
300
+ this . ignored ,
301
+ ) ;
270
302
}
271
303
} else {
272
304
const registered = this . _registered ( fullPath ) ;
@@ -300,48 +332,31 @@ module.exports = class NodeWatcher extends EventEmitter {
300
332
}
301
333
302
334
/**
303
- * Triggers a 'change' event after debounding it to take care of duplicate
304
- * events on os x.
335
+ * Emits the given event after debouncing, to 1) suppress 'change' events
336
+ * immediately following an 'add', and 2) to only emit the latest 'change'
337
+ * event when received in quick succession for a given file.
338
+ *
339
+ * See also note above for DEBOUNCE_MS.
305
340
*/
306
341
_emitEvent ( type : string , file : string , metadata ? : ChangeEventMetadata ) {
307
342
const key = type + '-' + file ;
308
343
const addKey = ADD_EVENT + '-' + file ;
309
- if ( type === CHANGE_EVENT && this . _changeTimers [ addKey ] ) {
344
+ if ( type === CHANGE_EVENT && this . _changeTimers . has ( addKey ) ) {
310
345
// Ignore the change event that is immediately fired after an add event.
311
346
// (This happens on Linux).
312
347
return ;
313
348
}
314
- clearTimeout ( this . _changeTimers [ key ] ) ;
315
- this . _changeTimers [ key ] = setTimeout ( ( ) => {
316
- delete this . _changeTimers [ key ] ;
317
- if ( type === ADD_EVENT && metadata ?. type === 'd' ) {
318
- // Recursively emit add events and watch for sub-files/folders
319
- common . recReaddir (
320
- path . resolve ( this . root , file ) ,
321
- ( dir , stats ) => {
322
- this . _watchdir ( dir ) ;
323
- this . _rawEmitEvent ( ADD_EVENT , path . relative ( this . root , dir ) , {
324
- modifiedTime : stats . mtime . getTime ( ) ,
325
- size : stats . size ,
326
- type : 'd' ,
327
- } ) ;
328
- } ,
329
- ( file , stats ) => {
330
- this . _register ( file ) ;
331
- this . _rawEmitEvent ( ADD_EVENT , path . relative ( this . root , file ) , {
332
- modifiedTime : stats . mtime . getTime ( ) ,
333
- size : stats . size ,
334
- type : 'f' ,
335
- } ) ;
336
- } ,
337
- function endCallback ( ) { } ,
338
- this . _checkedEmitError ,
339
- this . ignored ,
340
- ) ;
341
- } else {
349
+ const existingTimer = this . _changeTimers . get ( key ) ;
350
+ if ( existingTimer ) {
351
+ clearTimeout ( existingTimer ) ;
352
+ }
353
+ this . _changeTimers . set (
354
+ key ,
355
+ setTimeout ( ( ) => {
356
+ this . _changeTimers . delete ( key ) ;
342
357
this . _rawEmitEvent ( type , file , metadata ) ;
343
- }
344
- } , DEFAULT_DELAY ) ;
358
+ } , DEBOUNCE_MS ) ,
359
+ ) ;
345
360
}
346
361
347
362
/**
@@ -366,7 +381,8 @@ module.exports = class NodeWatcher extends EventEmitter {
366
381
function isIgnorableFileError ( error : Error | { code : string } ) {
367
382
return (
368
383
error . code === 'ENOENT' ||
369
- // Workaround Windows node issue #4337.
384
+ // Workaround Windows EPERM on watched folder deletion.
385
+ // https://github.com/nodejs/node-v0.x-archive/issues/4337
370
386
( error . code === 'EPERM' && platform === 'win32' )
371
387
) ;
372
388
}
0 commit comments