quonfig 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,28 +34,28 @@ module Quonfig
34
34
  end
35
35
 
36
36
  # Log methods
37
- def trace(message = nil, &block)
38
- log_message(:trace, message, &block)
37
+ def trace(message = nil, event: nil, &block)
38
+ log_message(:trace, message, event: event, &block)
39
39
  end
40
40
 
41
- def debug(message = nil, &block)
42
- log_message(:debug, message, &block)
41
+ def debug(message = nil, event: nil, &block)
42
+ log_message(:debug, message, event: event, &block)
43
43
  end
44
44
 
45
- def info(message = nil, &block)
46
- log_message(:info, message, &block)
45
+ def info(message = nil, event: nil, &block)
46
+ log_message(:info, message, event: event, &block)
47
47
  end
48
48
 
49
- def warn(message = nil, &block)
50
- log_message(:warn, message, &block)
49
+ def warn(message = nil, event: nil, &block)
50
+ log_message(:warn, message, event: event, &block)
51
51
  end
52
52
 
53
- def error(message = nil, &block)
54
- log_message(:error, message, &block)
53
+ def error(message = nil, event: nil, &block)
54
+ log_message(:error, message, event: event, &block)
55
55
  end
56
56
 
57
- def fatal(message = nil, &block)
58
- log_message(:fatal, message, &block)
57
+ def fatal(message = nil, event: nil, &block)
58
+ log_message(:fatal, message, event: event, &block)
59
59
  end
60
60
 
61
61
  def level
@@ -172,7 +172,10 @@ module Quonfig
172
172
  level_str&.downcase&.to_sym
173
173
  end
174
174
 
175
- def log_message(level, message, &block)
175
+ # +event+ (e.g. 'quonfig.init.failed') is a structured field: SemanticLogger
176
+ # gets it as the payload ({ event: ... }); plain Logger-shaped sinks only
177
+ # get the message, which already carries the full line.
178
+ def log_message(level, message, event: nil, &block)
176
179
  override = Quonfig::InternalLogger.user_logger
177
180
  if override
178
181
  write_to_user_logger(override, level, message, &block)
@@ -180,7 +183,11 @@ module Quonfig
180
183
  end
181
184
 
182
185
  if @using_semantic
183
- @logger.send(level, message, &block)
186
+ if event
187
+ @logger.send(level, message, { event: event }, &block)
188
+ else
189
+ @logger.send(level, message, &block)
190
+ end
184
191
  else
185
192
  # stdlib Logger doesn't have trace
186
193
  level = :debug if level == :trace
@@ -13,6 +13,11 @@ module Quonfig
13
13
  :telemetry_max_retained_bytes, :telemetry_max_retained_age_ms
14
14
  attr_accessor :is_fork
15
15
 
16
+ # The deprecated on_init_failure spelling passed (:raise / :return), or nil.
17
+ attr_reader :on_init_failure_alias
18
+ # An unrecognized on_init_failure value (treated as :fail), or nil.
19
+ attr_reader :on_init_failure_unknown
20
+
16
21
  # Default fallback poll interval, in milliseconds. The SDK polls api-delivery
17
22
  # at this cadence only when SSE is unavailable for >= 2x this value.
18
23
  DEFAULT_FALLBACK_POLL_INTERVAL_MS = 60_000
@@ -76,9 +81,42 @@ module Quonfig
76
81
  ms == ms.to_i ? ms.to_i : ms
77
82
  end
78
83
 
84
+ # What Client.new does when no config has loaded by +init_timeout_ms+
85
+ # (qfg-9p4i.10; project/plans/2026-10-05-init-failure-design.md B.1-B.3):
86
+ #
87
+ # - +:fail+ (default): raise Quonfig::Errors::InitializationError. Every
88
+ # delivery leg answering 401/403 raises at once, without waiting out the
89
+ # timeout.
90
+ # - +:wait+: keep blocking and retrying with no upper bound.
91
+ # - +:continue+: return a client over an empty store (getters answer with
92
+ # the caller's default) that keeps retrying in the background.
93
+ #
94
+ # +:raise+ and +:return+ are the deprecated earlier spellings of +:fail+ and
95
+ # +:continue+. They are still accepted and log a one-time WARN.
79
96
  module ON_INITIALIZATION_FAILURE
97
+ FAIL = :fail
98
+ WAIT = :wait
99
+ CONTINUE = :continue
100
+ # @deprecated Use FAIL.
80
101
  RAISE = :raise
102
+ # @deprecated Use CONTINUE.
81
103
  RETURN = :return
104
+
105
+ MODES = [FAIL, WAIT, CONTINUE].freeze
106
+ ALIASES = { RAISE => FAIL, RETURN => CONTINUE }.freeze
107
+
108
+ # [canonical mode, deprecated alias given or nil, unknown value or nil].
109
+ # Strings are accepted ('fail', ':raise'). An unknown value resolves to
110
+ # FAIL, the safe default, and is reported so the client can warn.
111
+ def self.resolve(value)
112
+ return [FAIL, nil, nil] if value.nil?
113
+
114
+ sym = value.to_s.delete_prefix(':').to_sym
115
+ return [sym, nil, nil] if MODES.include?(sym)
116
+ return [ALIASES[sym], sym, nil] if ALIASES.key?(sym)
117
+
118
+ [FAIL, nil, value]
119
+ end
82
120
  end
83
121
 
84
122
  module ON_NO_DEFAULT
@@ -217,6 +255,13 @@ module Quonfig
217
255
  private
218
256
 
219
257
  # @!method initialize(options = {})
258
+ # @option options [Integer] :init_timeout_ms (10000)
259
+ # How long +Client.new+ retries the initial config fetch before
260
+ # +:on_init_failure+ decides. A value <= 0 runs exactly one fetch
261
+ # attempt, lets it finish (it is bounded by the per-leg fetch timeouts,
262
+ # not by this option), then applies +:on_init_failure+: +:fail+ raises,
263
+ # +:continue+ starts on code defaults and keeps retrying in the
264
+ # background, +:wait+ keeps retrying. It never means "no deadline".
220
265
  # @option options [Boolean] :data_dir_auto_reload (false)
221
266
  # Datadir mode only. When +true+, the SDK watches the workspace
222
267
  # directory and re-reads the envelope whenever files inside it
@@ -271,7 +316,7 @@ module Quonfig
271
316
  on_no_default: ON_NO_DEFAULT::RAISE,
272
317
  init_timeout_ms: nil,
273
318
  initialization_timeout_sec: nil,
274
- on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
319
+ on_init_failure: ON_INITIALIZATION_FAILURE::FAIL,
275
320
  config_fetch_timeout_ms: nil,
276
321
  config_fetch_hedge_delay_ms: nil,
277
322
  config_fetch_hedge_abort_ms: nil,
@@ -334,7 +379,10 @@ module Quonfig
334
379
  else
335
380
  DEFAULT_INIT_TIMEOUT_MS
336
381
  end
337
- @on_init_failure = on_init_failure
382
+ # Stored canonical (:fail / :wait / :continue); the deprecated spelling
383
+ # the caller used, if any, is kept for the client's one-time WARN.
384
+ @on_init_failure, @on_init_failure_alias, @on_init_failure_unknown =
385
+ ON_INITIALIZATION_FAILURE.resolve(on_init_failure)
338
386
  # qfg-7h5d.1.9: per-URL config-fetch timeout. nil → DEFAULT_CONFIG_FETCH_TIMEOUT_MS.
339
387
  @config_fetch_timeout_ms = config_fetch_timeout_ms || DEFAULT_CONFIG_FETCH_TIMEOUT_MS
340
388
  # qfg-7h5d.1.14: parallel-failover hedge knobs. nil → defaults. The hedge
@@ -47,7 +47,33 @@ module Quonfig
47
47
  # the legacy "return nil if absent" shape can rescue and recover (see
48
48
  # IntegrationTestHelpers.assert_resolved, which folds a missing-key
49
49
  # raise into the test's expected default).
50
+ # The whole lookup reads one store snapshot (qfg-i86q): the flag, the
51
+ # segments it references and any decryptWith key all come from the same
52
+ # envelope even if an install lands mid-evaluation.
50
53
  def get(key, context = nil)
54
+ return get_unpinned(key, context) unless @store.respond_to?(:pinned)
55
+
56
+ @store.pinned { get_unpinned(key, context) }
57
+ end
58
+
59
+ # Post-evaluation value resolution. Mirrors sdk-node Resolver#resolveValue
60
+ # and sdk-go resolver.Resolve:
61
+ # - "provided" + ENV_VAR → read ENV[lookup], coerce to config's valueType
62
+ # - confidential + decryptWith → look up the key config, decrypt
63
+ # - everything else passes through unchanged
64
+ def resolve_value(value, config, context = nil, &on_weighted_index)
65
+ resolve_value_on_path(value, config, context, [], &on_weighted_index)
66
+ end
67
+
68
+ # Integration shims for code that expects a ConfigResolver. Keep these
69
+ # narrow; the real ConfigResolver still owns the production hot path.
70
+ def symbolize_json_names?
71
+ false
72
+ end
73
+
74
+ private
75
+
76
+ def get_unpinned(key, context)
51
77
  config = raw(key)
