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 CRUD 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) {|client| ... } ⇒ 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) ⇒ Hash, Struct
Bind a Hash or Struct to a config id; return the same object back, live.
-
#close ⇒ void
(also: #_close)
Release resources — only those this client owns.
-
#delete(id) ⇒ void
Delete a config by id.
-
#flush ⇒ void
Send any queued config and item declarations to the server.
-
#get(id) ⇒ Config
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) ⇒ Array<Config>
List configs for the authenticated account.
-
#new(id, name: nil, description: nil, parent: nil) ⇒ Config
Return a new unsaved
Config. -
#on_change(config_id = nil, item_key: nil) {|event| ... } ⇒ Proc
Register a change listener.
-
#pending_count ⇒ Integer
Number of pending config declarations awaiting flush.
-
#refresh ⇒ void
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) ⇒ void
Queue a configuration declaration for bulk-discovery upload.
-
#register_config_item(config_id, item_key, item_type, default, description = nil) ⇒ void
Queue a config item declaration.
-
#subscribe(id) ⇒ LiveConfigProxy
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.
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 |
# File 'lib/smplkit/config/client.rb', line 416 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) {|client| ... } ⇒ Object
Construct, yield to the block, and close on exit.
798 799 800 801 802 803 804 805 |
# File 'lib/smplkit/config/client.rb', line 798 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.
809 810 811 |
# File 'lib/smplkit/config/client.rb', line 809 def _cached_values(config_id) @lock.synchronize { (@config_cache[config_id] || {}).dup } end |
#_create_config(config) ⇒ Object
519 520 521 522 |
# File 'lib/smplkit/config/client.rb', line 519 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.
814 815 816 |
# File 'lib/smplkit/config/client.rb', line 814 def _ensure_connected ensure_connected end |
#_update_config(config) ⇒ Object
524 525 526 527 |
# File 'lib/smplkit/config/client.rb', line 524 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) ⇒ Hash, Struct
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.
640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 |
# File 'lib/smplkit/config/client.rb', line 640 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 ⇒ void Also known as: _close
This method returns an undefined value.
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.
782 783 784 785 786 787 788 789 |
# File 'lib/smplkit/config/client.rb', line 782 def close if @owns_ws && @ws_manager @ws_manager.stop @ws_manager = nil @owns_ws = false end nil end |
#delete(id) ⇒ void
This method returns an undefined value.
Delete a config by id.
514 515 516 517 |
# File 'lib/smplkit/config/client.rb', line 514 def delete(id) ApiSupport::ErrorMapping.call { @api.delete_config(id) } nil end |
#flush ⇒ void
This method returns an undefined value.
Send any queued config and item declarations to the server.
Discovery is best-effort — failures here never propagate to your code. Drained entries are not requeued; the SDK re-observes them on the next process start.
579 580 581 582 583 584 585 586 587 588 589 590 |
# File 'lib/smplkit/config/client.rb', line 579 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 — discovery failures never propagate to caller code. Smplkit.debug("registration", "config bulk register failed: #{e.class}: #{e.}") end end |
#get(id) ⇒ Config
Fetch the editable Config resource by id.
489 490 491 492 |
# File 'lib/smplkit/config/client.rb', line 489 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.
711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 |
# File 'lib/smplkit/config/client.rb', line 711 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) ⇒ Array<Config>
List configs for the authenticated account.
502 503 504 505 506 507 508 |
# File 'lib/smplkit/config/client.rb', line 502 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) ⇒ Config
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.
474 475 476 477 478 479 480 481 482 |
# File 'lib/smplkit/config/client.rb', line 474 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) {|event| ... } ⇒ Proc
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.
754 755 756 757 758 759 760 |
# File 'lib/smplkit/config/client.rb', line 754 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 ⇒ Integer
Number of pending config declarations awaiting flush.
595 596 597 |
# File 'lib/smplkit/config/client.rb', line 595 def pending_count @buffer.pending_count end |
#refresh ⇒ void
This method returns an undefined value.
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.
769 770 771 772 |
# File 'lib/smplkit/config/client.rb', line 769 def refresh ensure_connected do_refresh("manual") end |
#register_config(config_id, service:, environment:, parent: nil, name: nil, description: nil) ⇒ void
This method returns an undefined value.
Queue a configuration declaration for bulk-discovery upload.
The declaration is buffered and sent in the background; it surfaces the config in the smplkit console even if no values are set yet.
548 549 550 551 552 |
# File 'lib/smplkit/config/client.rb', line 548 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) ⇒ void
This method returns an undefined value.
Queue a config item declaration. register_config must run first.
The declaration is buffered and sent in the background, surfacing the item (with its type and default) in the smplkit console.
567 568 569 570 |
# File 'lib/smplkit/config/client.rb', line 567 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) ⇒ LiveConfigProxy
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.
670 671 672 673 674 675 676 677 678 |
# File 'lib/smplkit/config/client.rb', line 670 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 |