microsoft / microsoft/TypeScript
API returns a stale SourceFile after the latest snapshot is disposed
@andrewbranch đang làm issue này rồi.
Từ ngày 19/8/2026.
- Ngôn ngữ chính
- Go
- Star
- 111k
- Fork
- 14.3k
- Merge trung bình
- 2 ngày 4 giờ
- Pull request đã merge (30 ngày)
- 132
Mô tả
Summary
When an API client queries a source file, disposes the latest snapshot, edits the open file, and calls updateSnapshot again, the new snapshot can return the cached SourceFile from the disposed snapshot.
In VS Code this presents as follows: the first edit is reflected in the first API snapshot, but after that snapshot is disposed, a snapshot created for the second edit still contains the text from the first edit.
Observed with @typescript/native version 7.1.0-dev.20260818.1 using an API created through API.fromLSPConnection.
The caller follows this pattern:
const snapshot = await api.updateSnapshot({ openFiles: [{ uri }] });
try {
const project = await snapshot.getDefaultProjectForFile({ uri });
const sourceFile = await project?.program.getSourceFile({ uri });
// sourceFile.text can contain the previous edit here
} finally {
await snapshot.dispose();
}
Steps to reproduce
- Start the native preview language server and create an API with
API.fromLSPConnection. - Edit an open TypeScript file.
- Call
updateSnapshot, fetch itsSourceFile(populating the client-side source-file cache), and dispose the snapshot. - Edit the same file again and let the LSP
didChangenotification complete. - Call
updateSnapshotagain and fetch the sameSourceFile.
A direct API regression test can use the equivalent lifecycle:
const firstSnapshot = await api.updateSnapshot({ openProject: "/tsconfig.json" });
const firstSourceFile = await firstSnapshot
.getProject("/tsconfig.json")!
.program.getSourceFile("/src/foo.ts");
assert.ok(firstSourceFile);
await firstSnapshot.dispose();
fs.writeFile!("/src/foo.ts", `export const foo = "changed";`);
const secondSnapshot = await api.updateSnapshot({
fileChanges: { changed: ["/src/foo.ts"] },
});
const secondSourceFile = await secondSnapshot
.getProject("/tsconfig.json")!
.program.getSourceFile("/src/foo.ts");
assert.equal(secondSourceFile?.text, `export const foo = "changed";`);
Expected behavior
The second snapshot returns a SourceFile containing the second edit.
Actual behavior
The second snapshot can return the cached SourceFile containing the first edit.
Likely cause
The server and JS client appear to have different lifetime assumptions for the latest disposed snapshot:
Session.releaseSnapshotdeletes the latest snapshot froms.snapshotswhen its API refcount reaches zero, whiles.latestSnapshotstill points to that handle.- On the next
handleUpdateSnapshot,prevSD := s.snapshots[s.latestSnapshot]is thereforenil, so the response has noSnapshotChangesdiff. - The JS API intentionally retains cache references for a disposed latest snapshot. On the next update it calls
SourceFileCache.retainForSnapshotbefore releasing that snapshot's cache references. - With
data.changes === undefined, the cache treats every previously fetched source file as unchanged and retains it for the new snapshot.getSourceFilethen returns it throughgetRetainedwithout asking the server for the new content.
Relevant code paths:
internal/api/session.go:releaseSnapshotandhandleUpdateSnapshot_packages/native-preview/src/api/async/api.ts:API.updateSnapshot_packages/native-preview/src/api/sourceFileCache.ts:retainForSnapshotandgetRetained
Either the server needs to retain a diff base independently of the client-visible snapshot lifetime, or the client must not carry cache entries forward when its previous latest snapshot was disposed and no reliable diff is available.
Hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Đánh giá
Issue này chưa được đánh giá.