Class: Smplkit::Flags::FlagsClient

Inherits:
Object
  • Object
show all
Defined in:
lib/smplkit/flags/client.rb,
sig/smplkit/flags.rbs

Overview

The Smpl Flags client (sync).

One client exposes the full surface, reachable as client.flags (+Smplkit::Client+) or constructed directly:

flags = Smplkit::FlagsClient.new(environment: "production")
new_flag = flags.new_boolean_flag("beta", default: false)
new_flag.save
beta = flags.boolean_flag("beta", default: false)
beta.get # => ...

The CRUD surface (+new_*+ / get / list / delete and discovery) is pure CRUD. The live surface (+boolean_flag+ / string_flag / number_flag / json_flag / refresh / stats / on_change) connects lazily on first use — the first call flushes discovery, fetches all flag definitions into the local cache, and opens the live-updates WebSocket. No explicit install step is required.

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key = nil, environment: nil, service: nil, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, streaming: true, parent: nil, transport: nil, contexts: nil, metrics: nil) ⇒ FlagsClient

Returns a new instance of FlagsClient.

Parameters:

  • api_key (String, nil) (defaults to: nil)

    API key. When omitted, resolved from SMPLKIT_API_KEY or ~/.smplkit.

  • environment (String, nil) (defaults to: nil)

    Deployment environment used to resolve runtime flag values and to scope discovery declarations. When omitted, resolved from SMPLKIT_ENVIRONMENT or ~/.smplkit.

  • service (String, nil) (defaults to: nil)

    Service name attached to discovery declarations and auto-injected into the evaluation context. When omitted, resolved from SMPLKIT_SERVICE or ~/.smplkit. Optional.

  • base_url (String, nil) (defaults to: nil)

    Full flags-service base URL. Usually resolved from +base_domain+/+scheme+; supplied directly by the top-level clients which have already computed it.

  • profile (String, nil) (defaults to: nil)

    Named ~/.smplkit profile section.

  • base_domain (String, nil) (defaults to: nil)

    Base domain for API requests (default "smplkit.com").

  • scheme (String, nil) (defaults to: nil)

    URL scheme (default "https").

  • debug (Boolean, nil) (defaults to: nil)

    Enable SDK debug logging.

  • extra_headers (Hash{String => String}, nil) (defaults to: nil)

    Extra headers attached to every request.

  • streaming (Boolean) (defaults to: true)

    Live updates over WebSocket (default true): the first live call opens a shared socket and flag changes stream in. Set false for the stateless read-through surface: the first live call still fetches all flag definitions once (blocking), evaluation stays local, refresh re-fetches on demand, and NO socket or background thread is ever created — the right shape for serverless and edge runtimes. on_change listeners fire only from explicit refresh calls in this mode (there is no stream to drive them).

  • parent (Smplkit::Client, nil) (defaults to: nil)

    Internal — the owning client. Not for direct use.

  • transport (Object, nil) (defaults to: nil)

    Internal — a pre-built flags transport supplied by a top-level client so the flags surface shares one connection pool. Not for direct use.

  • contexts (Object, nil) (defaults to: nil)

    Internal — client.platform.contexts used for evaluation-context registration. Not for direct use.

  • metrics (Object, nil) (defaults to: nil)

    Internal — the parent's metrics reporter.



226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
# File 'lib/smplkit/flags/client.rb', line 226

