-
Notifications
You must be signed in to change notification settings - Fork 634
Scaffold IPC-based API #711
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This reverts commit 386fff2.
Just to note, that the existing |
|
FWIW I think this is the opposite of what I was trying to say; I can't imagine us offering a public API that doesn't let one construct a program, get its errors, and get the outputs. But I don't know which "guts" you're speaking to exactly. Of course, downstream API consumers will require modification for sure, so "not work" is true from that perspective. |
John is referring to offline chats with me, because I do know the guts in question, and ts-loader’s API usage is extremely broad, low-level, and implementation-specific. |
Ah, multiple private chats 😄 |
Failed to merge because the merge queue CI workflow also needs rust installed. I should probably just delete that thing and instead use the regular CI job with a bunch of if statements, or something. |
Co-authored-by: Copilot <[email protected]>
Important
Until libsyncrpc is set up to publish to npm, this PR takes a git dependency on it, which will build the binary from source during
npm install
. You need Rust 1.85 or higher to have a successfulnpm install
in typescript-go.Note
Takeaways from design meeting:
This PR is the start of a JavaScript API client and Go API server that communicate over STDIO. Only a few methods are implemented; the aim of this PR is to be the basis for discussions around the general architecture, then additional functionality can be filled in.
Same backend, different clients
This PR includes a synchronous JavaScript client for Node.js. It uses libsyncrpc to block during IPC calls to the server. Relatively small changes to the client could produce an asynchronous variant without Node.js-specific native bindings that could work in Deno or Bun. I don’t want to make specific promises about WASM without doing those experiments, but using the same async client with an adapter for calling WASM exports seems possible. I’m imagining that eventually we’ll publish the Node.js-specific sync client as a standalone library for those who need a sync API, and an async version adaptable to other use cases, ideally codegen’d from the same source. The same backend is intended to be used with any out-of-process client.
Client structure
This PR creates two JavaScript packages,
@typescript/ast
and@typescript/api
(which may make more sense as@typescript/api-sync
or@typescript/api-node
eventually). The former contains a copy of TS 5.9’s AST node definitions, related enums, and node tests (e.g.isIdentifier()
), with the minor changes that TS 7 has made to those definitions applied. The latter contains the implementation of the Node.js API client. It currently takes a path to the tsgo executable and spawns it as a child process. (I imagine eventually, the TypeScript 7.0+ compiler npm package will be a peerDependency of the API client, and resolution of the executable can happen automatically.)Backend structure
tsgo api
starts the API server communicating over STDIO. The server initializes theapi.API
struct which is responsible for handling requests and managing state, like a stripped-downproject.Service
. In fact, it uses the other components of the project system, storing documents and projects the same way. (As the project service gets built out with things like file watchers and optimizations for find-all-references, it would get increasingly unwieldy to use directly as an API service, but a future refactor might extract the basic project and document storage to a shared component.)The API already has methods that return projects, symbols, and types. These are returned as IDs plus bits of easily serializable info, like name and flags. When one of these objects is requested, the API server stores it with its ID so follow-up requests can be made against those IDs. This does create some memory management challenges, which I’ll discuss a bit later.
Implemented functionality
Here’s a selection of the API client type definitions that shows what methods exist as of this PR:
Here’s some example usage from benchmarks:
Client-side virtual file systems are also supported. There’s a helper for making a very simple one from a record:
Performance
These are the results of the included benchmarks on my M2 Mac. Note that IPC is very fast on Apple Silicon, and Windows seems to see significantly more overhead per call. Tasks prefixed
TS -
refer to the rough equivalent with the TypeScript 5.9 API. ThegetSymbolAtPosition
tasks are operating on TypeScript’sprogram.ts
, which has 10893 identifiers.To editorialize these numbers a bit: in absolute terms, this is pretty fast, even transferring large payloads like a binary-encoded
checker.ts
(10). On the order of tens, hundreds, or thousands of API calls, most applications probably wouldn’t notice a per-call regression over using the TypeScript 5.9 API, and may speed up if program creation / parsing multiple files is a significant portion of their API consumption today (5–7). However, the IPC overhead is pretty noticeable when looking at hundreds of thousands of back-to-back calls on an operation that would be essentially free in a native JavaScript API, like getting the symbol for every identifier in a large file (15, 18). For that reason, we’ll be very open to including bulk/batch/composite API methods that reduce the number of round trips needed to retrieve lots of information for common scenarios (16, 17).Memory management
The current API design uses opaque IDs for objects like symbols and types, so the client can receive a handle to one of these objects and then query for additional information about it. For example, implemented in this PR is
getTypeOfSymbol
, which takes a symbol ID. The server has to store the symbol in a map so it can be quickly retrieved when the client asks for its type. This client/server split presents two main challenges:To accomplish this, there is a client-side object registry that stores objects by their IDs. API users will need to explicitly dispose those objects to release them both from the client-side store and from the server. (Server objects may be automatically released in response to program updates, and making additional queries against them will result in an error.) This can be done with the
.dispose()
method:or with explicit resource management: