feat-sdk 0.1.0 → 0.1.1

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: 8f64fac3df2e3ba9d2dc9db569472dd8329bea2d4fb4f2117d44e65a45d9e652
4
- data.tar.gz: 4d60e7877bc7df301aa90a96bd19b11c211d6941387ffe834793c84db04ad353
3
+ metadata.gz: 50c482a000c1e2ef525db051f580656d302cfbc0cb983c0241507615cd3d6b87
4
+ data.tar.gz: 1e52811533d9ba5d42805e2a9b665b626b984a1d30df61dfaa97fdacc90fe2f1
5
5
  SHA512:
6
- metadata.gz: 2a990fc8a48b6176329fc664001c314b37fd5523122d91481700f804df7ec0d75f9705167eced27696627de0bfa25b895e0950b7aa69dd2d73785d917b7175b3
7
- data.tar.gz: 8bbbed51ad214a26652743a15f036ea1936f431d74606c634f63282bfaaaee67310606e9110748a46385b9fa5786e441a6cd3690666a146c924ba4324420a1f2
6
+ metadata.gz: 40b5b6c2ab9fa985ac32c17bf39b4606c8e38a271e0e526383d2c7e1824348107f526d67350cc06dd60e5c4fd186a5df0ff41bad20f12353d1ddbf5cb3de583b
7
+ data.tar.gz: c41a41b359e9cd68664189fa9271b99346f67fbd4fa6286d56ec116d9cfeabf3788b16d68ad9c20bb5eb4e65f14a0760c05bacb06fc2fcd607435bb751fae213
data/README.md CHANGED
@@ -1,4 +1,12 @@
1
- # feat-sdk
1
+ <p align="center">
2
+ <a href="https://feat.so">
3
+ <img src="https://feat.so/logo/wordmark.png" alt="feat.so" width="320" />
4
+ </a>
5
+ </p>
6
+
7
+ ---
8
+
9
+ # feat Ruby SDK
2
10
 
