Class: Smplkit::Config::ConfigClient

Inherits:
Object
  • Object
show all
Defined in:
lib/smplkit/config/client.rb

Overview

Synchronous runtime client for Smpl Config.

Obtained via Smplkit::Client#config. Exposes #bind (the recommended declarative API), #get (lookup-only escape hatch), #refresh, and #on_change. Management/CRUD lives on Smplkit::Client#manage.config.

Instance Method Summary collapse

Constructor Details

#initialize(parent, manage:, metrics:) ⇒ ConfigClient

Returns a new instance of ConfigClient.



224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/smplkit/config/client.rb', line 224

def initialize(parent, manage:, metrics:)
  @parent = parent
  @manage = manage
  @metrics = metrics
  @environment = parent._environment
  @service = parent._service

  @config_cache = {}        # config_key -> { item_key => resolved_value }
  @raw_config_store = {}    # config_key -> Smplkit::Config::Config
  @proxies = {}             # config_key -> LiveConfigProxy
  @bindings = {}            # config_key -> Hash | Struct (bound target)
  @listeners = []           # [callback, config_id_or_nil, item_key_or_nil]
  @connected = false
  @lock = Mutex.new
  @ws_manager = nil
end

Instance Method Details

#_cached_values(config_id) ⇒ Object

Internal: return (a copy of) the resolved values for a config id. Used by LiveConfigProxy.



365
366
367
368
369
# File 'lib/smplkit/config/client.rb', line 365

def _cached_values(config_id)
  @lock.synchronize do
    (@config_cache[config_id] || {}).dup
  end
end

#_closeObject



358
359
360
361
# File 'lib/smplkit/config/client.rb', line 358

def _close
  # No durable resources owned by this sub-client; the parent client
  # tears down the WebSocket and management transports.
end

#_observe_config_declaration(config_id, parent:, name:, description:) ⇒ Object

Internal: queue a config declaration with the management buffer.



372
373
374
375
376
377
378
379
380
381
# File 'lib/smplkit/config/client.rb', line 372

def _observe_config_declaration(config_id, parent:, name:, description:)
  @manage&.config&.register_config(
    config_id,
    service: @service,
    environment: @environment,
    parent: parent,
    name: name,
    description: description
  )
end

#_observe_item_declaration(config_id, item_key, item_type, default, description) ⇒ Object

Internal: queue a config item declaration with the management buffer.



384
385
386
# File 'lib/smplkit/config/client.rb', line 384

def _observe_item_declaration(config_id, item_key, item_type, default, description)
  @manage&.config&.register_config_item(config_id, item_key, item_type, default, description)
end

#bind(id, target, 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 from parent: 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 call the schema and values are registered with the server. After the local cache is populated, any server-side overrides for this config are applied to the bound object in place. WebSocket events thereafter mutate the bound object in place — readers always see the current resolved value with no indirection.

Idempotent. Repeat calls with the same id return the originally-bound object; the new config argument is ignored.



291
292
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
# File 'lib/smplkit/config/client.rb', line 291

def bind(id, target, parent: nil)
  unless target.is_a?(Hash) || target.is_a?(Struct)
    raise TypeError, "bind() requires a Hash or Struct; got #{target.class.name}"
  end

  return @bindings[id] if @bindings.key?(id)

  parent_id = resolve_parent_id(parent)

  if target.is_a?(Struct)
    class_name = target.class.name
    config_name = class_name&.split("::")&.last
  else
    config_name = nil
  end

  _observe_config_declaration(id, parent: parent_id, name: config_name, description: nil)

  Discovery.iter_items(target).each do |item_key, item_type, value, description|
    _observe_item_declaration(id, item_key, item_type, value, description)
  end

  # Register the binding BEFORE start() so any WS dispatch that fires
  # during the initial fetch finds it.
  @bindings[id] = target

  start unless @connected
  sync_target_from_cache(target, id)
  target
end

#get(id, key = MISSING, default = MISSING) ⇒ Object

Read a config (full) or a single value within a config.

Three forms dispatched by argument count:

get("id")                    # LiveConfigProxy (raises NotFoundError)
get("id", "key")             # value (raises NotFoundError / KeyError)
get("id", "key", default)    # value or default; auto-registers (never raises)


329
330
331
332
333
334
335
# File 'lib/smplkit/config/client.rb', line 329

def get(id, key = MISSING, default = MISSING)
  start unless @connected

  return get_full_config(id) if key.equal?(MISSING)

  get_single_value(id, key.to_s, default)
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

Raises:

  • (ArgumentError)


344
345
346
347
348
349
# File 'lib/smplkit/config/client.rb', line 344

def on_change(config_id = nil, item_key: nil, &block)
  raise ArgumentError, "on_change requires a block" unless block

  @listeners << [block, config_id, item_key&.to_s]
  block
end

#refreshObject

Re-fetch all configs and update resolved values, firing change listeners for anything that differs from the previous state.



353
354
355
356
# File 'lib/smplkit/config/client.rb', line 353

def refresh
  start unless @connected
  do_refresh("manual")
end

#startObject

Eagerly initialize the runtime. Flushes any buffered discovery declarations, fetches the full config list, resolves values for the SDK’s current environment into the local cache, and subscribes to config_changed / config_deleted / configs_changed events on the shared WebSocket.

Idempotent — safe to call multiple times. Invoked automatically on the first #get or #bind call.



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/smplkit/config/client.rb', line 249

def start
  return if @connected

  @environment = @parent._environment

  # Per ADR-037 §2.14: flush pending discovery declarations BEFORE
  # the initial fetch so newly-declared configs show up in the cache.
  begin
    @manage&.config&.flush
  rescue StandardError => e
    Smplkit.debug("config", "pre-start discovery flush failed: #{e.class}: #{e.message}")
  end

  do_refresh("initial")
  @connected = true

  @ws_manager = @parent._ensure_ws
  @ws_manager.on("config_changed") { |data| handle_config_changed(data) }
  @ws_manager.on("config_deleted") { |data| handle_config_deleted(data) }
  @ws_manager.on("configs_changed") { |data| handle_configs_changed(data) }
end