jsonrpc
A JSON-RPC 2.0 connection over a framed byte stream.
It holds everything the specification says and nothing about any particular use of it: requests and their answers, notifications, requests arriving the other way, and the error object a failure travels in.
Why it is its own package
It was lsp's, until a second protocol needed it. lsp speaks to a language server, acp speaks to a coding agent, and between them the only difference at this level is how one message is marked off from the next — a Content-Length header for one, a newline for the other. That difference is the Framer interface; everything above it is shared.
Layers
| File | What it is |
|---|---|
jsonrpc.go |
The wire types, the error codes, and the Framer seam |
conn.go |
The connection: concurrent calls matched to replies by id, notifications, inbound requests |
request.go |
An inbound request, and the answer that may come later |
NewConn takes an io.ReadWriteCloser, not a command. That is what lets a whole client be tested against a peer in the same process, over net.Pipe — real framing, real concurrency, real decoding, no subprocess and no timing to get lucky with.
Two things that are easy to get wrong
The two directions are separate id spaces. A peer numbers its requests from one, and so do we. An agent really does send id: 1 while a call of ours carrying the same id is in flight — that is ordinary behaviour, not a broken agent. Replies are matched only against the ids this connection sent; an inbound request is never looked up among them.
An answer may have to come later. Every question a language server asks can be answered from what the client already knows. An agent asking permission to run a command cannot be: the answer comes from a dialog somebody has to look at, and opening one belongs to the goroutine that draws. So RequestFunc is handed a *Request and returns nothing — it may reply now, or keep the Request and reply several turns of an event loop later, from another goroutine.
Reply is guarded by a sync.Once. A window closing tears down a pending permission that may already have been answered, and two responses carrying one id would desynchronise a peer that matches answers to requests by exactly that id.
Tests
go test ./jsonrpc/ drives a real connection over net.Pipe against a peer written by hand — deliberately not built on Conn, because a peer sharing the code under test could not catch that code writing a malformed frame.
The peer reads continuously into a buffered channel rather than on demand. net.Pipe is synchronous, so a write blocks until somebody reads, and an on-demand reader deadlocks against a Reply made from the test goroutine. It also never calls t.Fatalf: that is not allowed outside the test goroutine, and doing it there hangs the run instead of failing it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|