Class: Smplkit::Config::ConfigClient
- Inherits:
-
Object
- Object
- Smplkit::Config::ConfigClient
- Defined in:
- lib/smplkit/config/client.rb
Overview
The Smpl Config client (sync).
One client exposes the full surface, reachable as client.config (Smplkit::Client) or constructed directly:
config = Smplkit::ConfigClient.new(environment: "production")
billing = config.new("billing", name: "Billing")
billing.set_number("max_seats", 50)
billing.save
proxy = config.subscribe("billing")
puts proxy["max_seats"]
The management surface (new / get / list / delete and discovery) is pure CRUD. The live surface (subscribe / get_value / bind / on_change / refresh) connects lazily on first use — the first call flushes discovery, fetches and resolves all configs into the local cache, and opens the live-updates WebSocket. No explicit install step is required.
Class Method Summary collapse
-
.open(**kwargs) ⇒ Object
Construct, yield to the block, and close on exit.
Instance Method Summary collapse
-
#_cached_values(config_id) ⇒ Object
Internal: return (a copy of) the resolved values for a config id.
- #_create_config(config) ⇒ Object
-
#_ensure_connected ⇒ Object
Internal: trigger lazy connect.
- #_update_config(config) ⇒ Object
-
#bind(id, config, parent: nil) ⇒ Object
Bind a Hash or Struct to a config id; return the same object back, live.
-
#close ⇒ Object
(also: #_close)
Release resources — only those this client owns.
-
#delete(id) ⇒ Object
Delete a config by id.
-
#flush ⇒ Object
POST pending declarations to
/api/v1/configs/bulk. -
#get(id) ⇒ Object
Fetch the editable
Configresource by id. -
#get_value(id, key, default = MISSING) ⇒ Object
Read a single resolved config value (inheritance-aware).
-
#initialize(api_key = nil, environment: nil, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, parent: nil, transport: nil, metrics: nil) ⇒ ConfigClient
constructor
A new instance of ConfigClient.
-
#list(page_number: nil, page_size: nil) ⇒ Object
List configs for the authenticated account.
-
#new(id, name: nil, description: nil, parent: nil) ⇒ Object
Return a new unsaved
Config. -
#on_change(config_id = nil, item_key: nil, &block) ⇒ Object
Register a change listener.
-
#pending_count ⇒ Object
Number of pending config declarations awaiting flush.
-
#refresh ⇒ Object
Re-fetch all configs and update resolved values, firing change listeners for anything that differs from the previous state.
-
#register_config(config_id, service:, environment:, parent: nil, name: nil, description: nil) ⇒ Object
Queue a configuration declaration for bulk-discovery upload.
-
#register_config_item(config_id, item_key, item_type, default, description = nil) ⇒ Object
Queue a config item declaration.
-
#subscribe(id) ⇒ Object
Return a live, dict-like
LiveConfigProxyfor a config id.
Constructor Details
#initialize(api_key = nil, environment: nil, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, parent: nil, transport: nil, metrics: nil) ⇒ ConfigClient
Returns a new instance of ConfigClient.
293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 |
# File 'lib/smplkit/config/client.rb', line 293 def initialize(api_key = nil, environment: nil, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, parent: nil, transport: nil, metrics: nil) @parent = parent @metrics = metrics @environment = parent.nil? ? environment : parent._environment @service = parent&._service @standalone_api_key = nil if transport.nil? @http, @app_base_url, @standalone_api_key = Smplkit::Config.config_transport( api_key: api_key, base_url: base_url, profile: profile, base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers ) @owns_transport = true else @http = transport @app_base_url = nil @owns_transport = false end @api = SmplkitGeneratedClient::Config::ConfigsApi.new(@http) # Discovery buffer is owned by this client (no management delegation). @buffer = ConfigRegistrationBuffer.new # Live-surface state. @config_cache = {} # config_id -> { item_key => resolved_value } @raw_config_store = {} # config_id -> Config @proxies = {} # config_id -> LiveConfigProxy @bindings = {} # config_id -> Hash | Struct (bound target) # Parent config id each binding was bound under (nil for roots) — # drives in-memory cache seeding through the bound parent chain. @bound_parents = {} @connected = false @lock = Mutex.new @listeners = [] # [callback, config_id_or_nil, item_key_or_nil] @ws_manager = nil @owns_ws = false end |
Class Method Details
.open(**kwargs) ⇒ Object
Construct, yield to the block, and close on exit.
578 579 580 581 582 583 584 585 |
# File 'lib/smplkit/config/client.rb', line 578 def self.open(**kwargs) client = new(**kwargs) begin yield client ensure client.close end end |
Instance Method Details
#_cached_values(config_id) ⇒ Object
Internal: return (a copy of) the resolved values for a config id. Used by LiveConfigProxy.
589 590 591 |
# File 'lib/smplkit/config/client.rb', line 589 def _cached_values(config_id) @lock.synchronize { (@config_cache[config_id] || {}).dup } end |
#_create_config(config) ⇒ Object
374 375 376 377 |
# File 'lib/smplkit/config/client.rb', line 374 def _create_config(config) response = ApiSupport::ErrorMapping.call { @api.create_config(config_body(config)) } Helpers.config_from_json(self, ApiSupport::ResourceShim.from_model(response.data)) end |
#_ensure_connected ⇒ Object
Internal: trigger lazy connect. Used by Client#wait_until_ready.
594 595 596 |
# File 'lib/smplkit/config/client.rb', line 594 def _ensure_connected ensure_connected end |
#_update_config(config) ⇒ Object
379 380 381 382 |
# File 'lib/smplkit/config/client.rb', line 379 def _update_config(config) response = ApiSupport::ErrorMapping.call { @api.update_config(config.key, config_body(config)) } Helpers.config_from_json(self, ApiSupport::ResourceShim.from_model(response.data)) end |
#bind(id, config, parent: nil) ⇒ Object
Bind a Hash or Struct to a config id; return the same object back, live.
Declarative, code-first API. Two flavors:
-
Hash: keys present are leaves to register, with their values as the in-code defaults. Nested Hashes flatten to dot-notation. Keys the caller wants to inherit fromparent:are simply omitted. -
Struct: every member is registered as an explicit override. Ruby Structs do not track which members were “explicitly set” vs defaulted, so there is no Hash-style omit-to-inherit. For omit-to-inherit, use a Hash target.
On first boot the schema and values are registered with the server. The local cache is then seeded so reads work immediately: if the config already exists server-side (fetched on connect) its values are authoritative and synced onto the bound object; if it is brand-new, the cache entry is seeded in-memory from the bound object’s values resolved through its bound parent chain (no network round-trip). On every WebSocket-delivered change thereafter the bound object is mutated in place. Readers always see the current resolved value with no proxy indirection.
Idempotent. Repeated calls with the same id return the originally-bound object; the new config argument is ignored.
Connects lazily on first use — no explicit install step.
455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 |
# File 'lib/smplkit/config/client.rb', line 455 def bind(id, config, parent: nil) ensure_connected unless config.is_a?(Hash) || config.is_a?(Struct) raise TypeError, "bind() requires a Hash or Struct; got #{config.class.name}" end return @bindings[id] if @bindings.key?(id) parent_id = register_binding_declaration(id, config, parent) # Register the binding BEFORE syncing so WebSocket dispatch finds it. @bindings[id] = config @bound_parents[id] = parent_id seed_or_sync_binding(id, config) config end |
#close ⇒ Object Also known as: _close
Release resources — only those this client owns.
Tears down the owned WebSocket (opened by a standalone client on first live use) and the owned HTTP transport (standalone construction). A wired client borrows the parent’s transport and WebSocket and closes neither.
567 568 569 570 571 572 573 574 |
# File 'lib/smplkit/config/client.rb', line 567 def close if @owns_ws && @ws_manager @ws_manager.stop @ws_manager = nil @owns_ws = false end nil end |
#delete(id) ⇒ Object
Delete a config by id.
369 370 371 372 |
# File 'lib/smplkit/config/client.rb', line 369 def delete(id) ApiSupport::ErrorMapping.call { @api.delete_config(id) } nil end |
#flush ⇒ Object
POST pending declarations to /api/v1/configs/bulk.
Per ADR-024 §2.9, bulk registration always lands rows as managed=false and is plan-limit-exempt — failures here never propagate to customer code. Drained entries are not requeued; the SDK will re-observe on the next process start.
407 408 409 410 411 412 413 414 415 416 417 418 |
# File 'lib/smplkit/config/client.rb', line 407 def flush batch = @buffer.drain return if batch.empty? body = build_config_bulk_request(batch) begin ApiSupport::ErrorMapping.call { @api.bulk_register_configs(body) } rescue StandardError => e # Fire-and-forget per ADR-024 §2.9. Smplkit.debug("registration", "config bulk register failed: #{e.class}: #{e.}") end end |
#get(id) ⇒ Object
Fetch the editable Config resource by id.
Raises NotFoundError if no config with that id exists.
354 355 356 357 |
# File 'lib/smplkit/config/client.rb', line 354 def get(id) response = ApiSupport::ErrorMapping.call { @api.get_config(id) } Helpers.config_from_json(self, ApiSupport::ResourceShim.from_model(response.data)) end |
#get_value(id, key, default = MISSING) ⇒ Object
Read a single resolved config value (inheritance-aware).
The value comes from the locally-cached resolved chain, so parent configs are already folded in.
Two forms:
-
get_value(id, key) returns the resolved value. Raises
NotFoundErrorif the config is unknown andKeyErrorif the key is absent. -
get_value(id, key, default) returns the resolved value, falling back to
defaultif the config or key is missing. Never raises. Registers the config (if new) and the key (inferred type,defaultas default) for code-first observability, so the reference appears in the smplkit console.
For a live dict-like view use #subscribe; for typed access via a Struct schema use #bind. Connects lazily on first use — no explicit install step.
509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 |
# File 'lib/smplkit/config/client.rb', line 509 def get_value(id, key, default = MISSING) ensure_connected key = key.to_s has_default = !default.equal?(MISSING) if has_default # Register the config + key so the reference shows up in the console # even if it's never been declared via bind(). The buffer is # idempotent at the (config_id, item_key) level. observe_config_declaration(id, parent: nil, name: nil, description: nil) observe_item_declaration(id, key, Discovery.value_to_item_type(default), default, nil) end values = @lock.synchronize { @config_cache[id]&.dup } if values.nil? return default if has_default raise Smplkit::NotFoundError, "Config with id '#{id}' not found" end unless values.key?(key) return default if has_default raise KeyError, "Config item '#{key}' not found in config '#{id}'" end values[key] end |
#list(page_number: nil, page_size: nil) ⇒ Object
List configs for the authenticated account.
360 361 362 363 364 365 366 |
# File 'lib/smplkit/config/client.rb', line 360 def list(page_number: nil, page_size: nil) opts = {} opts[:page_number] = page_number unless page_number.nil? opts[:page_size] = page_size unless page_size.nil? response = ApiSupport::ErrorMapping.call { @api.list_configs(opts) } (response.data || []).map { |r| Helpers.config_from_json(self, ApiSupport::ResourceShim.from_model(r)) } end |
#new(id, name: nil, description: nil, parent: nil) ⇒ Object
Return a new unsaved Config. Call Config#save to persist.
parent accepts either a config id (string) or an existing Config instance — passing the instance lets you skip naming the id explicitly when you already have the parent in scope.
341 342 343 344 345 346 347 348 349 |
# File 'lib/smplkit/config/client.rb', line 341 def new(id, name: nil, description: nil, parent: nil) Config.new( self, key: id, name: name || Smplkit::Helpers.key_to_display_name(id), description: description, parent_id: Smplkit::Config.resolve_parent_id(parent) ) end |
#on_change(config_id = nil, item_key: nil, &block) ⇒ Object
Register a change listener.
Three forms:
client.config.on_change { |event| ... } # global
client.config.on_change("id") { |event| ... } # config-scoped
client.config.on_change("id", item_key: "key") { |event| ... } # item-scoped
Connects lazily on first use — no explicit install step.
544 545 546 547 548 549 550 |
# File 'lib/smplkit/config/client.rb', line 544 def on_change(config_id = nil, item_key: nil, &block) ensure_connected raise ArgumentError, "on_change requires a block" unless block @listeners << [block, config_id, item_key&.to_s] block end |
#pending_count ⇒ Object
Number of pending config declarations awaiting flush.
421 422 423 |
# File 'lib/smplkit/config/client.rb', line 421 def pending_count @buffer.pending_count end |
#refresh ⇒ Object
Re-fetch all configs and update resolved values, firing change listeners for anything that differs from the previous state.
Connects lazily on first use — no explicit install step.
556 557 558 559 |
# File 'lib/smplkit/config/client.rb', line 556 def refresh ensure_connected do_refresh("manual") end |
#register_config(config_id, service:, environment:, parent: nil, name: nil, description: nil) ⇒ Object
Queue a configuration declaration for bulk-discovery upload.
389 390 391 392 393 |
# File 'lib/smplkit/config/client.rb', line 389 def register_config(config_id, service:, environment:, parent: nil, name: nil, description: nil) @buffer.declare(config_id, service: service, environment: environment, parent: parent, name: name, description: description) trigger_background_flush_if_needed end |
#register_config_item(config_id, item_key, item_type, default, description = nil) ⇒ Object
Queue a config item declaration. register_config must run first.
396 397 398 399 |
# File 'lib/smplkit/config/client.rb', line 396 def register_config_item(config_id, item_key, item_type, default, description = nil) @buffer.add_item(config_id, item_key, item_type, default, description) trigger_background_flush_if_needed end |
#subscribe(id) ⇒ Object
Return a live, dict-like LiveConfigProxy for a config id.
The proxy always reflects the latest resolved values; reads happen through it (+proxy, proxy.get(“key”, default)+). Subscribing registers the config declaration for code-first observability so the reference appears in the smplkit console.
Connects lazily on first use — no explicit install step. Raises NotFoundError if the config is unknown.
481 482 483 484 485 486 487 488 489 |
# File 'lib/smplkit/config/client.rb', line 481 def subscribe(id) ensure_connected observe_config_declaration(id, parent: nil, name: nil, description: nil) in_cache = @lock.synchronize { @config_cache.key?(id) } raise Smplkit::NotFoundError, "Config with id '#{id}' not found" unless in_cache @metrics&.record("config.resolutions", unit: "resolutions", dimensions: { "config" => id }) cached_proxy(id) end |