mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-02-09 12:15:34 -06:00
256 lines
10 KiB
TypeScript
256 lines
10 KiB
TypeScript
import {
|
|
ApplyCodeActionCommandResult,
|
|
assertType,
|
|
createQueue,
|
|
Debug,
|
|
JsTyping,
|
|
MapLike,
|
|
server,
|
|
SortedReadonlyArray,
|
|
TypeAcquisition,
|
|
} from "./_namespaces/ts.js";
|
|
import {
|
|
ActionInvalidate,
|
|
ActionPackageInstalled,
|
|
ActionSet,
|
|
ActionWatchTypingLocations,
|
|
BeginInstallTypes,
|
|
createInstallTypingsRequest,
|
|
DiscoverTypings,
|
|
EndInstallTypes,
|
|
Event,
|
|
EventBeginInstallTypes,
|
|
EventEndInstallTypes,
|
|
EventInitializationFailed,
|
|
EventTypesRegistry,
|
|
InitializationFailedResponse,
|
|
InstallPackageOptionsWithProject,
|
|
InstallPackageRequest,
|
|
InvalidateCachedTypings,
|
|
ITypingsInstaller,
|
|
Logger,
|
|
LogLevel,
|
|
PackageInstalledResponse,
|
|
Project,
|
|
ProjectService,
|
|
protocol,
|
|
ServerHost,
|
|
SetTypings,
|
|
stringifyIndented,
|
|
TypesRegistryResponse,
|
|
TypingInstallerRequestUnion,
|
|
} from "./_namespaces/ts.server.js";
|
|
|
|
/** @internal */
|
|
export interface TypingsInstallerWorkerProcess {
|
|
send<T extends TypingInstallerRequestUnion>(rq: T): void;
|
|
}
|
|
|
|
interface PackageInstallPromise {
|
|
resolve(value: ApplyCodeActionCommandResult): void;
|
|
reject(reason: unknown): void;
|
|
}
|
|
|
|
/** @internal */
|
|
export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
|
|
protected installer!: TypingsInstallerWorkerProcess;
|
|
private projectService!: ProjectService;
|
|
protected activeRequestCount = 0;
|
|
private requestQueue = createQueue<DiscoverTypings>();
|
|
private requestMap = new Map<string, DiscoverTypings>(); // Maps project name to newest requestQueue entry for that project
|
|
/** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
|
|
private requestedRegistry = false;
|
|
private typesRegistryCache: Map<string, MapLike<string>> | undefined;
|
|
|
|
// This number is essentially arbitrary. Processing more than one typings request
|
|
// at a time makes sense, but having too many in the pipe results in a hang
|
|
// (see https://github.com/nodejs/node/issues/7657).
|
|
// It would be preferable to base our limit on the amount of space left in the
|
|
// buffer, but we have yet to find a way to retrieve that value.
|
|
private static readonly requestDelayMillis = 100;
|
|
private packageInstalledPromise: Map<number, PackageInstallPromise> | undefined;
|
|
private packageInstallId = 0;
|
|
|
|
constructor(
|
|
protected readonly telemetryEnabled: boolean,
|
|
protected readonly logger: Logger,
|
|
protected readonly host: ServerHost,
|
|
readonly globalTypingsCacheLocation: string,
|
|
protected event: Event,
|
|
private readonly maxActiveRequestCount: number,
|
|
) {
|
|
}
|
|
|
|
isKnownTypesPackageName(name: string): boolean {
|
|
// We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package.
|
|
const validationResult = JsTyping.validatePackageName(name);
|
|
if (validationResult !== JsTyping.NameValidationResult.Ok) {
|
|
return false;
|
|
}
|
|
if (!this.requestedRegistry) {
|
|
this.requestedRegistry = true;
|
|
this.installer.send({ kind: "typesRegistry" });
|
|
}
|
|
return !!this.typesRegistryCache?.has(name);
|
|
}
|
|
|
|
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> {
|
|
this.packageInstallId++;
|
|
const request: InstallPackageRequest = { kind: "installPackage", ...options, id: this.packageInstallId };
|
|
const promise = new Promise<ApplyCodeActionCommandResult>((resolve, reject) => {
|
|
(this.packageInstalledPromise ??= new Map()).set(this.packageInstallId, { resolve, reject });
|
|
});
|
|
this.installer.send(request);
|
|
return promise;
|
|
}
|
|
|
|
attach(projectService: ProjectService) {
|
|
this.projectService = projectService;
|
|
this.installer = this.createInstallerProcess();
|
|
}
|
|
|
|
onProjectClosed(p: Project): void {
|
|
this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" });
|
|
}
|
|
|
|
enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void {
|
|
const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Scheduling throttled operation:${stringifyIndented(request)}`);
|
|
}
|
|
|
|
if (this.activeRequestCount < this.maxActiveRequestCount) {
|
|
this.scheduleRequest(request);
|
|
}
|
|
else {
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Deferring request for: ${request.projectName}`);
|
|
}
|
|
this.requestQueue.enqueue(request);
|
|
this.requestMap.set(request.projectName, request);
|
|
}
|
|
}
|
|
|
|
handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse | server.WatchTypingLocations) {
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Received response:${stringifyIndented(response)}`);
|
|
}
|
|
|
|
switch (response.kind) {
|
|
case EventTypesRegistry:
|
|
this.typesRegistryCache = new Map(Object.entries(response.typesRegistry));
|
|
break;
|
|
case ActionPackageInstalled: {
|
|
const promise = this.packageInstalledPromise?.get(response.id);
|
|
Debug.assertIsDefined(promise, "Should find the promise for package install");
|
|
this.packageInstalledPromise?.delete(response.id);
|
|
if (response.success) {
|
|
promise.resolve({ successMessage: response.message });
|
|
}
|
|
else {
|
|
promise.reject(response.message);
|
|
}
|
|
this.projectService.updateTypingsForProject(response);
|
|
|
|
// The behavior is the same as for setTypings, so send the same event.
|
|
this.event(response, "setTypings");
|
|
break;
|
|
}
|
|
case EventInitializationFailed: {
|
|
const body: protocol.TypesInstallerInitializationFailedEventBody = {
|
|
message: response.message,
|
|
};
|
|
const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed";
|
|
this.event(body, eventName);
|
|
break;
|
|
}
|
|
case EventBeginInstallTypes: {
|
|
const body: protocol.BeginInstallTypesEventBody = {
|
|
eventId: response.eventId,
|
|
packages: response.packagesToInstall,
|
|
};
|
|
const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes";
|
|
this.event(body, eventName);
|
|
break;
|
|
}
|
|
case EventEndInstallTypes: {
|
|
if (this.telemetryEnabled) {
|
|
const body: protocol.TypingsInstalledTelemetryEventBody = {
|
|
telemetryEventName: "typingsInstalled",
|
|
payload: {
|
|
installedPackages: response.packagesToInstall.join(","),
|
|
installSuccess: response.installSuccess,
|
|
typingsInstallerVersion: response.typingsInstallerVersion,
|
|
},
|
|
};
|
|
const eventName: protocol.TelemetryEventName = "telemetry";
|
|
this.event(body, eventName);
|
|
}
|
|
|
|
const body: protocol.EndInstallTypesEventBody = {
|
|
eventId: response.eventId,
|
|
packages: response.packagesToInstall,
|
|
success: response.installSuccess,
|
|
};
|
|
const eventName: protocol.EndInstallTypesEventName = "endInstallTypes";
|
|
this.event(body, eventName);
|
|
break;
|
|
}
|
|
case ActionInvalidate: {
|
|
this.projectService.updateTypingsForProject(response);
|
|
break;
|
|
}
|
|
case ActionSet: {
|
|
if (this.activeRequestCount > 0) {
|
|
this.activeRequestCount--;
|
|
}
|
|
else {
|
|
Debug.fail("TIAdapter:: Received too many responses");
|
|
}
|
|
|
|
while (!this.requestQueue.isEmpty()) {
|
|
const queuedRequest = this.requestQueue.dequeue();
|
|
if (this.requestMap.get(queuedRequest.projectName) === queuedRequest) {
|
|
this.requestMap.delete(queuedRequest.projectName);
|
|
this.scheduleRequest(queuedRequest);
|
|
break;
|
|
}
|
|
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Skipping defunct request for: ${queuedRequest.projectName}`);
|
|
}
|
|
}
|
|
|
|
this.projectService.updateTypingsForProject(response);
|
|
this.event(response, "setTypings");
|
|
|
|
break;
|
|
}
|
|
case ActionWatchTypingLocations:
|
|
this.projectService.watchTypingLocations(response);
|
|
break;
|
|
default:
|
|
assertType<never>(response);
|
|
}
|
|
}
|
|
|
|
scheduleRequest(request: DiscoverTypings) {
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Scheduling request for: ${request.projectName}`);
|
|
}
|
|
this.activeRequestCount++;
|
|
this.host.setTimeout(
|
|
() => {
|
|
if (this.logger.hasLevel(LogLevel.verbose)) {
|
|
this.logger.info(`TIAdapter:: Sending request:${stringifyIndented(request)}`);
|
|
}
|
|
this.installer.send(request);
|
|
},
|
|
TypingsInstallerAdapter.requestDelayMillis,
|
|
`${request.projectName}::${request.kind}`,
|
|
);
|
|
}
|
|
|
|
protected abstract createInstallerProcess(): TypingsInstallerWorkerProcess;
|
|
}
|