jrpc 1.1.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +55 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +228 -0
  6. data/CHANGELOG.md +84 -0
  7. data/Gemfile +17 -0
  8. data/README.md +163 -13
  9. data/Rakefile +3 -1
  10. data/bin/console +15 -0
  11. data/bin/jrpc +111 -0
  12. data/bin/jrpc-shell +109 -0
  13. data/bin/setup +8 -0
  14. data/jrpc.gemspec +9 -8
  15. data/lib/jrpc/errors.rb +65 -0
  16. data/lib/jrpc/id_generator.rb +22 -0
  17. data/lib/jrpc/message.rb +78 -0
  18. data/lib/jrpc/shared_client/outbound_queue.rb +71 -0
  19. data/lib/jrpc/shared_client/registry.rb +46 -0
  20. data/lib/jrpc/shared_client/ticket.rb +84 -0
  21. data/lib/jrpc/shared_client/transport_loop.rb +290 -0
  22. data/lib/jrpc/shared_client.rb +194 -0
  23. data/lib/jrpc/simple_client.rb +89 -0
  24. data/lib/jrpc/transport/base.rb +60 -0
  25. data/lib/jrpc/transport/tcp.rb +243 -0
  26. data/lib/jrpc/transport.rb +12 -0
  27. data/lib/jrpc/version.rb +3 -1
  28. data/lib/jrpc.rb +15 -16
  29. metadata +35 -76
  30. data/.travis.yml +0 -4
  31. data/lib/jrpc/base_client.rb +0 -123
  32. data/lib/jrpc/error/client_error.rb +0 -5
  33. data/lib/jrpc/error/connection_error.rb +0 -11
  34. data/lib/jrpc/error/error.rb +0 -5
  35. data/lib/jrpc/error/internal_error.rb +0 -9
  36. data/lib/jrpc/error/internal_server_error.rb +0 -5
  37. data/lib/jrpc/error/invalid_params.rb +0 -9
  38. data/lib/jrpc/error/invalid_request.rb +0 -9
  39. data/lib/jrpc/error/method_not_found.rb +0 -9
  40. data/lib/jrpc/error/parse_error.rb +0 -9
  41. data/lib/jrpc/error/server_error.rb +0 -11
  42. data/lib/jrpc/error/unknown_error.rb +0 -5
  43. data/lib/jrpc/tcp_client.rb +0 -112
  44. data/lib/jrpc/transport/socket_base.rb +0 -88
  45. data/lib/jrpc/transport/socket_tcp.rb +0 -98
  46. data/lib/jrpc/utils.rb +0 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67534bff4d06dfed8a49ddf71bb56356d65f9c119731b220e6490394a187f720
4
- data.tar.gz: fd53da1804ddc60f54d549811b7d866541601dc4b3f58dc15de2a112c10039cf
3
+ metadata.gz: e9d76a4b0925e75829ba1a406594a0ef8f0c8045e38d6a06362c3eac2892affb
4
+ data.tar.gz: f6f28cf60f79bd9108f82299b8cac6ada99ae3a0a80bcc85baa3fa1b51c86560
5
5
  SHA512:
