Class: Smplkit::Flags::FlagsClient

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

Overview

Synchronous flags runtime namespace.

Obtained via Smplkit::Client#flags. Exposes typed handles (boolean_flag/string_flag/number_flag/json_flag) and runtime control (refresh, stats, on_change). CRUD has moved to mgmt.flags.*. Per-request context is set via client.set_context().

Constant Summary collapse

INITIAL_START_RETRY_DELAY =
1.0
MAX_START_RETRY_DELAY =
60.0

Instance Method Summary collapse

Constructor Details

#initialize(parent, manage:, metrics:, flags_base_url:, app_base_url:) ⇒ FlagsClient

Returns a new instance of FlagsClient.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/smplkit/flags/client.rb', line 85

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

  @flag_store = {}
  @connected = false
  @ws_subscribed = false
  @next_start_attempt_at = 0.0
  @start_retry_delay = INITIAL_START_RETRY_DELAY
  @cache = ResolutionCache.new
  @handles = {}
  @global_listeners = []
  @key_listeners = Hash.new { |h, k| h[k] = [] }
  @ws_manager = nil
  @lock = Mutex.new
end

Instance Method Details

#_closeObject



190
191
192
# File 'lib/smplkit/flags/client.rb', line 190

def _close
  # No durable resources here — kept for symmetry with Python SDK.
end

#_evaluate_handle(flag_id, default, context) ⇒ Object



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
228
229
230
# File 'lib/smplkit/flags/client.rb', line 194

def _evaluate_handle(flag_id, default, context)
  start unless @connected

  eval_dict =
    if context
      @manage.contexts.register(context) if @manage.respond_to?(:contexts)
      contexts_to_eval_dict(context)
    else
      current = Smplkit.request_context
      current.empty? ? {} : contexts_to_eval_dict(current)
    end

  eval_dict["service"] = { "key" => @service } if @service && !eval_dict.key?("service")

  ctx_hash = 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

#boolean_flag(id, default:) ⇒ Object



107
108
109
# File 'lib/smplkit/flags/client.rb', line 107

def boolean_flag(id, default:)
  register_handle(BooleanFlag, id, "BOOLEAN", default)
end

#json_flag(id, default:) ⇒ Object



119
120
121
# File 'lib/smplkit/flags/client.rb', line 119

def json_flag(id, default:)
  register_handle(JsonFlag, id, "JSON", default)
end

#number_flag(id, default:) ⇒ Object



115
116
117
# File 'lib/smplkit/flags/client.rb', line 115

def number_flag(id, default:)
  register_handle(NumberFlag, id, "NUMERIC", default)
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

Raises:

  • (ArgumentError)


179
180
181
182
183
184
185
186
187
188
# File 'lib/smplkit/flags/client.rb', line 179

def on_change(flag_id = nil, &block)
  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

#refreshObject



165
166
167
168
169
# File 'lib/smplkit/flags/client.rb', line 165

def refresh
  fetch_all_flags
  @cache.clear
  fire_change_listeners_all("manual")
end

#startObject

Eagerly initialize the flags subclient.

Flushes any pending flag-declaration buffer, fetches all flag definitions, opens the shared WebSocket and subscribes to flag_changed / flag_deleted / flags_changed events.

Idempotent — safe to call multiple times. Called automatically on first flag.get evaluation if not invoked manually.

If the flags-service is unhealthy (e.g. a coordinated rebuild where the app pod starts before the schema is loaded), the flush or refresh will fail. Pending declarations stay queued, the client remains disconnected, and the next call retries after an exponentially backed-off delay (capped at MAX_START_RETRY_DELAY seconds). Evaluations during that window fall back to handle defaults.



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/smplkit/flags/client.rb', line 138

def start
  return if @connected
  return if Process.clock_gettime(Process::CLOCK_MONOTONIC) < @next_start_attempt_at

  @environment = @parent._environment

  begin
    @manage.flags.flush
    refresh
  rescue StandardError => e
    schedule_start_retry(e)
    return
  end

  @connected = true
  @start_retry_delay = INITIAL_START_RETRY_DELAY
  @next_start_attempt_at = 0.0

  @ws_manager = @parent._ensure_ws
  return if @ws_subscribed

  @ws_manager.on("flag_changed") { |data| handle_flag_changed(data) }
  @ws_manager.on("flag_deleted") { |data| handle_flag_deleted(data) }
  @ws_manager.on("flags_changed") { |data| handle_flags_changed(data) }
  @ws_subscribed = true
end

#statsObject



171
172
173
# File 'lib/smplkit/flags/client.rb', line 171

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

#string_flag(id, default:) ⇒ Object



111
112
113
# File 'lib/smplkit/flags/client.rb', line 111

def string_flag(id, default:)
  register_handle(StringFlag, id, "STRING", default)
end