@@ -11,8 +11,286 @@ function NodeWatchFileSystem(inputFileSystem) {
1111}
1212module . exports = NodeWatchFileSystem ;
1313
14- NodeWatchFileSystem . prototype . watch = function ( files , dirs , startTime , delay , callback ) {
14+ /*
15+
16+ 1. merge files into context
17+
18+ 2. merge paths by common part
19+
20+ Sorted list of paths
21+
22+ /home/a
23+ /home/dir/a
24+ /home/dir/b
25+ /home/dir/bdir/x
26+ /home/dir/bdir/y
27+ /home/dir/subdir/c
28+ /home/dir/subdir/d
29+ /home/dir/subdir/e
30+ /home/z
31+ /guu
32+
33+ => find common part in between elements
34+
35+ 2
36+ 3
37+ 3
38+ 4
39+ 3
40+ 4
41+ 4
42+ 2
43+ 1
44+
45+ => find longest line of max element
46+
47+ 2
48+ 3
49+ 3
50+ 4
51+ 3
52+ 4 x
53+ 4 x
54+ 2
55+ 1
56+
57+ => get paths
58+
59+ /home/dir/subdir/c
60+ /home/dir/subdir/d
61+ /home/dir/subdir/e
62+
63+ => merge them by common part
64+
65+ /home/dir/subdir {c,d,e}
66+
67+ => go on until max path count reached.
68+
69+ 3. install watchers
70+
71+ 4. read timestamp
72+
73+ 5. on change: compare mtime to startTime
74+
75+ get mtime of directory by reading all files and max mtime
76+
77+ update new timestamps in timestampObject
78+
79+ 6. start timeout on first change, but continue updating timestamps
80+
81+ 7. on timeout: call handler and close watchers, abort async processes
82+
83+ */
84+
85+ /**
86+ *
87+ * @param files {String[]} a sorted array of paths to files
88+ * @param dirs {String[]} a sorted array of paths to directories
89+ * @param startTime {number} the virtual start time
90+ * @param delay {number} in ms, the time to wait to signal after the first change
91+ * @param callback {function(err, filesModified: String[], dirModified: String[], fileTimestamps: Object, dirTimestamps: Object)] called once after change plus delay
92+ * @param callbackUndelayed {function()} called once after first change
93+ */
94+ NodeWatchFileSystem . prototype . watch = function ( files , dirs , startTime , delay , callback , callbackUndelayed ) {
95+ if ( ! callbackUndelayed ) callbackUndelayed = function ( ) { }
1596 var closed = false ;
97+ var fileTimestamps = { } ;
98+ var dirTimestamps = { } ;
99+
100+ var items = dirs . map ( function ( path ) {
101+ return {
102+ type : 2 ,
103+ path : path ,
104+ children : null
105+ }
106+ } ) . concat ( files . map ( function ( path ) {
107+ return {
108+ type : 1 ,
109+ path : path ,
110+ children : null
111+ }
112+ } ) ) ;
113+ items . sort ( function ( a , b ) {
114+ if ( a . path == b . path ) return 0 ;
115+ return a . path < b . path ? - 1 : 1 ;
116+ } ) ;
117+
118+ // 1.
119+
120+
121+ // 2.
122+ while ( items . length > 100 )
123+ mergeCommonParts ( items ) ;
124+
125+ // 3.
126+ var initialChange = false ;
127+ var change = function ( ) {
128+ initialChange = true ;
129+ }
130+
131+ function readStat ( item , callback ) {
132+ if ( item . type == 1 ) { // file
133+ fs . stat ( item . path , function ( err , stat ) {
134+ var ts = err ? Infinity : stat . mtime . getTime ( ) ;
135+ fileTimestamps [ item . path ] = ts ;
136+ if ( ts > startTime ) {
137+ item . dirty = true ;
138+ change ( ) ;
139+ }
140+ callback ( ts ) ;
141+ } ) ;
142+ } else {
143+ fs . readdir ( item . path , function ( err , files ) {
144+ if ( err ) {
145+ item . dirty = true ;
146+ if ( item . type == 2 ) dirTimestamps [ item . path ] = Infinity ;
147+ change ( ) ;
148+ return callback ( Infinity ) ;
149+ }
150+ traverse ( item . path , files , function ( ts ) {
151+ if ( item . type == 2 ) dirTimestamps [ item . path ] = ts ;
152+ if ( ts > startTime ) {
153+ item . dirty = true ;
154+ change ( ) ;
155+ }
156+ return callback ( ts ) ;
157+ } ) ;
158+ } ) ;
159+ function flagAllDirty ( item ) {
160+ if ( item . children ) {
161+ item . children . forEach ( function ( i ) {
162+ i . dirty = true ;
163+ if ( i . type == 1 ) fileTimestamps [ i . path ] = Infinity ;
164+ else if ( i . type == 2 ) dirTimestamps [ i . path ] = Infinity ;
165+ } ) ;
166+ }
167+ }
168+ function traverse ( basePath , files , callback ) {
169+ async . map ( files , function ( file , callback ) {
170+ var thisPath = path . join ( basePath , file ) ;
171+ fs . stat ( thisPath , function ( err , stat ) {
172+ if ( err ) {
173+ // on error flag all files dirty
174+ flagAllDirty ( item ) ;
175+ return callback ( null , Infinity ) ;
176+ }
177+ var idx = item . children ? binarySearch ( item . children , function ( item ) {
178+ if ( item . path == thisPath ) return 0 ;
179+ return item . path > thisPath ? - 1 : 1 ;
180+ } ) : - 1 ;
181+ if ( idx >= 0 ) {
182+ var childItem = item . children [ idx ] ;
183+ if ( childItem . type == 1 ) {
184+ // file
185+ var ts = stat . mtime . getTime ( ) ;
186+ fileTimestamps [ childItem . path ] = ts ;
187+ if ( ts > startTime ) {
188+ childItem . dirty = true ;
189+ }
190+ return callback ( null , ts ) ;
191+ } else {
192+ // directory
193+ fs . readdir ( thisPath , function ( err , files ) {
194+ if ( err ) {
195+ // on error flag all files dirty
196+ flagAllDirty ( item ) ;
197+ return callback ( null , Infinity ) ;
198+ }
199+ traverse ( thisPath , files , function ( ts ) {
200+ dirTimestamps [ childItem . path ] = ts ;
201+ if ( ts > startTime ) {
202+ childItem . dirty = true ;
203+ }
204+ return callback ( null , ts ) ;
205+ } ) ;
206+ } ) ;
207+ }
208+ } else {
209+ if ( stat . isFile ( ) ) {
210+ return callback ( null , stat . mtime . getTime ( ) ) ;
211+ } else if ( stat . isDirectory ( ) ) {
212+ fs . readdir ( thisPath , function ( err , files ) {
213+ if ( err ) {
214+ // on error flag all files dirty
215+ flagAllDirty ( item ) ;
216+ return callback ( null , Infinity ) ;
217+ }
218+ traverse ( thisPath , files , function ( ts ) {
219+ return callback ( null , ts ) ;
220+ } ) ;
221+ } ) ;
222+ } else {
223+ // ignore other stuff
224+ return callback ( null , 0 ) ;
225+ }
226+ }
227+ } ) ;
228+ } , function ( err , timestamps ) {
229+ var ts = timestamps ? timestamps . reduce ( function ( a , b ) { return Math . max ( a , b ) ; } , 0 ) : 0 ;
230+ return callback ( ts ) ;
231+ } ) ;
232+ }
233+ }
234+ }
235+ async . forEach ( items , function ( item , callback ) {
236+ var isRunning = false ;
237+ var isScheduled = false ;
238+ item . watcher = fs . watch ( item . path , function ( ) {
239+ if ( isRunning ) return isScheduled = true ;
240+ isRunning = true ;
241+ readStat ( item , done ) ;
242+ } ) ;
243+ readStat ( item , function ( time ) {
244+ callback ( ) ;
245+ done ( ) ;
246+ } ) ;
247+ function done ( ) {
248+ if ( closed ) return ;
249+ if ( isScheduled ) {
250+ isScheduled = false ;
251+ readStat ( item , done ) ;
252+ } else {
253+ isRunning = false ;
254+ }
255+ }
256+ } , function ( ) {
257+ if ( initialChange ) {
258+
259+ // 6.
260+ callbackUndelayed ( ) ;
261+ setTimeout ( onTimeout , delay ) ;
262+
263+ } else {
264+
265+ change = function ( ) {
266+
267+ // 6.
268+ change = function ( ) { } ;
269+ callbackUndelayed ( ) ;
270+ setTimeout ( onTimeout , delay ) ;
271+ } ;
272+
273+ }
274+
275+ } ) ;
276+
277+ // 7.
278+ function onTimeout ( ) {
279+ if ( closed ) return ;
280+ callback ( null , [ ] , [ ] , fileTimestamps , dirTimestamps ) ;
281+
282+ close ( ) ;
283+ }
284+
285+ function close ( ) {
286+ closed = true ;
287+ items . forEach ( function ( item ) {
288+ item . watcher . close ( ) ;
289+ } ) ;
290+ }
291+
292+ return ;
293+
16294 setTimeout ( function ( ) {
17295 var fileTs = { } ;
18296 async . forEach ( files , function ( file , callback ) {
@@ -34,3 +312,74 @@ NodeWatchFileSystem.prototype.watch = function(files, dirs, startTime, delay, ca
34312 }
35313 }
36314} ;
315+
316+ function binarySearch ( array , comparator ) {
317+ var left = 0 ;
318+ var right = array . length - 1 ;
319+
320+ while ( left <= right ) {
321+ var middle = ( ( left + right ) / 2 ) | 0 ;
322+ var comp = comparator ( array [ middle ] ) ;
323+ if ( comp == 0 ) return middle ;
324+ if ( comp > 0 ) right = middle - 1 ;
325+ if ( comp < 0 ) left = middle + 1 ;
326+ }
327+ return - 1 ;
328+ }
329+
330+ function mergeCommonParts ( items ) {
331+ var maxCount = 1 ;
332+ var maxLength = 0 ;
333+ var maxLengthCommonPart = "" ;
334+ var maxLengthStart = - 1 ;
335+ var currentLength = 1 ;
336+ for ( var i = 1 ; i < items . length ; i ++ ) {
337+ var a = items [ i - 1 ] . path ;
338+ var b = items [ i ] . path ;
339+ var countChars = 0 ;
340+ var commonPart = 0 ;
341+ var count = 0 ;
342+ for ( var j = 0 ; j < a . length && j < b . length ; j ++ ) {
343+ if ( a [ j ] != b [ j ] ) break ;
344+ if ( a [ j ] == "/" || a [ j ] == "\\" ) {
345+ count ++ ;
346+ commonPart = countChars ;
347+ }
348+ countChars ++ ;
349+ }
350+ if ( maxCount < count ) {
351+ maxCount = count ;
352+ maxLength = 0 ;
353+ currentLength = 1 ;
354+ }
355+ if ( count == maxCount ) {
356+ currentLength ++ ;
357+ if ( maxLength < currentLength ) {
358+ maxLength = currentLength ;
359+ maxLengthStart = i - currentLength + 1 ;
360+ maxLengthCommonPart = a . substr ( 0 , commonPart ) ;
361+ }
362+ } else {
363+ currentLength = 1 ;
364+ }
365+ }
366+ if ( maxLengthCommonPart == "" ) return false ;
367+ var newItem = {
368+ type : 0 ,
369+ path : maxLengthCommonPart ,
370+ children : [ ]
371+ }
372+ for ( var i = maxLengthStart ; i < maxLengthStart + maxLength ; i ++ ) {
373+ var item = items [ i ] ;
374+ if ( item . children ) item . children . forEach ( function ( child ) {
375+ newItem . children . push ( child ) ;
376+ } ) ;
377+ item . children = null ;
378+ newItem . children . push ( item ) ;
379+ }
380+ newItem . children . sort ( function ( a , b ) {
381+ if ( a . path == b . path ) return 0 ;
382+ return a . path < b . path ? - 1 : 1 ;
383+ } ) ;
384+ items . splice ( maxLengthStart , maxLength , newItem ) ;
385+ }
0 commit comments