quonfig 0.0.18 → 0.0.19

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 125753bc634b155cdae7cf4772705ee74c5ec37f4a612c9c52ea873a94d0c5ca
4
- data.tar.gz: 2c6e09f01e7ed54cf7e6f0fbb33784ea62e0d2bd069a87e3d74dafe3ed97aeb1
3
+ metadata.gz: f71f10c80365079815e7b7d7f2a9a8926ecfc358697505bb3c6bd3f2e6142976
4
+ data.tar.gz: 3d0988da2eb9a17457dc53c358e32a55aaa17f59e1c7c280db5db52b3eb991fc
5
5
  SHA512:
6
- metadata.gz: 7e9474c7aa96611977db52658ba730efab5aafddb811e68dbd8ce90833d3c3017f6d5bc9b870db54be4b125844f43d46d96c067179a5cfd744880e4ca32cbd79
7
- data.tar.gz: 26e638dbdeb223f06d9742cd155fd2b5b33073b20ee6e397a3322564979042c9c9dff8a3f893127c30ca70544202fc04ba2e3ce6c6c2f58332613dba884f9c33
6
+ metadata.gz: e019ac9393b2644208aaa79fbbed9bfe0a27e346413ffac2cd126d47fd9ffa114a1af4bfcd597b527577935a586ea6636aa6e8c96be8ee657889bc324d9ea38c
7
+ data.tar.gz: 89026759a78f4219766b14366630a5df3a3126884c5bfa721764cffdf6799c8f47df5be1245c86c8966aedbbb9a4508b65d53a400cd3c6e4308f872bc975f6ce
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.19 - 2026-05-28
4
+
5
+ - **Deprecation (context): `in_context` is now a deprecated alias of `with_context` (qfg-e0kk).** The two methods have always been runtime-identical — both accept a properties hash and either yield a `Quonfig::BoundClient` to a given block or return the BoundClient directly. As part of the sdk-1.0 unification, every SDK in the family is converging on the `with_context` family; sdk-ruby keeps `in_context` as a YARD-`@deprecated` alias for the 1.0.0 cycle so existing customer code (especially Prefab-fork lineage call sites) keeps working without a runtime warning. Implementation collapses to a one-line forward (`in_context` now calls `with_context`), and the README example is updated to use `with_context`. Slated for removal in 2.0.0.
6
+ - **Feat (options): rename `initialization_timeout_sec` → `init_timeout_ms` (qfg-39za).** Of the six SDKs, only sdk-ruby (and sdk-python, also being renamed) expressed this in seconds; sdk-node uses `initTimeout` (ms), sdk-javascript uses `timeout` (ms), sdk-java uses `Duration`. Picking `init_timeout_ms` as the canonical snake_case name lets sdk-ruby and sdk-python converge. The default is unchanged in wall-clock terms: `init_timeout_ms: 10_000` (previously expressed as `initialization_timeout_sec: 10`). The legacy `initialization_timeout_sec` kwarg and accessor are kept as deprecated aliases for one minor cycle; passing `initialization_timeout_sec:` (seconds) is forwarded transparently as `* 1000` into the ms-based storage, and reading `Options#initialization_timeout_sec` returns the configured timeout in seconds. Will be removed in a future minor release.
7
+ - **Feat (options): rename `enable_polling` → `fallback_poll_enabled`, `poll_interval` → `fallback_poll_interval_ms` (qfg-thsn).** The old names predated the SSE+fallback architecture and read as "always poll", but the actual behavior since 0.0.4 has been "poll only when SSE is unavailable for >= 2x the interval". The new names match sdk-node / sdk-python / sdk-java. Defaults are unchanged: `fallback_poll_enabled: true`, `fallback_poll_interval_ms: 60_000` (previously expressed as 60 seconds). The legacy `enable_polling` and `poll_interval` kwargs and accessors are kept as deprecated aliases for one minor cycle; passing `poll_interval:` (seconds) is forwarded transparently as `poll_interval * 1000` into the new ms-based storage, and reading `Options#poll_interval` returns the configured interval in seconds. Will be removed in a future minor release.
8
+
3
9
  ## 0.0.18 - 2026-05-21
4
10
 
5
11
  - **Fix (SSE): give Net `read_timeout` headroom over the watchdog deadline (qfg-6y44).** `stream_once` armed two read deadlines at the identical `sse_read_timeout` value: `Net::HTTP#read_timeout` and the `ReadDeadlineWatchdog`. On the body read both were live, and the watchdog carries up to `POLL_INTERVAL` (0.25 s) of polling latency on top of its deadline — so when Net's (unreliable on the `read_body` path) stdlib timeout did fire, it could beat the watchdog and surface a `Net::ReadTimeout` instead of the `SSEReadDeadlineExceeded` the SDK is instrumented around. A new `READ_TIMEOUT_HEADROOM` (30 s) keeps Net's `read_timeout` as a redundant backstop while guaranteeing the watchdog fires first.
