Skip to content

Commit bcfa02f

Browse files
committed
Add option to watch using fs.watch instead of watching through polling
1 parent fa8d4cb commit bcfa02f

1 file changed

Lines changed: 95 additions & 32 deletions

File tree

src/compiler/sys.ts

Lines changed: 95 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,14 @@ namespace ts {
521521
// Use polling interval based on priority when create watch using host.watchFile
522522
return fsWatchFile;
523523
case "DynamicPriorityPolling":
524+
// Use polling interval but change the interval depending on file changes and their default polling interval
524525
return createDynamicPriorityPollingWatchFile(nodeSystem);
526+
case "UseFsEvents":
527+
// Use notifications from FS to watch with falling back to fs.watchFile
528+
return watchFileUsingFsWatch;
529+
case "UseFsEventsWithFallbackDynamicPolling":
530+
// Use notifications from FS to watch with falling back to dynamic watch file
531+
return watchFileUsingDynamicWatchFile;
525532
}
526533
return useNonPollingWatchers ?
527534
createNonPollingWatchFile() :
@@ -607,24 +614,58 @@ namespace ts {
607614
}
608615
}
609616

610-
function fsWatchDirectory(directoryName: string, callback: (eventName: string, relativeFileName: string) => void, recursive?: boolean): FileWatcher {
617+
type FsWatchCallback = (eventName: "rename" | "change", relativeFileName: string) => void;
618+
619+
function createFsWatchFileCallback(callback: FsWatchCallback): FileWatcherCallback {
620+
return (_fileName, eventKind) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "");
621+
}
622+
623+
function createFsWatchCallback(fileName: string, callback: FileWatcherCallback): FsWatchCallback {
624+
return eventName => {
625+
if (eventName === "rename") {
626+
callback(fileName, fileExists(fileName) ? FileWatcherEventKind.Created : FileWatcherEventKind.Deleted);
627+
}
628+
else {
629+
// Change
630+
callback(fileName, FileWatcherEventKind.Changed);
631+
}
632+
};
633+
}
634+
635+
function fsWatch(fileOrDirectory: string, entryKind: FileSystemEntryKind.File | FileSystemEntryKind.Directory, callback: FsWatchCallback, recursive: boolean, fallbackPollingWatchFile: HostWatchFile, pollingInterval?: number): FileWatcher {
611636
let options: any;
612-
/** Watcher for the directory depending on whether it is missing or present */
613-
let watcher = !directoryExists(directoryName) ?
614-
watchMissingDirectory() :
615-
watchPresentDirectory();
637+
/** Watcher for the file system entry depending on whether it is missing or present */
638+
let watcher = !fileSystemEntryExists(fileOrDirectory, entryKind) ?
639+
watchMissingFileSystemEntry() :
640+
watchPresentFileSystemEntry();
616641
return {
617642
close: () => {
618-
// Close the watcher (either existing directory watcher or missing directory watcher)
643+
// Close the watcher (either existing file system entry watcher or missing file system entry watcher)
619644
watcher.close();
645+
watcher = undefined;
620646
}
621647
};
622648

623649
/**
624-
* Watch the directory that is currently present
625-
* and when the watched directory is deleted, switch to missing directory watcher
650+
* Invoke the callback with rename and update the watcher if not closed
651+
* @param createWatcher
652+
*/
653+
function invokeCallbackAndUpdateWatcher(createWatcher: () => FileWatcher) {
654+
// Call the callback for current directory
655+
callback("rename", "");
656+
657+
// If watcher is not closed, update it
658+
if (watcher) {
659+
watcher.close();
660+
watcher = createWatcher();
661+
}
662+
}
663+
664+
/**
665+
* Watch the file or directory that is currently present
666+
* and when the watched file or directory is deleted, switch to missing file system entry watcher
626667
*/
627-
function watchPresentDirectory(): FileWatcher {
668+
function watchPresentFileSystemEntry(): FileWatcher {
628669
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
629670
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
630671
if (options === undefined) {
@@ -635,40 +676,62 @@ namespace ts {
635676
options = { persistent: true };
636677
}
637678
}
679+
try {
638680

639-
const dirWatcher = _fs.watch(
640-
directoryName,
641-
options,
642-
callback
643-
);
644-
dirWatcher.on("error", () => {
645-
// Watch the missing directory
646-
watcher.close();
647-
watcher = watchMissingDirectory();
648-
// Call the callback for current directory
649-
callback("rename", "");
650-
});
651-
return dirWatcher;
681+
const presentWatcher = _fs.watch(
682+
fileOrDirectory,
683+
options,
684+
callback
685+
);
686+
// Watch the missing file or directory or error
687+
presentWatcher.on("error", () => invokeCallbackAndUpdateWatcher(watchMissingFileSystemEntry));
688+
return presentWatcher;
689+
}
690+
catch (e) {
691+
// Catch the exception and use polling instead
692+
// Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
693+
// so instead of throwing error, use fs.watchFile
694+
return watchPresentFileSystemEntryWithFsWatchFile();
695+
}
696+
}
697+
698+
/**
699+
* Watch the file or directory using fs.watchFile since fs.watch threw exception
700+
* Eg. on linux the number of watches are limited and one could easily exhaust watches and the exception ENOSPC is thrown when creating watcher at that point
701+
*/
702+
function watchPresentFileSystemEntryWithFsWatchFile(): FileWatcher {
703+
return fallbackPollingWatchFile(fileOrDirectory, createFsWatchFileCallback(callback), pollingInterval);
652704
}
653705

654706
/**
655-
* Watch the directory that is missing
656-
* and switch to existing directory when the directory is created
707+
* Watch the file or directory that is missing
708+
* and switch to existing file or directory when the missing filesystem entry is created
657709
*/
658-
function watchMissingDirectory(): FileWatcher {
659-
return fsWatchFile(directoryName, (_fileName, eventKind) => {
660-
if (eventKind === FileWatcherEventKind.Created && directoryExists(directoryName)) {
661-
watcher.close();
662-
watcher = watchPresentDirectory();
663-
// Call the callback for current directory
710+
function watchMissingFileSystemEntry(): FileWatcher {
711+
return fallbackPollingWatchFile(fileOrDirectory, (_fileName, eventKind) => {
712+
if (eventKind === FileWatcherEventKind.Created && fileSystemEntryExists(fileOrDirectory, entryKind)) {
713+
// Call the callback for current file or directory
664714
// For now it could be callback for the inner directory creation,
665715
// but just return current directory, better than current no-op
666-
callback("rename", "");
716+
invokeCallbackAndUpdateWatcher(watchPresentFileSystemEntry);
667717
}
668-
});
718+
}, pollingInterval);
669719
}
670720
}
671721

722+
function watchFileUsingFsWatch(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
723+
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, fsWatchFile, pollingInterval);
724+
}
725+
726+
function watchFileUsingDynamicWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval?: number) {
727+
const watchFile = createDynamicPriorityPollingWatchFile(nodeSystem);
728+
return fsWatch(fileName, FileSystemEntryKind.File, createFsWatchCallback(fileName, callback), /*recursive*/ false, watchFile, pollingInterval);
729+
}
730+
731+
function fsWatchDirectory(directoryName: string, callback: FsWatchCallback, recursive?: boolean): FileWatcher {
732+
return fsWatch(directoryName, FileSystemEntryKind.Directory, callback, !!recursive, fsWatchFile);
733+
}
734+
672735
function readFile(fileName: string, _encoding?: string): string | undefined {
673736
if (!fileExists(fileName)) {
674737
return undefined;

0 commit comments

Comments
 (0)