Skip to content

Commit e170bc5

Browse files
authored
Pull out parts of TI Adapter so we can test that more correctly instead of having to copy things (#56387)
1 parent b970fa4 commit e170bc5

File tree

115 files changed

+4860
-1194
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

115 files changed

+4860
-1194
lines changed

src/server/_namespaces/ts.server.ts

+1
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ export * from "../moduleSpecifierCache";
1515
export * from "../packageJsonCache";
1616
export * from "../session";
1717
export * from "../scriptVersionCache";
18+
export * from "../typingInstallerAdapter";

src/server/typingInstallerAdapter.ts

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
import {
2+
ApplyCodeActionCommandResult,
3+
assertType,
4+
createQueue,
5+
Debug,
6+
JsTyping,
7+
MapLike,
8+
server,
9+
SortedReadonlyArray,
10+
TypeAcquisition,
11+
} from "./_namespaces/ts";
12+
import {
13+
ActionInvalidate,
14+
ActionPackageInstalled,
15+
ActionSet,
16+
ActionWatchTypingLocations,
17+
BeginInstallTypes,
18+
createInstallTypingsRequest,
19+
DiscoverTypings,
20+
EndInstallTypes,
21+
Event,
22+
EventBeginInstallTypes,
23+
EventEndInstallTypes,
24+
EventInitializationFailed,
25+
EventTypesRegistry,
26+
InitializationFailedResponse,
27+
InstallPackageOptionsWithProject,
28+
InstallPackageRequest,
29+
InvalidateCachedTypings,
30+
ITypingsInstaller,
31+
Logger,
32+
LogLevel,
33+
PackageInstalledResponse,
34+
Project,
35+
ProjectService,
36+
protocol,
37+
ServerHost,
38+
SetTypings,
39+
stringifyIndented,
40+
TypesRegistryResponse,
41+
TypingInstallerRequestUnion,
42+
} from "./_namespaces/ts.server";
43+
44+
/** @internal */
45+
export interface TypingsInstallerWorkerProcess {
46+
send<T extends TypingInstallerRequestUnion>(rq: T): void;
47+
}
48+
49+
/** @internal */
50+
export abstract class TypingsInstallerAdapter implements ITypingsInstaller {
51+
protected installer!: TypingsInstallerWorkerProcess;
52+
private projectService!: ProjectService;
53+
protected activeRequestCount = 0;
54+
private requestQueue = createQueue<DiscoverTypings>();
55+
private requestMap = new Map<string, DiscoverTypings>(); // Maps project name to newest requestQueue entry for that project
56+
/** We will lazily request the types registry on the first call to `isKnownTypesPackageName` and store it in `typesRegistryCache`. */
57+
private requestedRegistry = false;
58+
private typesRegistryCache: Map<string, MapLike<string>> | undefined;
59+
60+
// This number is essentially arbitrary. Processing more than one typings request
61+
// at a time makes sense, but having too many in the pipe results in a hang
62+
// (see https://github.com/nodejs/node/issues/7657).
63+
// It would be preferable to base our limit on the amount of space left in the
64+
// buffer, but we have yet to find a way to retrieve that value.
65+
private static readonly requestDelayMillis = 100;
66+
private packageInstalledPromise: {
67+
resolve(value: ApplyCodeActionCommandResult): void;
68+
reject(reason: unknown): void;
69+
} | undefined;
70+
71+
constructor(
72+
protected readonly telemetryEnabled: boolean,
73+
protected readonly logger: Logger,
74+
protected readonly host: ServerHost,
75+
readonly globalTypingsCacheLocation: string,
76+
protected event: Event,
77+
private readonly maxActiveRequestCount: number,
78+
) {
79+
}
80+
81+
isKnownTypesPackageName(name: string): boolean {
82+
// We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package.
83+
const validationResult = JsTyping.validatePackageName(name);
84+
if (validationResult !== JsTyping.NameValidationResult.Ok) {
85+
return false;
86+
}
87+
if (!this.requestedRegistry) {
88+
this.requestedRegistry = true;
89+
this.installer.send({ kind: "typesRegistry" });
90+
}
91+
return !!this.typesRegistryCache?.has(name);
92+
}
93+
94+
installPackage(options: InstallPackageOptionsWithProject): Promise<ApplyCodeActionCommandResult> {
95+
this.installer.send<InstallPackageRequest>({ kind: "installPackage", ...options });
96+
Debug.assert(this.packageInstalledPromise === undefined);
97+
return new Promise<ApplyCodeActionCommandResult>((resolve, reject) => {
98+
this.packageInstalledPromise = { resolve, reject };
99+
});
100+
}
101+
102+
attach(projectService: ProjectService) {
103+
this.projectService = projectService;
104+
this.installer = this.createInstallerProcess();
105+
}
106+
107+
onProjectClosed(p: Project): void {
108+
this.installer.send({ projectName: p.getProjectName(), kind: "closeProject" });
109+
}
110+
111+
enqueueInstallTypingsRequest(project: Project, typeAcquisition: TypeAcquisition, unresolvedImports: SortedReadonlyArray<string>): void {
112+
const request = createInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
113+
if (this.logger.hasLevel(LogLevel.verbose)) {
114+
this.logger.info(`TIAdapter:: Scheduling throttled operation:${stringifyIndented(request)}`);
115+
}
116+
117+
if (this.activeRequestCount < this.maxActiveRequestCount) {
118+
this.scheduleRequest(request);
119+
}
120+
else {
121+
if (this.logger.hasLevel(LogLevel.verbose)) {
122+
this.logger.info(`TIAdapter:: Deferring request for: ${request.projectName}`);
123+
}
124+
this.requestQueue.enqueue(request);
125+
this.requestMap.set(request.projectName, request);
126+
}
127+
}
128+
129+
handleMessage(response: TypesRegistryResponse | PackageInstalledResponse | SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes | InitializationFailedResponse | server.WatchTypingLocations) {
130+
if (this.logger.hasLevel(LogLevel.verbose)) {
131+
this.logger.info(`TIAdapter:: Received response:${stringifyIndented(response)}`);
132+
}
133+
134+
switch (response.kind) {
135+
case EventTypesRegistry:
136+
this.typesRegistryCache = new Map(Object.entries(response.typesRegistry));
137+
break;
138+
case ActionPackageInstalled: {
139+
const { success, message } = response;
140+
if (success) {
141+
this.packageInstalledPromise!.resolve({ successMessage: message });
142+
}
143+
else {
144+
this.packageInstalledPromise!.reject(message);
145+
}
146+
this.packageInstalledPromise = undefined;
147+
148+
this.projectService.updateTypingsForProject(response);
149+
150+
// The behavior is the same as for setTypings, so send the same event.
151+
this.event(response, "setTypings");
152+
break;
153+
}
154+
case EventInitializationFailed: {
155+
const body: protocol.TypesInstallerInitializationFailedEventBody = {
156+
message: response.message,
157+
};
158+
const eventName: protocol.TypesInstallerInitializationFailedEventName = "typesInstallerInitializationFailed";
159+
this.event(body, eventName);
160+
break;
161+
}
162+
case EventBeginInstallTypes: {
163+
const body: protocol.BeginInstallTypesEventBody = {
164+
eventId: response.eventId,
165+
packages: response.packagesToInstall,
166+
};
167+
const eventName: protocol.BeginInstallTypesEventName = "beginInstallTypes";
168+
this.event(body, eventName);
169+
break;
170+
}
171+
case EventEndInstallTypes: {
172+
if (this.telemetryEnabled) {
173+
const body: protocol.TypingsInstalledTelemetryEventBody = {
174+
telemetryEventName: "typingsInstalled",
175+
payload: {
176+
installedPackages: response.packagesToInstall.join(","),
177+
installSuccess: response.installSuccess,
178+
typingsInstallerVersion: response.typingsInstallerVersion,
179+
},
180+
};
181+
const eventName: protocol.TelemetryEventName = "telemetry";
182+
this.event(body, eventName);
183+
}
184+
185+
const body: protocol.EndInstallTypesEventBody = {
186+
eventId: response.eventId,
187+
packages: response.packagesToInstall,
188+
success: response.installSuccess,
189+
};
190+
const eventName: protocol.EndInstallTypesEventName = "endInstallTypes";
191+
this.event(body, eventName);
192+
break;
193+
}
194+
case ActionInvalidate: {
195+
this.projectService.updateTypingsForProject(response);
196+
break;
197+
}
198+
case ActionSet: {
199+
if (this.activeRequestCount > 0) {
200+
this.activeRequestCount--;
201+
}
202+
else {
203+
Debug.fail("TIAdapter:: Received too many responses");
204+
}
205+
206+
while (!this.requestQueue.isEmpty()) {
207+
const queuedRequest = this.requestQueue.dequeue();
208+
if (this.requestMap.get(queuedRequest.projectName) === queuedRequest) {
209+
this.requestMap.delete(queuedRequest.projectName);
210+
this.scheduleRequest(queuedRequest);
211+
break;
212+
}
213+
214+
if (this.logger.hasLevel(LogLevel.verbose)) {
215+
this.logger.info(`TIAdapter:: Skipping defunct request for: ${queuedRequest.projectName}`);
216+
}
217+
}
218+
219+
this.projectService.updateTypingsForProject(response);
220+
this.event(response, "setTypings");
221+
222+
break;
223+
}
224+
case ActionWatchTypingLocations:
225+
this.projectService.watchTypingLocations(response);
226+
break;
227+
default:
228+
assertType<never>(response);
229+
}
230+
}
231+
232+
scheduleRequest(request: DiscoverTypings) {
233+
if (this.logger.hasLevel(LogLevel.verbose)) {
234+
this.logger.info(`TIAdapter:: Scheduling request for: ${request.projectName}`);
235+
}
236+
this.activeRequestCount++;
237+
this.host.setTimeout(
238+
() => {
239+
if (this.logger.hasLevel(LogLevel.verbose)) {
240+
this.logger.info(`TIAdapter:: Sending request:${stringifyIndented(request)}`);
241+
}
242+
this.installer.send(request);
243+
},
244+
TypingsInstallerAdapter.requestDelayMillis,
245+
`${request.projectName}::${request.kind}`,
246+
);
247+
}
248+
249+
protected abstract createInstallerProcess(): TypingsInstallerWorkerProcess;
250+
}

src/server/typingsCache.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,8 @@ export class TypingsCache {
157157
}
158158

159159
onProjectClosed(project: Project) {
160-
this.perProjectCache.delete(project.getProjectName());
161-
this.installer.onProjectClosed(project);
160+
if (this.perProjectCache.delete(project.getProjectName())) {
161+
this.installer.onProjectClosed(project);
162+
}
162163
}
163164
}

