Skip to content

Commit 43a90f4

Browse files
authored
Make the API disposable (#64117)
1 parent 11b6dbf commit 43a90f4

9 files changed

Lines changed: 8394 additions & 10267 deletions

File tree

packages/typescript/scripts/generateSync.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,11 @@ function addSyncEdit(node: ts.Node, source: string, sourceFile: ts.SourceFile, e
454454
edits.push({ start: modifier.getStart(sourceFile) - offset, end: end - offset, newText: "" });
455455
}
456456
}
457+
if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.AwaitUsing) === ts.NodeFlags.AwaitUsing) {
458+
const awaitKeyword = node.getFirstToken(sourceFile);
459+
if (awaitKeyword?.kind !== ts.SyntaxKind.AwaitKeyword) throw new Error("Expected await using declaration");
460+
edits.push({ start: awaitKeyword.getStart(sourceFile) - offset, end: awaitKeyword.end - offset, newText: "" });
461+
}
457462
if (ts.isAwaitExpression(node)) {
458463
edits.push({
459464
start: node.getStart(sourceFile) - offset,

packages/typescript/src/api/async/api.ts

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
232232
private currentDirectory: string | undefined;
233233
private getCanonicalFileNameWorker: ((fileName: string) => string) | undefined;
234234
private initialized: boolean = false;
235+
private initializing: Promise<void> | undefined;
235236
private activeSnapshots: Set<Snapshot> = new Set();
236237
private latestSnapshot: Snapshot | undefined;
237238
readonly internal: InternalAPI;
@@ -269,7 +270,12 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
269270
// @sync-only-end
270271

271272
private async ensureInitialized(): Promise<void> {
272-
if (!this.initialized) {
273+
if (this.initialized) return;
274+
return this.initializing ??= this.initializeWorker();
275+
}
276+
277+
private async initializeWorker(): Promise<void> {
278+
try {
273279
const response = await this.client.apiRequest("initialize", null);
274280
const getCanonicalFileName = createGetCanonicalFileName(response.useCaseSensitiveFileNames);
275281
const currentDirectory = response.currentDirectory;
@@ -278,6 +284,10 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
278284
this.toPath = (fileName: string) => toPath(fileName, currentDirectory, getCanonicalFileName) as Path;
279285
this.initialized = true;
280286
}
287+
catch (error) {
288+
this.initializing = undefined;
289+
throw error;
290+
}
281291
}
282292

283293
getCurrentDirectory(): string {
@@ -376,18 +386,27 @@ export class API<FromLSP extends boolean = false> implements FormatDiagnosticsHo
376386
return snapshot;
377387
}
378388

389+
async [globalThis.Symbol.asyncDispose](): Promise<void> { // @sync: [globalThis.Symbol.dispose](): void {
390+
await this.close(); // @sync: this.close();
391+
}
392+
379393
async close(): Promise<void> {
394+
await this.initializing?.catch(() => {}); // @sync-skip
380395
// Dispose all active snapshots
381-
for (const snapshot of [...this.activeSnapshots]) {
382-
await snapshot.dispose();
396+
try {
397+
for (const snapshot of [...this.activeSnapshots]) {
398+
await snapshot.dispose();
399+
}
400+
// Release the latest snapshot's cache refs if still held
401+
if (this.latestSnapshot) {
402+
this.sourceFileCache.releaseSnapshot(this.latestSnapshot.id);
403+
this.latestSnapshot = undefined;
404+
}
405+
this.sourceFileCache.clear();
383406
}
384-
// Release the latest snapshot's cache refs if still held
385-
if (this.latestSnapshot) {
386-
this.sourceFileCache.releaseSnapshot(this.latestSnapshot.id);
387-
this.latestSnapshot = undefined;
407+
finally {
408+
await this.client.close(); // always close the underlying connection
388409
}
389-
await this.client.close();
390-
this.sourceFileCache.clear();
391410
}
392411

393412
clearSourceFileCache(): void {
@@ -539,6 +558,7 @@ export class Snapshot {
539558
private toPath: (fileName: string) => Path;
540559
private client: Client;
541560
private disposed: boolean = false;
561+
private disposePromise: Promise<void> | undefined;
542562
private onDispose: () => void;
543563
private snapshotRegistry: SnapshotObjectRegistry;
544564
readonly internal: SnapshotInternalAPI;
@@ -587,19 +607,27 @@ export class Snapshot {
587607
}
588608

589609
[globalThis.Symbol.dispose](): void {
590-
this.dispose();
610+
void this.dispose();
591611
}
592612

593-
async dispose(): Promise<void> {
613+
dispose(): Promise<void> {
614+
return this.disposePromise ??= this.disposeWorker();
615+
}
616+
617+
private async disposeWorker(): Promise<void> {
594618
if (this.disposed) return;
595619
this.disposed = true;
596620
for (const project of this.projectMap.values()) {
597621
project.dispose();
598622
}
599623
this.projectMap.clear();
600624
this.snapshotRegistry.clear();
601-
this.onDispose();
602-
await this.client.apiRequest("release", { snapshot: this.id });
625+
try {
626+
await this.client.apiRequest("release", { snapshot: this.id });
627+
}
628+
finally {
629+
this.onDispose();
630+
}
603631
}
604632

605633
isDisposed(): boolean {
@@ -1082,6 +1110,7 @@ export class Program implements FormatDiagnosticsHost {
10821110
private readonly decoder = new Wtf8Decoder();
10831111
private readonly sourceFileMetadataCache = new Map<Path, Promise<SourceFileMetadata | undefined>>();
10841112
private ownedSnapshot: Snapshot | undefined;
1113+
private disposePromise: Promise<void> | undefined;
10851114

10861115
constructor(
10871116
snapshotId: number,
@@ -1117,10 +1146,14 @@ export class Program implements FormatDiagnosticsHost {
11171146
}
11181147

11191148
[globalThis.Symbol.dispose](): void {
1120-
this.dispose();
1149+
void this.dispose();
1150+
}
1151+
1152+
dispose(): Promise<void> {
1153+
return this.disposePromise ??= this.disposeWorker();
11211154
}
11221155

1123-
async dispose(): Promise<void> {
1156+
private async disposeWorker(): Promise<void> {
11241157
const snapshot = this.ownedSnapshot;
11251158
this.ownedSnapshot = undefined;
11261159
if (snapshot) await snapshot.dispose();

packages/typescript/src/api/async/client.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ export class Client {
4949
private connection: MessageConnection | undefined;
5050
private options: ClientOptions;
5151
private connected = false;
52+
private closed = false;
53+
private connecting: Promise<void> | undefined;
5254
private timing: TimingCollector | undefined;
5355
private batchedRequests: { method: APIRequest["method"]; params: APIRequest["params"]; resolve: (value: unknown) => void; reject: (reason?: any) => void; }[] = [];
5456
private nextBatch: NodeJS.Immediate | "manual" | undefined;
@@ -60,9 +62,15 @@ export class Client {
6062
}
6163
}
6264

63-
async connect(): Promise<void> {
64-
if (this.connected) return;
65+
connect(): Promise<void> {
66+
if (this.closed) return Promise.reject(new Error("Client is closed"));
67+
if (this.connected) return Promise.resolve();
68+
return this.connecting ??= this.connectWorker().finally(() => {
69+
this.connecting = undefined;
70+
});
71+
}
6572

73+
private async connectWorker(): Promise<void> {
6674
if (isSpawnOptions(this.options)) {
6775
await this.connectViaSpawn(this.options);
6876
}
@@ -254,6 +262,7 @@ export class Client {
254262
}
255263

256264
async apiRequest<K extends keyof APIMethodInfo>(method: K, params: APIMethodInfo[K]["params"]): Promise<APIMethodInfo[K]["result"]> {
265+
if (this.closed) throw new Error("Client is closed");
257266
if (!this.connected) {
258267
await this.connect();
259268
}
@@ -323,6 +332,8 @@ export class Client {
323332
}
324333

325334
async close(): Promise<void> {
335+
await this.connecting?.catch(() => {}); // if connection is still in-progress, wait for it to finish before closing the connection
336+
this.closed = true;
326337
if (this.connection) {
327338
this.connection.dispose();
328339
this.connection = undefined;

0 commit comments

Comments
 (0)