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



166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/smplkit/flags/client.rb', line 166

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) ⇒ Object

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



411
412
413
414
415
416
417
418
# File 'lib/smplkit/flags/client.rb', line 411

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

Instance Method Details

#_create_flag(flag) ⇒ Object



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

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.



465
466
467
# File 'lib/smplkit/flags/client.rb', line 465

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.



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
454
455
456
457
458
459
460
461
462
# File 'lib/smplkit/flags/client.rb', line 424

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



270
271
272
273
# File 'lib/smplkit/flags/client.rb', line 270

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:) ⇒ Object

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



322
323
324
325
326
327
328
# File 'lib/smplkit/flags/client.rb', line 322

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

#closeObject Also known as: _close

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.



398
399
400
401
402
403
404
405
406
407
# File 'lib/smplkit/flags/client.rb', line 398

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) ⇒ Object

Delete a flag by id.



260
261
262
263
# File 'lib/smplkit/flags/client.rb', line 260

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

#flushObject

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



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

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_syncObject

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



308
309
310
# File 'lib/smplkit/flags/client.rb', line 308

def flush_sync
  flush
end

#get(id) ⇒ Object

Fetch the editable Flag resource by id.



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

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:) ⇒ Object

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



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

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) ⇒ Object

List flags for the authenticated account.



251
252
253
254
255
256
257
# File 'lib/smplkit/flags/client.rb', line 251

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) ⇒ Object

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



211
212
213
214
215
216
217
218
# File 'lib/smplkit/flags/client.rb', line 211

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) ⇒ Object

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



237
238
239
240
241
242
# File 'lib/smplkit/flags/client.rb', line 237

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) ⇒ Object

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



229
230
231
232
233
234
# File 'lib/smplkit/flags/client.rb', line 229

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) ⇒ Object

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



221
222
223
224
225
226
# File 'lib/smplkit/flags/client.rb', line 221

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:) ⇒ Object

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



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

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, &block) ⇒ Object

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.

Raises:

  • (ArgumentError)


381
382
383
384
385
386
387
388
389
390
391
# File 'lib/smplkit/flags/client.rb', line 381

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_countObject

Number of pending flag declarations awaiting flush.



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

def pending_count
  @buffer.pending_count
end

#refreshObject

Re-fetch all flag definitions and clear cache.

Connects lazily on first use — no explicit install step.



364
365
366
367
# File 'lib/smplkit/flags/client.rb', line 364

def refresh
  ensure_connected
  do_refresh("manual")
end

#register(items, flush: false) ⇒ Object

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



280
281
282
283
284
285
286
287
288
289
290
# File 'lib/smplkit/flags/client.rb', line 280

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

#statsObject

Return evaluation statistics. Connects lazily on first use.



370
371
372
373
# File 'lib/smplkit/flags/client.rb', line 370

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

#string_flag(id, default:) ⇒ Object

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



331
332
333
334
335
336
337
# File 'lib/smplkit/flags/client.rb', line 331

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