def initialize(api_key = nil, environment: nil, service: nil, base_url: nil, profile: nil,
               base_domain: nil, scheme: nil, debug: nil, extra_headers: nil,
               streaming: true, parent: nil, transport: nil, contexts: nil, metrics: nil)
  @parent = parent
  @metrics = metrics
  @streaming = streaming ? true : false
  @standalone_api_key = nil
  if transport.nil?
    # Standalone: resolve like Smplkit::Client — defaults → ~/.smplkit →
    # SMPLKIT_* env vars → constructor args — environment and service
    # included.
    @flags_http, app_http, @app_base_url, @standalone_api_key, resolved_env, resolved_service =
      Flags.flags_transport(
        api_key: api_key, base_url: base_url, profile: profile,
        base_domain: base_domain, scheme: scheme, environment: environment,
        service: service, debug: debug, extra_headers: extra_headers
      )
    @environment = parent.nil? ? resolved_env : parent._environment
    @service = parent.nil? ? resolved_service : parent._service
    # Standalone: build our own contexts client (and own its app transport).
    @contexts = Platform::ContextsClient.new(app_http, ContextRegistrationBuffer.new)
  else
    @flags_http = transport
    @app_base_url = nil
    # Wired: the parent has already resolved environment/service once —
    # its values win over both the raw kwargs and re-resolution.
    @environment = parent.nil? ? environment : parent._environment
    @service = parent.nil? ? service : parent._service
    # Wired: borrow client.platform.contexts as the evaluation-context
    # registration seam.
    @contexts = contexts
  end
  @api = SmplkitGeneratedClient::Flags::FlagsApi.new(@flags_http)

  # Discovery buffer is owned by this client (no management delegation).
  @buffer = FlagRegistrationBuffer.new

  # Live-surface state.
  @flag_store = {}
  @connected = false
  @ws_subscribed = false
  @cache = ResolutionCache.new
  @handles = {}
  @global_listeners = []
  @key_listeners = Hash.new { |h, k| h[k] = [] }
  @ws_manager = nil
  @owns_ws = false
  @lock = Mutex.new
end

Class Method Details

.open(**kwargs) {|FlagsClient| ... } ⇒ Object

Construct, yield to the block, and close on exit.

Yields:

Returns:

  • (Object)

    the block's return value; closes the client on exit.



590
591
592
593
594
595
596
597
# File 'lib/smplkit/flags/client.rb', line 590

def self.open(**kwargs)
  client = new(**kwargs)
  begin
    yield client
  ensure
    client.close
  end
end

Instance Method Details

#_create_flag(flag) ⇒ Object



390
391
392
393
# File 'lib/smplkit/flags/client.rb', line 390

def _create_flag(flag)
  response = ApiSupport::ErrorMapping.call { @api.create_flag(flag_body(flag)) }
  model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
end

#_ensure_connectedObject

Internal: trigger lazy connect. Used by Client#wait_until_ready.



644
645
646
# File 'lib/smplkit/flags/client.rb', line 644

def _ensure_connected
  ensure_connected
end

#_evaluate_handle(flag_id, default, context) ⇒ Object

Core evaluation used by flag handles (the .get path).

Connects lazily on first use so flag.get works without an explicit install step.



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
# File 'lib/smplkit/flags/client.rb', line 603

def _evaluate_handle(flag_id, default, context)
  ensure_connected
  if context
    # Explicit context: register here. (Implicit set_context registers at
    # the entry point, so the request-context branch below doesn't need
    # to.)
    @contexts&.register(context)
    eval_dict = Flags.contexts_to_eval_dict(context)
  else
    contexts = Smplkit.request_context
    eval_dict = contexts.empty? ? {} : Flags.contexts_to_eval_dict(contexts)
  end

  # Auto-inject service context if set and not already provided.
  eval_dict["service"] = { "key" => @service } if @service && !eval_dict.key?("service")

  ctx_hash = Flags.hash_context(eval_dict)
  cache_key = "#{flag_id}:#{ctx_hash}"

  hit, cached_value = @cache.get(cache_key)
  if hit
    @metrics&.record("flags.cache_hits", unit: "hits")
    @metrics&.record("flags.evaluations", unit: "evaluations", dimensions: { "flag" => flag_id })
    return cached_value
  end

  flag_def = @flag_store[flag_id]
  if flag_def.nil?
    @cache.put(cache_key, default)
    return default
  end

  value = evaluate_flag(flag_def, @environment, eval_dict)
  value = default if value.nil?
  @cache.put(cache_key, value)
  @metrics&.record("flags.cache_misses", unit: "misses")
  @metrics&.record("flags.evaluations", unit: "evaluations", dimensions: { "flag" => flag_id })
  value
end

#_update_flag(flag) ⇒ Object



395
396
397
398
# File 'lib/smplkit/flags/client.rb', line 395

def _update_flag(flag)
  response = ApiSupport::ErrorMapping.call { @api.update_flag(flag.id, flag_body(flag)) }
  model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
end

#boolean_flag(id, default:) ⇒ BooleanFlag

Declare a boolean flag handle for live evaluation. Connects lazily on first use.