data/README.md CHANGED
@@ -50,13 +50,13 @@ attach a context in three ways:
50
50
  client.get_bool('beta-feature', user: { key: 'user-123', plan: 'pro' })
51
51
  ```
52
52
 
53
- ### 2. `in_context` block
53
+ ### 2. `with_context` block
54
54
 
55
55
  Everything evaluated inside the block sees the supplied context. The block's
56
- return value is returned from `in_context`.
56
+ return value is returned from `with_context`.
57
57
 
58
58
  ```ruby
59
- result = client.in_context(user: { key: 'user-123', plan: 'pro' }) do |bound|
59
+ result = client.with_context(user: { key: 'user-123', plan: 'pro' }) do |bound|
60
60
  {
61
61
  hero: bound.get_string('homepage-hero'),
62
62
  limit: bound.get_int('rate-limit'),
@@ -67,8 +67,9 @@ end
67
67
 
68
68
  ### 3. `with_context` — BoundClient for repeated lookups
69
69
 
70
- `with_context` returns an immutable `BoundClient` that carries the context on
71
- every call. Useful when you want to pass a context-bound handle down the stack.
70
+ Called without a block, `with_context` returns an immutable `BoundClient` that
71
+ carries the context on every call. Useful when you want to pass a
72
+ context-bound handle down the stack.
72
73
 
73
74
  ```ruby
74
75
  bound = client.with_context(user: { key: 'user-123', plan: 'pro' })
@@ -78,6 +79,9 @@ bound.enabled?('beta-feature')
78
79
  bound.get_int('rate-limit')
79
80
  ```
80
81
 
82
+ > `in_context` is a deprecated alias of `with_context` kept for backward
83
+ > compatibility through 1.0.0. New code should use `with_context`.
84
+
81
85
  ## Datadir / offline mode
82
86
 
83
87
  For tests, CI, or air-gapped environments, point the client at a local workspace
@@ -221,17 +225,17 @@ for the cross-SDK story (sdk-node, sdk-go, sdk-ruby, sdk-python, sdk-java).
221
225
 
222
226
  ```ruby
223
227
  Quonfig::Client.new(
224
- sdk_key: '...', # required unless QUONFIG_BACKEND_SDK_KEY is set
225
- api_urls: ['https://primary.quonfig.com', 'https://secondary.quonfig.com'],
226
- telemetry_url: 'https://telemetry.quonfig.com',
227
- enable_sse: true,
228
- enable_polling: false,
229
- poll_interval: 60,
230
- init_timeout: 10,
231
- on_no_default: :error,
232
- global_context: {},
233
- datadir: '/path/to/workspace',
234
- environment: 'production',
228
+ sdk_key: '...', # required unless QUONFIG_BACKEND_SDK_KEY is set
229
+ api_urls: ['https://primary.quonfig.com', 'https://secondary.quonfig.com'],
230
+ telemetry_url: 'https://telemetry.quonfig.com',
231
+ enable_sse: true,
232
+ fallback_poll_enabled: true,
233
+ fallback_poll_interval_ms: 60_000,
234
+ init_timeout_ms: 10_000,
235
+ on_no_default: :error,
236
+ global_context: {},
237
+ datadir: '/path/to/workspace',
238
+ environment: 'production',
235
239
  data_dir_auto_reload: false,
236
240
  data_dir_auto_reload_debounce_ms: 200
237
241
  )
@@ -242,10 +246,10 @@ Quonfig::Client.new(
242
246
  | `sdk_key` | `String` | `ENV['QUONFIG_BACKEND_SDK_KEY']` | SDK key for API authentication. |
243
247
  | `api_urls` | `Array<String>` | `["https://primary.${QUONFIG_DOMAIN}", "https://secondary.${QUONFIG_DOMAIN}"]` | Ordered list of API base URLs to try. SSE stream URLs are derived by prepending `stream.` to each hostname. Defaults derive from `QUONFIG_DOMAIN` (default `quonfig.com`). |
244
248
  | `telemetry_url` | `String` | `https://telemetry.${QUONFIG_DOMAIN}` | Base URL for the telemetry service. Default derives from `QUONFIG_DOMAIN`. |
245
- | `enable_sse` | `Boolean` | `true` | Receive real-time updates over Server-Sent Events. |
246
- | `enable_polling` | `Boolean` | `false` | Poll the API on an interval as a fallback. |
247
- | `poll_interval` | `Integer` (seconds) | `60` | Polling interval when `enable_polling` is `true`. |
248
- | `init_timeout` | `Integer` (seconds) | `10` | Maximum time to wait for the initial config load. |
249
+ | `enable_sse` | `Boolean` | `true` | Receive real-time updates over Server-Sent Events. |
250
+ | `fallback_poll_enabled` | `Boolean` | `true` | Engage HTTP polling as a fallback when SSE is unavailable for >= 2x `fallback_poll_interval_ms`. Deprecated alias: `enable_polling`. |
251
+ | `fallback_poll_interval_ms` | `Integer` (ms) | `60_000` | Interval between fallback HTTP polls, in milliseconds. Deprecated alias: `poll_interval` (seconds, multiplied by 1000 internally). |
252
+ | `init_timeout_ms` | `Integer` (ms) | `10_000` | Maximum time to wait for the initial config load, in milliseconds. Deprecated alias: `initialization_timeout_sec` (seconds, multiplied by 1000 internally). |
249
253
  | `on_no_default` | `Symbol` | `:error` | Behavior when a key has no value and no default: `:error`, `:warn`, or `:ignore`. |
250
254
  | `global_context` | `Hash` | `{}` | Context applied to every evaluation. |
251
255
  | `datadir` | `String` | `ENV['QUONFIG_DIR']` | Path to a local workspace. When set, the SDK runs offline from disk. |
@@ -185,17 +185,27 @@ module Quonfig
185
185
 
186
186
  # ---- Context binding ----------------------------------------------
187
187
 
188
- def in_context(properties)
189
- bound = Quonfig::BoundClient.new(self, properties)
190
- block_given? ? yield(bound) : bound
188
+ # Bind +properties+ as a context. With a block, yields a
189
+ # {Quonfig::BoundClient} and returns the block's value. Without a block,
190
+ # returns the BoundClient directly.
191
+ #
192
+ # qfg-e0kk: kept as a deprecated alias of {#with_context}. The two methods
193
+ # have always been runtime-identical; sdk-1.0 unifies on +with_context+
194
+ # across all SDKs. No runtime warning is emitted (Prefab-fork lineage,
195
+ # heavy customer usage). Slated for removal in 2.0.0.
196
+ #
197
+ # @deprecated Use {#with_context} instead.
198
+ def in_context(properties, &block)
199
+ with_context(properties, &block)
191
200
  end
192
201
 
193
- def with_context(properties, &block)
194
- if block_given?
195
- in_context(properties, &block)
196
- else
197
- Quonfig::BoundClient.new(self, properties)
198
- end
202
+ # Bind +properties+ as a context. With a block, yields a
203
+ # {Quonfig::BoundClient} and returns the block's value. Without a block,
204
+ # returns the BoundClient directly — useful for passing a context-bound
205
+ # handle down the stack.
206
+ def with_context(properties)
207
+ bound = Quonfig::BoundClient.new(self, properties)
208
+ block_given? ? yield(bound) : bound
199
209
  end
200
210
 
201
211
  # ---- Filters & helpers --------------------------------------------
@@ -334,7 +344,7 @@ module Quonfig
334
344
  end
335
345
 
336
346
  sse_started = @options.enable_sse && start_sse
337
- start_polling if @options.enable_polling && !sse_started
347
+ start_polling if @options.fallback_poll_enabled && !sse_started
338
348
 
339
349
  restart_telemetry_in_child
340
350
  end
@@ -515,7 +525,7 @@ module Quonfig
515
525
  [@sse_ever_connected, @sse_terminal_failure]
516
526
  end
517
527
 
518
- return unless @options.respond_to?(:enable_polling) && @options.enable_polling
528
+ return unless @options.respond_to?(:fallback_poll_enabled) && @options.fallback_poll_enabled
519
529
  return if @stopped
520
530
  # qfg-i5xv: a terminal SSE classification suppresses polling engage in
521
531
  # every branch — the customer's key is bad and HTTP polling will fail
@@ -575,15 +585,19 @@ module Quonfig
575
585
  end
576
586
  end
577
587
 
578
- # Schedule a 2*poll_interval grace timer after a connected->error edge.
579
- # If SSE recovers before the timer fires, +cancel_fallback_engage_timer+
580
- # tears it down. Idempotent — does nothing if a timer is already pending
581
- # or the supervisor is already alive.
588
+ # Schedule a 2*fallback_poll_interval grace timer after a connected->error
589
+ # edge. If SSE recovers before the timer fires,
590
+ # +cancel_fallback_engage_timer+ tears it down. Idempotent — does nothing
591
+ # if a timer is already pending or the supervisor is already alive.
582
592
  def schedule_fallback_engage
583
- poll_interval = @options.respond_to?(:poll_interval) && @options.poll_interval ? @options.poll_interval : 60
584
- return if poll_interval <= 0
593
+ poll_ms = if @options.respond_to?(:fallback_poll_interval_ms) && @options.fallback_poll_interval_ms
594
+ @options.fallback_poll_interval_ms
595
+ else
596
+ 60_000
597
+ end
598
+ return if poll_ms <= 0
585
599
 
586
- grace_seconds = poll_interval * 2.0
600
+ grace_seconds = (poll_ms / 1000.0) * 2.0
587
601
 
588
602
  @state_mutex.synchronize do
589
603
  return if @fallback_engage_timer&.alive?
@@ -743,7 +757,7 @@ module Quonfig
743
757
  end
744
758
 
745
759
  # Initialize network mode: sync HTTP fetch (bounded by
746
- # initialization_timeout_sec) then start SSE + polling as requested.
760
+ # init_timeout_ms) then start SSE + polling as requested.
747
761
  def initialize_network_mode
748
762
  raise Quonfig::Errors::InvalidSdkKeyError, @options.sdk_key if @options.sdk_key.nil? || @options.sdk_key.to_s.strip.empty?
749
763
 
@@ -760,7 +774,7 @@ module Quonfig
760
774
  end
761
775
 
762
776
  def perform_initial_fetch
763
- timeout = @options.initialization_timeout_sec || 10
777
+ timeout = (@options.init_timeout_ms || 10_000) / 1000.0
764
778
  result = :failed
765
779
 
766
780
  begin
@@ -827,15 +841,20 @@ module Quonfig
827
841
  return if @stopped
828
842
  return if @poll_supervisor&.alive?
829
843
 
830
- poll_interval = @options.respond_to?(:poll_interval) && @options.poll_interval ? @options.poll_interval : 60
831
- return if poll_interval <= 0
844
+ poll_ms = if @options.respond_to?(:fallback_poll_interval_ms) && @options.fallback_poll_interval_ms
845
+ @options.fallback_poll_interval_ms
846
+ else
847
+ 60_000
848
+ end
849
+ return if poll_ms <= 0
832
850
 
851
+ poll_seconds = poll_ms / 1000.0
833
852
  stopped_ref = -> { @stopped }
834
853
  worker = lambda do |notify_delivered|
835
854
  loop do
836
855
  break if stopped_ref.call
837
856
 
838
- sleep poll_interval
857
+ sleep poll_seconds
839
858
  break if stopped_ref.call
840
859
 
841
860
  @config_loader.fetch!
@@ -12,7 +12,7 @@ module Quonfig
12
12
  # -> 304 Not Modified (ETag honored via If-None-Match)
13
13
  #
14
14
  # The fetch is synchronous; Client is responsible for timing out the initial
15
- # fetch per `initialization_timeout_sec`.
15
+ # fetch per `init_timeout_ms`.
16
16
  class ConfigLoader
17
17
  LOG = Quonfig::InternalLogger.new(self)
18
18
 
@@ -6,10 +6,41 @@ module Quonfig
6
6
  # Options passed to Quonfig::Client at construction time.
7
7
  class Options
8
8
  attr_reader :sdk_key, :environment, :api_urls, :sse_api_urls, :telemetry_destination, :config_api_urls,
9
- :on_no_default, :initialization_timeout_sec, :on_init_failure, :collect_sync_interval, :datadir, :enable_sse, :enable_polling, :poll_interval, :global_context, :logger_key, :logger, :enable_quonfig_user_context,
9
+ :on_no_default, :init_timeout_ms, :on_init_failure, :collect_sync_interval, :datadir, :enable_sse, :fallback_poll_enabled, :fallback_poll_interval_ms, :global_context, :logger_key, :logger, :enable_quonfig_user_context,
10
10
  :data_dir_auto_reload, :data_dir_auto_reload_debounce_ms
11
11
  attr_accessor :is_fork
12
12
 
13
+ # Default fallback poll interval, in milliseconds. The SDK polls api-delivery
14
+ # at this cadence only when SSE is unavailable for >= 2x this value.
15
+ DEFAULT_FALLBACK_POLL_INTERVAL_MS = 60_000
16
+
17
+ # Default initialization timeout, in milliseconds. The SDK waits up to this
18
+ # long for the initial config fetch before failing per :on_init_failure.
19
+ DEFAULT_INIT_TIMEOUT_MS = 10_000
20
+
21
+ # Deprecated alias for #fallback_poll_enabled. Will be removed in a future
22
+ # minor release.
23
+ def enable_polling
24
+ @fallback_poll_enabled
25
+ end
26
+
27
+ # Deprecated alias for #fallback_poll_interval_ms, in seconds. Reads back the
28
+ # interval in the legacy unit so existing callers (e.g. internal code that
29
+ # `sleep`s on this value) keep working. Will be removed in a future minor
30
+ # release.
31
+ def poll_interval
32
+ @fallback_poll_interval_ms / 1000.0
33
+ end
34
+
35
+ # Deprecated alias for #init_timeout_ms, in seconds. Reads back the timeout
36
+ # in the legacy unit so existing callers (e.g. internal code that passes
37
+ # this to Timeout.timeout) keep working. Will be removed in a future minor
38
+ # release.
39
+ def initialization_timeout_sec
40
+ ms = @init_timeout_ms.to_f / 1000.0
41
+ ms == ms.to_i ? ms.to_i : ms
42
+ end
43
+
13
44
  module ON_INITIALIZATION_FAILURE
14
45
  RAISE = :raise
15
46
  RETURN = :return
@@ -145,10 +176,13 @@ module Quonfig
145
176
  environment: ENV.fetch('QUONFIG_ENVIRONMENT', nil),
146
177
  datadir: ENV.fetch('QUONFIG_DIR', nil),
147
178
  enable_sse: true,
148
- enable_polling: true,
149
- poll_interval: 60,
179
+ fallback_poll_enabled: nil,
180
+ fallback_poll_interval_ms: nil,
181
+ enable_polling: nil,
182
+ poll_interval: nil,
150
183
  on_no_default: ON_NO_DEFAULT::RAISE,
151
- initialization_timeout_sec: 10,
184
+ init_timeout_ms: nil,
185
+ initialization_timeout_sec: nil,
152
186
  on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
153
187
  collect_max_paths: DEFAULT_MAX_PATHS,
154
188
  collect_sync_interval: nil,
@@ -168,10 +202,39 @@ module Quonfig
168
202
  @environment = environment
169
203
  @datadir = datadir
170
204
  @enable_sse = enable_sse
171
- @enable_polling = enable_polling
172
- @poll_interval = poll_interval
205
+ # qfg-thsn: canonical names are fallback_poll_enabled and
206
+ # fallback_poll_interval_ms (matches sdk-node / sdk-python / sdk-java).
207
+ # The legacy enable_polling / poll_interval (seconds) kwargs are kept
208
+ # as deprecated aliases for one minor cycle. The canonical kwarg wins
209
+ # if both are passed; otherwise the legacy value is forwarded (and the
210
+ # seconds-based interval is multiplied *1000 transparently).
211
+ @fallback_poll_enabled = if !fallback_poll_enabled.nil?
212
+ fallback_poll_enabled
213
+ elsif !enable_polling.nil?
214
+ enable_polling
215
+ else
216
+ true
217
+ end
218
+ @fallback_poll_interval_ms = if !fallback_poll_interval_ms.nil?
219
+ fallback_poll_interval_ms
220
+ elsif !poll_interval.nil?
221
+ poll_interval * 1000
222
+ else
223
+ DEFAULT_FALLBACK_POLL_INTERVAL_MS
224
+ end
173
225
  @on_no_default = on_no_default
174
- @initialization_timeout_sec = initialization_timeout_sec
226
+ # qfg-39za: canonical name is init_timeout_ms. The legacy
227
+ # initialization_timeout_sec (seconds) kwarg is kept as a deprecated
228
+ # alias for one minor cycle. The canonical kwarg wins if both are
229
+ # passed; otherwise the legacy value is forwarded (and the seconds-based
230
+ # timeout is multiplied *1000 transparently).
231
+ @init_timeout_ms = if !init_timeout_ms.nil?
232
+ init_timeout_ms
233
+ elsif !initialization_timeout_sec.nil?
234
+ (initialization_timeout_sec * 1000).to_i
235
+ else
236
+ DEFAULT_INIT_TIMEOUT_MS
237
+ end
175
238
  @on_init_failure = on_init_failure
176
239
 
177
240
  @collect_max_paths = collect_max_paths
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quonfig
4
- VERSION = '0.0.18'
4
+ VERSION = '0.0.19'
5
5
  end
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: 0.0.18
4
+ version: 0.0.19
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-05-21 00:00:00.000000000 Z
11
+ date: 2026-05-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport