Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
andrewbranch merged 71 commits intomicrosoft:mainfromandrewbranch:api
Apr 10, 2025
Merged

Conversation

andrewbranch
Copy link
Member

@andrewbranchandrewbranch commentedMar 25, 2025
edited
Loading

Important

Untillibsyncrpc is set up to publish to npm, this PR takes a git dependency on it, which will build the binary from source duringnpm install.You needRust 1.85 or higher to have a successfulnpm install in typescript-go.

Note

Takeaways from design meeting:

  • Investigate some kind of query system for composing batched requests. Muffled screams of “Not GraphQL!” and “I want GraphQL!” could be heard on the other end of the line.
  • FFI via napi-go still on the table to investigate as an additional option (not replacing IPC), but may need a different API surface or refactoring since the one included here returns shallow serializable objects.
  • No consensus on whether remoting ASTs means we don’t need to provide a client-side parser. The performance of remoting is pretty good, but requires loading the large tsgo binary, and the perf maybe isn’t good enough for linters that need to remote thousands of ASTs. There are other JS parsers though, and not having to maintain one with identical behavior to the Go-based parser is seen as a real win.

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

exportinterfaceAPIOptions{tsserverPath:string;cwd?:string;logFile?:string;fs?:FileSystem;}exportinterfaceFileSystem{directoryExists?:(directoryName:string)=>boolean|undefined;fileExists?:(fileName:string)=>boolean|undefined;getAccessibleEntries?:(directoryName:string)=>FileSystemEntries|undefined;readFile?:(fileName:string)=>string|null|undefined;realpath?:(path:string)=>string|undefined;}exportdeclareclassAPI{constructor(options:APIOptions);parseConfigFile(fileName:string):ConfigResponse;loadProject(configFileName:string):Project;}exportinterfaceConfigResponse{options:Record<string,unknown>;fileNames:string[];}exportdeclareclassProject{configFileName:string;compilerOptions:Record<string,unknown>;rootFiles:readonlystring[];reload():void;getSourceFile(fileName:string):SourceFile|undefined;getSymbolAtLocation(node:Node):Symbol|undefined;getSymbolAtLocation(nodes:readonlyNode[]):Symbol|undefined;getSymbolAtPosition(fileName:string,position:number):Symbol|undefined;getSymbolAtPosition(fileName:string,positions:readonlynumber[]):(Symbol|undefined)[];getTypeOfSymbol(symbol:Symbol):Type|undefined;getTypeOfSymbol(symbols:readonlySymbol[]):(Type|undefined)[];}exportinterfaceNode{readonlyid:number;readonlypos:number;readonlyend:number;readonlykind:SyntaxKind;readonlyparent:Node;forEachChild<T>(visitor:(node:Node)=>T):T|undefined;getSourceFile():SourceFile;}exportinterfaceSourceFileextendsNode{readonlykind:SyntaxKind.SourceFile;// Node types are basically same as Strada, without additional methodsreadonlystatements:NodeArray<Statement>;readonlytext:string;}exportdeclareclassSymbol{id:string;name:string;flags:SymbolFlags;checkFlags:number;}exportdeclareclassType{flags:TypeFlags;}

Here’s some example usage from benchmarks:

import{API}from"@typescript/api";import{SyntaxKind}from"@typescript/ast";constapi=newAPI({cwd:newURL("../../../",import.meta.url).pathname,tsserverPath:newURL("../../../built/local/tsgo",import.meta.url).pathname,});constproject=api.loadProject("_submodules/TypeScript/src/compiler/tsconfig.json");constfile=project.getSourceFile("program.ts")!;file.forEachChild(functionvisit(node){if(node.kind===SyntaxKind.Identifier){constsymbol=project.getSymbolAtPosition("program.ts",node.pos);// ...}node.forEachChild(visit);});

Client-side virtual file systems are also supported. There’s a helper for making a very simple one from a record:

import{API}from"@typescript/api";import{createVirtualFileSystem}from"@typescript/api/fs";import{SyntaxKind}from"@typescript/ast";constapi=newAPI({cwd:newURL("../../../",import.meta.url).pathname,tsserverPath:newURL("../../../built/local/tsgo",import.meta.url).pathname,fs:createVirtualFileSystem({"/tsconfig.json":"{}","/src/index.ts":`import { foo } from './foo';`,"/src/foo.ts":`export const foo = 42;`,}),});

Performance

These are the results of the included benchmarks on my M2 Mac. Note that IPC isvery fast on Apple Silicon, and Windows seems to see significantly more overhead per call. Tasks prefixedTS - refer to the rough equivalent with the TypeScript 5.9 API. ThegetSymbolAtPosition tasks are operating on TypeScript’sprogram.ts, which has 10893 identifiers.

┌─────────┬─────────────────────────────────────────────────────┬─────────────────────┬───────────────────────────┬────────────────────────┬────────────────────────┬─────────┐│ (index) │ Task name                                           │ Latency avg (ns)    │ Latency med (ns)          │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │├─────────┼─────────────────────────────────────────────────────┼─────────────────────┼───────────────────────────┼────────────────────────┼────────────────────────┼─────────┤│ 0       │ 'spawn API'                                         │ '3811417 ± 2.32%'   │ '3562750 ± 137292.00'     │ '268 ± 1.51%'          │ '281 ± 11'             │ 263     ││ 1       │ 'echo (small string)'                               │ '10145 ± 0.30%'     │ '8792.0 ± 2375.00'        │ '116283 ± 0.24%'       │ '113740 ± 32663'       │ 98570   ││ 2       │ 'echo (large string)'                               │ '802872 ± 1.27%'    │ '783375 ± 82458.00'       │ '1285 ± 0.86%'         │ '1277 ± 133'           │ 1246    ││ 3       │ 'echo (small Uint8Array)'                           │ '11170 ± 0.40%'     │ '9750.0 ± 2458.00'        │ '104702 ± 0.24%'       │ '102564 ± 27325'       │ 89529   ││ 4       │ 'echo (large Uint8Array)'                           │ '540871 ± 1.95%'    │ '498542 ± 71500.00'       │ '1989 ± 0.99%'         │ '2006 ± 285'           │ 1849    ││ 5       │ 'load project'                                      │ '8275640 ± 19.35%'  │ '7099958 ± 337374.00'     │ '136 ± 2.83%'          │ '141 ± 7'              │ 121     ││ 6       │ 'load project (client FS)'                          │ '95038294 ± 5.86%'  │ '93161042 ± 7466791.50'   │ '11 ± 3.38%'           │ '11 ± 1'               │ 64      ││ 7       │ 'TS - load project'                                 │ '380236148 ± 1.55%' │ '375040917 ± 14878416.50' │ '3 ± 1.47%'            │ '3 ± 0'                │ 64      ││ 8       │ 'transfer debug.ts'                                 │ '732232 ± 3.04%'    │ '681083 ± 12334.00'       │ '1434 ± 0.56%'         │ '1468 ± 27'            │ 1366    ││ 9       │ 'transfer program.ts'                               │ '2660346 ± 4.04%'   │ '2431708 ± 53875.00'      │ '395 ± 1.33%'          │ '411 ± 9'              │ 377     ││ 10      │ 'transfer checker.ts'                               │ '27890882 ± 2.28%'  │ '26820333 ± 433416.50'    │ '36 ± 1.92%'           │ '37 ± 1'               │ 64      ││ 11      │ 'materialize program.ts'                            │ '1418572 ± 2.33%'   │ '1376500 ± 10417.00'      │ '715 ± 0.46%'          │ '726 ± 5'              │ 705     ││ 12      │ 'materialize checker.ts'                            │ '23262525 ± 18.32%' │ '21435625 ± 1205000.00'   │ '47 ± 3.34%'           │ '47 ± 3'               │ 64      ││ 13      │ 'getSymbolAtPosition - one location'                │ '11805 ± 1.84%'     │ '11042 ± 959.00'          │ '88571 ± 0.11%'        │ '90563 ± 8202'         │ 84712   ││ 14      │ 'TS - getSymbolAtPosition - one location'           │ '918.37 ± 0.36%'    │ '917.00 ± 1.00'           │ '1098668 ± 0.01%'      │ '1090513 ± 1191'       │ 1088885 ││ 15      │ 'getSymbolAtPosition - 10893 identifiers'           │ '140520504 ± 1.44%' │ '138652312 ± 2446271.00'  │ '7 ± 1.13%'            │ '7 ± 0'                │ 64      ││ 16      │ 'getSymbolAtPosition - 10893 identifiers (batched)' │ '27321398 ± 4.48%'  │ '26289875 ± 672916.50'    │ '37 ± 2.24%'           │ '38 ± 1'               │ 64      ││ 17      │ 'getSymbolAtLocation - 10893 identifiers'           │ '130045149 ± 1.26%' │ '128063583 ± 2391709.00'  │ '8 ± 1.09%'            │ '8 ± 0'                │ 64      ││ 18      │ 'getSymbolAtLocation - 10893 identifiers (batched)' │ '20973507 ± 6.26%'  │ '19680167 ± 432749.50'    │ '49 ± 2.82%'           │ '51 ± 1'               │ 64      ││ 19      │ 'TS - getSymbolAtLocation - 10893 identifiers'      │ '12942349 ± 27.49%' │ '11076708 ± 130521.00'    │ '89 ± 2.62%'           │ '90 ± 1'               │ 78      │└─────────┴─────────────────────────────────────────────────────┴─────────────────────┴───────────────────────────┴────────────────────────┴────────────────────────┴─────────┘

To editorialize these numbers a bit: in absolute terms, this is pretty fast, even transferring large payloads like a binary-encodedchecker.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 isgetTypeOfSymbol, 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:

  1. When the client makes two calls that result in the same symbol or same type, the client should return the strict-equal same object, while allowing garbage collection to work on those objects.
  2. When one of those client objects goes out of scope, it should eventually be released from the server, so server memory doesn’t grow indefinitely.

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:

{constsymbol=project.getSymbolAtPosition("program.ts",0);if(symbol){// ...}symbol.dispose();}

or withexplicit resource management:

{  usingsymbol=project.getSymbolAtPosition("program.ts",0);if(symbol){// ...}}

maxpatiiuk reacted with thumbs up emojicontroversial, JoshuaKGoldberg, kirkwaiblinger, petamoriken, mrazauskas, subframe7536, marcus-sa, tombl, johnnyreilly, acutmore, and 7 more reacted with hooray emoji
@andrewbranch
Copy link
MemberAuthor

transpileModule is not implemented here yet—this PR contains a very small fraction of methods we’ll want to expose eventually.

@jakebailey
Copy link
Member

From offline chats it sounds likets-loader would probably not work with the new Go API in type checking mode, asts-loader goes deep into the guts of the TypeScript APIs.

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.

@andrewbranch
Copy link
MemberAuthor

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.

johnnyreilly reacted with thumbs up emoji

@jakebailey
Copy link
Member

Ah, multiple private chats 😄

andrewbranch and johnnyreilly reacted with laugh emoji

@andrewbranchandrewbranch added this pull request to themerge queueApr 10, 2025
@github-merge-queuegithub-merge-queuebot removed this pull request from themerge queue due to failed status checksApr 10, 2025
@jakebailey
Copy link
Member

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.

@andrewbranchandrewbranch added this pull request to themerge queueApr 10, 2025
@github-merge-queuegithub-merge-queuebot removed this pull request from themerge queue due to failed status checksApr 10, 2025
@andrewbranchandrewbranch added this pull request to themerge queueApr 10, 2025
Merged via the queue intomicrosoft:main with commita5454ebApr 10, 2025
23 checks passed
@andrewbranchandrewbranch deleted the api branchApril 10, 2025 23:51
shinichy pushed a commit to shinichy/typescript-go that referenced this pull requestApr 12, 2025
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment
Reviewers

@controversialcontroversialcontroversial left review comments

Copilot code reviewCopilotCopilot left review comments

@jakebaileyjakebaileyjakebailey approved these changes

@gabrielluizsfgabrielluizsfgabrielluizsf approved these changes

@rbucktonrbucktonAwaiting requested review from rbuckton

@ahejlsbergahejlsbergAwaiting requested review from ahejlsberg

@sandersnsandersnAwaiting requested review from sandersn

Assignees
No one assigned
Labels
None yet
Projects
None yet
Milestone
No milestone
Development

Successfully merging this pull request may close these issues.

6 participants
@andrewbranch@jakebailey@johnnyreilly@acutmore@controversial@gabrielluizsf

[8]ページ先頭

©2009-2025 Movatter.jp