From fdcd2ee80dd1059bf5237077164c0f3ebedfa236 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 21 Sep 2026 09:21:40 -0700 Subject: [PATCH 1/2] Preserve snapshot identity across no-op updates --- packages/typescript/src/api/async/api.ts | 3 +++ packages/typescript/src/api/sync/api.ts | 6 ++++++ packages/typescript/test/async/api.test.ts | 16 ++++++++++++++++ packages/typescript/test/sync/api.test.ts | 16 ++++++++++++++++ 4 files changed, 41 insertions(+) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 912710b5cb4a5..6702a6984fe62 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -407,6 +407,9 @@ export class API implements FormatDiagnosticsHo snapshot: baseSnapshot.id, changes: toCreateSnapshotRequest(params), }); + if (data.snapshot === baseSnapshot.id) { + return baseSnapshot; + } this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); const snapshot = new Snapshot( data, diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index 8f942c3feb83f..d925d4a900b1f 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -639,6 +639,9 @@ export class API implements FormatDiagnosticsHo snapshot: baseSnapshot.id, changes: toCreateSnapshotRequest(params), }); + if (data.snapshot === baseSnapshot.id) { + return baseSnapshot; + } owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); const snapshot = new Snapshot( data, @@ -666,6 +669,9 @@ export class API implements FormatDiagnosticsHo snapshot: baseSnapshot.id, changes: toCreateSnapshotRequest(params), }); + if (data.snapshot === baseSnapshot.id) { + return baseSnapshot; + } owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); const snapshot = new Snapshot( data, diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 0a15cdff436bc..2e2e26bfa58bc 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -400,6 +400,22 @@ describe("API", () => { assert.equal("openedFiles" in empty.operation, false); }); + test("snapshot.update preserves identity when the server returns the same snapshot", async () => { + await using api = spawnAPI(); + const snapshot = await api.createSnapshot(); + const client = (api as unknown as { + client: { apiRequest(method: string, params: unknown): Promise; }; + }).client; + const apiRequest = client.apiRequest.bind(client); + client.apiRequest = async (method, params) => { + const response = await apiRequest(method, params); + if (method !== "updateSnapshot" || typeof response !== "object" || response === null) return response; + return { ...response, snapshot: snapshot.id }; + }; + + assert.strictEqual(await snapshot.update({}), snapshot); + }); + test("snapshot.update reconfigures a synthetic program", async () => { await using api = spawnAPI({ "/src/a.ts": `export const a = 1;`, diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index f20c229bbfce3..a9df751167f7f 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -389,6 +389,22 @@ describe("API", () => { assert.equal("openedFiles" in empty.operation, false); }); + test("snapshot.update preserves identity when the server returns the same snapshot", () => { + using api = spawnAPI(); + const snapshot = api.createSnapshot(); + const client = (api as unknown as { + client: { apiRequest(method: string, params: unknown): unknown; }; + }).client; + const apiRequest = client.apiRequest.bind(client); + client.apiRequest = (method, params) => { + const response = apiRequest(method, params); + if (method !== "updateSnapshot" || typeof response !== "object" || response === null) return response; + return { ...response, snapshot: snapshot.id }; + }; + + assert.strictEqual(snapshot.update({}), snapshot); + }); + test("snapshot.update reconfigures a synthetic program", () => { using api = spawnAPI({ "/src/a.ts": `export const a = 1;`, From c5bc1f3fce94f5d496bfb1708b6f5fe4329366b4 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Mon, 21 Sep 2026 09:42:48 -0700 Subject: [PATCH 2/2] Add missing release --- packages/typescript/src/api/async/api.ts | 1 + packages/typescript/src/api/sync/api.ts | 2 ++ packages/typescript/test/async/api.test.ts | 16 ++++++++++++++++ packages/typescript/test/sync/api.test.ts | 16 ++++++++++++++++ 4 files changed, 35 insertions(+) diff --git a/packages/typescript/src/api/async/api.ts b/packages/typescript/src/api/async/api.ts index 6702a6984fe62..bba0b24ac7d51 100644 --- a/packages/typescript/src/api/async/api.ts +++ b/packages/typescript/src/api/async/api.ts @@ -408,6 +408,7 @@ export class API implements FormatDiagnosticsHo changes: toCreateSnapshotRequest(params), }); if (data.snapshot === baseSnapshot.id) { + await this.client.apiRequest("release", { snapshot: data.snapshot }); return baseSnapshot; } this.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); diff --git a/packages/typescript/src/api/sync/api.ts b/packages/typescript/src/api/sync/api.ts index d925d4a900b1f..16a70705229a4 100644 --- a/packages/typescript/src/api/sync/api.ts +++ b/packages/typescript/src/api/sync/api.ts @@ -640,6 +640,7 @@ export class API implements FormatDiagnosticsHo changes: toCreateSnapshotRequest(params), }); if (data.snapshot === baseSnapshot.id) { + owner.client.apiRequest("release", { snapshot: data.snapshot }); return baseSnapshot; } owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); @@ -670,6 +671,7 @@ export class API implements FormatDiagnosticsHo changes: toCreateSnapshotRequest(params), }); if (data.snapshot === baseSnapshot.id) { + yield* apiRequest("release", { snapshot: data.snapshot }); return baseSnapshot; } owner.sourceFileCache.retainForSnapshot(data.snapshot, baseSnapshot.id, data.changes); diff --git a/packages/typescript/test/async/api.test.ts b/packages/typescript/test/async/api.test.ts index 2e2e26bfa58bc..534e2ae00e7f4 100644 --- a/packages/typescript/test/async/api.test.ts +++ b/packages/typescript/test/async/api.test.ts @@ -407,13 +407,29 @@ describe("API", () => { client: { apiRequest(method: string, params: unknown): Promise; }; }).client; const apiRequest = client.apiRequest.bind(client); + let duplicateReference = false; client.apiRequest = async (method, params) => { + if ( + method === "release" + && typeof params === "object" + && params !== null + && "snapshot" in params + && params.snapshot === snapshot.id + && duplicateReference + ) { + duplicateReference = false; + return true; + } const response = await apiRequest(method, params); if (method !== "updateSnapshot" || typeof response !== "object" || response === null) return response; + if (!("snapshot" in response) || typeof response.snapshot !== "number") return response; + await apiRequest("release", { snapshot: response.snapshot }); + duplicateReference = true; return { ...response, snapshot: snapshot.id }; }; assert.strictEqual(await snapshot.update({}), snapshot); + assert.equal(duplicateReference, false); }); test("snapshot.update reconfigures a synthetic program", async () => { diff --git a/packages/typescript/test/sync/api.test.ts b/packages/typescript/test/sync/api.test.ts index a9df751167f7f..1aaad229ecc76 100644 --- a/packages/typescript/test/sync/api.test.ts +++ b/packages/typescript/test/sync/api.test.ts @@ -396,13 +396,29 @@ describe("API", () => { client: { apiRequest(method: string, params: unknown): unknown; }; }).client; const apiRequest = client.apiRequest.bind(client); + let duplicateReference = false; client.apiRequest = (method, params) => { + if ( + method === "release" + && typeof params === "object" + && params !== null + && "snapshot" in params + && params.snapshot === snapshot.id + && duplicateReference + ) { + duplicateReference = false; + return true; + } const response = apiRequest(method, params); if (method !== "updateSnapshot" || typeof response !== "object" || response === null) return response; + if (!("snapshot" in response) || typeof response.snapshot !== "number") return response; + apiRequest("release", { snapshot: response.snapshot }); + duplicateReference = true; return { ...response, snapshot: snapshot.id }; }; assert.strictEqual(snapshot.update({}), snapshot); + assert.equal(duplicateReference, false); }); test("snapshot.update reconfigures a synthetic program", () => {