6
- metadata.gz: c99a86b3b5a59ff431d5356604d4cfcd1803212a8e3be22819de4bbab9d826d985a47bec9fed72cc82f8adfa80f208b1823ab85566325fe3f7965db52d631911
7
- data.tar.gz: d565e6cf0cd0afa20b935d1ca6e554d2aa6142dd46bfbefd1468f4910175f326656a6015fae7a160ef5ad8da41574e0c8c9d000ea8d624e5e989e5f8683ca262
6
+ metadata.gz: 365d4ab7d191d4853b48f9c05875afb11e4619bf4a8a733b66bf52f8dd79bcb702c4d91bdff561088bee4086dcda01af58c3ee4ce0e89d838c8490019c827aaa
7
+ data.tar.gz: cb74295dd00108aa5e2de6bf93584204e903a36aa523cb6ea44cc5f18bf787c3273969a8cb18d89efbed89db8757b65ad24a45ba5ba66cd49aa2518413f989b8
@@ -0,0 +1,55 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - master
7
+ pull_request:
8
+
9
+ permissions:
10
+ contents: read
11
+ pull-requests: write
12
+
13
+ jobs:
14
+ rspec:
15
+ name: RSpec (Ruby ${{ matrix.ruby }})
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ ruby:
21
+ - '3.3'
22
+ - '3.4'
23
+ steps:
24
+ - uses: actions/checkout@v4
25
+
26
+ - name: Set up Ruby
27
+ uses: ruby/setup-ruby@v1
28
+ with:
29
+ ruby-version: ${{ matrix.ruby }}
30
+ bundler-cache: true
31
+
32
+ - name: Run RuboCop
33
+ run: bundle exec rubocop --parallel
34
+
35
+ - name: Run tests
36
+ run: bundle exec rake spec
37
+
38
+ - name: Code Coverage Summary
39
+ if: matrix.ruby == '3.4'
40
+ uses: irongut/CodeCoverageSummary@v1.3.0
41
+ with:
42
+ filename: coverage/coverage.xml
43
+ format: markdown
44
+ output: both
45
+
46
+ - name: Write coverage to job summary
47
+ if: matrix.ruby == '3.4'
48
+ run: cat code-coverage-results.md >> "$GITHUB_STEP_SUMMARY"
49
+
50
+ - name: Add coverage comment to PR
51
+ if: matrix.ruby == '3.4' && github.event_name == 'pull_request'
52
+ uses: marocchino/sticky-pull-request-comment@v2
53
+ with:
54
+ recreate: true
55
+ path: code-coverage-results.md
data/.gitignore CHANGED
@@ -7,3 +7,4 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ .rake_tasks~
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
1
  --format documentation
2
2
  --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,228 @@