Parameters:

  • id (String)

    identifier of the flag to evaluate.

  • default (Boolean)

    value returned by handle.get when the flag is unknown or no environment override or rule applies.

  • (String)
  • default: (Boolean)

Returns:

  • (BooleanFlag)

    a handle whose get evaluates against the live cache.



472
473
474
475
476
477
478
# File 'lib/smplkit/flags/client.rb', line 472

def boolean_flag(id, default:)
  ensure_connected
  handle = BooleanFlag.new(self, id: id, name: id, type: "BOOLEAN", default: default)
  @handles[id] = handle
  observe_declaration(id, "BOOLEAN", default)
  handle
end

#closevoid Also known as: _close

This method returns an undefined value.

Release resources — only those this client owns.

Tears down the owned WebSocket (standalone install). A wired client borrows the parent's transport, WebSocket, and contexts client and closes none of them.



574
575
576
577
578
579
580
581
582
583
# File 'lib/smplkit/flags/client.rb', line 574

def close
  if @owns_ws && @ws_manager
    @ws_manager.stop
    @ws_manager = nil
    @owns_ws = false
  end
  # Owned flags/app transports (standalone construction) release their
  # Faraday connections on GC; there is no explicit shutdown to call.
  nil
end

#delete(id) ⇒ void

This method returns an undefined value.

Delete a flag by id.

Parameters:

  • id (String)

    identifier of the flag to delete.

Raises:



385
386
387
388
# File 'lib/smplkit/flags/client.rb', line 385

def delete(id)
  ApiSupport::ErrorMapping.call { @api.delete_flag(id) }
  nil
end

#flushvoid

This method returns an undefined value.

POST pending declarations to the flags bulk endpoint.

Items remain in the buffer until the request succeeds, so a flush against an unhealthy flags service is automatically retried by the next flush call (periodic background flush, install retry, or final flush on close).



439
440
441
442
443
444
445
446
# File 'lib/smplkit/flags/client.rb', line 439

def flush
  batch = @buffer.peek
  return if batch.empty?

  body = build_flag_bulk_request(batch)
  ApiSupport::ErrorMapping.call { @api.bulk_register_flags(body) }
  @buffer.commit(batch.map { |b| b["id"] })
end

#flush_syncvoid

This method returns an undefined value.

Synchronous flush — alias of flush for the periodic-flush path.



451
452
453
# File 'lib/smplkit/flags/client.rb', line 451

def flush_sync
  flush
end

#get(id) ⇒ Flag

Fetch the editable Flag resource by id.

Parameters:

  • id (String)

    identifier of the flag to fetch.

Returns:

  • (Flag)

    the flag, ready to mutate and save.

Raises:



360
361
362
363
# File 'lib/smplkit/flags/client.rb', line 360

def get(id)
  response = ApiSupport::ErrorMapping.call { @api.get_flag(id) }
  model_from_resource(ApiSupport::ResourceShim.from_model(response.data))
end

#json_flag(id, default:) ⇒ JsonFlag

Declare a JSON flag handle for live evaluation. Connects lazily on first use.

Parameters:

  • id (String)

    identifier of the flag to evaluate.

  • default (Hash)

    value returned by handle.get when the flag is unknown or no environment override or rule applies.

  • (String)
  • default: (Object)

Returns:

  • (JsonFlag)

    a handle whose get evaluates against the live cache.



514
515
516
517
518
519
520
# File 'lib/smplkit/flags/client.rb', line 514

def json_flag(id, default:)
  ensure_connected
  handle = JsonFlag.new(self, id: id, name: id, type: "JSON", default: default)
  @handles[id] = handle
  observe_declaration(id, "JSON", default)
  handle
end

#list(page_number: nil, page_size: nil) ⇒ Array<Flag>

List flags for the authenticated account.

Parameters:

  • page_number (Integer, nil) (defaults to: nil)

    1-based page index to fetch; when omitted the server default applies.

  • page_size (Integer, nil) (defaults to: nil)

    number of flags per page; when omitted the server default applies.

Returns:

  • (Array<Flag>)

    the flags on the requested page.



372
373
374
375
376
377
378
# File 'lib/smplkit/flags/client.rb', line 372

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_flags(opts) }
  (response.data || []).map { |r| model_from_resource(ApiSupport::ResourceShim.from_model(r)) }
