@@ -25,57 +25,89 @@ lacking in functionality (e.g., https://github.com/paulmillr/chokidar/issues/113
25
25
and declared all bugs fixed, so we steer clear. It had a lot of issues
26
26
with just noticing actual file changes.
27
27
28
+ I tried using node:fs's built-in watchFile and it randomly stopped working.
29
+ Very weird. I think this might have something to do with file paths versus inodes.
30
+
31
+ I ended up just writing a file watcher using polling from scratch.
32
+
28
33
We *always* use polling to fully support networked filesystems.
34
+ We use exponential backoff though which doesn't seem to be in any other
35
+ polling implementation, but reduces load and make sense for our use case.
29
36
*/
30
37
31
38
import { EventEmitter } from "node:events" ;
32
- import { unwatchFile , watchFile } from "node:fs" ;
33
39
import { getLogger } from "./logger" ;
34
40
import { debounce as lodashDebounce } from "lodash" ;
41
+ import { stat } from "fs/promises" ;
35
42
36
43
const logger = getLogger ( "backend:watcher" ) ;
37
44
45
+ // exponential backoff to reduce load for inactive files
46
+ const BACKOFF = 1.2 ;
47
+ const MIN_INTERVAL_MS = 750 ;
48
+ const MAX_INTERVAL_MS = 5000 ;
49
+
38
50
export class Watcher extends EventEmitter {
39
- private path : string ;
51
+ private path ?: string ;
52
+ private prev : any = null ;
53
+ private interval : number ;
54
+ private minInterval : number ;
55
+ private maxInterval : number ;
40
56
41
57
constructor (
42
58
path : string ,
43
- { debounce, interval = 750 } : { debounce ?: number ; interval ?: number } = { } ,
59
+ {
60
+ debounce,
61
+ interval = MIN_INTERVAL_MS ,
62
+ maxInterval = MAX_INTERVAL_MS ,
63
+ } : { debounce ?: number ; interval ?: number ; maxInterval ?: number } = { } ,
44
64
) {
45
65
super ( ) ;
46
- this . path = path ;
47
-
48
- logger . debug ( "watchFile" , { path, debounce, interval } ) ;
49
- watchFile ( this . path , { persistent : false , interval } , this . handleChange ) ;
50
-
51
66
if ( debounce ) {
52
67
this . emitChange = lodashDebounce ( this . emitChange , debounce ) ;
53
68
}
69
+ logger . debug ( "Watcher" , { path, debounce, interval, maxInterval } ) ;
70
+ this . path = path ;
71
+ this . minInterval = interval ;
72
+ this . maxInterval = maxInterval ;
73
+ this . interval = interval ;
74
+ setTimeout ( this . update , interval ) ;
54
75
}
55
76
56
- private emitChange = ( stats ) => {
57
- this . emit ( "change" , stats . ctime , stats ) ;
58
- } ;
59
-
60
- private handleChange = ( curr , prev ) => {
61
- const path = this . path ;
62
- if ( ! curr . dev ) {
63
- logger . debug ( "handleChange: delete" , { path } ) ;
64
- this . emit ( "delete" ) ;
77
+ private update = async ( ) => {
78
+ if ( this . path == null ) {
79
+ // closed
65
80
return ;
66
81
}
67
- if ( curr . mtimeMs == prev . mtimeMs && curr . mode == prev . mode ) {
68
- logger . debug ( "handleChange: access but no change" , { path } ) ;
69
- // just *accessing* triggers watchFile (really StatWatcher), of course.
70
- return ;
82
+ try {
83
+ const prev = this . prev ;
84
+ const curr = await stat ( this . path ) ;
85
+ if ( curr . mtimeMs != prev ?. mtimeMs || curr . mode != prev ?. mode ) {
86
+ this . prev = curr ;
87
+ this . interval = this . minInterval ;
88
+ this . emitChange ( curr ) ;
89
+ }
90
+ } catch ( _err ) {
91
+ if ( this . prev != null ) {
92
+ this . interval = this . minInterval ;
93
+ this . prev = null ;
94
+ logger . debug ( "delete" , this . path ) ;
95
+ this . emit ( "delete" ) ;
96
+ }
97
+ } finally {
98
+ setTimeout ( this . update , this . interval ) ;
99
+ this . interval = Math . min ( this . maxInterval , this . interval * BACKOFF ) ;
71
100
}
72
- logger . debug ( "handleChange: change" , { path } ) ;
73
- this . emitChange ( curr ) ;
101
+ } ;
102
+
103
+ private emitChange = ( stats ) => {
104
+ logger . debug ( "change" , this . path ) ;
105
+ this . emit ( "change" , stats . ctime , stats ) ;
74
106
} ;
75
107
76
108
close = ( ) => {
77
109
logger . debug ( "close" , this . path ) ;
78
110
this . removeAllListeners ( ) ;
79
- unwatchFile ( this . path , this . handleChange ) ;
111
+ delete this . path ;
80
112
} ;
81
113
}
0 commit comments