1
+ plugins:
2
+ - rubocop-performance
3
+ - rubocop-rspec
4
+ - rubocop-rake
5
+
6
+ AllCops:
7
+ TargetRubyVersion: 3.3
8
+ DisplayStyleGuide: true
9
+ ExtraDetails: true
10
+ NewCops: enable
11
+ Exclude:
12
+ - 'vendor/**/*'
13
+ - 'tmp/**/*'
14
+
15
+ ##################### Bundler ###############################
16
+
17
+ Bundler/OrderedGems:
18
+ Exclude:
19
+ - Gemfile
20
+
21
+ ##################### Gemspec ###############################
22
+
23
+ Gemspec/RequireMFA:
24
+ Enabled: false
25
+
26
+ Gemspec/RequiredRubyVersion:
27
+ Enabled: false
28
+
29
+ ##################### Lint ###############################
30
+
31
+ Lint/UnderscorePrefixedVariableName:
32
+ Enabled: false
33
+
34
+ Lint/AmbiguousOperatorPrecedence:
35
+ Enabled: false
36
+
37
+ Lint/MissingSuper:
38
+ Enabled: false
39
+
40
+ Lint/AmbiguousBlockAssociation:
41
+ Exclude:
42
+ - 'spec/**/*.rb'
43
+
44
+ Lint/UselessAccessModifier:
45
+ Enabled: true
46
+ MethodCreatingMethods:
47
+ - delegate
48
+ - attribute
49
+ - service_entity
50
+ - parameter
51
+ - parameters
52
+ ContextCreatingMethods:
53
+ - concerning
54
+ - included
55
+
56
+ ##################### Layout #############################
57
+
58
+ Layout/LineLength:
59
+ Max: 311
60
+
61
+ ##################### Performance #########################
62
+
63
+ Performance/CollectionLiteralInLoop:
64
+ Exclude:
65
+ - 'spec/**/*.rb'
66
+
67
+ ##################### Style ###############################
68
+
69
+ Style/StringLiterals:
70
+ EnforcedStyle: single_quotes
71
+
72
+ Style/StringLiteralsInInterpolation:
73
+ EnforcedStyle: single_quotes
74
+
75
+ Style/KeywordParametersOrder:
76
+ Enabled: false
77
+
78
+ Style/IfUnlessModifier:
79
+ Enabled: false
80
+
81
+ Style/WhenThen:
82
+ Enabled: false
83
+
84
+ Style/SafeNavigation:
85
+ Enabled: false
86
+
87
+ Style/Documentation:
88
+ Enabled: false
89
+
90
+ Style/SymbolArray:
91
+ Enabled: false
92
+
93
+ Style/HashAsLastArrayItem:
94
+ Enabled: false
95
+
96
+ Style/ClassAndModuleChildren:
97
+ Enabled: false
98
+
99
+ Style/GuardClause:
100
+ Enabled: false
101
+
102
+ Style/Lambda:
103
+ Enabled: false
104
+ EnforcedStyle: literal
105
+
106
+ Style/WordArray:
107
+ Enabled: false
108
+
109
+ Style/BlockDelimiters:
110
+ Enabled: true
111
+ EnforcedStyle: braces_for_chaining
112
+
113
+ Style/InvertibleUnlessCondition:
114
+ Enabled: true
115
+ InverseMethods:
116
+ :!=: :==
117
+ :>: :<=
118
+ :<=: :>
119
+ :<: :>=
120
+ :>=: :<
121
+ :!~: :=~
122
+ :zero?: :nonzero?
123
+ :nonzero?: :zero?
124
+ :any?: :none?
125
+ :none?: :any?
126
+ :even?: :odd?
127
+ :odd?: :even?
128
+ :present?: :blank?
129
+ :blank?: :present?
130
+
131
+ ##################### Metrics #############################
132
+
133
+ Metrics/PerceivedComplexity:
134
+ Max: 45
135
+
136
+ Metrics/ClassLength:
137
+ Max: 1060
138
+
139
+ Metrics/BlockNesting:
140
+ Max: 5
141
+
142
+ Metrics/BlockLength:
143
+ Enabled: false
144
+
145
+ Metrics/CyclomaticComplexity:
146
+ Max: 45
147
+
148
+ Metrics/MethodLength:
149
+ Enabled: false
150
+
151
+ Metrics/ModuleLength:
152
+ Enabled: false
153
+
154
+ Metrics/AbcSize:
155
+ Max: 241
156
+
157
+ Metrics/ParameterLists:
158
+ Max: 9
159
+
160
+ ##################### Naming ##############################
161
+
162
+ Naming/MethodParameterName:
163
+ AllowedNames:
164
+ - "iv"
165
+ - "by"
166
+ - "to"
167
+ - "id"
168
+ - "io"
169
+ - "on"
170
+
171
+ Naming/VariableNumber:
172
+ Enabled: false
173
+
174
+ Naming/PredicatePrefix:
175
+ Enabled: false
176
+
177
+ Naming/PredicateMethod:
178
+ Enabled: false
179
+
180
+ ##################### RSpec ###############################
181
+
182
+ RSpec/NamedSubject:
183
+ Enabled: false
184
+
185
+ RSpec/MultipleMemoizedHelpers:
186
+ Enabled: false
187
+
188
+ RSpec/ExampleLength:
189
+ Max: 141
190
+
191
+ RSpec/MultipleExpectations:
192
+ Max: 79
193
+
194
+ RSpec/DescribeClass:
195
+ Enabled: false
196
+
197
+ RSpec/MessageSpies:
198
+ Enabled: false
199
+
200
+ RSpec/SharedExamples:
201
+ Enabled: false
202
+
203
+ RSpec/NestedGroups:
204
+ Max: 18
205
+
206
+ RSpec/ContextWording:
207
+ Enabled: false
208
+
209
+ RSpec/ExampleWording:
210
+ Enabled: false
211
+
212
+ RSpec/ExpectChange:
213
+ Enabled: false
214
+
215
+ RSpec/AnyInstance:
216
+ Enabled: false
217
+
218
+ RSpec/SpecFilePathFormat:
219
+ Enabled: false
220
+
221
+ RSpec/IndexedLet:
222
+ Enabled: false
223
+
224
+ RSpec/ExpectInLet:
225
+ Enabled: true
226
+
227
+ RSpec/IncludeExamples:
228
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,84 @@
1
+ # Changelog
2
+
3
+ ### Unreleased
4
+
5
+ ### 2.0.0
6
+
7
+ Full rewrite. JRPC 2.0 is not API-compatible with 1.x.
8
+
9
+ **New**
10
+
11
+ * `JRPC::SharedClient` — one shared instance, one connection, serving many caller
12
+ threads and/or fibers. Owns a dedicated transport thread that multiplexes
13
+ responses by id. Supports Puma threads, rage-rb/Falcon fibers, and mixed
14
+ thread/fiber callers. Fiber callers require a spec-compliant `Fiber.scheduler`.
15
+ (Internally drafted as `ThreadQueueClient`; never shipped under that name.)
16
+ * `JRPC::SimpleClient` — single-threaded client, the functional replacement for
17
+ the old `TcpClient`.
18
+ * `concurrent-ruby` (`~> 1.2`) added as a runtime dependency (backs the shared
19
+ client's result futures).
20
+ * `logger` added as an explicit runtime dependency (no longer guaranteed bundled
21
+ on Ruby 3.5+).
22
+
23
+ **Removed / breaking**
24
+
25
+ * `JRPC::TcpClient` removed — use `JRPC::SimpleClient`.
26
+ * `JRPC::BaseClient` removed, including the `BaseClient.connect` block helper.
27
+ * All top-level error constants moved under `JRPC::Errors::*`.
28
+ * `method_missing` magic removed — pass the full method name as a String or Symbol.
29
+ * `invoke_request` / `invoke_notification` removed.
30
+ * `perform_request` removed — use `request` and `notification`.
31
+ * `namespace:` option removed.
32
+ * Umbrella `timeout:` option removed — use `read_timeout` / `write_timeout` /
33
+ `connect_timeout` (`SimpleClient`), or `ttl:` (`SharedClient`).
34
+ * `close_after_sent:` renamed to `autoclose:`.
35
+ * `connect_retry_count` default changed from `10` to `0`.
36
+ * Constructors no longer connect eagerly — the first call connects.
37
+ * Malformed responses now raise `Errors::MalformedResponseError` (a `ServerError`),
38
+ not `ClientError`. In 1.x the missing-comma-terminator case raised `ClientError`.
39
+ * `SimpleClient` read/write/connect timeouts now raise `Errors::Timeout`, not
40
+ `ConnectionError`.
41
+ * `oj` runtime dependency dropped — JRPC uses stdlib `json`. For Oj speed,
42
+ `require 'oj'; Oj.mimic_JSON` yourself.
43
+ * `netstring` is no longer a dependency — framing is owned in-tree by the transport.
44
+ * `required_ruby_version` set to `>= 3.3` (the floor where the
45
+ `ConditionVariable` ↔ `Fiber.scheduler` cooperation that fiber callers depend on
46
+ is verified).
47
+ * `bin/jrpc` and `bin/jrpc-shell` rewritten on top of `SimpleClient`; flag/usage
48
+ changes (see `README.md` and `jrpc --help`).
49
+
50
+ ### 1.1.8
51
+ * handling FIN signal for TCP socket [didww/jrpc#19](https://github.com/didww/jrpc/pull/19)
52
+ * add gem executables [didww/jrpc#19](https://github.com/didww/jrpc/pull/19)
53
+
54
+ ### 1.1.7
55
+ * connect ot socket in nonblock mode
56
+
57
+ ### 1.1.6
58
+ * update oj version to ~> 3.0
59
+
60
+ ### 1.1.5
61
+ * update oj version to ~> 2.0
62
+
63
+ ### 1.1.4
64
+ * handle EOF on read
65
+ * fix jrpc error require
66
+ * use JRPC::Error as base class for JRPC::Transport::SocketBase::Error
67
+
68
+ ### 1.1.3
69
+ * close socket when clearing socket if it's not closed
70
+
71
+ ### 1.1.2
72
+ * reset socket when broken pipe error appears
73
+
74
+ ### 1.1.1
75
+ * fix rescuing error in TcpClient initializer
76
+
77
+ ### 1.1.0
78
+ * use own socket wrapper
79
+
80
+ ### 1.0.1
81
+ * Net::TCPClient#read method process data with buffer variable
82
+
83
+ ### 1.0.0
84
+ * stable release
data/Gemfile CHANGED
@@ -1,4 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in jrpc.gemspec
4
6
  gemspec
7
+
8
+ gem 'bundler'
9
+ gem 'rake', '~> 13.0'
10
+ gem 'rspec', '~> 3.0'
11
+
12
+ # Provides a real Fiber.scheduler for the fiber-caller specs (SharedClient §9.8).
13
+ gem 'async', '~> 2.0'
14
+
15
+ gem 'rubocop', '~> 1.21'
16
+ gem 'rubocop-performance'
17
+ gem 'rubocop-rspec'
18
+ gem 'rubocop-rake', '~> 0.7.1'
19
+
20
+ gem 'simplecov', '~> 0.22', require: false
21
+ gem 'simplecov-cobertura', '~> 3.1', require: false
data/README.md CHANGED
@@ -1,33 +1,183 @@
1
1
  # JRPC
2
2
 
3
- JSON RPC TCP client
3
+ A JSON-RPC 2.0 client for Ruby, over TCP, with netstring framing.
4
4
 
5
- ## Installation
5
+ JRPC ships two clients with sharp, separate responsibilities:
6
+
7
+ | | `JRPC::SimpleClient` | `JRPC::SharedClient` |
8
+ |---|---|---|
9
+ | Concurrency | single thread/fiber only | shared across many threads **and/or** fibers |
10
+ | Connection | one socket, lazy connect | one shared socket, dedicated transport thread |
11
+ | Multiplexing | one in-flight call at a time | many in-flight calls, id-demuxed |
12
+ | Timeouts | per-call `read_timeout`/`write_timeout` | per-message `ttl` |
13
+ | Use it for | CLI tools, scripts, one-shot calls, per-thread pools | Rails+Puma, rage-rb, Falcon, any long-lived shared client |
14
+
15
+ Pick `SimpleClient` unless you need one client instance to serve concurrent callers. It is not thread-safe or fiber-safe; use one instance per thread/fiber (or a pool). Pick `SharedClient` when a single process-wide instance must serve many caller threads or fibers over a single connection.
6
16
 
7
- Add this line to your application's Gemfile:
17
+ ## Installation
8
18
 
9
19
  ```ruby
10
20
  gem 'jrpc'
11
21
  ```
12
22
 
13
- And then execute:
23
+ ```sh
24
+ $ bundle install
25
+ ```
14
26
 
15
- $ bundle
27
+ Requires **Ruby >= 3.3**. Fiber callers additionally require a spec-compliant `Fiber.scheduler` (e.g. [`async`](https://github.com/socketry/async)) on their thread — see [Fiber callers](#fiber-callers).
16
28
 
17
- Or install it yourself as:
29
+ ## SimpleClient
18
30
 
19
- $ gem install jrpc
31
+ ```ruby
32
+ client = JRPC::SimpleClient.new(
33
+ "127.0.0.1:1234",
34
+ connect_timeout: 60, # total wall-clock budget for connect, across retries (seconds)
35
+ read_timeout: 60,
36
+ write_timeout: 60,
37
+ connect_retry_count: 0, # retries after the first failed connect
38
+ autoclose: false, # close the socket after every call
39
+ id_prefix: nil, # random per instance if nil
40
+ logger: nil
41
+ )
42
+
43
+ result = client.request(:sum, [1, 2])
44
+ result = client.request(:sum, [1, 2], read_timeout: 10, write_timeout: 10)
45
+ client.notification(:log, { msg: "hi" })
46
+ client.notification(:log, { msg: "hi" }, write_timeout: 10)
47
+
48
+ client.close # terminal; the instance cannot be reused
49
+ client.closed? # => true
50
+ client.server # => "127.0.0.1:1234"
51
+ ```
20
52
 
21
- ## Usage
53
+ Behavior:
22
54
 
23
- TODO: Write usage instructions here
55
+ - The constructor does **not** open the connection. The first `request`/`notification` connects.
56
+ - `autoclose: true` closes the socket in an `ensure` after each call. The **client is still reusable** — the next call reconnects. `autoclose` controls the socket, not the client.
57
+ - `#close` is **terminal**. After it, `#closed?` is `true` and every call raises `ClientError("client closed")`. There is no reopen — make a new client.
58
+ - Not thread-safe, not fiber-safe.
24
59
 
25
- ## Contributing
60
+ ## SharedClient
26
61
 
27
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jrpc.
62
+ One instance, one connection, many concurrent callers. A dedicated transport thread owns the socket and demultiplexes responses by id.
28
63
 
64
+ ```ruby
65
+ client = JRPC::SharedClient.new(
66
+ "127.0.0.1:1234",
67
+ connect_timeout: 60,
68
+ connect_retry_count: 0,
69
+ connect_retry_interval: 0.5,
70
+ write_timeout: 5, # MUST be < default_ttl (see below)
71
+ reap_timeout: nil, # nil = never close an idle connection
72
+ default_ttl: 30, # per-message lifetime, seconds
73
+ max_queue_size: 10_000, # bounded; pass nil for unbounded (opt-in OOM risk)
74
+ id_prefix: nil,
75
+ logger: nil
76
+ )
77
+
78
+ result = client.request(:sum, [1, 2])
79
+ result = client.request(:sum, [1, 2], ttl: 10)
80
+
81
+ client.notification(:log, { msg: "hi" })
82
+ client.notification(:log, { msg: "hi" }, ttl: 5)
83
+ client.notification(:metric, [1], fire_and_forget: true) # send errors/TTL expiry are logged, not raised
84
+
85
+ client.close # graceful shutdown, default timeout: 5 seconds
86
+ client.close(timeout: 10)
87
+ client.closed?
88
+ client.server
89
+ ```
29
90
 
30
- ## License
91
+ Behavior:
92
+
93
+ - **TTL, not per-call timeout.** Each message carries `expires_at = now + ttl`. The transport thread is the timer authority; the caller blocks until the message resolves, fails, or its TTL elapses. `ttl: nil` blocks forever (opt-in).
94
+ - **`write_timeout < default_ttl` is enforced.** While the transport thread is parked in a single `write_frame`, it cannot fire TTL deadlines for other messages, so `write_timeout` is the maximum TTL-firing lag. The constructor raises `ArgumentError` if `write_timeout >= default_ttl`.
95
+ - **`notification` blocks until sent by default** (send errors propagate). Pass `fire_and_forget: true` to return immediately; then send errors and TTL expiry are logged, not raised. `request` never accepts `fire_and_forget`.
96
+ - **Bounded queue.** When the outbound queue is at `max_queue_size`, enqueue raises `ClientError("queue full")`.
97
+ - **Connection drops** resolve every in-flight request with `ConnectionError`; the transport thread keeps running and reconnects on the next message.
98
+ - **Reaping.** With `reap_timeout` set, the connection closes after that many idle seconds (no in-flight messages, empty queue, no bytes received) and reopens on the next message.
99
+ - `#close` is graceful: it lets in-flight work drain up to `timeout`, then force-closes. It returns `true` on a clean join, `false` on a forced close. Idempotent.
100
+ - A crash in the transport thread is surfaced, not hidden: in-flight requests fail with `ConnectionError`, the client transitions to an unusable state, and every subsequent call raises `ClientError("client unusable: transport thread exited")`. The client does not auto-restart — instantiate a new one.
101
+
102
+ ### Fiber callers
103
+
104
+ `SharedClient` is shareable across the full Ruby concurrency matrix:
105
+
106
+ | Deployment | Caller is a... |
107
+ |---|---|
108
+ | Rails + Puma | Thread |
109
+ | rage-rb | Fiber under a single-thread Async reactor |
110
+ | Rails + Falcon | Fiber under a multi-thread Async reactor |
111
+ | Mixed | Some threads, some fibers, one client instance |
112
+
113
+ A caller blocks in a scheduler-aware wait, so a fiber under `Async`/`Falcon`/`rage-rb` **yields to the reactor** instead of stalling its OS thread; other fibers keep running and the response is routed back to the right fiber. This requires:
114
+
115
+ - Ruby **>= 3.3** (where the `ConditionVariable` ↔ `Fiber.scheduler` cooperation is verified), and
116
+ - a spec-compliant `Fiber.scheduler` active on the caller's thread, with correct cross-thread `unblock` (Async and Polyphony qualify).
117
+
118
+ No scheduler library is a runtime dependency — callers bring their own. Plain (non-scheduler) fibers are unsupported: they would block the OS thread on every wait. Use `SimpleClient` for non-scheduler code.
119
+
120
+ ## Errors
121
+
122
+ All errors live under `JRPC::Errors::*` and descend from `JRPC::Errors::Error`. The four public-facing classes are siblings (no inheritance between them), so rescue each by name or rescue `Errors::Error` to catch all:
123
+
124
+ ```
125
+ Errors::Error (RuntimeError)
126
+ ├── Errors::ClientError # caller-side: bad args, bad URI, client closed, queue full
127
+ ├── Errors::ConnectionError # cannot connect, or connection died (see Exception#cause)
128
+ ├── Errors::Timeout # message TTL elapsed, or SimpleClient read/write/connect timeout
129
+ └── Errors::ServerError # peer returned an error, or the response was unusable
130
+ attr_reader :code # nil for malformed responses
131
+ ├── Errors::ParseError # -32700
132
+ ├── Errors::InvalidRequest # -32600
133
+ ├── Errors::MethodNotFound # -32601
134
+ ├── Errors::InvalidParams # -32602
135
+ ├── Errors::InternalError # -32603
136
+ ├── Errors::InternalServerError # -32099..-32000
137
+ ├── Errors::UnknownError # any other code
138
+ └── Errors::MalformedResponseError # bad framing/JSON, id mismatch, wrong jsonrpc version
139
+ ```
140
+
141
+ `MalformedResponseError` is a `ServerError`, not a `ClientError`: a malformed response is the peer's fault.
142
+
143
+ ```ruby
144
+ begin
145
+ client.request(:do_thing, [1, 2])
146
+ rescue JRPC::Errors::ServerError => e
147
+ warn "rpc error #{e.code}: #{e.message}"
148
+ rescue JRPC::Errors::Timeout
149
+ warn "timed out"
150
+ rescue JRPC::Errors::ConnectionError => e
151
+ warn "connection: #{e.message} (cause: #{e.cause})"
152
+ end
153
+ ```
154
+
155
+ ## JSON serialization
156
+
157
+ JRPC uses the stdlib `json`. To swap in [oj](https://github.com/ohler55/oj) for speed, monkey-patch it yourself before use:
158
+
159
+ ```ruby
160
+ require 'oj'
161
+ Oj.mimic_JSON
162
+ ```
31
163
 
32
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
164
+ ## CLI tools
165
+
166
+ Two executables ship with the gem:
167
+
168
+ - `jrpc` — one-shot request/notification from the shell (`jrpc --help`).
169
+ - `jrpc-shell` — an interactive REPL (`connect`, `request`, `notification`, `disconnect`).
170
+
171
+ Both use `SimpleClient`.
172
+
173
+ ## Upgrading from 1.x
174
+
175
+ 2.0 is a full rewrite with many breaking changes (`JRPC::TcpClient`/`BaseClient` removed, error constants moved under `JRPC::Errors::*`, `method_missing`/`namespace:` dropped, no eager connect, and more). See the [CHANGELOG](CHANGELOG.md) for the complete list.
176
+
177
+ ## Contributing
178
+
179
+ Bug reports and pull requests are welcome on GitHub at https://github.com/didww/jrpc.
180
+
181
+ ## License
33
182
 
183
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
 
4
6
  RSpec::Core::RakeTask.new(:spec)
5
7
 
6
- task :default => :spec
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'jrpc'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)