end

#new_boolean_flag(id, default:, name: nil, description: nil) ⇒ BooleanFlag

Return a new unsaved boolean BooleanFlag. Call save to persist.

Parameters:

  • id (String)

    stable flag identifier, unique per account.

  • default (Boolean)

    value served when no environment override or rule applies.

  • name (String, nil) (defaults to: nil)

    human-readable display name; defaults to a title-cased form of id.

  • description (String, nil) (defaults to: nil)

    optional free-text description of the flag.

Returns:

  • (BooleanFlag)

    an unsaved flag; call save to persist it.



289
290
291
292
293
294
295
296
# File 'lib/smplkit/flags/client.rb', line 289

def new_boolean_flag(id, default:, name: nil, description: nil)
  BooleanFlag.new(
    self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
          type: "BOOLEAN", default: default,
          values: [FlagValue.new(name: "True", value: true), FlagValue.new(name: "False", value: false)],
          description: description
  )
end

#new_json_flag(id, default:, name: nil, description: nil, values: nil) ⇒ JsonFlag

Return a new unsaved JSON JsonFlag. Call save to persist.

Parameters:

  • id (String)

    stable flag identifier, unique per account.

  • default (Hash)

    value served when no environment override or rule applies.

  • name (String, nil) (defaults to: nil)

    human-readable display name; defaults to a title-cased form of id.

  • description (String, nil) (defaults to: nil)

    optional free-text description of the flag.

  • values (Array<FlagValue>, nil) (defaults to: nil)

    optional list of allowed values constraining what the flag may serve; when omitted the flag is unconstrained.

Returns:

  • (JsonFlag)

    an unsaved flag; call save to persist it.



348
349
350
351
352
353
# File 'lib/smplkit/flags/client.rb', line 348

def new_json_flag(id, default:, name: nil, description: nil, values: nil)
  JsonFlag.new(
    self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
          type: "JSON", default: default, values: values, description: description
  )
end

#new_number_flag(id, default:, name: nil, description: nil, values: nil) ⇒ NumberFlag

Return a new unsaved numeric NumberFlag. Call save to persist.

Parameters:

  • id (String)

    stable flag identifier, unique per account.

  • default (Numeric)

    value served when no environment override or rule applies.

  • name (String, nil) (defaults to: nil)

    human-readable display name; defaults to a title-cased form of id.

  • description (String, nil) (defaults to: nil)

    optional free-text description of the flag.

  • values (Array<FlagValue>, nil) (defaults to: nil)

    optional list of allowed values constraining what the flag may serve; when omitted the flag is unconstrained.

Returns:

  • (NumberFlag)

    an unsaved flag; call save to persist it.



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

def new_number_flag(id, default:, name: nil, description: nil, values: nil)
  NumberFlag.new(
    self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
          type: "NUMERIC", default: default, values: values, description: description
  )
end

#new_string_flag(id, default:, name: nil, description: nil, values: nil) ⇒ StringFlag

Return a new unsaved string StringFlag. Call save to persist.

Parameters:

  • id (String)

    stable flag identifier, unique per account.

  • default (String)

    value served when no environment override or rule applies.

  • name (String, nil) (defaults to: nil)

    human-readable display name; defaults to a title-cased form of id.

  • description (String, nil) (defaults to: nil)

    optional free-text description of the flag.

  • values (Array<FlagValue>, nil) (defaults to: nil)

    optional list of allowed values constraining what the flag may serve; when omitted the flag is unconstrained.

Returns:

  • (StringFlag)

    an unsaved flag; call save to persist it.



310
311
312
313
314
315
# File 'lib/smplkit/flags/client.rb', line 310

def new_string_flag(id, default:, name: nil, description: nil, values: nil)
  StringFlag.new(
    self, id: id, name: name || Smplkit::Helpers.key_to_display_name(id),
          type: "STRING", default: default, values: values, description: description
  )
end

#number_flag(id, default:) ⇒ NumberFlag

Declare a numeric flag handle for live evaluation. Connects lazily on first use.

Parameters:

  • id (String)

    identifier of the flag to evaluate.

  • default (Numeric)

    value returned by handle.get when the flag is unknown or no environment override or rule applies.

  • (String)
  • default: (Integer, Float)