3
11
  Server-side Ruby SDK for [feat](https://feat.so) feature flags. Local flag evaluation against a polled datafile. Standard library only - no gem dependencies.
4
12
 
data/lib/feat/client.rb CHANGED
@@ -1,6 +1,8 @@
1
1
  require "json"
2
2
  require "net/http"
3
+ require "socket"
3
4
  require "uri"
5
+ require_relative "version"
4
6
 
5
7
  module Feat
6
8
  # Polling HTTP client. Uses stdlib only - zero gem dependencies.
@@ -8,6 +10,15 @@ module Feat
8
10
  DEFAULT_POLL_INTERVAL = 30.0
9
11
  MIN_POLL_INTERVAL = 5.0
10
12
  MAX_DATAFILE_BYTES = 10 * 1024 * 1024
13
+ OPEN_TIMEOUT_SECONDS = 3
14
+ READ_TIMEOUT_SECONDS = 10
15
+ RETRYABLE_CONNECT_ERRORS = [
16
+ Net::OpenTimeout,
17
+ Errno::ETIMEDOUT,
18
+ Errno::ECONNREFUSED,
19
+ Errno::EHOSTUNREACH,
20
+ Errno::ENETUNREACH,
21
+ ].freeze
11
22
 
12
23
  def initialize(api_key:, data_plane_url:, poll_interval: DEFAULT_POLL_INTERVAL, http_client: nil)
13
24
  raise ArgumentError, "api_key is required" if api_key.nil? || api_key.empty?
@@ -24,6 +35,7 @@ module Feat
24
35
  @mutex = Mutex.new
25
36
  @stop = false
26
37
  @thread = nil
38
+ @sticky_ip = nil
27
39
  end
28
40
 
29
41
  # Blocking initial fetch; spawns a background poller thread.
@@ -99,11 +111,10 @@ module Feat
99
111
  uri = URI.parse("#{@data_plane_url}/sdk/v1/datafile")
100
112
  req = Net::HTTP::Get.new(uri)
101
113
  req["Authorization"] = "Bearer #{@api_key}"
114
+ req["User-Agent"] = "feat-sdk-ruby/#{Feat::VERSION}"
102
115
  @mutex.synchronize { req["If-None-Match"] = @etag if @etag }
103
116
 
104
- res = (@http_client || Net::HTTP).start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
105
- http.request(req)
106
- end
117
+ res = with_http_connection(uri) { |http| http.request(req) }
107
118
 
108
119
  case res.code.to_i
109
120
  when 304, 404
@@ -124,5 +135,66 @@ module Feat
124
135
  raise "feat: fetch datafile failed: #{res.code}"
125
136
  end
126
137
  end
138
+
139
+ # Net::HTTP doesn't iterate getaddrinfo results on connect failure
140
+ # (Ruby 3.3 has no Happy Eyeballs); ipaddr= lets us pin each attempt
141
+ # to a specific IP while keeping the hostname for SNI.
142
+ def with_http_connection(uri, &block)
143
+ if @http_client
144
+ return @http_client.start(
145
+ uri.host, uri.port,
146
+ use_ssl: uri.scheme == "https",
147
+ open_timeout: OPEN_TIMEOUT_SECONDS,
148
+ read_timeout: READ_TIMEOUT_SECONDS,
149
+ ) { |h| block.call(h) }
150
+ end
151
+
152
+ sticky = @mutex.synchronize { @sticky_ip }
153
+ if sticky
154
+ begin
155
+ return attempt_request(uri, sticky, &block)
156
+ rescue *RETRYABLE_CONNECT_ERRORS
157
+ @mutex.synchronize { @sticky_ip = nil if @sticky_ip == sticky }
158
+ end
159
+ end
160
+
161
+ addresses = resolve_addresses(uri.host)
162
+ if addresses.empty?
163
+ return Net::HTTP.start(
164
+ uri.host, uri.port,
165
+ use_ssl: uri.scheme == "https",
166
+ open_timeout: OPEN_TIMEOUT_SECONDS,
167
+ read_timeout: READ_TIMEOUT_SECONDS,
168
+ ) { |h| block.call(h) }
169
+ end
170
+
171
+ last_error = nil
172
+ addresses.each do |ip|
173
+ next if ip == sticky
174
+ begin
175
+ result = attempt_request(uri, ip, &block)
176
+ @mutex.synchronize { @sticky_ip = ip }
177
+ return result
178
+ rescue *RETRYABLE_CONNECT_ERRORS => e
179
+ last_error = e
180
+ end
181
+ end
182
+ raise last_error
183
+ end
184
+
185
+ def attempt_request(uri, ip, &block)
186
+ http = Net::HTTP.new(uri.host, uri.port)
187
+ http.ipaddr = ip
188
+ http.use_ssl = (uri.scheme == "https")
189
+ http.open_timeout = OPEN_TIMEOUT_SECONDS
190
+ http.read_timeout = READ_TIMEOUT_SECONDS
191
+ http.start { |h| block.call(h) }
192
+ end
193
+
194
+ def resolve_addresses(host)
195
+ Addrinfo.getaddrinfo(host, nil, nil, :STREAM).map(&:ip_address).uniq
196
+ rescue StandardError
197
+ []
198
+ end
127
199
  end
128
200
  end
data/lib/feat/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Feat
2
- VERSION = "0.1.0".freeze
2
+ VERSION = "0.1.1".freeze
3
3
  end
metadata CHANGED
@@ -1,18 +1,19 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: feat-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - feat HQ
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-06-04 00:00:00.000000000 Z
11
12
  dependencies: []
12
13
  description: Server-side Ruby SDK for feat. Polls a per-environment datafile and evaluates
13
14
  flags locally with no per-flag network call. Stdlib only.
14
15
  email:
15
- - engineering@feat.so
16
+ - support@feat.so
16
17
  executables: []
17
18
  extensions: []
18
19
  extra_rdoc_files: []
@@ -36,6 +37,7 @@ metadata:
36
37
  source_code_uri: https://github.com/feathq/ruby-sdk
37
38
  bug_tracker_uri: https://github.com/feathq/ruby-sdk/issues
38
39
  changelog_uri: https://github.com/feathq/ruby-sdk/releases
40
+ post_install_message:
39
41
  rdoc_options: []
40
42
  require_paths:
41
43
  - lib
@@ -50,7 +52,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
52
  - !ruby/object:Gem::Version
51
53
  version: '0'
52
54
  requirements: []
53
- rubygems_version: 3.6.9
55
+ rubygems_version: 3.5.22
56
+ signing_key:
54
57
  specification_version: 4
55
58
  summary: feat feature-flag SDK for Ruby (server-side, local evaluation)
56
59
  test_files: []