Class: Smplkit::Client

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

Overview

Synchronous entry point for the smplkit SDK.

client = Smplkit::Client.new(environment: "production", service: "my-svc")
checkout_v2 = client.flags.boolean_flag("checkout-v2", default: false)
if checkout_v2.get
  # ...
end
client.close

Block form:

Smplkit::Client.open(environment: "production", service: "my-svc") do |client|
  # ...
end

All parameters are optional. When omitted, the SDK resolves them from environment variables (SMPLKIT_*) or the ~/.smplkit configuration file. See ADR-021 for the full resolution algorithm.

Smplkit::Client is thread-safe by construction. Background work runs on internal SDK-owned threads; public methods block the calling thread and return values directly.

Constant Summary collapse

PERIODIC_FLUSH_INTERVAL =

Periodic flush of all sub-client registration buffers (contexts, flags, loggers). Threshold flushes still fire immediately when buffers fill up; this timer is the liveness guarantee for the tail.

60.0

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(api_key: nil, environment: nil, service: nil, profile: nil, base_domain: nil, scheme: nil, debug: nil, telemetry: nil, extra_headers: nil) ⇒ Client

Returns a new instance of Client.



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# File 'lib/smplkit/client.rb', line 46

def initialize(api_key: nil, environment: nil, service: nil, profile: nil,
               base_domain: nil, scheme: nil, debug: nil, telemetry: nil,
               extra_headers: nil)
  cfg = ConfigResolution.resolve_config(
    profile: profile, api_key: api_key, base_domain: base_domain, scheme: scheme,
    environment: environment, service: service, debug: debug, telemetry: telemetry
  )
  Smplkit.enable_debug if cfg.debug

  @api_key = cfg.api_key
  @environment = cfg.environment
  @service = cfg.service
  @base_domain = cfg.base_domain
  @scheme = cfg.scheme
  @extra_headers = extra_headers

  masked_key = cfg.api_key.length > 10 ? "#{cfg.api_key[0, 10]}..." : cfg.api_key
  Smplkit.debug(
    "lifecycle",
    "Client init: api_key=#{masked_key} env=#{cfg.environment.inspect} service=#{cfg.service.inspect} " \
    "base_domain=#{cfg.base_domain.inspect} scheme=#{cfg.scheme.inspect} " \
    "debug=#{cfg.debug} telemetry=#{cfg.telemetry}"
  )

  # Build the per-service HTTP transports + the context-registration buffer.
  # Side-effect-free: each transport connects lazily on first call.
  # client.platform owns the buffer; client.config/flags/logging/jobs borrow
  # their transports from here.
  @transports = Transport.build_service_transports(Transport.to_transport_config(cfg, extra_headers))

  app_url = @transports.app_url
  audit_url = ConfigResolution.service_url(cfg.scheme, "audit", cfg.base_domain)

  # Alias the shared HTTP transports — single connection pool per service.
  @http_client = @transports.config_http
  @app_http = @transports.app_http
  @app_base_url = app_url

  # Metrics reporter
  @metrics = if cfg.telemetry
               MetricsReporter.new(http_client: @app_http, environment: cfg.environment, service: cfg.service)
             end

  @ws_manager = nil
  # Platform's cross-cutting CRUD on one client; wired into this parent so
  # it borrows the shared app transport, and owns the context-registration
  # buffer. Built BEFORE flags so the contexts seam below is available.
  @platform = Platform::PlatformClient.new(app_transport: @app_http)
  # Account-level settings on one client; built from the app url + api key
  # (the settings sub-client uses Faraday directly).
  @account = Account::AccountClient.new(api_key: cfg.api_key, base_url: app_url, extra_headers: extra_headers)
  # Config's full surface on one client; wired into this parent so it
  # borrows the shared config transport and WebSocket.
  @config = Config::ConfigClient.new(parent: self, transport: @transports.config_http, metrics: @metrics)
  # Flags' full surface on one client; wired into this parent so it borrows
  # the shared flags transport and WebSocket. ``contexts`` is the injection
  # seam for evaluation-context registration, wired to
  # ``client.platform.contexts``.
  @flags = Flags::FlagsClient.new(
    parent: self, transport: @transports.flags_http, contexts: @platform.contexts, metrics: @metrics
  )
  # Logging's full surface on one client; wired into this parent so it
  # borrows the shared logging transport and WebSocket. The two management
  # sub-clients live at client.logging.loggers / client.logging.log_groups.
  @logging = Logging::LoggingClient.new(parent: self, transport: @transports.logging_http, metrics: @metrics)
  # Audit's full surface on one client; this runtime instance carries the
  # configured environment as ``X-Smplkit-Environment`` and owns its own
  # transport (closed in ``close``).
  @audit = Audit::AuditClient.new(
    api_key: cfg.api_key, base_url: audit_url, environment: cfg.environment, extra_headers: extra_headers
  )
  # Jobs has no runtime/management split — reuse the shared jobs transport
  # (single connection pool) so ``client.jobs`` is one-stop.
  @jobs = Jobs::JobsClient.new(auth_client: @transports.jobs_http)

  # Construction is side-effect-free: no background threads, no phone-home.
  # The periodic registration-buffer flush and the service-context
  # registration are deferred until the first config/flags/logging operation
  # or set_context via ``_ensure_started`` — so an audit-only or jobs-only
  # customer pays zero threads and zero network at construction.
  @closed = false
  @started = false
  @start_lock = Mutex.new
  @flush_timer = nil
  @init_thread = nil
