microsoft / microsoft/TypeScript

API returns a stale SourceFile after the latest snapshot is disposed

Aperta
#63,890 0 commenti 0 reazioni 1 assegnatario Vedi su GitHub

@andrewbranch ci sta già lavorando.

Dal 19/8/2026.

Needs Investigation
Lingua principale
Go
Stelle
111k
Fork
14.3k
Merge medio
2g 4h
PR unite (30g)
132

Descrizione

## 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:

```ts
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

1. Start the native preview language server and create an API with `API.fromLSPConnection`.
2. Edit an open TypeScript file.
3. Call `updateSnapshot`, fetch its `SourceFile` (populating the client-side source-file cache), and dispose the snapshot.
4. Edit the same file again and let the LSP `didChange` notification complete.
5. Call `updateSnapshot` again and fetch the same `SourceFile`.

A direct API regression test can use the equivalent lifecycle:

```ts
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.releaseSnapshot` deletes the latest snapshot from `s.snapshots` when its API refcount reaches zero, while `s.latestSnapshot` still points to that handle.
- On the next `handleUpdateSnapshot`, `prevSD := s.snapshots[s.latestSnapshot]` is therefore `nil`, so the response has no `SnapshotChanges` diff.
- The JS API intentionally retains cache references for a disposed latest snapshot. On the next update it calls `SourceFileCache.retainForSnapshot` before 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. `getSourceFile` then returns it through `getRetained` without asking the server for the new content.

Relevant code paths:

- `internal/api/session.go`: `releaseSnapshot` and `handleUpdateSnapshot`
- `_packages/native-preview/src/api/async/api.ts`: `API.updateSnapshot`
- `_packages/native-preview/src/api/sourceFileCache.ts`: `retainForSnapshot` and `getRetained`

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.

Guida per i contributori

Apri la guida per i contributori

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.