Skip to content

Commit caa6eb4

Browse files
author
Zhengbo Li
committed
Reuse watchers between 'watchDirectory' and 'watchFile'
1 parent 178b2da commit caa6eb4

1 file changed

Lines changed: 118 additions & 58 deletions

File tree

src/compiler/sys.ts

Lines changed: 118 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/// <reference path="core.ts"/>
22

33
namespace ts {
4-
export type CallbackForWatchedFile = (path: string, removed?: boolean) => void;
5-
export type CallbackForWatchedDirectory = (path: string) => void;
4+
export type FileWatcherCallback = (path: string, removed?: boolean) => void;
5+
export type DirWatcherCallback = (path: string) => void;
66

77
export interface System {
88
args: string[];
@@ -11,8 +11,8 @@ namespace ts {
1111
write(s: string): void;
1212
readFile(path: string, encoding?: string): string;
1313
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
14-
watchFile?(path: string, callback: CallbackForWatchedFile): FileWatcher;
15-
watchDirectory?(path: string, callback: CallbackForWatchedDirectory, recursive?: boolean): FileWatcher;
14+
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
15+
watchDirectory?(path: string, callback: DirWatcherCallback, recursive?: boolean): FileWatcher;
1616
resolvePath(path: string): string;
1717
fileExists(path: string): boolean;
1818
directoryExists(path: string): boolean;
@@ -26,13 +26,17 @@ namespace ts {
2626

2727
interface WatchedFile {
2828
fileName: string;
29-
callback: CallbackForWatchedFile;
29+
callback: FileWatcherCallback;
3030
mtime?: Date;
3131
}
3232

3333
export interface FileWatcher {
3434
close(): void;
3535
}
36+
37+
export interface DirWatcher extends FileWatcher {
38+
referenceCount: number;
39+
}
3640

3741
declare var require: any;
3842
declare var module: any;
@@ -65,8 +69,8 @@ namespace ts {
6569
readFile(path: string): string;
6670
writeFile(path: string, contents: string): void;
6771
readDirectory(path: string, extension?: string, exclude?: string[]): string[];
68-
watchFile?(path: string, callback: CallbackForWatchedFile): FileWatcher;
69-
watchDirectory?(path: string, callback: CallbackForWatchedDirectory, recursive?: boolean): FileWatcher;
72+
watchFile?(path: string, callback: FileWatcherCallback): FileWatcher;
73+
watchDirectory?(path: string, callback: DirWatcherCallback, recursive?: boolean): FileWatcher;
7074
};
7175

7276
export var sys: System = (function () {
@@ -274,7 +278,7 @@ namespace ts {
274278
}, interval);
275279
}
276280

277-
function addFile(fileName: string, callback: CallbackForWatchedFile): WatchedFile {
281+
function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile {
278282
const file: WatchedFile = {
279283
fileName,
280284
callback,
@@ -301,53 +305,124 @@ namespace ts {
301305
};
302306
}
303307

304-
305-
306308
function createWatchedFileSet() {
307-
const watchedDirectories = createFileMap<FileWatcher>();
308-
const watchedFiles = createFileMap<CallbackForWatchedFile>();
309+
const dirWatchers = createFileMap<DirWatcher>();
310+
const recursiveDirWatchers = createFileMap<DirWatcher>();
311+
const fileWatcherCallbacks = createFileMap<FileWatcherCallback>();
312+
const dirWatcherCallbacks = createFileMap<DirWatcherCallback>();
313+
309314
const currentDirectory = process.cwd();
315+
return { addFile, removeFile, addDir };
316+
317+
function addDir(dirName: string, callback: DirWatcherCallback, recursive?: boolean) {
318+
const dirPath = toPath(dirName, currentDirectory, getCanonicalPath);
319+
dirWatcherCallbacks.set(dirPath, callback);
320+
const { watcher, isRecursive } = addDirWatcher(dirPath, recursive);
321+
return {
322+
close: () => reduceDirWatcherRefCount(watcher, dirPath, isRecursive)
323+
}
324+
}
325+
326+
function reduceDirWatcherRefCount(watcher: DirWatcher, dirPath: Path, isRecursive: boolean) {
327+
watcher.referenceCount -= 1;
328+
if (watcher.referenceCount <= 0) {
329+
watcher.close();
330+
if (isRecursive) {
331+
recursiveDirWatchers.remove(dirPath);
332+
} else {
333+
dirWatchers.remove(dirPath);
334+
}
335+
}
336+
}
337+
338+
function addDirWatcher(dirPath: Path, recursive?: boolean): { watcher: DirWatcher, isRecursive: boolean } {
339+
let watchers: FileMap<DirWatcher>;
340+
let options: { persistent: boolean, recursive?: boolean } = { persistent: true };
341+
342+
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
343+
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
344+
if (isNode4OrLater() && recursive === true) {
345+
if (recursiveDirWatchers.contains(dirPath)) {
346+
const watcher = recursiveDirWatchers.get(dirPath);
347+
watcher.referenceCount += 1;
348+
return { watcher, isRecursive: true };
349+
}
350+
watchers = recursiveDirWatchers;
351+
options.recursive = true;
352+
} else {
353+
if (dirWatchers.contains(dirPath)) {
354+
const watcher = dirWatchers.get(dirPath);
355+
watcher.referenceCount += 1;
356+
return { watcher, isRecursive: false };
357+
}
358+
watchers = dirWatchers;
359+
}
310360

311-
return { addFile, removeFile };
361+
const watcher: DirWatcher = _fs.watch(dirPath, options, (eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, dirPath));
362+
watcher.referenceCount = 1;
363+
watchers.set(dirPath, watcher);
364+
return { watcher, isRecursive: false };
365+
}
312366

313-
function addFile(fileName: string, callback: CallbackForWatchedFile): WatchedFile {
314-
const path = toPath(fileName, currentDirectory, getCanonicalPath);
315-
const parentDirPath = getDirectoryPath(path);
367+
function findDirWatcherForFile(filePath: Path): { watcher: DirWatcher, watcherPath: Path, isRecursive: boolean } {
368+
let watcher: DirWatcher;
369+
let watcherPath: Path;
370+
let isRecursive = false;
371+
recursiveDirWatchers.forEachValue(dirPath => {
372+
if (filePath.indexOf(dirPath) === 0) {
373+
watcherPath = dirPath;
374+
watcher = recursiveDirWatchers.get(dirPath);
375+
isRecursive = true;
376+
return;
377+
}
378+
});
379+
if (!watcher) {
380+
const parentDirPath = getDirectoryPath(filePath);
381+
if (dirWatchers.contains(parentDirPath)) {
382+
watcherPath = parentDirPath;
383+
watcher = dirWatchers.get(parentDirPath);
384+
}
385+
}
386+
return { watcher, watcherPath, isRecursive };
387+
}
316388

317-
if (!watchedDirectories.contains(parentDirPath)) {
318-
watchedDirectories.set(parentDirPath, _fs.watch(
319-
parentDirPath,
320-
(eventName: string, relativeFileName: string) => fileEventHandler(eventName, relativeFileName, parentDirPath)
321-
));
389+
function addFile(fileName: string, callback: FileWatcherCallback): WatchedFile {
390+
const filePath = toPath(fileName, currentDirectory, getCanonicalPath);
391+
const { watcher } = findDirWatcherForFile(filePath);
392+
if (!watcher) {
393+
addDirWatcher(getDirectoryPath(filePath));
394+
} else {
395+
watcher.referenceCount += 1;
322396
}
323-
watchedFiles.set(path, callback);
397+
fileWatcherCallbacks.set(filePath, callback);
324398
return { fileName, callback };
325399
}
326400

327401
function removeFile(file: WatchedFile) {
328-
const path = toPath(file.fileName, currentDirectory, getCanonicalPath);
329-
watchedFiles.remove(path);
330-
331-
const parentDirPath = getDirectoryPath(path);
332-
if (watchedDirectories.contains(parentDirPath)) {
333-
let hasWatchedChildren = false;
334-
watchedFiles.forEachValue((key, _) => {
335-
if (ts.getDirectoryPath(key) === parentDirPath) {
336-
hasWatchedChildren = true;
337-
}
338-
});
339-
if (!hasWatchedChildren) {
340-
watchedDirectories.get(parentDirPath).close();
341-
watchedDirectories.remove(parentDirPath);
342-
}
402+
const filePath = toPath(file.fileName, currentDirectory, getCanonicalPath);
403+
fileWatcherCallbacks.remove(filePath);
404+
405+
const { watcher, watcherPath, isRecursive } = findDirWatcherForFile(filePath);
406+
if (watcher) {
407+
reduceDirWatcherRefCount(watcher, watcherPath, isRecursive);
343408
}
344409
}
345410

346-
function fileEventHandler(eventName: string, fileName: string, basePath: string) {
347-
const path = ts.toPath(fileName, basePath, getCanonicalPath);
348-
if (watchedFiles.contains(path)) {
349-
const callback = watchedFiles.get(path);
350-
callback(fileName);
411+
/**
412+
* @param watcherPath is the path from which the watcher is triggered.
413+
*/
414+
function fileEventHandler(eventName: string, relativefileName: string, baseDirPath: Path) {
415+
// When files are deleted from disk, the triggered "rename" event would have a relativefileName of "undefined"
416+
const filePath = relativefileName === undefined ? undefined : toPath(relativefileName, baseDirPath, getCanonicalPath);
417+
// Directory callbacks are not set for file content changes, they are more often used for
418+
// adding/removing/renaming files, which corresponds to the "rename" event
419+
if (eventName === "rename" && dirWatcherCallbacks.contains(baseDirPath)) {
420+
const dirCallback = dirWatcherCallbacks.get(baseDirPath);
421+
dirCallback(filePath);
422+
}
423+
if (fileWatcherCallbacks.contains(filePath)) {
424+
const fileCallback = fileWatcherCallbacks.get(filePath);
425+
fileCallback(filePath);
351426
}
352427
}
353428
}
@@ -477,22 +552,7 @@ namespace ts {
477552
};
478553
},
479554
watchDirectory: (path, callback, recursive) => {
480-
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
481-
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
482-
const options = isNode4OrLater() ? { persistent: true } : { persistent: true, recursive: !!recursive };
483-
return _fs.watch(
484-
path,
485-
options,
486-
(eventName: string, relativeFileName: string) => {
487-
// In watchDirectory we only care about adding and removing files (when event name is
488-
// "rename"); changes made within files are handled by corresponding fileWatchers (when
489-
// event name is "change")
490-
if (eventName === "rename") {
491-
// When deleting a file, the passed baseFileName is null
492-
callback(!relativeFileName ? relativeFileName : normalizePath(ts.combinePaths(path, relativeFileName)));
493-
};
494-
}
495-
);
555+
return watchedFileSet.addDir(path, callback, recursive);
496556
},
497557
resolvePath: function (path: string): string {
498558
return _path.resolve(path);

0 commit comments

Comments
 (0)