Returns:

  • (NumberFlag)

    a handle whose get evaluates against the live cache.



500
501
502
503
504
505
506
# File 'lib/smplkit/flags/client.rb', line 500

def number_flag(id, default:)
  ensure_connected
  handle = NumberFlag.new(self, id: id, name: id, type: "NUMERIC", default: default)
  @handles[id] = handle
  observe_declaration(id, "NUMERIC", default)
  handle
end

#on_change(flag_id = nil) {|FlagChangeEvent| ... } ⇒ Proc

Register a change listener.

client.flags.on_change { |event| ... }            # global
client.flags.on_change("checkout-v2") { |e| ... } # flag-scoped

Connects lazily on first use — no explicit install step.

Parameters:

  • flag_id (String, nil) (defaults to: nil)

    optional flag id scoping the listener to that flag; when nil a global listener is registered.

  • (String, nil)

Yields:

Yield Parameters:

Yield Returns:

  • (void)

Returns:

  • (Proc)

    the registered listener block.

Raises:

  • (ArgumentError)


555
556
557
558
559
560
561
562
563
564
565
# File 'lib/smplkit/flags/client.rb', line 555

def on_change(flag_id = nil, &block)
  ensure_connected
  raise ArgumentError, "on_change requires a block" unless block

  if flag_id.nil?
    @global_listeners << block
  else
    @key_listeners[flag_id] << block
  end
  block
end

#pending_countInteger

Number of pending flag declarations awaiting flush.

Returns:

  • (Integer)

    number of pending flag declarations awaiting flush.



458
459
460
# File 'lib/smplkit/flags/client.rb', line 458

def pending_count
  @buffer.pending_count
end

#refreshvoid

This method returns an undefined value.

Re-fetch all flag definitions and clear cache.

Connects lazily on first use — no explicit install step.



531
532
533
534
# File 'lib/smplkit/flags/client.rb', line 531

def refresh
  ensure_connected
  do_refresh("manual")
end

#register(items, flush: false) ⇒ void

This method returns an undefined value.

Buffer flag declarations for bulk-discovery upload; optionally flush now.

Parameters:

  • items (FlagDeclaration, Array<FlagDeclaration>)

    a single declaration or an array of them to queue.

  • flush (Boolean) (defaults to: false)

    when true, send the buffered declarations immediately via flush before returning. When false (the default), they stay buffered and are sent on the next flush — automatic once the buffer reaches its batch size, or on the first live call.



413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
# File 'lib/smplkit/flags/client.rb', line 413

def register(items, flush: false)
  batch = items.is_a?(Array) ? items : [items]
  batch.each { |d| @buffer.add(d) }
  if flush
    self.flush
    return
  end
  return unless @buffer.pending_count >= FLAG_BATCH_FLUSH_SIZE

  # Stateless mode (+streaming: false+) never spawns background threads —
  # the threshold flush runs inline (blocking) instead.
  if @streaming
    Thread.new { threshold_flush }
  else
    threshold_flush
  end
end

#startvoid

This method returns an undefined value.



83
# File 'sig/smplkit/flags.rbs', line 83

def start: () -> void

#statsFlagStats

Return evaluation statistics. Connects lazily on first use.

Returns:



539
540
541
542
# File 'lib/smplkit/flags/client.rb', line 539

def stats
  ensure_connected
  FlagStats.new(cache_hits: @cache.cache_hits, cache_misses: @cache.cache_misses)
end

#string_flag(id, default:) ⇒ StringFlag

Declare a string flag handle for live evaluation. Connects lazily on first use.

Parameters:

  • id (String)

    identifier of the flag to evaluate.

  • default (String)

    value returned by handle.get when the flag is unknown or no environment override or rule applies.

  • (String)
  • default: (String)

Returns:

  • (StringFlag)

    a handle whose get evaluates against the live cache.



486
487
488
489
490
491
492
# File 'lib/smplkit/flags/client.rb', line 486

def string_flag(id, default:)
  ensure_connected
  handle = StringFlag.new(self, id: id, name: id, type: "STRING", default: default)
  @handles[id] = handle
  observe_declaration(id, "STRING", default)
  handle
end