Class: Smplkit::Flags::FlagsClient

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

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, base_url: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, extra_headers: nil, parent: nil, transport: nil, contexts: nil, metrics: nil) ⇒ FlagsClient

Returns a new instance of FlagsClient.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/smplkit/flags/client.rb', line 189

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, contexts: nil, metrics: nil)
  @parent = parent
  @metrics = metrics
  @environment = parent.nil? ? environment : parent._environment
  @service = parent&._service
  @standalone_api_key = nil
  if transport.nil?
    @flags_http, app_http, @app_base_url, @standalone_api_key = Flags.flags_transport(
      api_key: api_key, base_url: base_url, profile: profile,
      base_domain: base_domain, scheme: scheme, debug: debug, extra_headers: extra_headers
    )
    # 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: 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.



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

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

Instance Method Details

#_create_flag(flag) ⇒ Object



343
344
345
346
# File 'lib/smplkit/flags/client.rb', line 343

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.



591
592
593
# File 'lib/smplkit/flags/client.rb', line 591

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.



550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
# File 'lib/smplkit/flags/client.rb', line 550

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



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

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.

Returns:

  • (BooleanFlag)

    a handle whose get evaluates against the live cache.



419
420
421
422
423
424
425
# File 'lib/smplkit/flags/client.rb', line 419

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.



521
522
523
524
525
526
527
528
529
530
# File 'lib/smplkit/flags/client.rb', line 521

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:



338
339
340
341
# File 'lib/smplkit/flags/client.rb', line 338

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).



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

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.



398
399
400
# File 'lib/smplkit/flags/client.rb', line 398

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:



313
314
315
316
# File 'lib/smplkit/flags/client.rb', line 313

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.

Returns:

  • (JsonFlag)

    a handle whose get evaluates against the live cache.



461
462
463
464
465
466
467
# File 'lib/smplkit/flags/client.rb', line 461

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.



325
326
327
328
329
330
331
# File 'lib/smplkit/flags/client.rb', line 325

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.



242
243
244
245
246
247
248
249
# File 'lib/smplkit/flags/client.rb', line 242

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.



301
302
303
304
305
306
# File 'lib/smplkit/flags/client.rb', line 301

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.



282
283
284
285
286
287
# File 'lib/smplkit/flags/client.rb', line 282

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.



263
264
265
266
267
268
# File 'lib/smplkit/flags/client.rb', line 263

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.

Returns:

  • (NumberFlag)

    a handle whose get evaluates against the live cache.



447
448
449
450
451
452
453
# File 'lib/smplkit/flags/client.rb', line 447

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.

Yields:

Returns:

  • (Proc)

    the registered listener block.

Raises:

  • (ArgumentError)


502
503
504
505
506
507
508
509
510
511
512
# File 'lib/smplkit/flags/client.rb', line 502

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.



405
406
407
# File 'lib/smplkit/flags/client.rb', line 405

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.



478
479
480
481
# File 'lib/smplkit/flags/client.rb', line 478

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.



366
367
368
369
370
371
372
373
374
375
376
# File 'lib/smplkit/flags/client.rb', line 366

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

  Thread.new { threshold_flush }
end

#statsFlagStats

Return evaluation statistics. Connects lazily on first use.

Returns:



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

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.

Returns:

  • (StringFlag)

    a handle whose get evaluates against the live cache.



433
434
435
436
437
438
439
# File 'lib/smplkit/flags/client.rb', line 433

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