src/testRunner/unittests/helpers/tsserver.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "./solutionBuilder";
1313
import {
1414
customTypesMap,
15-
TestTypingsInstaller,
15+
TestTypingsInstallerAdapter,
1616
TestTypingsInstallerOptions,
1717
} from "./typingsInstaller";
1818
import {
@@ -103,13 +103,13 @@ export class TestSession extends ts.server.Session {
103103
private seq = 0;
104104
public override host!: TestSessionAndServiceHost;
105105
public override logger!: LoggerWithInMemoryLogs;
106-
public override readonly typingsInstaller!: TestTypingsInstaller;
106+
public override readonly typingsInstaller!: TestTypingsInstallerAdapter;
107107
public serverCancellationToken: TestServerCancellationToken;
108108

109109
constructor(optsOrHost: TestSessionConstructorOptions) {
110110
const opts = getTestSessionPartialOptionsAndHost(optsOrHost);
111111
opts.logger = opts.logger || createLoggerWithInMemoryLogs(opts.host);
112-
const typingsInstaller = !opts.disableAutomaticTypingAcquisition ? new TestTypingsInstaller(opts) : undefined;
112+
const typingsInstaller = !opts.disableAutomaticTypingAcquisition ? new TestTypingsInstallerAdapter(opts) : undefined;
113113
const cancellationToken = opts.useCancellationToken ?
114114
new TestServerCancellationToken(
115115
opts.logger,

0 commit comments

Comments
 (0)