turbo-editors/turbo-corepublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/turbo-editors/turbo-core.git
git clone ssh://git@rickub.com/turbo-editors/turbo-core.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

README.md · 33 lines · 2.9 KBmarkdown Blame HistoryRaw
🛟 Updated. 28d5985 k33g 6h ago1# jsonrpc
2
3A JSON-RPC 2.0 connection over a framed byte stream.
4
5It 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
9It 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
33The 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.