Class: Copilot::JsonRpcClient

Inherits:
Object
  • Object
show all
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

Constructor Details

#initialize(input, output) ⇒ JsonRpcClient

Returns a new instance of JsonRpcClient.

Parameters:

  • input (IO)

    readable IO (e.g. process stdout or TCP socket)

  • output (IO)

    writable IO (e.g. process stdin or TCP socket)



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).

Parameters:

  • method (String)

    the RPC method name

  • params (Hash, nil) (defaults to: nil)

    optional parameters



129
130
131
132
133
134
135
# File 'lib/copilot/json_rpc_client.rb', line 129

def notify(method, params = nil)
  send_message({
    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).

Yields:

  • (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.

Parameters:

  • method (String)

    the RPC method to handle

Yields:

  • (params)

    the request parameters

Yield Returns:

  • (Hash)

    the result to send back



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.

Parameters:

  • method (String)

    the RPC method name

  • params (Hash, nil) (defaults to: nil)

    optional parameters

  • timeout (Numeric, nil) (defaults to: 30)

    seconds to wait (default 30)

Returns:

  • (Object)

    the result from the JSON-RPC response

Raises:

  • (JsonRpcError)

    if the server returns an error

  • (Timeout::Error)

    if the request times out



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

  send_message({
    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

#startObject

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

#stopObject

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