fix(lsp): deferred idle reuse until cleanup

Kept closing sessions attached to their server until orphan document closes and pending request cancellations are queued.

Added coverage that reopens the last orphan URI after reuse and verifies the replacement overlay remains active.

Fixes #8371
This commit is contained in:
roboomp
2026-08-13 03:15:29 +00:00
parent 757066e965
commit d1278762f8
2 changed files with 22 additions and 10 deletions
+4 -2
View File
@@ -643,21 +643,23 @@ export class LspMuxServer {
this.#sessions.delete(session);
const server = session.server;
if (server) {
server.sessions.delete(session);
let cleanup: Promise<void> | undefined;
for (const uri of session.openUris) {
server.documents.delete(uri);
await this.#writeServer(server, {
cleanup = this.#writeServer(server, {
jsonrpc: "2.0",
method: "textDocument/didClose",
params: { textDocument: { uri } },
});
}
await cleanup;
for (const [muxId, pending] of server.pending) {
if (pending.session !== session) continue;
pending.drop = true;
await this.#writeServer(server, { jsonrpc: "2.0", method: "$/cancelRequest", params: { id: muxId } });
}
server.initializeWaiters.delete(session);
server.sessions.delete(session);
if (server.sessions.size === 0 && !server.stopping) {
server.lingerTimer = setTimeout(() => {
if (server.sessions.size === 0) void this.#stopServer(server);
+18 -8
View File
@@ -365,21 +365,31 @@ describe("LspMuxServer", () => {
);
it.skipIf(process.platform === "win32")(
"closes orphaned documents before reusing an idle server",
"finishes orphan document closes before reusing a server",
async () => {
const first = await link();
await initialize(first.client);
const uri = "file:///orphan.ts";
first.client.notify("textDocument/didOpen", {
textDocument: { uri, languageId: "typescript", version: 1, text: "orphan" },
});
await pollUntil(async () => (await state(first.client)).didOpen[uri] === 1, "orphan didOpen");
const uris = Array.from({ length: 128 }, (_, index) => `file:///orphan-${index}.ts`);
for (const uri of uris) {
first.client.notify("textDocument/didOpen", {
textDocument: { uri, languageId: "typescript", version: 1, text: "orphan" },
});
}
await first.client.request("test/echo", { barrier: true });
const firstClosed = first.client.waitForClose();
first.client.destroy();
await pollUntil(() => Promise.resolve(server.sessionCount === 0), "orphan session close");
await firstClosed;
const second = await link();
expect(second.connected.spawned).toBe(false);
await pollUntil(async () => (await state(second.client)).didClose.includes(uri), "orphan didClose");
const uri = uris.at(-1);
expect(uri).toBeDefined();
await initialize(second.client);
second.client.notify("textDocument/didOpen", {
textDocument: { uri, languageId: "typescript", version: 1, text: "replacement" },
});
await second.client.request("test/echo", { barrier: true });
expect(await second.client.request<string | null>("test/documentText", { uri })).toBe("replacement");
},
10_000,
);