Class: Copilot::JsonRpcClient
- Inherits:
-
Object
- Object
- Copilot::JsonRpcClient
- Defined in:
- lib/copilot/json_rpc_client.rb
Overview
Minimal threaded JSON-RPC 2.0 client for stdio / TCP IO transport.
Messages use Content-Length header framing (the LSP/JSON-RPC wire format):
Content-Length: <byte-length>\r\n
\r\n
<JSON payload>
The client runs a background reader thread that dispatches:
-
Responses to pending requests (via per-request Queue)
-
Notifications to the registered notification handler
-
Incoming requests (server -> client) to registered request handlers
Instance Method Summary collapse
-
#initialize(input, output) ⇒ JsonRpcClient
constructor
A new instance of JsonRpcClient.
-
#notify(method, params = nil) ⇒ Object
Send a JSON-RPC notification (fire-and-forget, no response expected).
-
#on_notification {|method, params| ... } ⇒ Object
Register a handler for incoming notifications from the server.
-
#on_request(method) {|params| ... } ⇒ Object
Register a handler for incoming requests from the server.
-
#request(method, params = nil, timeout: 30) ⇒ Object
Send a JSON-RPC request and wait synchronously for the response.
-
#start ⇒ Object
Start the background reader thread.
-
#stop ⇒ Object
Stop the background reader thread.
Constructor Details
#initialize(input, output) ⇒ JsonRpcClient
Returns a new instance of JsonRpcClient.
35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/copilot/json_rpc_client.rb', line 35 def initialize(input, output) @input = input @output = output @pending_requests = {} # id => Queue @pending_lock = Mutex.new @write_lock = Mutex.new @notification_handler = nil # Proc(method, params) @request_handlers = {} # method => Proc(params) -> Hash @running = false @reader_thread = nil end |
Instance Method Details
#notify(method, params = nil) ⇒ Object
Send a JSON-RPC notification (fire-and-forget, no response expected).
129 130 131 132 133 134 135 |
# File 'lib/copilot/json_rpc_client.rb', line 129 def notify(method, params = nil) ({ jsonrpc: "2.0", method: method, params: params || {}, }) end |
#on_notification {|method, params| ... } ⇒ Object
Register a handler for incoming notifications from the server. The handler receives (method, params).
141 142 143 |
# File 'lib/copilot/json_rpc_client.rb', line 141 def on_notification(&handler) @notification_handler = handler end |
#on_request(method) {|params| ... } ⇒ Object
Register a handler for incoming requests from the server. The handler receives (params) and must return a Hash result.
151 152 153 154 155 156 157 |
# File 'lib/copilot/json_rpc_client.rb', line 151 def on_request(method, &handler) if handler.nil? @request_handlers.delete(method) else @request_handlers[method] = handler end end |
#request(method, params = nil, timeout: 30) ⇒ Object
Send a JSON-RPC request and wait synchronously for the response.
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/copilot/json_rpc_client.rb', line 82 def request(method, params = nil, timeout: 30) request_id = SecureRandom.uuid queue = Queue.new @pending_lock.synchronize do @pending_requests[request_id] = queue end ({ jsonrpc: "2.0", id: request_id, method: method, params: params || {}, }) response = nil begin # Queue#pop with a timeout: use Timeout or poll. We use a simple # polling approach to avoid Timeout's thread-safety issues. deadline = Time.now + (timeout || 30) loop do begin response = queue.pop(true) # non-blocking break rescue ThreadError # queue empty if Time.now > deadline raise Timeout::Error, "JSON-RPC request '#{method}' timed out after #{timeout}s" end sleep 0.01 end end ensure @pending_lock.synchronize { @pending_requests.delete(request_id) } end if response.is_a?(Hash) && response.key?(:__error) raise response[:__error] end response end |
#start ⇒ Object
Start the background reader thread.
52 53 54 55 56 57 58 |
# File 'lib/copilot/json_rpc_client.rb', line 52 def start return if @running @running = true @reader_thread = Thread.new { read_loop } @reader_thread.abort_on_exception = false end |
#stop ⇒ Object
Stop the background reader thread.
61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/copilot/json_rpc_client.rb', line 61 def stop @running = false @reader_thread&.join(2.0) # Unblock any pending requests so callers don't hang forever. @pending_lock.synchronize do @pending_requests.each_value do |queue| queue << { __error: JsonRpcError.new(-32000, "Client stopped") } end @pending_requests.clear end end |