| 🛟 Updated. 28d5985 k33g 6h ago | 1 | # jsonrpc |
| 2 | |
| 3 | A JSON-RPC 2.0 connection over a framed byte stream. |
| 4 | |
| 5 | 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. |
| 6 | |
| 7 | ## Why it is its own package |
| 8 | |
| 9 | It was `lsp`'s, until a second protocol needed it. [`lsp`](../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. |
| 10 | |
| 11 | ## Layers |
| 12 | |
| 13 | | File | What it is | |
| 14 | | --- | --- | |
| 15 | | `jsonrpc.go` | The wire types, the error codes, and the `Framer` seam | |
| 16 | | `conn.go` | The connection: concurrent calls matched to replies by id, notifications, inbound requests | |
| 17 | | `request.go` | An inbound request, and the answer that may come later | |
| 18 | |
| 19 | `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. |
| 20 | |
| 21 | ## Two things that are easy to get wrong |
| 22 | |
| 23 | **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. |
| 24 | |
| 25 | **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. |
| 26 | |
| 27 | `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. |
| 28 | |
| 29 | ## Tests |
| 30 | |
| 31 | `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. |
| 32 | |
| 33 | 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. |