quonfig 0.0.18 → 0.0.20

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: a08513df922f41ef1dc16b37bb31b226e71aadaa38f324bdc223a90c4371b1a6
4
+ data.tar.gz: ae7f4292d6dba19047434eb39047f004269a50a59e39926ea02363a51b0a99f5
5
5
  SHA512:
6
- metadata.gz: 7e9474c7aa96611977db52658ba730efab5aafddb811e68dbd8ce90833d3c3017f6d5bc9b870db54be4b125844f43d46d96c067179a5cfd744880e4ca32cbd79
7
- data.tar.gz: 26e638dbdeb223f06d9742cd155fd2b5b33073b20ee6e397a3322564979042c9c9dff8a3f893127c30ca70544202fc04ba2e3ce6c6c2f58332613dba884f9c33
6
+ metadata.gz: bf23519f237e32d9c56453dcb4614a9540ce51094bb7e873c1787abf6ecf672fd0f78e9228fbc477d7f4732cff6d026f437ff4c91d08a47ebca6773b96d004ab
7
+ data.tar.gz: cd804bde09b2f3795a6bc42f2d73483153ec8a85b7e833bdd973080edd8b02d31ba5977d1fc954441e41ac7e860b28925ae24a5a40c509c355bcd999be56b71e
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.20 - 2026-05-29
4
+
5
+ - **Fix (delivery): derive evaluator env from `meta.environment` in HTTP+SSE delivery mode (qfg-xpln.2).** Per-environment overrides were not being applied when the SDK ran in delivery mode (HTTP fetch + SSE stream). The evaluator now derives its `env_id` from the authoritative `meta.environment` carried on every delivery envelope, so environment-scoped rules and overrides resolve correctly against the environment api-delivery actually served — matching datadir-mode behavior.
6
+ - **Fix (options): an explicit environment pin is datadir-only; ignored in delivery mode with a WARN (qfg-pinh).** In delivery mode the served envelope's `meta.environment` is authoritative — the environment is decided server-side by api-delivery, not by the client. A client-supplied environment pin (`with_environment` / `QUONFIG_ENVIRONMENT`) is therefore only meaningful in datadir mode. In delivery mode the pin is now ignored and a one-time WARN is logged so the conflict is visible rather than silently misleading.
7
+
8
+ ## 0.0.19 - 2026-05-28
9
+
10
+ - **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.
11
+ - **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.
12
+ - **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.
13
+
3
14
  ## 0.0.18 - 2026-05-21
4
15
 
5
16
  - **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?
@@ -700,6 +714,14 @@ module Quonfig
700
714
  old_keys = @store.keys.to_set
701
715
  (old_keys - new_keys).each { |k| @store.delete(k) }
702
716
  envelope.configs.each { |cfg| @store.set(cfg['key'], cfg) }
717
+ # qfg-pinh: evaluate against the installed envelope's meta.environment,
718
+ # matching sdk-go. In datadir mode the loader stamps meta.environment =
719
+ # the resolved env (the `environment:` pin or QUONFIG_ENVIRONMENT), so
720
+ # this also covers the env-var-only case where @options.environment is
721
+ # nil at evaluator construction.
722
+ meta = envelope.respond_to?(:meta) ? envelope.meta : nil
723
+ env_id = meta && (meta['environment'] || meta[:environment])
724
+ @evaluator.env_id = env_id if env_id && !env_id.to_s.empty?
703
725
  record_refresh!
704
726
  end
705
727
 
@@ -743,10 +765,12 @@ module Quonfig
743
765
  end
744
766
 
745
767
  # Initialize network mode: sync HTTP fetch (bounded by
746
- # initialization_timeout_sec) then start SSE + polling as requested.
768
+ # init_timeout_ms) then start SSE + polling as requested.
747
769
  def initialize_network_mode
748
770
  raise Quonfig::Errors::InvalidSdkKeyError, @options.sdk_key if @options.sdk_key.nil? || @options.sdk_key.to_s.strip.empty?
749
771
 
772
+ warn_if_pin_ignored_in_delivery_mode
773
+
750
774
  @config_loader = Quonfig::ConfigLoader.new(@store, @options)
751
775
 
752
776
  perform_initial_fetch
@@ -760,7 +784,7 @@ module Quonfig
760
784
  end
761
785
 
762
786
  def perform_initial_fetch
763
- timeout = @options.initialization_timeout_sec || 10
787
+ timeout = (@options.init_timeout_ms || 10_000) / 1000.0
764
788
  result = :failed
765
789
 
766
790
  begin