52
78
  raise Quonfig::Errors::MissingDefaultError, key if config.nil?
53
79
 
@@ -75,23 +101,6 @@ module Quonfig
75
101
  )
76
102
  end
77
103
 
78
- # Post-evaluation value resolution. Mirrors sdk-node Resolver#resolveValue
79
- # and sdk-go resolver.Resolve:
80
- # - "provided" + ENV_VAR → read ENV[lookup], coerce to config's valueType
81
- # - confidential + decryptWith → look up the key config, decrypt
82
- # - everything else passes through unchanged
83
- def resolve_value(value, config, context = nil, &on_weighted_index)
84
- resolve_value_on_path(value, config, context, [], &on_weighted_index)
85
- end
86
-
87
- # Integration shims for code that expects a ConfigResolver. Keep these
88
- # narrow; the real ConfigResolver still owns the production hot path.
89
- def symbolize_json_names?
90
- false
91
- end
92
-
93
- private
94
-
95
104
  # resolve_value plus +key_path+: the config keys already being resolved
96
105
  # above this one through decryptWith, so a decryptWith cycle raises a
97
106
  # DecryptionError instead of recursing until SystemStackError
@@ -34,10 +34,12 @@ module Quonfig
34
34
  #
35
35
  # sse_initial_reconnect_delay / sse_max_reconnect_delay: backoff bounds.
36
36
  # Each failed reconnect doubles the delay (with +/-50% jitter) up to the
