## Context The API currently exposes several overlapping ways to describe state changes: - `api.updateSnapshot({ fileChanges })` applies caller-reported changes to the API client's latest snapshot. - `api.createProgram(..., oldProgram, fileChanges)` derives a program from the snapshot that owns `oldProgram`. - #64115 adds `snapshot.update({ fileSystem })`, which layers a virtual filesystem over a snapshot. - The LSP-connected API adopts the language server's canonical state through an empty `updateSnapshot()` call. These operations grew from different requirements and do not form one coherent state-transition model. In particular: - A standalone API client does not inherently have a meaningful "latest" snapshot. Only an LSP session has canonical current state. Currently, if you call `api.createProgram` without passing an `oldProgram`, the behavior implicitly depends on what's in the "latest snapshot." It looks totally unexpected that you could get different results at different times because of intermediate calls. - `fileChanges` describes out-of-band mutations rather than performing a state change. It is redundant for in-band virtual filesystem updates that you can do with #64115 and awkward as the default way to obtain TS 6-style incremental behavior. - `createProgram` taking an `oldProgram` as the source for the base snapshot to clone from means you can't have a sequence like create a program (S1), perform a file system update (S2), then update the program from S1 while keeping the file system changes from S2. On the other hand, allowing `createProgram` to be based off of an arbitrary snapshot while also taking an arbitrary `oldProgram` means the server doesn't know the relationship between the base snapshot and the snapshot that produced `oldProgram`, so `oldProgram` could never be reused. - `createProgram` currently consumes the one inferred-project slot, preventing a snapshot from containing multiple independently API-created programs. (Having multiple programs in a single snapshot can be desirable if they overlap in source files, since binder symbols have referential equality within the same snapshot.) ## Redesign ### Clarifying the initial state and removing "latest" state First, I want to replace `api.updateSnapshot` with two different methods that make it a little more clear what you're getting: ```ts // Only available in LSP-connected API! // Gets the canonical state of the LSP server: const s = api.getLatestSnapshot(); // Maybe even name it `getLatestLSPSnapshot()`? // Available in both LSP mode and standalone mode: const s = api.getEmptySnapshot(); ``` The API client will have no concept of a "latest snapshot"; you can get the LSP server's latest snapshot if you're connected to the LSP. ### Updating a snapshot Any snapshot can be updated with changes passed to `snapshot.update()`, and everything you can do to clone a snapshot with changes should be expressible through that API, including creating/updating/removing a program, opening/closing a tsconfig project, applying virtual file system overlays, etc.: ```ts const s0 = api.getEmptySnapshot(); const s1 = s0.update({ fileSystem: /* ... */, createPrograms: [{ rootFiles, options }], openProjects: [pathToTsconfig], }); ``` `getEmptySnapshot` (and `getLatestSnapshot`) can also take a set of initial changes to incorporate (there's no reason to round-trip to get an empty snapshot if you're going to add to it in another request right afterwards). Maybe we should name that method `getInitialSnapshot` or something, since it's confusing to request an empty snapshot with stuff in it: ```ts const s0 = api.getEmptySnapshot().update({ /* ... */ }); // better written as: const s0 = api.getInitialSnapshot({ /* ... */ }); ``` Or perhaps `api.createSnapshot()` encodes the meaning well enough whether or not it has arguments: ```ts const s0 = api.createSnapshot(); // empty, not a ton of reason to do this const s0 = api.createSnapshot({ createPrograms: [/* ... */] }); ``` File system updates shouldn't automatically refresh affected projects/programs; we need to allow the API client to specify what it needs: ```ts const s0 = api.getInitialSnapshot({ createPrograms: [{ rootFiles, options }] }); // No program update performed, but program is marked dirty if affected const s1 = s0.update({ fileSystem: /* ... */ }); let program = s1.programs[0]; program.dirty; // true // Updates the program if dirty const s2 = s1.update({ ensurePrograms: [program.id] }); program = s2.programs[0]; program.dirty; // false ``` (We may want to have `createProgram` pass a program id and look it up from a map rather than autogenerating one and accessing programs in an array.) This solves the problem of combining efficient updates of existing programs with other snapshot changes without introducing the possibility of grafting a program from one snapshot into an unrelated one with unpredictable results. (It loses the ability to “move” a tsconfig project’s program into an isolated snapshot, but that was sort of a side effect of the current design rather than a use case we were designing for.) ### Convenience APIs One motivation of #63950 was to hide snapshot management for API users who only need to create one program, or multiple disconnected programs, and never need to update them over time. `api.createProgram` should still exist, but it can be sugar over the lower-level snapshot update API: ```ts const program = api.createProgram({ rootFiles, options }); // Sugar for: const program = api.getInitialSnapshot({ createPrograms: [{ rootFiles, options }] }).programs[0]; ``` I'm undecided whether we should expose a way to update that program without going back out to the `snapshot.update` API. I think it would be reasonable to say that if you want to do anything that evolves a non-initial snapshot, you should get comfortable using `snapshot.update`. ## Architectural changes We need to change a few things on the server side to make this happen. 1. The `api.Session` really doesn't need to run its snapshot updating work through `project.Session` unless it's in LSP-connection mode. That will remove the concept of a latest/canonical snapshot from the API server when running in standalone mode. We should extract the parts of `project.Session` that own global caches, i.e. the interface that `snapshot.Clone` relies on, into an object that's separate from the `project.Session`'s ownership of a single latest state and reactions to updating that state. In standalone mode, the `api.Session` can fulfill that interface or own an object implementing that interface rather than owning a full `project.Session`. 2. We need to use a different storage mechanism for API-created programs than the single inferred project of `project.ProjectCollection`. Probably a separate map of projects, leaving both configured and inferred projects alone. (This is needed both to support multiple API-created program in the same snapshot as well as adding a single API-created program to an LSP snapshot where there may already be an inferred project.) These are both moderately sized refactors, but I think they'll actually simplify the mental model, and everything else should fall cleanly out of these moves.