@@ -777,10 +801,52 @@ module Quonfig
777
801
  if result == :failed
778
802
  handle_init_failure(RuntimeError.new('Config fetch failed against all api_urls'))
779
803
  else
804
+ sync_evaluator_env_id!
780
805
  record_refresh!
781
806
  end
782
807
  end
783
808
 
809
+ # qfg-pinh: In SDK-key DELIVERY mode the server's `meta.environment` is
810
+ # AUTHORITATIVE. The server scopes each config to a single environment and
811
+ # reports the active env id in `meta.environment` (the loader captures it
812
+ # as @config_loader.environment_id). The evaluator must always evaluate
813
+ # against that installed env id — matching sdk-go, where eval never
814
+ # branches on the pin (c.envID = envelope.Meta.Environment, quonfig.go:850).
815
+ #
816
+ # An explicit environment pin (`environment:` option / QUONFIG_ENVIRONMENT)
817
+ # is DATADIR-ONLY: in delivery mode it is IGNORED (it only feeds the datadir
818
+ # loader, which stamps meta.environment = pin). So we always adopt the
819
+ # server's env id here regardless of the pin. A WARN is emitted once at init
820
+ # (see #warn_if_pin_ignored_in_delivery_mode) when a pin is set in delivery
821
+ # mode so customers aren't surprised that it has no effect.
822
+ #
823
+ # qfg-xpln.2 originally only adopted the server env when NO pin was set,
824
+ # which let the pin win in delivery mode — qfg-pinh reverses that.
825
+ def sync_evaluator_env_id!
826
+ return unless @config_loader.respond_to?(:environment_id)
827
+
828
+ server_env = @config_loader.environment_id
829
+ @evaluator.env_id = server_env if server_env && !server_env.to_s.empty?
830
+ end
831
+
832
+ # qfg-pinh: an explicit environment pin (`environment:` option or
833
+ # QUONFIG_ENVIRONMENT) is DATADIR-ONLY. In delivery (SDK-key) mode the
834
+ # active environment is determined by the SDK key and reported via
835
+ # `meta.environment`, so the pin is ignored. Warn once at init so the
836
+ # customer isn't surprised the setting has no effect. Fired only on the
837
+ # delivery-mode init path (datadir mode honors the pin and never calls
838
+ # this).
839
+ def warn_if_pin_ignored_in_delivery_mode
840
+ env = @options.environment
841
+ return if env.nil? || env.to_s.empty?
842
+
843
+ LOG.warn(
844
+ "[quonfig] environment '#{env}' was set but the client is in delivery " \
845
+ '(SDK-key) mode; the active environment is determined by the SDK key, ' \
846
+ 'so this setting is ignored (it applies only when loading from a local data dir)'
847
+ )
848
+ end
849
+
784
850
  def handle_init_failure(err)
785
851
  if @options.on_init_failure == Quonfig::Options::ON_INITIALIZATION_FAILURE::RETURN
786
852
  LOG.warn "[quonfig] Initialization did not complete cleanly; continuing with empty store: #{err.message}"
@@ -807,6 +873,7 @@ module Quonfig
807
873
 
808
874
  begin
809
875
  @config_loader.apply_envelope(envelope)
876
+ sync_evaluator_env_id!
810
877
  handle_sse_state_change(:connected)
811
878
  record_refresh!
812
879
  rescue StandardError => e
@@ -827,18 +894,24 @@ module Quonfig
827
894
  return if @stopped
828
895
  return if @poll_supervisor&.alive?
829
896
 
830
- poll_interval = @options.respond_to?(:poll_interval) && @options.poll_interval ? @options.poll_interval : 60
831
- return if poll_interval <= 0
897
+ poll_ms = if @options.respond_to?(:fallback_poll_interval_ms) && @options.fallback_poll_interval_ms
898
+ @options.fallback_poll_interval_ms
899
+ else
900
+ 60_000
901
+ end
902
+ return if poll_ms <= 0
832
903
 
904
+ poll_seconds = poll_ms / 1000.0
833
905
  stopped_ref = -> { @stopped }
834
906
  worker = lambda do |notify_delivered|
835
907
  loop do
836
908
  break if stopped_ref.call
837
909
 
838
- sleep poll_interval
910
+ sleep poll_seconds
839
911
  break if stopped_ref.call
840
912
 
841
913
  @config_loader.fetch!
914
+ sync_evaluator_env_id!
842
915
  record_refresh!
843
916
  notify_delivered.call
844
917
  notify_on_update_callback
@@ -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.20'
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.20
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-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport