octaspace 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9dd2b5c9ae96cbc0bef9e36fd492b2a1dbdcf53af596a82193986039ff8a4da9
4
+ data.tar.gz: b0f0a7a1a3301be4f3d1dbe33f60652fc5c9e4b36e709779076d374fb0e16ff4
5
+ SHA512:
6
+ metadata.gz: 59cb1140ec7e34939b2535857d4d7afd395b8b25d45ac69df2c14c524bd1e14503f969704299fe6aa3d51021fa2491a8f00d0cf4dee3a3d848308a9aa9fb577d
7
+ data.tar.gz: 6b4353b1524bae1e594ef4109a93a371e539d48b157dcdd4e165a1283c60e4a86e719818f62276d3cac2628e4bf9cd419b651f5a4633302d391e3d5fc41f476d
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.1.0] - 2026-04-12
9
+
10
+ ### Added
11
+
12
+ - Initial release of the official OctaSpace Ruby SDK.
13
+ - Support for core resources: Accounts, Nodes, Sessions, Apps, Network, and Idle Jobs.
14
+ - Resource-oriented client: `client.nodes.list`, `client.services.session(uuid).stop`.
15
+ - Persistent HTTP connections (keep-alive mode) via `ConnectionPool`.
16
+ - Automatic URL rotation and failover for high availability.
17
+ - Retry with exponential backoff and jitter for transient errors.
18
+ - Typed error hierarchy based on HTTP status codes.
19
+ - Rails integration with automatic client sharing and graceful shutdown.
20
+ - Interactive Playground app for manual testing and diagnostics.
21
+
22
+ [0.1.0]: https://github.com/octaspace/ruby-sdk/compare/v0.1.0...HEAD
data/MIT-LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2024-present OctaSpace
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included
14
+ in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,336 @@
1
+ # octaspace
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/octaspace)](https://rubygems.org/gems/octaspace)
4
+ [![CI](https://github.com/octaspace/ruby-sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/octaspace/ruby-sdk/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](MIT-LICENSE)
6
+
7
+ Official Ruby SDK for the [OctaSpace API](https://api.octa.space/api-docs).
8
+
9
+ ## Features
10
+
11
+ - **Resource-oriented API** — `client.nodes.list`, `client.services.session(uuid).stop`
12
+ - **Keep-alive mode** — persistent connections via `faraday-net_http_persistent` + `connection_pool`
13
+ - **URL rotation / failover** — round-robin across multiple endpoints with per-URL cooldown
14
+ - **Retry with exponential backoff + jitter** — configurable retries on transient failures
15
+ - **Typed error hierarchy** — 12 exception classes mapped from HTTP status codes
16
+ - **`on_request` / `on_response` hooks** — for logging, tracing, APM
17
+ - **Rails integration** — Railtie, shared client, graceful shutdown at_exit
18
+ - **Playground app** — `bin/playground` with live diagnostics page
19
+ - **Ruby ≥ 3.2**, Rails 7.1 / 7.2 / 8.0
20
+
21
+ ## Installation
22
+
23
+ ```ruby
24
+ # Gemfile
25
+ gem "octaspace"
26
+
27
+ # Optional — required for keep_alive: true
28
+ gem "faraday-net_http_persistent"
29
+ gem "connection_pool"
30
+ ```
31
+
32
+ ```ruby
33
+ bundle install
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```ruby
39
+ require "octaspace"
40
+
41
+ # Public endpoints (no API key required)
42
+ client = OctaSpace::Client.new
43
+ client.network.info # GET /network
44
+
45
+ # Authenticated endpoints
46
+ client = OctaSpace::Client.new(api_key: ENV["OCTA_API_KEY"])
47
+
48
+ # Accounts
49
+ client.accounts.profile # GET /accounts
50
+ client.accounts.balance # GET /accounts/balance
51
+
52
+ # Nodes
53
+ client.nodes.list # GET /nodes
54
+ client.nodes.find(123) # GET /nodes/:id
55
+ client.nodes.reboot(123) # GET /nodes/:id/reboot
56
+ client.nodes.update_prices(123, gpu_hour: 0.5) # PATCH /nodes/:id/prices
57
+
58
+ # Sessions (list)
59
+ client.sessions.list # GET /sessions
60
+
61
+ # Session proxy — operations on a specific session
62
+ session = client.services.session("uuid-here")
63
+ session.info # GET /services/:uuid/info
64
+ session.logs # GET /services/:uuid/logs
65
+ session.stop(score: 5) # POST /services/:uuid/stop
66
+
67
+ # Services
68
+ client.services.mr.list # GET /services/mr
69
+ client.services.mr.create(
70
+ node_id: 1,
71
+ disk_size: 10,
72
+ image: "ubuntu:24.04",
73
+ app: "249b4cb3-3db1-4c06-98a4-772ba88cd81c"
74
+ ) # POST /services/mr
75
+ client.services.vpn.list # GET /services/vpn
76
+ client.services.vpn.create(node_id: 1, subkind: "wg") # POST /services/vpn
77
+ client.services.render.list # GET /services/render
78
+ client.services.render.create(node_id: 1, disk_size: 100) # POST /services/render
79
+
80
+ # Apps
81
+ client.apps.list # GET /apps
82
+
83
+ # Network
84
+ client.network.info # GET /network
85
+
86
+ # Idle Jobs
87
+ client.idle_jobs.find(node_id: 69, job_id: 42) # GET /idle_jobs/:node_id/:job_id
88
+ client.idle_jobs.logs(node_id: 69, job_id: 42) # GET /idle_jobs/:node_id/:job_id/logs
89
+ ```
90
+
91
+ ## Rails Integration
92
+
93
+ ### Initializer
94
+
95
+ ```ruby
96
+ # config/initializers/octaspace.rb
97
+ OctaSpace.configure do |config|
98
+ config.api_key = ENV["OCTA_API_KEY"]
99
+ config.keep_alive = true
100
+ config.pool_size = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
101
+ config.logger = Rails.logger
102
+ end
103
+ ```
104
+
105
+ ### Shared client
106
+
107
+ `OctaSpace.client` (called without arguments) returns a **lazily-initialized shared client** built from global configuration. It is safe to call it on every request:
108
+
109
+ ```ruby
110
+ # app/controllers/application_controller.rb
111
+ def octa_client
112
+ OctaSpace.client # returns the same instance each time — no new connections
113
+ end
114
+ ```
115
+
116
+ Pass arguments to create a **one-off client** instead (e.g., for per-user API keys):
117
+
118
+ ```ruby
119
+ OctaSpace.client(api_key: current_user.octa_api_key) # new client, not cached
120
+ ```
121
+
122
+ ### Graceful shutdown
123
+
124
+ The Railtie registers an `at_exit` hook that automatically shuts down the shared client's connection pool when the Rails process stops. No manual cleanup needed.
125
+
126
+ ## Configuration Reference
127
+
128
+ | Option | Default | Description |
129
+ |---|---|---|
130
+ | `api_key` | `nil` | API key sent as `Authorization` header (optional for public endpoints) |
131
+ | `base_url` | `https://api.octa.space` | Single API endpoint |
132
+ | `base_urls` | `nil` | Multiple endpoints — enables URL rotation |
133
+ | `keep_alive` | `false` | Persistent HTTP connections + pool |
134
+ | `pool_size` | `5` | Connection pool size (keep-alive mode) |
135
+ | `pool_timeout` | `5` | Seconds to wait for a pool slot |
136
+ | `idle_timeout` | `60` | Seconds before an idle connection closes |
137
+ | `open_timeout` | `10` | Seconds to open TCP connection |
138
+ | `read_timeout` | `30` | Seconds to read response |
139
+ | `write_timeout` | `30` | Seconds to write request body |
140
+ | `max_retries` | `2` | Retry attempts on transient failures |
141
+ | `retry_interval` | `0.5` | Base retry interval in seconds |
142
+ | `backoff_factor` | `2.0` | Exponential backoff multiplier |
143
+ | `ssl_verify` | `true` | Verify SSL certificates |
144
+ | `on_request` | `nil` | `callable(req_hash)` — before each request |
145
+ | `on_response` | `nil` | `callable(response)` — after each response |
146
+ | `logger` | `nil` | Ruby `Logger` instance |
147
+ | `log_level` | `:info` | Log level |
148
+ | `user_agent` | auto | `User-Agent` header value |
149
+
150
+ `persistent` is an alias for `keep_alive` for compatibility with Cube internals.
151
+
152
+ ## Keep-Alive Mode
153
+
154
+ Requires `faraday-net_http_persistent` and `connection_pool` in your Gemfile.
155
+
156
+ ```ruby
157
+ client = OctaSpace::Client.new(
158
+ api_key: ENV["OCTA_API_KEY"],
159
+ keep_alive: true,
160
+ pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i,
161
+ idle_timeout: 120
162
+ )
163
+
164
+ # Diagnostics
165
+ client.transport_stats
166
+ # => { mode: :persistent, pools: { "https://api.octa.space" => { size: 5, available: 4 } } }
167
+
168
+ # Explicit shutdown (optional — Railtie does this automatically in Rails)
169
+ client.shutdown
170
+ ```
171
+
172
+ ## URL Rotation / Failover
173
+
174
+ ```ruby
175
+ client = OctaSpace::Client.new(
176
+ api_key: ENV["OCTA_API_KEY"],
177
+ base_urls: ["https://api.octa.space", "https://api2.octa.space"]
178
+ )
179
+ ```
180
+
181
+ - Requests are distributed round-robin across healthy endpoints.
182
+ - If an endpoint raises a connection or timeout error, it enters a **30-second cooldown** and traffic shifts to the remaining endpoints.
183
+ - After cooldown, the endpoint is re-admitted automatically.
184
+
185
+ ## Hooks
186
+
187
+ ```ruby
188
+ OctaSpace.configure do |config|
189
+ config.on_request = ->(req) { Rails.logger.debug "→ #{req[:method].upcase} #{req[:path]}" }
190
+ config.on_response = ->(resp) { Rails.logger.debug "← #{resp.status} (#{resp.request_id})" }
191
+ end
192
+ ```
193
+
194
+ `on_request` receives `{ method:, path:, params: }`.
195
+ `on_response` receives an `OctaSpace::Response` instance.
196
+
197
+ ## Error Handling
198
+
199
+ ```ruby
200
+ begin
201
+ client.nodes.find(999_999)
202
+ rescue OctaSpace::NotFoundError => e
203
+ puts "Not found — request_id: #{e.request_id}"
204
+ rescue OctaSpace::AuthenticationError
205
+ puts "Invalid API key"
206
+ rescue OctaSpace::RateLimitError => e
207
+ sleep e.retry_after
208
+ retry
209
+ rescue OctaSpace::ConnectionError, OctaSpace::TimeoutError => e
210
+ puts "Network error: #{e.message}"
211
+ rescue OctaSpace::Error => e
212
+ puts "API error #{e.status}: #{e.message}"
213
+ end
214
+ ```
215
+
216
+ ### Error hierarchy
217
+
218
+ ```
219
+ OctaSpace::Error
220
+ ├── ConfigurationError — missing gems, invalid config
221
+ ├── NetworkError — no HTTP response received
222
+ │ ├── ConnectionError — TCP connection refused / failed
223
+ │ └── TimeoutError — open/read timeout
224
+ └── ApiError — HTTP response received with error status
225
+ ├── AuthenticationError 401
226
+ ├── PermissionError 403
227
+ ├── NotFoundError 404
228
+ ├── ValidationError 422
229
+ ├── RateLimitError 429 → #retry_after (seconds)
230
+ └── ServerError 5xx
231
+ ├── BadGatewayError 502
232
+ ├── ServiceUnavailableError 503
233
+ └── GatewayTimeoutError 504
234
+ ```
235
+
236
+ All `ApiError` subclasses expose:
237
+
238
+ - `#status` — HTTP status code
239
+ - `#request_id` — value of `X-Request-Id` response header
240
+ - `#response` — the raw `OctaSpace::Response` object
241
+
242
+ ## Types (Value Objects)
243
+
244
+ The `OctaSpace::Types` namespace provides immutable `Data.define` value objects for domain entities. They are **not** returned by default — resources return raw `response.data` (Hash/Array). Use them explicitly when you want structured objects:
245
+
246
+ ```ruby
247
+ response = client.nodes.find(123)
248
+ node = OctaSpace::Types::Node.new(**response.data.transform_keys(&:to_sym))
249
+
250
+ node.online? # => true / false
251
+ node.state # => "online"
252
+ node.id # => 123
253
+ ```
254
+
255
+ Available types: `Node`, `Account`, `Balance`, `Session`.
256
+
257
+ ## Playground App
258
+
259
+ Interactive demo for manual testing against a real API key:
260
+
261
+ ```bash
262
+ OCTA_API_KEY=your_key bin/playground
263
+ # → http://localhost:3000
264
+ ```
265
+
266
+ Pages:
267
+
268
+ | Route | Content |
269
+ |---|---|
270
+ | `/playground/account` | Profile + balance |
271
+ | `/playground/nodes` | Node list with state badges |
272
+ | `/playground/sessions` | Active sessions |
273
+ | `/playground/services` | Machine Rentals + VPN sessions |
274
+ | `/playground/diagnostics` | Transport mode, pool stats, URL rotator state (auto-refresh every 5s) |
275
+
276
+ ## Development
277
+
278
+ ### Gem Development
279
+
280
+ ```bash
281
+ bin/console # IRB with gem loaded
282
+ bundle exec standardrb # lint (StandardRB)
283
+ bundle exec rake test # tests only
284
+ ```
285
+
286
+ ### Local Standalone Testing
287
+
288
+ You can verify the gem in a clean Ruby environment without Rails:
289
+
290
+ 1. Build the gem: `gem build octaspace.gemspec`
291
+ 2. Install it locally: `gem install ./octaspace-0.1.0.gem`
292
+ 3. Test in IRB:
293
+
294
+ ```ruby
295
+ irb
296
+ > require 'octaspace'
297
+ > client = OctaSpace::Client.new # Test public endpoints
298
+ > client.network.info
299
+ > client_auth = OctaSpace::Client.new(api_key: "token") # Test authenticated client
300
+ > client_auth.accounts.profile.data # Retrieving profile data (email, ID, etc.)
301
+ ```
302
+
303
+ ### Running tests against multiple Rails versions
304
+
305
+ ```bash
306
+ bundle exec appraisal rails-7-1 rake test
307
+ bundle exec appraisal rails-7-2 rake test
308
+ bundle exec appraisal rails-8-0 rake test
309
+ ```
310
+
311
+ ### Dummy Application (Playground)
312
+
313
+ The repository includes a Rails "Dummy" application for manual testing and UI prototyping. It is located in `test/dummy`.`.
314
+
315
+ To run the dummy app:
316
+
317
+ 1. Ensure you have an API key: `export OCTA_API_KEY=your_key`
318
+ 2. Run the playground script: `bin/playground` (starts Puma on port 3000)
319
+ 3. Visit `http://localhost:3000/playground/diagnostics`
320
+
321
+ You can also run it manually from the directory:
322
+
323
+ ```bash
324
+ cd test/dummy
325
+ bin/rails server
326
+ ```
327
+
328
+ The dummy app is configured to use the local version of the gem. It is **not** included in the published gem package.
329
+
330
+ ## Packaging and Publishing
331
+
332
+ See [PUBLISHING.md](PUBLISHING.md) for instructions on how to package and release new versions of the gem.
333
+
334
+ ## License
335
+
336
+ [MIT](MIT-LICENSE) © OctaSpace Team
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OctaSpace
4
+ # Main entry point for the OctaSpace SDK
5
+ #
6
+ # Aggregates all resource groups and constructs the appropriate
7
+ # HTTP transport based on configuration.
8
+ #
9
+ # @example Standard mode (default)
10
+ # client = OctaSpace::Client.new(api_key: ENV["OCTA_API_KEY"])
11
+ # client.nodes.list
12
+ # client.accounts.balance
13
+ #
14
+ # @example Keep-alive mode (persistent connections + pool)
15
+ # client = OctaSpace::Client.new(
16
+ # api_key: ENV["OCTA_API_KEY"],
17
+ # keep_alive: true,
18
+ # pool_size: ENV.fetch("RAILS_MAX_THREADS", 5).to_i
19
+ # )
20
+ #
21
+ # @example Multiple API endpoints with failover
22
+ # client = OctaSpace::Client.new(
23
+ # api_key: ENV["OCTA_API_KEY"],
24
+ # base_urls: ["https://api.octa.space", "https://api2.octa.space"]
25
+ # )
26
+ #
27
+ # @example Without API key (public endpoints only)
28
+ # client = OctaSpace::Client.new
29
+ # client.network.info
30
+ #
31
+ # @example With hooks
32
+ # client = OctaSpace::Client.new(
33
+ # api_key: ENV["OCTA_API_KEY"],
34
+ # on_request: ->(req) { puts "→ #{req[:method].upcase} #{req[:path]}" },
35
+ # on_response: ->(resp) { puts "← #{resp.status}" }
36
+ # )
37
+ class Client
38
+ attr_reader :accounts, :nodes, :sessions, :apps,
39
+ :network, :services, :idle_jobs
40
+
41
+ # @param api_key [String, nil] API key for authentication (optional for public endpoints)
42
+ # @param opts [Hash] Per-instance configuration overrides.
43
+ # Any attribute from OctaSpace::Configuration can be passed here.
44
+ def initialize(api_key: nil, transport: nil, **opts)
45
+ @config = build_config(api_key, opts)
46
+ @transport = transport || build_transport
47
+ @accounts = Resources::Accounts.new(@transport)
48
+ @nodes = Resources::Nodes.new(@transport)
49
+ @sessions = Resources::Sessions.new(@transport)
50
+ @apps = Resources::Apps.new(@transport)
51
+ @network = Resources::Network.new(@transport)
52
+ @services = Resources::Services.new(@transport)
53
+ @idle_jobs = Resources::IdleJobs.new(@transport)
54
+ end
55
+
56
+ # Shut down persistent connections (only relevant in keep_alive mode)
57
+ def shutdown
58
+ @transport.respond_to?(:shutdown) && @transport.shutdown
59
+ end
60
+
61
+ # Transport diagnostics (pool stats when in keep_alive mode)
62
+ # @return [Hash]
63
+ def transport_stats
64
+ if @transport.respond_to?(:transport_stats)
65
+ @transport.transport_stats
66
+ elsif @transport.respond_to?(:pool_stats)
67
+ {mode: :persistent, pools: @transport.pool_stats}
68
+ elsif @config.urls.size > 1
69
+ {mode: :standard, rotator: @transport.instance_variable_get(:@rotator)&.stats}
70
+ else
71
+ {mode: :standard, url: @config.urls.first}
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # Merge global config with per-instance overrides
78
+ def build_config(api_key, overrides)
79
+ cfg = OctaSpace.configuration.dup
80
+ cfg.api_key = api_key
81
+ overrides.each do |key, value|
82
+ cfg.public_send(:"#{key}=", value) if cfg.respond_to?(:"#{key}=")
83
+ end
84
+ cfg
85
+ end
86
+
87
+ def build_transport
88
+ if @config.keep_alive?
89
+ require_persistent_transport!
90
+ Transport::PersistentTransport.new(@config)
91
+ else
92
+ Transport::FaradayTransport.new(@config)
93
+ end
94
+ end
95
+
96
+ def require_persistent_transport!
97
+ require "faraday/net_http_persistent"
98
+ require "connection_pool"
99
+ require "octaspace/transport/persistent_transport"
100
+ rescue LoadError => e
101
+ raise ConfigurationError,
102
+ "keep_alive: true requires the following gems.\n" \
103
+ "Add to your Gemfile:\n" \
104
+ " gem 'faraday-net_http_persistent'\n" \
105
+ " gem 'connection_pool'\n\n" \
106
+ "Original error: #{e.message}"
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OctaSpace
4
+ # Global and per-client configuration for the OctaSpace SDK
5
+ #
6
+ # @example Global configuration (Rails initializer)
7
+ # OctaSpace.configure do |config|
8
+ # config.api_key = ENV["OCTA_API_KEY"]
9
+ # config.keep_alive = true
10
+ # config.pool_size = ENV.fetch("RAILS_MAX_THREADS", 5).to_i
11
+ # config.logger = Rails.logger
12
+ # end
13
+ class Configuration
14
+ # --- Authentication ---
15
+
16
+ # @return [String, nil] API key for Authorization header
17
+ attr_accessor :api_key
18
+
19
+ # --- Connection ---
20
+
21
+ # @return [String] Base URL for the API (single endpoint)
22
+ attr_accessor :base_url
23
+
24
+ # @return [Array<String>, nil] Multiple API endpoints — enables URL rotation/failover
25
+ attr_accessor :base_urls
26
+
27
+ # @return [Integer] Seconds to wait for connection to open
28
+ attr_accessor :open_timeout
29
+
30
+ # @return [Integer] Seconds to wait for a response
31
+ attr_accessor :read_timeout
32
+
33
+ # @return [Integer] Seconds to wait when writing request body
34
+ attr_accessor :write_timeout
35
+
36
+ # --- Keep-Alive / Persistent connections ---
37
+ # Requires: gem "faraday-net_http_persistent" and gem "connection_pool"
38
+
39
+ # @return [Boolean] Enable persistent HTTP connections with connection pooling
40
+ attr_accessor :keep_alive
41
+
42
+ # @return [Integer] Number of persistent connections in the pool
43
+ attr_accessor :pool_size
44
+
45
+ # @return [Integer] Seconds to wait for a connection from the pool
46
+ attr_accessor :pool_timeout
47
+
48
+ # @return [Integer] Seconds before an idle persistent connection is closed
49
+ attr_accessor :idle_timeout
50
+
51
+ # --- Retry ---
52
+
53
+ # @return [Integer] Maximum number of retries on transient failures
54
+ attr_accessor :max_retries
55
+
56
+ # @return [Float] Base interval in seconds between retries
57
+ attr_accessor :retry_interval
58
+
59
+ # @return [Float] Exponential backoff multiplier
60
+ attr_accessor :backoff_factor
61
+
62
+ # --- Hooks ---
63
+
64
+ # @return [#call, nil] Callable invoked before each request; receives request context hash
65
+ attr_accessor :on_request
66
+
67
+ # @return [#call, nil] Callable invoked after each response; receives OctaSpace::Response
68
+ attr_accessor :on_response
69
+
70
+ # --- Logging ---
71
+
72
+ # @return [Logger, nil] Ruby Logger instance (or any object responding to #debug/#info/#warn/#error)
73
+ attr_accessor :logger
74
+
75
+ # @return [Symbol] Log level (:debug, :info, :warn, :error)
76
+ attr_accessor :log_level
77
+
78
+ # --- SSL ---
79
+
80
+ # @return [Boolean] Verify SSL certificates (set false only in development/test)
81
+ attr_accessor :ssl_verify
82
+
83
+ # --- Identity ---
84
+
85
+ # @return [String] User-Agent header value
86
+ attr_accessor :user_agent
87
+
88
+ DEFAULTS = {
89
+ base_url: "https://api.octa.space",
90
+ open_timeout: 10,
91
+ read_timeout: 30,
92
+ write_timeout: 30,
93
+ keep_alive: false,
94
+ pool_size: 5,
95
+ pool_timeout: 5,
96
+ idle_timeout: 60,
97
+ max_retries: 2,
98
+ retry_interval: 0.5,
99
+ backoff_factor: 2.0,
100
+ ssl_verify: true,
101
+ log_level: :info
102
+ }.freeze
103
+
104
+ def initialize
105
+ DEFAULTS.each { |k, v| public_send(:"#{k}=", v) }
106
+ @user_agent = "octaspace-ruby/#{OctaSpace::VERSION} Ruby/#{RUBY_VERSION}"
107
+ end
108
+
109
+ # Alias: `persistent` is the Cube-internal term; `keep_alive` is the public SDK term
110
+ alias_method :persistent, :keep_alive
111
+ alias_method :persistent=, :keep_alive=
112
+
113
+ # @return [Boolean]
114
+ def keep_alive? = !!keep_alive
115
+
116
+ # Returns effective list of API URLs.
117
+ # base_urls takes priority over base_url; always returns an Array.
118
+ # @return [Array<String>]
119
+ def urls
120
+ candidates = Array(base_urls).map(&:to_s).reject(&:empty?)
121
+ candidates.empty? ? Array(base_url).map(&:to_s).reject(&:empty?) : candidates
122
+ end
123
+
124
+ # Deep-clone configuration for per-client overrides
125
+ # @return [Configuration]
126
+ def dup
127
+ copy = self.class.new
128
+ instance_variables.each do |var|
129
+ copy.instance_variable_set(var, instance_variable_get(var).dup)
130
+ rescue TypeError
131
+ copy.instance_variable_set(var, instance_variable_get(var))
132
+ end
133
+ copy
134
+ end
135
+ end
136
+ end