Replace api.updateSnapshot - #64204
Andrew Branch (andrewbranch) merged 30 commits into
Conversation
…hange, with RemoveProgram counterpart
…pshot() and getCurrentLanguageServerSnapshot()
There was a problem hiding this comment.
🟡 Changes recommended
Snapshot refresh and repeated-open handling contain correctness issues, and removePrograms exposes an overly broad project-ID type.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Replaces latest-snapshot state with explicit snapshot creation and derivation, adding synthetic-program lifecycle support and branded project IDs.
Changes:
- Adds
createSnapshot,Snapshot.update, and LSP snapshot retrieval. - Supports creating, removing, and ensuring synthetic programs.
- Updates protocols, generators, caches, and tests for the new model.
File summaries
| File | Description |
|---|---|
tsc/internal/project/snapshothost.go |
Adds independent root snapshots. |
tsc/internal/project/snapshot.go |
Adds synthetic-program operations. |
tsc/internal/project/snapshot_test.go |
Tests synthetic lifecycle. |
tsc/internal/project/session.go |
Passes clients explicitly during cloning. |
tsc/internal/project/refcountcache_test.go |
Updates cache tests for synthetic programs. |
tsc/internal/project/projectcollectionbuilder.go |
Manages synthetic projects and program updates. |
tsc/internal/project/projectcollection.go |
Integrates synthetic projects into lookups. |
tsc/internal/project/project.go |
Defines synthetic projects and IDs. |
tsc/internal/project/project_stringer_generated.go |
Adds generated synthetic-kind text. |
tsc/internal/api/session.go |
Implements the redesigned snapshot API. |
tsc/internal/api/session_temporary_test.go |
Tests explicit-base updates. |
tsc/internal/api/session_createprogram_test.go |
Tests snapshot-created programs. |
tsc/internal/api/session_completion_test.go |
Migrates completion setup. |
tsc/internal/api/session_apistate_test.go |
Tests LSP snapshot state and ownership. |
tsc/internal/api/proto.go |
Defines new protocol methods and types. |
tsc/internal/api/proto_test.go |
Tests ensurePrograms decoding. |
tools/gen-proto/main.go |
Generates branded IDs and embedded interfaces. |
tools/gen-proto/main_test.go |
Verifies generated protocol output. |
packages/typescript/test/sync/astnav.test.ts |
Migrates synchronous AST navigation tests. |
packages/typescript/test/sync/ast.test.ts |
Migrates synchronous AST tests. |
packages/typescript/test/sync/api.bench.ts |
Migrates synchronous benchmarks. |
packages/typescript/test/sync/api-generators.test.ts |
Updates generator parity coverage. |
packages/typescript/test/diagnosticFormatter.test.ts |
Migrates diagnostic tests. |
packages/typescript/test/async/astnav.test.ts |
Migrates asynchronous AST navigation tests. |
packages/typescript/test/async/api.bench.ts |
Migrates asynchronous benchmarks. |
packages/typescript/src/api/sync/api.ts |
Exposes the synchronous snapshot model. |
packages/typescript/src/api/sourceFileCache.ts |
Supports branded project IDs in caching. |
packages/typescript/src/api/proto.ts |
Adds snapshot request compatibility conversion. |
packages/typescript/src/api/proto.generated.ts |
Updates generated wire declarations. |
packages/typescript/src/api/async/api.ts |
Exposes the asynchronous snapshot model. |
Review details
Files not reviewed (1)
- tsc/internal/project/project_stringer_generated.go: Generated file
- Files reviewed: 29/32 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
ea254df to
8399b74
Compare
|
I have a refactor on top of this to use strongly typed project IDs that are not just |
There was a problem hiding this comment.
🟡 Changes recommended
LSP reconciliation can mishandle close-and-reopen requests, and solution-wide operations may consume stale synthetic programs.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- tsc/internal/project/project_stringer_generated.go: Generated file
Suppressed comments (1)
tsc/internal/api/session.go:1245
- The same pre-request filtering breaks close-and-reopen for files: when a currently owned file appears in both lists,
OpenFilesis removed here andCloseFilesis retained below, leaving the file closed. Independent snapshot reconciliation processes closes before opens, so the two APIs now produce different final states for the same change set. Compute both deltas from a temporary open-file state so the reopen wins.
for uri := range apiRequest.OpenFiles.Keys() {
path := s.toPath(uri.FileName())
if s.openFiles.Has(path) {
apiRequest.OpenFiles.Delete(uri)
} else {
- Files reviewed: 31/34 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🟡 Changes recommended
Malformed or unknown program identifiers can currently cause incorrect success, unintended removal, or a server panic.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Files not reviewed (1)
- tsc/internal/project/project_stringer_generated.go: Generated file
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tsc/internal/project/projectcollectionbuilder.go:310
- Unknown project IDs are silently ignored because
DidRequestProjectreturns without indicating whether it found anything. Consequently,ensureProgramscan report success even though the requested program is absent (for example, when an ID from an unrelated snapshot is supplied). Validate every requested ID against this snapshot and return a client error when one is missing.
- Files reviewed: 33/36 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
🔵 Needs a closer look
An API-owned configured project cannot be reopened after its tsconfig is deleted and recreated.
Review details
Suppressed comments (1)
Previously missed (1) — in code that hasn't changed since the last review.
tsc/internal/project/projectcollectionbuilder.go:212
- An already-owned
openProjectsrequest is reduced toEnsurePrograms, which only updates an existing map entry. If the tsconfig was deleted, ensuring it removes the configured project but leaves the API open reference; after the file is recreated, anotheropenProjectscall can therefore never reachfindOrCreateProject, so the project remains absent. Preserve a distinct “ensure open project” signal that can recreate a missing entry without incrementing the API ref count.
- Files reviewed: 38/40 changed files
- Comments generated: 0 new
- Review effort level: Balanced
Wesley Wigham (weswigham)
left a comment
There was a problem hiding this comment.
I think it looks good, but if you'd like, I think we could use more dedicated test coverage of forking snapshots. I only saw, like, one assertion that file contents didn't leak between siblings reading through the test changes - but there's a lot more potential shared state between sibling snapshots than just file contents. There's obviously nothing in the typesystem ensuring that we're only sharing data by-copy on snapshot update and not by-ref, after all, and by-ref was sufficient for sequential, single snapshots. In particular, I don't see any changes to Snapshot.Clone, which implies it was already totally multi-snapshot-safe, which is great if true, but we're not really testing it rigorously. Auto-imports caches, config file caches, and probably more all look probably immutable/independent, but we're not really exercising them via tests.
|
That's never a bad thing to have more of, and while there could always be some corner where there is a bug, I think we do have a decent amount of pre-existing coverage of that because we actually have had the possibility of sibling snapshots for a long time, just not ones that an API consumer could get their hands on, and not siblings that were themselves cloned. Both auto-imports snapshots and the previous dedicated “temporary file update” snapshots were forking the main line, with auto-imports having the possibility of being adopted back into the main line if it never actually had a sibling. But those, along with routine parallel processing of LSP requests, means we have spent a while hardening the snapshot's immutability guarantee by tracking down panics from editor telemetry. |
|
Hey Andrew Branch (@andrewbranch)! I migrated ts-loader's branch for the new API to these changes in this commit: TypeStrong/ts-loader@339d06d It went pretty well - though it looks like there might be a regression. I enclose a report written with the help of Claude. Hopefully it makes sense. Changes madeAll changes are in
Verification
The one remaining failure:
|
John Reilly (@johnnyreilly), according to Copilot, this is happening because ts-loader is using the filename |
|
I think the answer is partly but not entirely. With a little bit of help from Claude I've made some changes which you can see in TypeStrong/ts-loader@693fd7c - here's a write up which is hopefully readable enough: (incidentally I'm not sure all of the changes in my last commit will be required when your changes land?) Issue 1 (the one you diagnosed) — confirmed, and it was a bug on our side. As you guessed, we were aliasing Issue 2 — a second, distinct repro of the same panic, Windows-only, and it corrects something in my earlier report. Once issue 1 was fixed, we still hit "no project found for opened file" on Windows for a different scenario: our older This is the part that corrects my original report: I'd claimed the panic was specific to We worked around it on our end by catching that failure and retrying without |
|
Hm, I've investigated, but I can't reproduce that. Can you get Claude to generate a contained repro, or even repro instructions tied to a specific commit of your PR? |
|
Yup will do - I wonder if you haven't been able to repro because it only surfaced on Windows during my testing? Claude has reproduced - I'll get it to drop a comment below. See failure on Windows: https://github.com/TypeStrong/ts-loader/actions/runs/35692331446/job/106631768957 |
|
Got it — put together a minimal, ts-loader/webpack-free repro. It only reproduces on Windows, which is presumably why you couldn't hit it locally. Repro instructions (tied to a specific commit):
Or standalone, no checkout needed — just // repro.js
const path = require('path');
const { API } = require('typescript/unstable/sync');
const projectDir = __dirname; // needs a tsconfig.json here, e.g. { "compilerOptions": { "module": "commonjs" } }
const tsconfigPath = path.join(projectDir, 'tsconfig.json');
// Never written to real disk - served only via the fs overrides below, and
// not listed by the tsconfig, so it isn't part of that project's file list.
const virtualFileName = path.join(projectDir, 'virtual.ts');
const virtualFiles = new Map([[virtualFileName, 'export const hello = "world";\n']]);
const api = new API({
fs: {
fileExists: fileName => (virtualFiles.has(fileName) ? true : undefined),
readFile: fileName => virtualFiles.get(fileName),
},
});
// The very first createSnapshot() call for this API instance, opening a
// project and a file not in that project in the same request.
const snapshot = api.createSnapshot({
openProjects: [tsconfigPath],
openFiles: [virtualFileName],
});
console.log('No panic:', !!snapshot);On Windows this throws: On macOS it prints Confirmed via GitHub Actions ( |
|
John Reilly (@johnnyreilly) hmmm, I think I may have inadvertently fixed you with #64391. I think the issue was your drive letter getting accidentally lowercased by us during |
|
Oh nice! I'll try and test with the latest nightly today and report back |
|
Your diagnosis is correct - I see |
…jects Per microsoft/TypeScript#64204 (comment) and microsoft/TypeScript#64204 (comment), the "no project found for opened file" failure traces back to a Windows drive-letter casing mismatch. ts-loader was itself contributing to that: `openProjects` always carried `configFilePath`'s resolvedFilePathCache'd (lowercased) form, but `openFiles` and `fileNotifications.changed`/`deleted` used `fileName`'s natural, unmodified casing - so a single snapshot request could mix a lowercase-drive-letter project path with a differently-cased file path. Canonicalizes every file name sent to the API the same way project paths already are, at the single point (`updateSnapshot`) where the wire params are built - doesn't touch how `apiFileName` is used anywhere else (getSourceFile, getConfiguredProject, dependency tracking, etc.), keeping this a narrow, wire-boundary-only change. The existing catch-and-retry workaround for this same failure shape stays in place as a safety net regardless of how much this narrows the problem. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…jects Per microsoft/TypeScript#64204 (comment) and microsoft/TypeScript#64204 (comment), the "no project found for opened file" failure traces back to a Windows drive-letter casing mismatch. ts-loader was itself contributing to that: `openProjects` always carried `configFilePath`'s resolvedFilePathCache'd (lowercased) form, but `openFiles` and `fileNotifications.changed`/`deleted` used `fileName`'s natural, unmodified casing - so a single snapshot request could mix a lowercase-drive-letter project path with a differently-cased file path. Canonicalizes every file name sent to the API the same way project paths already are, at the single point (`updateSnapshot`) where the wire params are built - doesn't touch how `apiFileName` is used anywhere else (getSourceFile, getConfiguredProject, dependency tracking, etc.), keeping this a narrow, wire-boundary-only change. This supersedes (and lets us remove) the earlier catch-and-retry workaround for the same symptom: confirmed via .github/workflows/windows-test-probe.yml that this fix alone (no retry logic) is sufficient - appendSuffixTo and the full 50-suite comparison run both pass on Windows without it. Removed rather than kept as a redundant safety net since it was catching *any* thrown error and retrying, which risked masking a genuinely different failure (e.g. a broken tsconfig) behind a confusing second error. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Yeah I've moved ts-loader to using the normalised filenames for Thanks Andrew Branch (@andrewbranch)! |
…jects Upgrades to typescript@next 7.1.0-dev.20260922.1 and fixes the "no project found for opened file" failure that persisted against it on Windows (see microsoft/TypeScript#64204 (comment) onwards). Per microsoft/TypeScript#64204 (comment) and microsoft/TypeScript#64204 (comment), the failure traces back to a Windows drive-letter casing mismatch. ts-loader was itself contributing to that: `openProjects` always carried `configFilePath`'s resolvedFilePathCache'd (lowercased) form, but `openFiles` and `fileNotifications.changed`/`deleted` used `fileName`'s natural, unmodified casing - so a single snapshot request could mix a lowercase-drive-letter project path with a differently-cased file path. Canonicalizes every file name sent to the API the same way project paths already are, at the single point (`updateSnapshot`) where the wire params are built - doesn't touch how `apiFileName` is used anywhere else (getSourceFile, getConfiguredProject, dependency tracking, etc.), keeping this a narrow, wire-boundary-only change. Confirmed via .github/workflows/windows-test-probe.yml that this fix alone is sufficient - appendSuffixTo and the full 50-suite comparison run both pass on Windows without any additional workaround. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes #64154. Read that issue for the big picture overview—here’s the list of changes and decisions:
api.updateSnapshot()is replaced by:api.getCurrentLanguageServerSnapshot(changes?)available in LSP mode onlyapi.createSnapshot(changes?)available alwaysconst newSnapshot = snapshot.update(changes)changestakes three new operations:snap.update({ createPrograms: [/* ... */] })adds programs to any snapshotsnap.update({ reconfigurePrograms: [/* ... */] })changes the root files, options, or references of a program created withcreateProgramssnap.update({ ensurePrograms: [/* ... */] })returns a snapshot where projects with the given IDs have up-to-date programs.snap.update({ ensurePrograms: true })ensures all projects are up to date.oldProgramas an option in creating a program. Instead, a program can be incrementally updated by notifying the API of its changed files and usingensurePrograms:updateSnapshotrequest. Now, you have to useensurePrograms. (The exception is that projects returned byopenProjectsoropenFilesare automatically updated without need for a separateensurePrograms, even if they're already open/created.ensureProgramsis mainly needed in combination withfileChanges, so you can say which projects you care about.)operationwith information about the request that created it. Currently onlycreateProgramsandopenFilescontribute tooperation, since those result in the creation of projects with an ID that might not be known to the caller ahead of time.project.idnow has the typeProjectId = ConfiguredProjectId | InferredProjectId | SyntheticProjectId, each of which is a branded string.ConfiguredProjectIdis a subtype ofPath. The same ID is also exposed onprogram.idfor convenience.api.createProgram(rootFiles, options)is basically shorthand forapi.createSnapshot({ createPrograms: [{ rootFiles, options }] }).operation.createdPrograms[0].fileChangestofileNotificationsandAPIFileChangestoFileNotifications