From 404aa8f0be1fdc29fc34d5913628407f402e9af9 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Thu, 13 Jul 2017 20:08:57 -0700 Subject: [PATCH] Logging of the watch add/remove/event --- src/server/editorServices.ts | 109 ++++++++++++++---- src/server/project.ts | 210 +++++++++++++++++------------------ src/server/scriptInfo.ts | 15 +-- src/server/utilities.ts | 73 ++++++++++++ 4 files changed, 264 insertions(+), 143 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index db7234c8824..0aed0ab99fa 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -303,6 +303,31 @@ namespace ts.server { } } + /* @internal */ + export const enum WatchType { + ConfigFilePath = "Config file for the program", + MissingFilePath = "Missing file from program", + WildCardDirectories = "Wild card directory", + TypeRoot = "Type root of the project", + ClosedScriptInfo = "Closed Script info" + } + + /* @internal */ + export const enum WatcherCloseReason { + ProjectClose = "Project close", + NotNeeded = "After project update isnt required any more", + FileCreated = "File got created", + RecursiveChanged = "Recursive changed for the watch", + ProjectReloadHitMaxSize = "Project reloaded and hit the max file size capacity", + OrphanScriptInfoWithChange = "Orphan script info, Detected change in file thats not needed any more", + OrphanScriptInfo = "Removing Orphan script info as part of cleanup", + FileDeleted = "File was deleted", + FileOpened = "File opened" + } + + /* @internal */ + export type ServerDirectoryWatcherCallback = (path: NormalizedPath) => void; + export interface ProjectServiceOptions { host: ServerHost; logger: Logger; @@ -599,7 +624,7 @@ namespace ts.server { if (!info.isScriptOpen()) { if (info.containingProjects.length === 0) { // Orphan script info, remove it as we can always reload it on next open file request - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfoWithChange); this.filenameToScriptInfo.delete(info.path); } else { @@ -613,9 +638,7 @@ namespace ts.server { } private handleDeletedFile(info: ScriptInfo) { - this.logger.info(`${info.fileName} deleted`); - - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.FileDeleted); // TODO: handle isOpen = true case @@ -644,7 +667,8 @@ namespace ts.server { } } - private onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { + /* @internal */ + onTypeRootFileChanged(project: ConfiguredProject, fileName: NormalizedPath) { this.logger.info(`Type root file ${fileName} changed`); project.getCachedServerHost().addOrDeleteFileOrFolder(fileName); project.updateTypes(); @@ -667,10 +691,10 @@ namespace ts.server { return; } - this.logger.info(`Detected source file changes: ${fileName}`); + const configFilename = project.getConfigFilePath(); + this.logger.info(`Project: ${configFilename} Detected source file add/remove: ${fileName}`); const configFileSpecs = project.configFileSpecs; - const configFilename = normalizePath(project.getConfigFilePath()); const result = getFileNamesFromConfigSpecs(configFileSpecs, getDirectoryPath(configFilename), project.getCompilerOptions(), project.getCachedServerHost(), this.hostConfiguration.extraFileExtensions); const errors = project.getAllProjectErrors(); if (result.fileNames.length === 0) { @@ -693,9 +717,7 @@ namespace ts.server { } private onConfigChangedForConfiguredProject(project: ConfiguredProject, eventKind: FileWatcherEventKind) { - const configFileName = project.getConfigFilePath(); if (eventKind === FileWatcherEventKind.Deleted) { - this.logger.info(`Config file deleted: ${configFileName}`); this.removeProject(project); // Reload the configured projects for these open files in the project as // they could be held up by another config file somewhere in the parent directory @@ -704,7 +726,6 @@ namespace ts.server { this.delayInferredProjectsRefresh(); } else { - this.logger.info(`Config file changed: ${configFileName}`); project.pendingReload = true; this.delayUpdateProjectGraphAndInferredProjectsRefresh(project); } @@ -894,7 +915,7 @@ namespace ts.server { this.filenameToScriptInfo.forEach(info => { if (!info.isScriptOpen() && info.containingProjects.length === 0) { // if there are not projects that include this script info - delete it - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.OrphanScriptInfo); this.filenameToScriptInfo.delete(info.path); } }); @@ -1155,22 +1176,26 @@ namespace ts.server { } private createAndAddConfiguredProject(configFileName: NormalizedPath, projectOptions: ProjectOptions, configFileErrors: Diagnostic[], configFileSpecs: ConfigFileSpecs, cachedServerHost: CachedServerHost, clientFileName?: string) { - const sizeLimitExceeded = this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); + const languageServiceEnabled = !this.exceededTotalSizeLimitForNonTsFiles(configFileName, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader); const project = new ConfiguredProject( configFileName, this, this.documentRegistry, projectOptions.configHasFilesProperty, projectOptions.compilerOptions, - /*languageServiceEnabled*/ !sizeLimitExceeded, + languageServiceEnabled, projectOptions.compileOnSave === undefined ? false : projectOptions.compileOnSave, cachedServerHost); this.addFilesToProjectAndUpdateGraph(project, projectOptions.files, fileNamePropertyReader, clientFileName, projectOptions.typeAcquisition, configFileErrors); project.configFileSpecs = configFileSpecs; - project.watchConfigFile((project, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind)); - project.watchWildcards(projectOptions.wildcardDirectories); - project.watchTypeRoots((project, path) => this.onTypeRootFileChanged(project, path)); + project.configFileWatcher = this.addFileWatcher(WatchType.ConfigFilePath, project, + configFileName, (_fileName, eventKind) => this.onConfigChangedForConfiguredProject(project, eventKind) + ); + if (languageServiceEnabled) { + project.watchWildcards(projectOptions.wildcardDirectories); + project.watchTypeRoots(); + } this.configuredProjects.push(project); this.sendProjectTelemetry(project.getConfigFilePath(), project, projectOptions); @@ -1314,11 +1339,13 @@ namespace ts.server { private updateConfiguredProject(project: ConfiguredProject, projectOptions: ProjectOptions, configFileErrors: Diagnostic[]) { if (this.exceededTotalSizeLimitForNonTsFiles(project.canonicalConfigFilePath, projectOptions.compilerOptions, projectOptions.files, fileNamePropertyReader)) { project.disableLanguageService(); - project.stopWatchingWildCards(); + project.stopWatchingWildCards(WatcherCloseReason.ProjectReloadHitMaxSize); + project.stopWatchingTypeRoots(WatcherCloseReason.ProjectReloadHitMaxSize); } else { project.enableLanguageService(); project.watchWildcards(projectOptions.wildcardDirectories); + project.watchTypeRoots(); } this.updateNonInferredProject(project, projectOptions.files, fileNamePropertyReader, projectOptions.compilerOptions, projectOptions.typeAcquisition, projectOptions.compileOnSave, configFileErrors); } @@ -1357,11 +1384,21 @@ namespace ts.server { return this.getScriptInfoForNormalizedPath(toNormalizedPath(uncheckedFileName)); } - watchClosedScriptInfo(info: ScriptInfo) { + private watchClosedScriptInfo(info: ScriptInfo) { + Debug.assert(!info.fileWatcher); // do not watch files with mixed content - server doesn't know how to interpret it if (!info.hasMixedContent) { const { fileName } = info; - info.setWatcher(this.host.watchFile(fileName, (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind))); + info.fileWatcher = this.addFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, fileName, + (_fileName, eventKind) => this.onSourceFileChanged(fileName, eventKind) + ); + } + } + + private stopWatchingScriptInfo(info: ScriptInfo, reason: WatcherCloseReason) { + if (info.fileWatcher) { + this.closeFileWatcher(WatchType.ClosedScriptInfo, /*project*/ undefined, info.fileName, info.fileWatcher, reason); + info.fileWatcher = undefined; } } @@ -1387,7 +1424,7 @@ namespace ts.server { } if (info) { if (openedByClient && !info.isScriptOpen()) { - info.stopWatcher(); + this.stopWatchingScriptInfo(info, WatcherCloseReason.FileOpened); info.open(fileContent); if (hasMixedContent) { info.registerFileUpdate(); @@ -1408,7 +1445,6 @@ namespace ts.server { return this.filenameToScriptInfo.get(fileName); } - setHostConfiguration(args: protocol.ConfigureRequestArguments) { if (args.file) { const info = this.getScriptInfoForNormalizedPath(toNormalizedPath(args.file)); @@ -1436,6 +1472,37 @@ namespace ts.server { } } + /* @internal */ + closeFileWatcher(watchType: WatchType, project: Project, file: string, watcher: FileWatcher, reason: WatcherCloseReason) { + this.logger.info(`FileWatcher:: Close: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType} Reason: ${reason}`); + watcher.close(); + } + + /* @internal */ + addFileWatcher(watchType: WatchType, project: Project, file: string, cb: FileWatcherCallback) { + this.logger.info(`FileWatcher:: Added: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); + return this.host.watchFile(file, (fileName, eventKind) => { + this.logger.info(`FileWatcher:: File ${FileWatcherEventKind[eventKind]}: ${file} Project: ${project ? project.getProjectName() : ""} WatchType: ${watchType}`); + cb(fileName, eventKind); + }); + } + + /* @internal */ + closeDirectoryWatcher(watchType: WatchType, project: Project, directory: string, watcher: FileWatcher, recursive: boolean, reason: WatcherCloseReason) { + this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Close: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType} Reason: ${reason}`); + watcher.close(); + } + + /* @internal */ + addDirectoryWatcher(watchType: WatchType, project: Project, directory: string, cb: ServerDirectoryWatcherCallback, recursive: boolean) { + this.logger.info(`DirectoryWatcher ${recursive ? "recursive" : ""}:: Added: ${directory} Project: ${project.getProjectName()} WatchType: ${watchType}`); + return this.host.watchDirectory(directory, fileName => { + const path = toNormalizedPath(getNormalizedAbsolutePath(fileName, directory)); + this.logger.info(`DirectoryWatcher:: EventOn: ${directory} Trigger: ${fileName} Path: ${path} Project: ${project.getProjectName()} WatchType: ${watchType}`); + cb(path); + }, recursive); + } + closeLog() { this.logger.close(); } diff --git a/src/server/project.ts b/src/server/project.ts index 5a397bdf3a4..aa492d246dc 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -117,7 +117,7 @@ namespace ts.server { private rootFilesMap: Map = createMap(); private program: Program; private externalFiles: SortedReadonlyArray; - private missingFilesMap: Map = createMap(); + private missingFilesMap: Map; private cachedUnresolvedImportsPerFile = new UnresolvedImportsMap(); private lastCachedUnresolvedImportsList: SortedReadonlyArray; @@ -327,7 +327,9 @@ namespace ts.server { this.lsHost = undefined; // Clean up file watchers waiting for missing files - this.missingFilesMap.forEach(fileWatcher => fileWatcher.close()); + cleanExistingMap(this.missingFilesMap, (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.ProjectClose); + }); this.missingFilesMap = undefined; // signal language service to release source files acquired from document registry @@ -639,38 +641,38 @@ namespace ts.server { } const missingFilePaths = this.program.getMissingFilePaths(); - const missingFilePathsSet = arrayToSet(missingFilePaths); + const newMissingFilePathMap = arrayToSet(missingFilePaths); + // Update the missing file paths watcher + this.missingFilesMap = mutateExistingMapWithNewSet( + this.missingFilesMap, newMissingFilePathMap, + // Watch the missing files + missingFilePath => { + const fileWatcher = this.projectService.addFileWatcher( + WatchType.MissingFilePath, this, missingFilePath, + (filename: string, eventKind: FileWatcherEventKind) => { + if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { + this.missingFilesMap.delete(missingFilePath); + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.FileCreated); - // Files that are no longer missing (e.g. because they are no longer required) - // should no longer be watched. - this.missingFilesMap.forEach((fileWatcher, missingFilePath) => { - if (!missingFilePathsSet.has(missingFilePath)) { - this.missingFilesMap.delete(missingFilePath); - fileWatcher.close(); - } - }); + if (this.projectKind === ProjectKind.Configured) { + const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); + (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + } - // Missing files that are not yet watched should be added to the map. - for (const missingFilePath of missingFilePaths) { - if (!this.missingFilesMap.has(missingFilePath)) { - const fileWatcher = this.projectService.host.watchFile(missingFilePath, (filename: string, eventKind: FileWatcherEventKind) => { - if (eventKind === FileWatcherEventKind.Created && this.missingFilesMap.has(missingFilePath)) { - fileWatcher.close(); - this.missingFilesMap.delete(missingFilePath); - - if (this.projectKind === ProjectKind.Configured) { - const absoluteNormalizedPath = getNormalizedAbsolutePath(filename, getDirectoryPath(missingFilePath)); - (this.lsHost.host as CachedServerHost).addOrDeleteFileOrFolder(toNormalizedPath(absoluteNormalizedPath)); + // When a missing file is created, we should update the graph. + this.markAsDirty(); + this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - - // When a missing file is created, we should update the graph. - this.markAsDirty(); - this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this); } - }); - this.missingFilesMap.set(missingFilePath, fileWatcher); + ); + return fileWatcher; + }, + // Files that are no longer missing (e.g. because they are no longer required) + // should no longer be watched. + (missingFilePath, fileWatcher) => { + this.projectService.closeFileWatcher(WatchType.MissingFilePath, this, missingFilePath, fileWatcher, WatcherCloseReason.NotNeeded); } - } + ); } const oldExternalFiles = this.externalFiles || emptyArray as SortedReadonlyArray; @@ -694,7 +696,7 @@ namespace ts.server { } isWatchedMissingFile(path: Path) { - return this.missingFilesMap.has(path); + return this.missingFilesMap && this.missingFilesMap.has(path); } getScriptInfoLSHost(fileName: string) { @@ -976,9 +978,10 @@ namespace ts.server { */ export class ConfiguredProject extends Project { private typeAcquisition: TypeAcquisition; - private configFileWatcher: FileWatcher; + /* @internal */ + configFileWatcher: FileWatcher; private directoriesWatchedForWildcards: Map; - private typeRootsWatchers: FileWatcher[]; + private typeRootsWatchers: Map; readonly canonicalConfigFilePath: NormalizedPath; /* @internal */ @@ -1023,7 +1026,7 @@ namespace ts.server { } getConfigFilePath() { - return this.getProjectName(); + return asNormalizedPath(this.getProjectName()); } enablePlugins() { @@ -1131,94 +1134,83 @@ namespace ts.server { })); } - watchConfigFile(callback: (project: ConfiguredProject, eventKind: FileWatcherEventKind) => void) { - this.configFileWatcher = this.projectService.host.watchFile(this.getConfigFilePath(), (_fileName, eventKind) => callback(this, eventKind)); - } - - watchTypeRoots(callback: (project: ConfiguredProject, path: NormalizedPath) => void) { - const roots = this.getEffectiveTypeRoots(); - const watchers: FileWatcher[] = []; - for (const root of roots) { - this.projectService.logger.info(`Add type root watcher for: ${root}`); - watchers.push(this.projectService.host.watchDirectory(root, path => callback(this, toNormalizedPath(getNormalizedAbsolutePath(path, root))), /*recursive*/ false)); - } - this.typeRootsWatchers = watchers; - } - - private addWatcherForDirectory(flag: WatchDirectoryFlags, directory: string, replaceExisting: boolean) { - if (replaceExisting || !this.directoriesWatchedForWildcards.has(directory)) { - const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; - this.projectService.logger.info(`Add ${recursive ? "recursive " : ""} watcher for: ${directory}`); - this.directoriesWatchedForWildcards.set(directory, { - watcher: this.projectService.host.watchDirectory( - directory, - path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, toNormalizedPath(getNormalizedAbsolutePath(path, directory))), - recursive - ), - recursive - }); - } - } - watchWildcards(wildcardDirectories: Map) { - if (wildcardDirectories) { - if (this.directoriesWatchedForWildcards) { - this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { - const currentFlag = wildcardDirectories.get(directory); - // Remove already watching wild card if it isnt in updated map - if (currentFlag === undefined) { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - this.directoriesWatchedForWildcards.delete(directory); - } - // Or if the recursive doesnt match (add the updated one here) - else { - const currentRecursive = (currentFlag & WatchDirectoryFlags.Recursive) !== 0; - if (currentRecursive !== recursive) { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - this.addWatcherForDirectory(currentFlag, directory, /*replaceExisting*/ true); - } - } - }); - } - else { - this.directoriesWatchedForWildcards = createMap(); - } - wildcardDirectories.forEach((flag, directory) => - this.addWatcherForDirectory(flag, directory, /*replaceExisting*/ false)); - } - else { - this.stopWatchingWildCards(); - } + this.directoriesWatchedForWildcards = mutateExistingMap( + this.directoriesWatchedForWildcards, wildcardDirectories, + // Watcher is same if the recursive flags match + ({ recursive: existingRecursive }, flag) => { + // If the recursive dont match, it needs update + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return existingRecursive !== recursive; + }, + // Create new watch + (directory, flag) => { + const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0; + return { + watcher: this.projectService.addDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, + path => this.projectService.onFileAddOrRemoveInWatchedDirectoryOfProject(this, path), + recursive + ), + recursive + }; + }, + // Close existing watch thats not needed any more + (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.NotNeeded + ), + // Close existing watch that doesnt match in recursive flag + (directory, { watcher, recursive }) => this.projectService.closeDirectoryWatcher( + WatchType.WildCardDirectories, this, directory, watcher, recursive, WatcherCloseReason.RecursiveChanged + ) + ); } - stopWatchingWildCards() { - if (this.directoriesWatchedForWildcards) { - this.directoriesWatchedForWildcards.forEach(({ watcher, recursive }, directory) => { - this.projectService.logger.info(`Removing ${recursive ? "recursive " : ""} watcher for: ${directory}`); - watcher.close(); - }); - this.directoriesWatchedForWildcards = undefined; - } + stopWatchingWildCards(reason: WatcherCloseReason) { + cleanExistingMap( + this.directoriesWatchedForWildcards, + (directory, { watcher, recursive }) => + this.projectService.closeDirectoryWatcher(WatchType.WildCardDirectories, this, + directory, watcher, recursive, reason) + ); + this.directoriesWatchedForWildcards = undefined; + } + + watchTypeRoots() { + const newTypeRoots = arrayToSet(this.getEffectiveTypeRoots(), dir => this.projectService.toCanonicalFileName(dir)); + this.typeRootsWatchers = mutateExistingMapWithNewSet( + this.typeRootsWatchers, newTypeRoots, + // Create new watch + root => this.projectService.addDirectoryWatcher(WatchType.TypeRoot, this, root, + path => this.projectService.onTypeRootFileChanged(this, path), /*recursive*/ false + ), + // Close existing watch thats not needed any more + (directory, watcher) => this.projectService.closeDirectoryWatcher( + WatchType.TypeRoot, this, directory, watcher, /*recursive*/ false, WatcherCloseReason.NotNeeded + ) + ); + } + + stopWatchingTypeRoots(reason: WatcherCloseReason) { + cleanExistingMap( + this.typeRootsWatchers, + (directory, watcher) => + this.projectService.closeDirectoryWatcher(WatchType.TypeRoot, this, + directory, watcher, /*recursive*/ false, reason) + ); + this.typeRootsWatchers = undefined; } close() { super.close(); if (this.configFileWatcher) { - this.configFileWatcher.close(); + this.projectService.closeFileWatcher(WatchType.ConfigFilePath, this, this.getConfigFilePath(), this.configFileWatcher, WatcherCloseReason.ProjectClose); this.configFileWatcher = undefined; } - if (this.typeRootsWatchers) { - for (const watcher of this.typeRootsWatchers) { - watcher.close(); - } - this.typeRootsWatchers = undefined; - } - - this.stopWatchingWildCards(); + this.stopWatchingTypeRoots(WatcherCloseReason.ProjectClose); + this.stopWatchingWildCards(WatcherCloseReason.ProjectClose); } addOpenRef() { diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 46008e77340..902e9d0d97e 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -149,7 +149,8 @@ namespace ts.server { readonly containingProjects: Project[] = []; private formatCodeSettings: FormatCodeSettings; - private fileWatcher: FileWatcher; + /* @internal */ + fileWatcher: FileWatcher; private textStorage: TextStorage; private isOpen: boolean; @@ -291,18 +292,6 @@ namespace ts.server { } } - setWatcher(watcher: FileWatcher): void { - this.stopWatcher(); - this.fileWatcher = watcher; - } - - stopWatcher() { - if (this.fileWatcher) { - this.fileWatcher.close(); - this.fileWatcher = undefined; - } - } - getLatestVersion() { return this.textStorage.getVersion(); } diff --git a/src/server/utilities.ts b/src/server/utilities.ts index 3949e7b58dd..ce49fcdb9bf 100644 --- a/src/server/utilities.ts +++ b/src/server/utilities.ts @@ -226,6 +226,79 @@ namespace ts.server { } } + export function cleanExistingMap( + existingMap: Map, + onDeleteExistingValue: (key: string, existingValue: T) => void) { + if (existingMap) { + // Remove all + existingMap.forEach((existingValue, key) => { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + }); + } + } + + export function mutateExistingMapWithNewSet( + existingMap: Map, newMap: Map, + createNewValue: (key: string) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void + ): Map { + return mutateExistingMap( + existingMap, newMap, + // Same value if the value is set in the map + /*isSameValue*/(_existingValue, _valueInNewMap) => true, + /*createNewValue*/(key, _valueInNewMap) => createNewValue(key), + onDeleteExistingValue, + // Should never be called since we say yes to same values all the time + /*OnDeleteExistingMismatchValue*/(_key, _existingValue) => notImplemented() + ); + } + + export function mutateExistingMap( + existingMap: Map, newMap: Map, + isSameValue: (existingValue: T, valueInNewMap: U) => boolean, + createNewValue: (key: string, valueInNewMap: U) => T, + onDeleteExistingValue: (key: string, existingValue: T) => void, + OnDeleteExistingMismatchValue: (key: string, existingValue: T) => void + ): Map { + // If there are new values update them + if (newMap) { + if (existingMap) { + // Needs update + existingMap.forEach((existingValue, key) => { + const valueInNewMap = newMap.get(key); + // Existing value - remove it + if (valueInNewMap === undefined) { + existingMap.delete(key); + onDeleteExistingValue(key, existingValue); + } + // different value - remove it + else if (!isSameValue(existingValue, valueInNewMap)) { + existingMap.delete(key); + OnDeleteExistingMismatchValue(key, existingValue); + } + }); + } + else { + // Create new + existingMap = createMap(); + } + + // Add new values that are not already present + newMap.forEach((valueInNewMap, key) => { + if (!existingMap.has(key)) { + // New values + existingMap.set(key, createNewValue(key, valueInNewMap)); + } + }); + + return existingMap; + } + + cleanExistingMap(existingMap, onDeleteExistingValue); + return undefined; + } + export class ThrottledOperations { private pendingTimeouts: Map = createMap(); constructor(private readonly host: ServerHost) {