end

Instance Attribute Details

#accountObject (readonly)

Returns the value of attribute account.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def 
  @account
end

#auditObject (readonly)

Returns the value of attribute audit.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def audit
  @audit
end

#configObject (readonly)

Returns the value of attribute config.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def config
  @config
end

#flagsObject (readonly)

Returns the value of attribute flags.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def flags
  @flags
end

#jobsObject (readonly)

Returns the value of attribute jobs.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def jobs
  @jobs
end

#loggingObject (readonly)

Returns the value of attribute logging.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def logging
  @logging
end

#platformObject (readonly)

Returns the value of attribute platform.



34
35
36
# File 'lib/smplkit/client.rb', line 34

def platform
  @platform
end

Class Method Details

.open(**kwargs) ⇒ Object

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



37
38
39
40
41
42
43
44
# File 'lib/smplkit/client.rb', line 37

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

Instance Method Details

#_api_keyObject



216
# File 'lib/smplkit/client.rb', line 216

def _api_key = @api_key

#_app_base_urlObject



217
# File 'lib/smplkit/client.rb', line 217

def _app_base_url = @app_base_url

#_ensure_startedObject

Start the deferred background machinery exactly once.

Idempotent and thread-safe (lock + flag); a no-op after close. Triggered by the first config/flags/logging operation, set_context, wait_until_ready, or WebSocket open — never at construction.



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

def _ensure_started
  @start_lock.synchronize do
    return if @started || @closed

    @started = true
  end
  schedule_periodic_flush
  @init_thread = Thread.new { register_service_context }
end

#_ensure_wsObject



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

def _ensure_ws
  _ensure_started
  if @ws_manager.nil?
    @ws_manager = SharedWebSocket.new(app_base_url: @app_base_url, api_key: @api_key, metrics: @metrics)
    @ws_manager.start
  end
  @ws_manager
end

#_environmentObject



215
# File 'lib/smplkit/client.rb', line 215

def _environment = @environment

#_extra_headersObject



219
# File 'lib/smplkit/client.rb', line 219

def _extra_headers = @extra_headers

#_metricsObject



218
# File 'lib/smplkit/client.rb', line 218

def _metrics = @metrics

#_serviceObject

Internal accessors used by sub-clients ——————————–



214
# File 'lib/smplkit/client.rb', line 214

def _service = @service

#closeObject

Release all resources held by this client.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/smplkit/client.rb', line 194

def close
  Smplkit.debug("lifecycle", "Client.close called")
  @closed = true
  @flush_timer&.shutdown
  @flush_timer = nil
  final_flush
  @metrics&.close
  @logging._close
  @flags._close
  @audit._close
  @ws_manager&.stop
  @ws_manager = nil
  # Close the shared per-service HTTP transports (app/config/flags/logging/
  # jobs). client.platform/account borrow the app transport and close
  # nothing; client.audit owns and closed its own transport above.
  @transports.close
end

#set_context(contexts, &block) ⇒ Object

Stash contexts as the current request’s evaluation context.

Typical use is from middleware — set the context once at request entry and every flag.get (and other context-sensitive evaluations) inside that request automatically picks it up.

Each unique (type, key) is also queued for bulk registration on the management API (deduplicated; flushed in the background).

Two usage shapes:

# Fire-and-forget (typical middleware)
client.set_context([Smplkit::Context.new("user", "u-123")])

# Scoped block (e.g. impersonation or one-off override)
client.set_context([Smplkit::Context.new("user", "impersonated")]) do
  # ...
end
# original context restored here


181
182
183
184
185
186
187
188
189
190
191
# File 'lib/smplkit/client.rb', line 181

def set_context(contexts, &block)
  _ensure_started
  @platform.contexts.register(contexts) if contexts && !contexts.empty?

  scope = Smplkit.set_request_context(contexts || [])
  if block
    scope.call(&block)
  else
    scope
  end
end

#wait_until_ready(timeout: 10.0) ⇒ Object

Optionally pre-warm the SDK and block until the live socket is up.

Eagerly connects config and flags — flushing discovery, pre-fetching all flags and configs into the local cache, opening the live-updates WebSocket — and waits for the handshake to complete. After this returns, flag.get / client.config.subscribe hit cache (no first-request connect tax) and any on_change listeners receive every server event from this point forward.

Optional: config and flags connect lazily on first live use, so this is purely a pre-warm / WebSocket-ready barrier. Logging integration is not connected here — call client.logging.install separately if you want it (it installs adapters and hooks into your application’s logger, which should be opt-in).



147
148
149
150
151
152
153
154
155
156
157
158
159
160
# File 'lib/smplkit/client.rb', line 147

def wait_until_ready(timeout: 10.0)
  @flags._ensure_connected
  @config._ensure_connected
  ws = _ensure_ws
  deadline = monotonic_now + timeout
  while ws.connection_status != "connected"
    if monotonic_now >= deadline
      raise TimeoutError, "Live-updates websocket did not connect within #{timeout}s " \
                          "(status: #{ws.connection_status.inspect})"
    end

    sleep(0.05)
  end
end