Skip to content

Typings cache need not be a map but directly on the project #59043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/server/_namespaces/ts.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export * from "../utilities.js";
import * as protocol from "./ts.server.protocol.js";
export { protocol };
export * from "../scriptInfo.js";
export * from "../typingsCache.js";
export * from "../project.js";
export * from "../editorServices.js";
export * from "../moduleSpecifierCache.js";
Expand Down
27 changes: 18 additions & 9 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ import {
noop,
normalizePath,
normalizeSlashes,
notImplemented,
optionDeclarations,
optionsForWatch,
orderedRemoveItem,
Expand Down Expand Up @@ -170,7 +171,6 @@ import {
Msg,
NormalizedPath,
normalizedPathToPath,
nullTypingsInstaller,
PackageInstalledResponse,
PackageJsonCache,
Project,
Expand All @@ -183,7 +183,6 @@ import {
SetTypings,
ThrottledOperations,
toNormalizedPath,
TypingsCache,
WatchTypingLocations,
} from "./_namespaces/ts.server.js";
import * as protocol from "./protocol.js";
Expand Down Expand Up @@ -569,6 +568,16 @@ function findProjectByName<T extends Project>(projectName: string, projects: T[]
}
}

export const nullTypingsInstaller: ITypingsInstaller = {
isKnownTypesPackageName: returnFalse,
// Should never be called because we never provide a types registry.
installPackage: notImplemented,
enqueueInstallTypingsRequest: noop,
attach: noop,
onProjectClosed: noop,
globalTypingsCacheLocation: undefined!, // TODO: GH#18217
};

const noopConfigFileWatcher: FileWatcher = { close: noop };

/** @internal */
Expand Down Expand Up @@ -1173,9 +1182,6 @@ function createWatchFactoryHostUsingWatchEvents(service: ProjectService, canUseW
}

export class ProjectService {
/** @internal */
readonly typingsCache: TypingsCache;

/** @internal */
readonly documentRegistry: DocumentRegistry;

Expand Down Expand Up @@ -1367,8 +1373,6 @@ export class ProjectService {

this.typingsInstaller.attach(this);

this.typingsCache = new TypingsCache(this.typingsInstaller);

this.hostConfiguration = {
formatCodeOptions: getDefaultFormatCodeSettings(this.host.newLine),
preferences: emptyOptions,
Expand Down Expand Up @@ -1481,11 +1485,16 @@ export class ProjectService {
switch (response.kind) {
case ActionSet:
// Update the typing files and update the project
project.updateTypingFiles(this.typingsCache.updateTypingsForProject(response.projectName, response.compilerOptions, response.typeAcquisition, response.unresolvedImports, response.typings));
project.updateTypingFiles(
response.compilerOptions,
response.typeAcquisition,
response.unresolvedImports,
response.typings,
);
return;
case ActionInvalidate:
// Do not clear resolution cache, there was changes detected in typings, so enque typing request and let it get us correct results
this.typingsCache.enqueueInstallTypingsForProject(project, project.lastCachedUnresolvedImportsList, /*forceRefresh*/ true);
project.enqueueInstallTypingsForProject(/*forceRefresh*/ true);
return;
}
}
Expand Down
115 changes: 105 additions & 10 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
append,
ApplyCodeActionCommandResult,
arrayFrom,
arrayIsEqualTo,
arrayToMap,
BuilderState,
CachedDirectoryStructureHost,
Expand Down Expand Up @@ -143,14 +144,14 @@ import {
ModuleImportResult,
Msg,
NormalizedPath,
nullTypingsInstaller,
PackageJsonWatcher,
ProjectOptions,
ProjectService,
ScriptInfo,
ServerHost,
Session,
toNormalizedPath,
TypingsCache,
updateProjectIfDirty,
} from "./_namespaces/ts.server.js";
import * as protocol from "./protocol.js";
Expand Down Expand Up @@ -306,6 +307,62 @@ const enum TypingWatcherType {

type TypingWatchers = Map<Path, FileWatcher> & { isInvoked?: boolean; };

interface TypingsCacheEntry {
readonly typeAcquisition: TypeAcquisition;
readonly compilerOptions: CompilerOptions;
readonly typings: SortedReadonlyArray<string>;
readonly unresolvedImports: SortedReadonlyArray<string> | undefined;
/* mainly useful for debugging */
poisoned: boolean;
}

function setIsEqualTo(arr1: string[] | undefined, arr2: string[] | undefined): boolean {
if (arr1 === arr2) {
return true;
}
if ((arr1 || emptyArray).length === 0 && (arr2 || emptyArray).length === 0) {
return true;
}
const set = new Map<string, boolean>();
let unique = 0;

for (const v of arr1!) {
if (set.get(v) !== true) {
set.set(v, true);
unique++;
}
}
for (const v of arr2!) {
const isSet = set.get(v);
if (isSet === undefined) {
return false;
}
if (isSet === true) {
set.set(v, false);
unique--;
}
}
return unique === 0;
}

function typeAcquisitionChanged(opt1: TypeAcquisition, opt2: TypeAcquisition): boolean {
return opt1.enable !== opt2.enable ||
!setIsEqualTo(opt1.include, opt2.include) ||
!setIsEqualTo(opt1.exclude, opt2.exclude);
}

function compilerOptionsChanged(opt1: CompilerOptions, opt2: CompilerOptions): boolean {
// TODO: add more relevant properties
return getAllowJSCompilerOption(opt1) !== getAllowJSCompilerOption(opt2);
}

function unresolvedImportsChanged(imports1: SortedReadonlyArray<string> | undefined, imports2: SortedReadonlyArray<string> | undefined): boolean {
if (imports1 === imports2) {
return false;
}
return !arrayIsEqualTo(imports1, imports2);
}

export abstract class Project implements LanguageServiceHost, ModuleResolutionHost {
private rootFilesMap = new Map<Path, ProjectRootFile>();
private program: Program | undefined;
Expand Down Expand Up @@ -388,6 +445,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
/** @internal */
typingFiles: SortedReadonlyArray<string> = emptyArray;

private typingsCache: TypingsCacheEntry | undefined;

private typingWatchers: TypingWatchers | undefined;

/** @internal */
Expand Down Expand Up @@ -578,21 +637,17 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}

isKnownTypesPackageName(name: string): boolean {
return this.typingsCache.isKnownTypesPackageName(name);
return this.projectService.typingsInstaller.isKnownTypesPackageName(name);
}
installPackage(options: InstallPackageOptions): Promise<ApplyCodeActionCommandResult> {
return this.typingsCache.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) });
return this.projectService.typingsInstaller.installPackage({ ...options, projectName: this.projectName, projectRootPath: this.toPath(this.currentDirectory) });
}

/** @internal */
getGlobalTypingsCacheLocation() {
return this.getGlobalCache();
}

private get typingsCache(): TypingsCache {
return this.projectService.typingsCache;
}

/** @internal */
getSymlinkCache(): SymlinkCache {
if (!this.symlinks) {
Expand Down Expand Up @@ -1059,7 +1114,8 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}

close() {
this.projectService.typingsCache.onProjectClosed(this);
if (this.typingsCache) this.projectService.typingsInstaller.onProjectClosed(this);
this.typingsCache = undefined;
this.closeWatchingTypingLocations();
// if we have a program - release all files that are enlisted in program but arent root
// The releasing of the roots happens later
Expand Down Expand Up @@ -1379,7 +1435,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
this.lastCachedUnresolvedImportsList = getUnresolvedImports(this.program!, this.cachedUnresolvedImportsPerFile);
}

this.projectService.typingsCache.enqueueInstallTypingsForProject(this, this.lastCachedUnresolvedImportsList, hasAddedorRemovedFiles);
this.enqueueInstallTypingsForProject(hasAddedorRemovedFiles);
}
else {
this.lastCachedUnresolvedImportsList = undefined;
Expand All @@ -1401,7 +1457,46 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
}

/** @internal */
updateTypingFiles(typingFiles: SortedReadonlyArray<string>) {
enqueueInstallTypingsForProject(forceRefresh: boolean) {
const typeAcquisition = this.getTypeAcquisition();

if (!typeAcquisition || !typeAcquisition.enable || this.projectService.typingsInstaller === nullTypingsInstaller) {
return;
}

const entry = this.typingsCache;
if (
forceRefresh ||
!entry ||
typeAcquisitionChanged(typeAcquisition, entry.typeAcquisition) ||
compilerOptionsChanged(this.getCompilationSettings(), entry.compilerOptions) ||
unresolvedImportsChanged(this.lastCachedUnresolvedImportsList, entry.unresolvedImports)
) {
// Note: entry is now poisoned since it does not really contain typings for a given combination of compiler options\typings options.
// instead it acts as a placeholder to prevent issuing multiple requests
this.typingsCache = {
compilerOptions: this.getCompilationSettings(),
typeAcquisition,
typings: entry ? entry.typings : emptyArray,
unresolvedImports: this.lastCachedUnresolvedImportsList,
poisoned: true,
};
// something has been changed, issue a request to update typings
this.projectService.typingsInstaller.enqueueInstallTypingsRequest(this, typeAcquisition, this.lastCachedUnresolvedImportsList);
}
}

/** @internal */
updateTypingFiles(compilerOptions: CompilerOptions, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>, newTypings: string[]) {
const typings = sort(newTypings);
this.typingsCache = {
compilerOptions,
typeAcquisition,
typings,
unresolvedImports,
poisoned: false,
};
const typingFiles = !typeAcquisition || !typeAcquisition.enable ? emptyArray : typings;
if (enumerateInsertsAndDeletes<string, string>(typingFiles, this.typingFiles, getStringComparer(!this.useCaseSensitiveFileNames()), /*inserted*/ noop, removed => this.detachScriptInfoFromProject(removed))) {
// If typing files changed, then only schedule project update
this.typingFiles = typingFiles;
Expand Down
25 changes: 25 additions & 0 deletions src/server/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
import {
ApplyCodeActionCommandResult,
DirectoryWatcherCallback,
FileWatcher,
FileWatcherCallback,
InstallPackageOptions,
Path,
SortedReadonlyArray,
System,
TypeAcquisition,
WatchOptions,
} from "./_namespaces/ts.js";
import {
Project,
ProjectService,
} from "./_namespaces/ts.server.js";

export interface CompressedData {
length: number;
Expand All @@ -30,3 +39,19 @@ export interface ServerHost extends System {
/** @internal */
importPlugin?(root: string, moduleName: string): Promise<ModuleImportResult>;
}

export interface InstallPackageOptionsWithProject extends InstallPackageOptions {
projectName: string;
projectRootPath: Path;
}

// for backwards-compatibility
// eslint-disable-next-line @typescript-eslint/naming-convention
export interface ITypingsInstaller {
isKnownTypesPackageName(name: string): boolean;
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult>;
enqueueInstallTypingsRequest(p: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string> | undefined): void;
attach(projectService: ProjectService): void;
onProjectClosed(p: Project): void;
readonly globalTypingsCacheLocation: string | undefined;
}
Loading