quonfig 0.0.17 → 0.0.18

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: 68d9721e3220acc150e33b43e993c8b8b2380056453939b62b06b05cc4ef4255
4
- data.tar.gz: 643c409f2b8fa3d5291d92fcf8a0d39cf5a2a67b4d697036497a19296f584d72
3
+ metadata.gz: 125753bc634b155cdae7cf4772705ee74c5ec37f4a612c9c52ea873a94d0c5ca
4
+ data.tar.gz: 2c6e09f01e7ed54cf7e6f0fbb33784ea62e0d2bd069a87e3d74dafe3ed97aeb1
5
5
  SHA512:
6
- metadata.gz: c133fdcdf47da1b026465f42dfc71e98be03590bb0df80ebd247a572bce6404a59b5f411b014e83349ca2278af2c4c62974c22c1ea9c0c7b1af474d933e91725
7
- data.tar.gz: c982a887a21dcfe7545e2b50b71a09f2fb7465827819676a238bdbd87cbeaa7626f26e723eb3535c8e3f893de4f7bcebc93e31264783b76716add5057ece836c
6
+ metadata.gz: 7e9474c7aa96611977db52658ba730efab5aafddb811e68dbd8ce90833d3c3017f6d5bc9b870db54be4b125844f43d46d96c067179a5cfd744880e4ca32cbd79
7
+ data.tar.gz: 26e638dbdeb223f06d9742cd155fd2b5b33073b20ee6e397a3322564979042c9c9dff8a3f893127c30ca70544202fc04ba2e3ce6c6c2f58332613dba884f9c33
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.0.18 - 2026-05-21
4
+
5
+ - **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.
6
+ - **Fix (datadir): coerce int/double config values to numbers at load time (qfg-38sf.8).** Config files store `int`/`double` Value fields as JSON strings (`{"type":"int","value":"123"}`). api-delivery normalizes these to real numbers at config-load time (`Value.UnmarshalJSON`), so every HTTP/SSE envelope already carries JSON numbers — but the datadir loader read the files directly and passed the strings through verbatim. `Quonfig::Datadir` now runs a generic recursive `coerce_numeric_values` walk over each parsed config document before projecting it to a `ConfigResponse`: any Value node (`type` of `"int"`/`"double"` with a String `value`) is coerced via `Integer(value, 10)` / `Float(value)`, covering `default.rules[].value`, environment rules, `criteria[].valueToMatch`, weighted-value arms, and variants. An unparseable numeric string is left untouched (passthrough — never raises). Brings the datadir loader in line with sdk-go and api-delivery so the loaded envelope always carries real numbers regardless of who consumes it.
7
+ - **CI: pin `integration-test-data` to v2026.05.20 and guard against stale generated tests (#12).** The integration suite now resolves its YAML specs from a pinned tag, and a guard fails the build if the generated tests drift from the templates.
8
+ - **CI: skip the Chaos workflow on Dependabot PRs and de-flake the datadir-reload test (8f60d9c).**
9
+ - **Dependency bumps (CI actions):** `actions/setup-go` 5.6.0 → 6.4.0 (#7), `actions/checkout` 4.3.1 → 6.0.2 (#8), `actions/upload-artifact` 4.6.2 → 7.0.1 (#9), `ruby/setup-ruby` 1.306.0 → 1.310.0 (#10).
10
+
3
11
  ## 0.0.17 - 2026-05-19
4
12
 
5
13
  - **Feat (datadir): opt-in `data_dir_auto_reload` (qfg-mol-2da).** Datadir mode previously loaded the workspace once at construction and served purely from memory. Set `data_dir_auto_reload: true` to have the SDK watch the configured `datadir`, re-read `Quonfig::Datadir.load_envelope`, and fire the existing `on_update` callback whenever files change. Adds `listen ~> 3.8` (FSEvents on macOS, inotify on Linux, polling fallback on Windows) as a runtime dep. Behavior: parse-then-swap (a failed parse keeps the previous envelope and skips the callback), debounced (`data_dir_auto_reload_debounce_ms`, default 200 ms — bursts coalesce to one reload), and gracefully downgrades when watch registration fails (read-only fs, immutable container, missing native backend). Symlinked datadirs are resolved to their real path before watching. Default is `false`; opt-in only.
@@ -42,6 +42,7 @@ module Quonfig
42
42
  raw = JSON.parse(File.read(path))
43
43
  raise ArgumentError, "[quonfig] config has empty key — file is not a Quonfig Config: #{path}" if raw['key'].nil? || raw['key'].to_s.empty?
44
44
 
45
+ coerce_numeric_values(raw)
45
46
  configs << to_config_response(raw, env_id)
46
47
  end
47
48
  end
@@ -94,5 +95,42 @@ module Quonfig
94
95
 
95
96
  raw || false
96
97
  end
98
+
99
+ # Config files store int/double Value fields as JSON strings
100
+ # (`{"type":"int","value":"123"}`). api-delivery normalizes these to real
101
+ # numbers at config-load time (`Value.UnmarshalJSON`), so every envelope it
102
+ # emits over HTTP/SSE already carries JSON numbers. In datadir mode we read
103
+ # the files directly, so we must coerce here to match.
104
+ #
105
+ # Walks the parsed config document in place, coercing every Value node — any
106
+ # Hash with a `type` of `"int"`/`"double"` and a String `value` — to a real
107
+ # number. A generic recursive walk covers `default.rules[].value`,
108
+ # environment rules, `criteria[].valueToMatch`, weighted-value arms, and
109
+ # variants without enumerating each location. On parse failure the original
110
+ # string is left in place (passthrough — never raise).
111
+ def coerce_numeric_values(node)
112
+ case node
113
+ when Hash
114
+ coerce_numeric_value_field(node)
115
+ node.each_value { |child| coerce_numeric_values(child) }
116
+ when Array
117
+ node.each { |child| coerce_numeric_values(child) }
118
+ end
119
+ node
120
+ end
121
+
122
+ def coerce_numeric_value_field(hash)
123
+ value = hash['value']
124
+ return unless value.is_a?(String)
125
+
126
+ case hash['type']
127
+ when 'int'
128
+ hash['value'] = Integer(value, 10)
129
+ when 'double'
130
+ hash['value'] = Float(value)
131
+ end
132
+ rescue ArgumentError, TypeError
133
+ # Unparseable numeric string — leave the original value untouched.
134
+ end
97
135
  end
98
136
  end
@@ -56,6 +56,17 @@ module Quonfig
56
56
  # Anything else (5xx, 429, network errors) stays on the transient path.
57
57
  TERMINAL_HTTP_CODES = [401, 403, 404].freeze
58
58
 
59
+ # qfg-6y44: headroom added to +sse_read_timeout+ when configuring
60
+ # Net::HTTP#read_timeout. The ReadDeadlineWatchdog already covers both the
61
+ # header and body reads at exactly +sse_read_timeout+; Net's own
62
+ # read_timeout is only a redundant backstop. Arming it at the *same* value
63
+ # as the watchdog makes the two race — and on the body path Net's
64
+ # (unreliable) timeout can fire first, surfacing a Net::ReadTimeout instead
65
+ # of the SSEReadDeadlineExceeded the SDK is instrumented around. Giving
66
+ # Net's read_timeout this much headroom keeps it a backstop without ever
67
+ # letting it win the race.
68
+ READ_TIMEOUT_HEADROOM = 30
69
+
59
70
  # +on_error+: optional callable invoked on every SSE error edge. Parent
60
71
  # Quonfig::Client wires this to drive @sse_state -> :error so that
61
72
  # +connection_state+ reflects the disconnect (qfg-47c2.27).
@@ -254,8 +265,11 @@ module Quonfig
254
265
  http.use_ssl = (uri.scheme == 'https')
255
266
  http.open_timeout = @options.sse_connect_timeout
256
267
  # Keep Net::HTTP's read_timeout as a backstop for the header read
257
- # (where it does apply reliably). The watchdog covers the body path.
258
- http.read_timeout = @options.sse_read_timeout
268
+ # (where it does apply reliably). The watchdog covers the body path
269
+ # so read_timeout gets READ_TIMEOUT_HEADROOM over the watchdog deadline
270
+ # to guarantee the watchdog fires first and we get a deterministic
271
+ # SSEReadDeadlineExceeded rather than a racy Net::ReadTimeout (qfg-6y44).
272
+ http.read_timeout = @options.sse_read_timeout + READ_TIMEOUT_HEADROOM
259
273
 
260
274
  req = Net::HTTP::Get.new(uri.request_uri, headers)
261
275
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Quonfig
4
- VERSION = '0.0.17'
4
+ VERSION = '0.0.18'
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.17
4
+ version: 0.0.18
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-19 00:00:00.000000000 Z
11
+ date: 2026-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport