Skip to content

Commit 961ad40

Browse files
committed
file watching
1 parent 22126e9 commit 961ad40

4 files changed

Lines changed: 357 additions & 3 deletions

File tree

lib/Compilation.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Compilation.prototype.addModule = function(module) {
5252
if(!cacheModule.error && cacheModule.cacheable && this.fileTimestamps && this.contextTimestamps) {
5353
rebuild = cacheModule.needRebuild(this.fileTimestamps, this.contextTimestamps);
5454
}
55-
55+
5656
if(!rebuild) {
5757
cacheModule.disconnect();
5858
this._modules[identifier] = cacheModule;

lib/Compiler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Watching.prototype._done = function(err, compilation) {
7272
};
7373

7474
Watching.prototype.watch = function(files, dirs) {
75-
this.watcher = this.compiler.watchFileSystem.watch(files, dirs, this.startTime, this.watchDelay, function(err, modified, fileTimestamps, contextTimestamps) {
75+
this.watcher = this.compiler.watchFileSystem.watch(files, dirs, this.startTime, this.watchDelay, function(err, filesModified, contextModified, fileTimestamps, contextTimestamps) {
7676
this.watcher = null;
7777
if(err) return this.handler(err);
7878

lib/ContextModule.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ ContextModule.prototype.needRebuild = function(fileTimestamps, contextTimestamps
5454
return ts > this.builtTime;
5555
};
5656

57+
ContextModule.prototype.disconnect = function disconnect() {
58+
this.built = false;
59+
Module.prototype.disconnect.call(this);
60+
};
61+
5762
ContextModule.prototype.build = function(options, compilation, resolver, fs, callback) {
5863
this.built = true;
5964
this.builtTime = new Date().getTime();

lib/node/NodeWatchFileSystem.js

Lines changed: 350 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,286 @@ function NodeWatchFileSystem(inputFileSystem) {
1111
}
1212
module.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

Comments
 (0)