37
- # max. A successful event delivery resets the delay to the initial
38
- # value — matches sdk-python's policy. A clean server-initiated FIN is
39
- # treated as "not a failure for backoff purposes" because LBs recycling
40
- # connections is normal; the reconnect counter still increments.
37
+ # max. A connection that delivers its first valid frame (a config
38
+ # envelope or a keepalive) resets the delay to the initial value. A
39
+ # clean server-initiated FIN after such a frame is "not a failure for
40
+ # backoff purposes" because LBs recycling connections is normal; the
41
+ # reconnect counter still increments. A connection that ends before any
42
+ # frame is a failed attempt (qfg-goi1.10.43).
41
43
  def initialize(sse_read_timeout: 90,
42
44
  sse_connect_timeout: 10,
43
45
  sse_initial_reconnect_delay: 1.0,
@@ -76,13 +78,22 @@ module Quonfig
76
78
 
77
79
  # +on_error+: optional callable invoked on every SSE error edge. Parent
78
80
  # Quonfig::Client wires this to drive @sse_state -> :error so that
79
- # +connection_state+ reflects the disconnect (qfg-47c2.27).
80
- def initialize(prefab_options, config_loader, options = nil, logger = nil, on_error: nil)
81
+ # +connection_state+ reflects the disconnect (qfg-47c2.27). A connection
82
+ # that ends before its first valid frame, clean EOF included, is an error
83
+ # edge too (qfg-goi1.10.43).
84
+ #
85
+ # +on_connected+: optional callable invoked once per connection, at its
86
+ # first valid frame (a keepalive, or a data block that validates as a
87
+ # config envelope), before that frame's envelope reaches +on_envelope+.
88
+ # Never on the 200 headers alone (supervisor-test-contract.md "Rule — a
89
+ # connection is `connected` at its first valid frame").
90
+ def initialize(prefab_options, config_loader, options = nil, logger = nil, on_error: nil, on_connected: nil)
81
91
  @prefab_options = prefab_options
82
92
  @options = options || Options.new
83
93
  @config_loader = config_loader
84
94
  @logger = logger || LOG
85
95
  @on_error = on_error
96
+ @on_connected = on_connected
86
97
 
87
98
  @stopped = Concurrent::AtomicBoolean.new(false)
88
99
  @restart_total = 0
@@ -234,10 +245,18 @@ module Quonfig
234
245
  end
235
246
  first_attempt = false
236
247
 
237
- connected_at_least_once = false
248
+ # First-frame rule (qfg-goi1.10.43): this connection counts as
249
+ # connected only at its first valid frame. Raised once, from the
250
+ # parser, before the frame's envelope is installed.
251
+ frame_seen = false
252
+ on_frame = lambda do
253
+ next if frame_seen
254
+
255
+ frame_seen = true
256
+ invoke_on_connected
257
+ end
238
258
  begin
239
- stream_once do |event|
240
- connected_at_least_once = true
259
+ ended_url = stream_once(on_frame) do |event|
241
260
  # Persist the most recent id so the next reconnect resumes
242
261
  # from there via Last-Event-Id. Updated *before* the user
243
262
  # callback runs so a raising listener still advances the
@@ -248,22 +267,30 @@ module Quonfig
248
267
  # listener must not look like a transport error and trigger
249
268
  # a reconnect.
250
269
  invoke_on_envelope_safely(on_envelope, event)
251
- # A connection healthy enough to deliver a real envelope
252
- # earns a reset of the backoff. Sustained outages never
253
- # reach this branch (no event ever delivered) so the
254
- # exponential growth still holds.
255
- delay = @options.sse_initial_reconnect_delay
256
270
  end
271
+ # A clean EOF before any frame (200 headers then close) is a
272
+ # failed attempt, exactly like a connect error: report it so
273
+ # the client leaves :connected and arms the fallback poller.
274
+ # This is what bounds ruby's transparent reconnect: connected
275
+ # survives a clean-EOF reconnect only until the next connection
276
+ # ends frameless.
277
+ handle_frameless_end(ended_url) unless frame_seen || @stopped.value
257
278
  rescue StandardError => e
258
279
  handle_error(e) unless @stopped.value
259
280
  end
260
281
 
261
- # Backoff only grows on failed connect attempts. A server-
262
- # initiated clean FIN after a healthy session (normal LB
263
- # recycling) reuses the same delay — punishing it would make
264
- # us look broken under benign rolling restarts. Matches
265
- # sdk-go's `connectedOK` distinction.
266
- delay = [delay * 2, @options.sse_max_reconnect_delay].min unless connected_at_least_once
282
+ # The backoff resets only for a connection that delivered a
283
+ # frame; it then reconnects at the initial delay even after a
284
+ # clean FIN (normal LB recycling) or an IO error later in the
285
+ # session. Every other attempt (connect error, non-200, or a
286
+ # frameless 200) doubles it. Sustained outages never deliver a
287
+ # frame, so the exponential growth still holds. Matches sdk-go's
288
+ # connectOnce "frame seen" result.
289
+ delay = if frame_seen
290
+ @options.sse_initial_reconnect_delay
291
+ else
292
+ [delay * 2, @options.sse_max_reconnect_delay].min
293
+ end
267
294
  rescue SSEReadDeadlineExceeded => e
268
295
  # Paranoid backstop (qfg-tj18). A watchdog raise that landed
269
296
  # outside +stream_once+ — typically in +interruptible_sleep+
@@ -279,7 +306,8 @@ module Quonfig
279
306
 
280
307
  # Opens one SSE request and yields each parsed event until the stream
281
308
  # ends (clean FIN, error, or stop). Raises on transport errors so the
282
- # caller can apply backoff. Clean FIN returns without raising.
309
+ # caller can apply backoff. Clean FIN returns the URL without raising.
310
+ # +on_frame+ is called at every valid frame (see EventParser#feed).
283
311
  #
284
312
  # A watchdog thread closes the socket if no bytes arrive within
285
313
  # +sse_read_timeout+. Net::HTTP#read_timeout is NOT reliable for the
@@ -288,7 +316,7 @@ module Quonfig
288
316
  # against a configured deadline). sdk-go and sdk-node hit the same
289
317
  # gotcha and solve it the same way: per-chunk reset, async close on
290
318
  # expiry (chaos scenario 02 — sse_silent_stall).
291
- def stream_once(&block)
319
+ def stream_once(on_frame = nil, &block)
292
320
  base = current_url
293
321
  # f05 latch: record if this connection attempt targets a non-primary
294
322
  # stream leg. With the pin in #current_url this is structurally false;
@@ -333,9 +361,14 @@ module Quonfig
333
361
  Thread.handle_interrupt(SSEReadDeadlineExceeded => :immediate) do
334
362
  resp.read_body do |chunk|
335
363
  watchdog.reset!
336
- break if @stopped.value
337
-
338
- parser.feed(chunk, &block)
364
+ # Never +break+ here (qfg-yrly): a break returns into
365
+ # Net::HTTPResponse#reading_body, which then calls +body+ to
366
+ # drain the rest of the response with no block. On an endless
367
+ # SSE stream that blocks the worker forever. Raising skips the
368
+ # drain, and Net::HTTP closes the socket on the way out.
369
+ raise SSEStopRequested if @stopped.value
370
+
371
+ parser.feed(chunk, on_frame: on_frame, &block)
339
372
  end
340
373
  end
341
374
  # read_body returned cleanly — either a server-initiated FIN, or
@@ -344,6 +377,9 @@ module Quonfig
344
377
  # next iteration.
345
378
  @logger.debug "SSE stream ended for url #{url}"
346
379
  end
380
+ url
381
+ rescue SSEStopRequested
382
+ url
347
383
  ensure
348
384
  watchdog.stop
349
385
  register_active(nil)
@@ -394,6 +430,25 @@ module Quonfig
394
430
  @restart_mutex.synchronize { @restart_total += 1 }
395
431
  end
396
432
 
433
+ # A connection that returned 200 and ended cleanly before its first valid
434
+ # frame (qfg-goi1.10.43).
435
+ def handle_frameless_end(url)
436
+ err = SSEFramelessSessionError.new
437
+ @logger.warn "SSE Streaming Error: stream for url #{url} ended before its first frame; " \
438
+ 'treating it as a failed attempt'
439
+ invoke_on_error(err)
440
+ end
441
+
442
+ def invoke_on_connected
443
+ return unless @on_connected
444
+
445
+ begin
446
+ @on_connected.call
447
+ rescue StandardError => e
448
+ @logger.error "SSE on_connected callback raised: #{e.inspect}"
449
+ end
450
+ end
451
+
397
452
  def handle_error(error)
398
453
  if error.is_a?(SSEHTTPAuthError)
399
454
  # Already logged once by check_response_status! (R5).
@@ -478,11 +533,25 @@ module Quonfig
478
533
  # no longer stops on these statuses.
479
534
  SSEHTTPTerminalError = SSEHTTPAuthError # rubocop:disable Lint/UselessConstantScoping
480
535
 
536
+ # Passed to on_error when a 200 stream ends cleanly before its first valid
537
+ # frame (qfg-goi1.10.43). Not raised.
538
+ class SSEFramelessSessionError < StandardError
539
+ def initialize(msg = 'SSE stream ended before its first frame')
540
+ super
541
+ end
542
+ end
543
+
481
544
  # Raised by the watchdog into the worker thread when the per-chunk
482
545
  # read deadline elapses. Caught by run_loop's rescue, indistinguishable
483
546
  # from any other transport error for backoff/restart purposes.
484
547
  class SSEReadDeadlineExceeded < StandardError; end
485
548
 
549
+ # Private: raised from inside +read_body+ when +close+ has been called,
550
+ # so the worker leaves the stream without Net::HTTP draining the body
551
+ # (qfg-yrly). Rescued in +stream_once+; never reaches callers.
552
+ class SSEStopRequested < StandardError; end
553
+ private_constant :SSEStopRequested
554
+
486
555
  # Background watchdog that interrupts the worker thread if no chunk
487
556
  # arrives within +deadline_s+ seconds. Uses Thread#raise — the only
488
557
  # reliable cross-platform way to unblock a Ruby thread blocked in
@@ -560,7 +629,7 @@ module Quonfig
560
629
  # - chunks that split a line mid-way
561
630
  # - any of CR / LF / CRLF as line terminators
562
631
  # - +data:+, +data: + (optional space per SSE spec)
563
- # - +:comment+ lines (keepalives — ignored)
632
+ # - +:comment+ lines (keepalives; a comment-only block is a frame)
564
633
  # - multi-line +data:+ (concatenated with +\n+, per spec)
565
634
  # Ignores +event:+ and +retry:+ — api-delivery does not emit them and the
566
635
  # Quonfig wire contract does not honor reconnect-time directives.
@@ -573,16 +642,34 @@ module Quonfig
573
642
  @data = +''
574
643
  @have_data = false
575
644
  @id = nil
645
+ @saw_comment = false
646
+ @saw_field = false
576
647
  end
577
648
 
578
- def feed(chunk)
649
+ # Yields each config envelope. +on_frame+ (optional) is called at every
650
+ # valid frame, before that frame's envelope is yielded. A valid frame is
651
+ # a block ended by a blank line that is either comment-only (the
652
+ # +: keepalive+ heartbeat; +:+ alone counts) or carries +data:+ that
653
+ # decodes and validates as a config envelope. A block cut off by EOF, a
654
+ # malformed or non-envelope data block, an +id:+-only block and a bare
655
+ # blank line are not frames (qfg-goi1.10.43).
656
+ def feed(chunk, on_frame: nil)
579
657
  @reader.feed(chunk) do |line|
580
658
  if line.empty?
659
+ comment_only = @saw_comment && !@saw_field
660
+ @saw_comment = false
661
+ @saw_field = false
581
662
  event = flush
582
- yield event if event
663
+ if event
664
+ on_frame&.call
665
+ yield event
666
+ elsif comment_only
667
+ on_frame&.call
668
+ end
583
669
  elsif line.start_with?(':')
584
- # comment / keepalive — ignore
670
+ @saw_comment = true
585
671
  else
672
+ @saw_field = true
586
673
  process_field(line)
587
674
  end
588
675
  end
@@ -637,10 +724,18 @@ module Quonfig
637
724
  return nil
638
725
  end
639
726
 
640
- envelope = Quonfig::ConfigEnvelope.new(
641
- configs: parsed['configs'] || [],
642
- meta: parsed['meta'] || {}
643
- )
727
+ meta = parsed['meta'] || {}
728
+ # Skip one malformed config, keep the rest (qfg-krjt); an event with
729
+ # no loadable config is dropped like malformed JSON.
730
+ begin
731
+ configs = Quonfig::ConfigShape.filter(parsed['configs'] || [], logger: @logger || LOG,
732
+ version: meta['version'])
733
+ rescue Quonfig::ConfigShape::AllInvalidError => e
734
+ (@logger || LOG).error "SSE Streaming Error: #{e.message}; event dropped"
735
+ return nil
736
+ end
737
+
738
+ envelope = Quonfig::ConfigEnvelope.new(configs: configs, meta: meta)
644
739
  StreamEvent.new(envelope, id, data)
645
740
  end
646
741
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quonfig
4
- VERSION = '1.8.0'
4
+ VERSION = '1.10.0'
5
5
  end
@@ -43,7 +43,9 @@ module Quonfig
43
43
  private
44
44
 
45
45
  def weight_of(variant)
46
- variant[:weight] || variant['weight']
46
+ # A missing weight counts as 0 (qfg-krjt); a present one is checked to
47
+ # be an Integer at decode (ConfigShape).
48
+ variant[:weight] || variant['weight'] || 0
47
49
  end
48
50
  end
49
51
  end
data/lib/quonfig.rb CHANGED
@@ -32,6 +32,7 @@ require 'quonfig/exponential_backoff'
32
32
  require 'quonfig/waits'
33
33
  require 'quonfig/worker_supervisor'
34
34
  require 'quonfig/periodic_sync'
35
+ require 'quonfig/errors/initialization_error'
35
36
  require 'quonfig/errors/initialization_timeout_error'
36
37
  require 'quonfig/errors/invalid_sdk_key_error'
37
38
  require 'quonfig/errors/missing_default_error'
@@ -50,6 +51,7 @@ require 'quonfig/config_store'
50
51
  require 'quonfig/evaluator'
51
52
  require 'quonfig/resolver'
52
53
  require 'quonfig/config_envelope'
54
+ require 'quonfig/config_shape'
53
55
  require 'quonfig/config_loader'
54
56
  require 'quonfig/datadir'
55
57
  require 'listen'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quonfig
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Dwyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-16 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -90,6 +90,7 @@ files:
90
90
  - lib/quonfig/client.rb
91
91
  - lib/quonfig/config_envelope.rb
92
92
  - lib/quonfig/config_loader.rb
93
+ - lib/quonfig/config_shape.rb
93
94
  - lib/quonfig/config_store.rb
94
95
  - lib/quonfig/context.rb
95
96
  - lib/quonfig/datadir.rb
@@ -100,6 +101,7 @@ files:
100
101
  - lib/quonfig/error.rb
101
102
  - lib/quonfig/errors/decryption_error.rb
102
103
  - lib/quonfig/errors/env_var_parse_error.rb
104
+ - lib/quonfig/errors/initialization_error.rb
103
105
  - lib/quonfig/errors/initialization_timeout_error.rb
104
106
  - lib/quonfig/errors/invalid_environment_error.rb
105
107
  - lib/quonfig/errors/invalid_sdk_key_error.rb