featurehub-sdk 1.3.0 → 2.0.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/CLAUDE.md +85 -0
  3. data/.ruby-version +1 -1
  4. data/CHANGELOG.md +18 -1
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +20 -8
  7. data/README.md +306 -119
  8. data/examples/rails_example/.ruby-version +1 -1
  9. data/examples/rails_example/Dockerfile +1 -1
  10. data/examples/sinatra/.dockerignore +7 -0
  11. data/examples/sinatra/.ruby-version +1 -1
  12. data/examples/sinatra/Dockerfile +14 -25
  13. data/examples/sinatra/Gemfile +5 -4
  14. data/examples/sinatra/Gemfile.lock +40 -32
  15. data/examples/sinatra/app/application.rb +21 -9
  16. data/examples/sinatra/docker-compose.yaml +24 -0
  17. data/examples/sinatra/feature-flags.yaml +6 -0
  18. data/examples/sinatra/sinatra.iml +35 -14
  19. data/examples/sinatra/start.sh +2 -0
  20. data/featurehub-sdk.gemspec +4 -1
  21. data/lib/feature_hub/sdk/context.rb +28 -7
  22. data/lib/feature_hub/sdk/feature_hub_config.rb +68 -12
  23. data/lib/feature_hub/sdk/feature_repository.rb +52 -13
  24. data/lib/feature_hub/sdk/{feature_state.rb → feature_state_holder.rb} +13 -9
  25. data/lib/feature_hub/sdk/interceptors.rb +10 -6
  26. data/lib/feature_hub/sdk/internal_feature_repository.rb +7 -3
  27. data/lib/feature_hub/sdk/local_yaml_interceptor.rb +99 -0
  28. data/lib/feature_hub/sdk/local_yaml_store.rb +71 -0
  29. data/lib/feature_hub/sdk/poll_edge_service.rb +10 -15
  30. data/lib/feature_hub/sdk/raw_update_feature_listener.rb +19 -0
  31. data/lib/feature_hub/sdk/redis_session_store.rb +130 -0
  32. data/lib/feature_hub/sdk/strategy_attributes.rb +7 -0
  33. data/lib/feature_hub/sdk/streaming_edge_service.rb +5 -7
  34. data/lib/feature_hub/sdk/version.rb +1 -10
  35. data/lib/featurehub-sdk.rb +5 -1
  36. data/sig/feature_hub/featurehub.rbs +127 -28
  37. metadata +27 -5
@@ -1,55 +1,63 @@
1
- GIT
2
- remote: https://github.com/featurehub-io/featurehub-ruby-sdk.git
3
- revision: fa551295ec9703e630452e094d3856117c3d013d
4
- glob: featurehub-sdk/*.gemspec
1
+ PATH
2
+ remote: ../..
5
3
  specs:
6
- featurehub-sdk (1.2.1)
7
- concurrent-ruby (~> 1.1.10)
8
- faraday (~> 2.3)
9
- ld-eventsource (~> 2.2.0)
10
- murmurhash3 (~> 0.1.6)
4
+ featurehub-sdk (2.0.0)
5
+ concurrent-ruby (~> 1.3)
6
+ faraday (~> 2)
7
+ ld-eventsource (~> 2.5.1)
8
+ murmurhash3 (~> 0.1.7)
11
9
  sem_version (~> 2.0.0)
12
10
 
13
11
  GEM
14
12
  remote: https://rubygems.org/
15
13
  specs:
16
- addressable (2.8.1)
17
- public_suffix (>= 2.0.2, < 6.0)
18
- concurrent-ruby (1.1.10)
14
+ addressable (2.8.9)
15
+ public_suffix (>= 2.0.2, < 8.0)
16
+ concurrent-ruby (1.3.6)
17
+ connection_pool (3.0.2)
19
18
  daemons (1.4.1)
20
- domain_name (0.5.20190701)
21
- unf (>= 0.0.5, < 1.0.0)
19
+ domain_name (0.6.20240107)
22
20
  eventmachine (1.2.7)
23
- faraday (2.7.4)
24
- faraday-net_http (>= 2.0, < 3.1)
25
- ruby2_keywords (>= 0.0.4)
26
- faraday-net_http (3.0.2)
27
- ffi (1.15.5)
28
- ffi-compiler (1.0.1)
29
- ffi (>= 1.0.0)
21
+ faraday (2.14.1)
22
+ faraday-net_http (>= 2.0, < 3.5)
23
+ json
24
+ logger
25
+ faraday-net_http (3.4.2)
26
+ net-http (~> 0.5)
27
+ ffi (1.17.3-x86_64-darwin)
28
+ ffi-compiler (1.3.2)
29
+ ffi (>= 1.15.5)
30
30
  rake
31
- http (5.1.1)
31
+ http (5.3.1)
32
32
  addressable (~> 2.8)
33
33
  http-cookie (~> 1.0)
34
34
  http-form_data (~> 2.2)
35
- llhttp-ffi (~> 0.4.0)
36
- http-cookie (1.0.5)
35
+ llhttp-ffi (~> 0.5.0)
36
+ http-cookie (1.1.0)
37
37
  domain_name (~> 0.5)
38
38
  http-form_data (2.3.0)
39
- ld-eventsource (2.2.1)
39
+ json (2.19.2)
40
+ ld-eventsource (2.5.1)
40
41
  concurrent-ruby (~> 1.0)
41
42
  http (>= 4.4.1, < 6.0.0)
42
- llhttp-ffi (0.4.0)
43
+ llhttp-ffi (0.5.1)
43
44
  ffi-compiler (~> 1.0)
44
45
  rake (~> 13.0)
46
+ logger (1.7.0)
45
47
  murmurhash3 (0.1.7)
46
48
  mustermann (2.0.2)
47
49
  ruby2_keywords (~> 0.0.1)
48
- public_suffix (5.0.1)
50
+ net-http (0.9.1)
51
+ uri (>= 0.11.1)
52
+ public_suffix (7.0.5)
49
53
  rack (2.2.6.4)
50
54
  rack-protection (2.2.3)
51
55
  rack
52
- rake (13.0.6)
56
+ rake (13.3.1)
57
+ redis (5.4.1)
58
+ redis-client (>= 0.22.0)
59
+ redis-client (0.28.0)
60
+ connection_pool
53
61
  ruby2_keywords (0.0.5)
54
62
  sem_version (2.0.1)
55
63
  shotgun (0.9.2)
@@ -64,23 +72,23 @@ GEM
64
72
  eventmachine (~> 1.0, >= 1.0.4)
65
73
  rack (>= 1, < 3)
66
74
  tilt (2.1.0)
67
- unf (0.1.4)
68
- unf_ext
69
- unf_ext (0.0.8.2)
75
+ uri (1.1.1)
70
76
 
71
77
  PLATFORMS
72
78
  x86_64-darwin-21
79
+ x86_64-darwin-24
73
80
  x86_64-linux
74
81
 
75
82
  DEPENDENCIES
76
83
  featurehub-sdk!
77
84
  rack
85
+ redis
78
86
  shotgun
79
87
  sinatra
80
88
  thin
81
89
 
82
90
  RUBY VERSION
83
- ruby 2.7.6p219
91
+ ruby 3.3.10p183
84
92
 
85
93
  BUNDLED WITH
86
94
  2.3.15
@@ -7,12 +7,20 @@ require "json"
7
7
 
8
8
  def configure_featurehub
9
9
  puts "FeatureHub SDK Version is #{FeatureHub::Sdk::VERSION}"
10
- config = FeatureHub::Sdk::FeatureHubConfig.new(ENV.fetch("FEATUREHUB_EDGE_URL", "http://localhost:8903"),
11
- [
12
- ENV.fetch("FEATUREHUB_CLIENT_API_KEY",
13
- "41ef6f5e-a70b-4ace-b6b5-8a3f1d636101/2PTj12Bn50Xn7Wt4yS9heBXtok3KGFAE9KW0Cms3") # rubocop:disable Layout/LineLength
14
- ])
15
- config.use_polling_edge_service(1)
10
+
11
+ config = FeatureHub::Sdk::FeatureHubConfig.new
12
+ repo = config.repository
13
+
14
+ if ENV["FEATUREHUB_REDIS_STORE"]
15
+ config.register_raw_update_listener(FeatureHub::Sdk::RedisSessionStore.new(ENV["FEATUREHUB_REDIS_STORE"], repo))
16
+ end
17
+
18
+ if ENV["FEATUREHUB_LOCAL_YAML"]
19
+ config.register_raw_update_listener(FeatureHub::Sdk::LocalYamlStore.new(repo))
20
+ config.register_interceptor(FeatureHub::Sdk::LocalYamlValueInterceptor.new(watch: true))
21
+ end
22
+
23
+ # connect to edge service
16
24
  config.init
17
25
  end
18
26
 
@@ -34,6 +42,10 @@ class App < Sinatra::Base
34
42
  content_type "application/json"
35
43
  end
36
44
 
45
+ get("/config/disconnect-edge") do
46
+ settings.fh_config.close_edge
47
+ end
48
+
37
49
  # Routes
38
50
  # "resolve" a specific todo for this user
39
51
  put("/todo/:user/:id/resolve") do
@@ -127,10 +139,10 @@ class App < Sinatra::Base
127
139
 
128
140
  new_title = new_title.upcase if ctx.enabled?("FEATURE_TITLE_TO_UPPERCASE")
129
141
 
130
- puts("features via repository: #{settings.fh_config.repository.features}")
131
- puts("features via edge service: #{settings.fh_config.get_or_create_edge_service.repository}")
142
+ # puts("features via repository: #{settings.fh_config.repository.features}")
143
+ # puts("features via edge service: #{settings.fh_config.get_or_create_edge_service.repository}")
132
144
 
133
- puts("enabled? #{ctx.repo.features}")
145
+ # puts("enabled? #{ctx.repo.features}")
134
146
  puts(ctx.enabled?("FEATURE_TITLE_TO_UPPERCASE"))
135
147
  puts(ctx.flag("FEATURE_TITLE_TO_UPPERCASE"))
136
148
  puts(settings.fh_config.repository.feature("FEATURE_TITLE_TO_UPPERCASE").feature_type)
@@ -0,0 +1,24 @@
1
+ services:
2
+ party-server:
3
+ image: featurehub/party-server:1.9.4
4
+ ports:
5
+ - 8085:8085
6
+ user: 999:999
7
+ # allow us to restart the server causing the client to lose connectivity
8
+ # but when we start it again its OK
9
+ volumes:
10
+ - featurehub-h2-data:/db
11
+ redis:
12
+ image: redis:latest
13
+ ports:
14
+ - 6379:6379
15
+ ruby-sinatra:
16
+ image: ruby-sinatra:latest
17
+ ports:
18
+ - 8099:8099
19
+ environment:
20
+ FEATUREHUB_EDGE_URL: http://party-server:8085
21
+ FEATUREHUB_BASE_URL: http://party-server:8085
22
+ FEATUREHUB_REDIS_STORE: redis
23
+ volumes:
24
+ featurehub-h2-data:
@@ -0,0 +1,6 @@
1
+ flagValues:
2
+ FEATURE_STRING: buy
3
+ FEATURE_NUMBER: 26
4
+ FEATURE_JSON:
5
+ some: number
6
+ FEATURE_TITLE_TO_UPPERCASE: false
@@ -3,20 +3,41 @@
3
3
  <component name="NewModuleRootManager" inherit-compiler-output="true">
4
4
  <exclude-output />
5
5
  <content url="file://$MODULE_DIR$" />
6
- <orderEntry type="jdk" jdkName="rbenv: 3.0.6" jdkType="RUBY_SDK" />
6
+ <orderEntry type="jdk" jdkName="rbenv: 3.3.10" jdkType="RUBY_SDK" />
7
7
  <orderEntry type="sourceFolder" forTests="false" />
8
- <orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.1.10, rbenv: 3.0.6) [gem]" level="application" />
9
- <orderEntry type="library" scope="PROVIDED" name="domain_name (v0.5.20190701, rbenv: 3.0.6) [gem]" level="application" />
10
- <orderEntry type="library" scope="PROVIDED" name="eventmachine (v1.2.7, rbenv: 3.0.6) [gem]" level="application" />
11
- <orderEntry type="library" scope="PROVIDED" name="ffi (v1.15.5, rbenv: 3.0.6) [gem]" level="application" />
12
- <orderEntry type="library" scope="PROVIDED" name="ffi-compiler (v1.0.1, rbenv: 3.0.6) [gem]" level="application" />
13
- <orderEntry type="library" scope="PROVIDED" name="http-cookie (v1.0.5, rbenv: 3.0.6) [gem]" level="application" />
14
- <orderEntry type="library" scope="PROVIDED" name="http-form_data (v2.3.0, rbenv: 3.0.6) [gem]" level="application" />
15
- <orderEntry type="library" scope="PROVIDED" name="llhttp-ffi (v0.4.0, rbenv: 3.0.6) [gem]" level="application" />
16
- <orderEntry type="library" scope="PROVIDED" name="rake (v13.0.6, rbenv: 3.0.6) [gem]" level="application" />
17
- <orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, rbenv: 3.0.6) [gem]" level="application" />
18
- <orderEntry type="library" scope="PROVIDED" name="sem_version (v2.0.1, rbenv: 3.0.6) [gem]" level="application" />
19
- <orderEntry type="library" scope="PROVIDED" name="unf (v0.1.4, rbenv: 3.0.6) [gem]" level="application" />
20
- <orderEntry type="library" scope="PROVIDED" name="unf_ext (v0.0.8.2, rbenv: 3.0.6) [gem]" level="application" />
8
+ <orderEntry type="library" scope="PROVIDED" name="addressable (v2.8.9, rbenv: 3.3.10) [gem]" level="application" />
9
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v2.3.15, rbenv: 3.3.10) [gem]" level="application" />
10
+ <orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.3.6, rbenv: 3.3.10) [gem]" level="application" />
11
+ <orderEntry type="library" scope="PROVIDED" name="connection_pool (v3.0.2, rbenv: 3.3.10) [gem]" level="application" />
12
+ <orderEntry type="library" scope="PROVIDED" name="daemons (v1.4.1, rbenv: 3.3.10) [gem]" level="application" />
13
+ <orderEntry type="library" scope="PROVIDED" name="domain_name (v0.6.20240107, rbenv: 3.3.10) [gem]" level="application" />
14
+ <orderEntry type="library" scope="PROVIDED" name="eventmachine (v1.2.7, rbenv: 3.3.10) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="faraday (v2.14.1, rbenv: 3.3.10) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="faraday-net_http (v3.4.2, rbenv: 3.3.10) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="ffi (v1.17.3, rbenv: 3.3.10) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="ffi-compiler (v1.3.2, rbenv: 3.3.10) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="http (v5.3.1, rbenv: 3.3.10) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="http-cookie (v1.1.0, rbenv: 3.3.10) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="http-form_data (v2.3.0, rbenv: 3.3.10) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="json (v2.19.2, rbenv: 3.3.10) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="ld-eventsource (v2.5.1, rbenv: 3.3.10) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="llhttp-ffi (v0.5.1, rbenv: 3.3.10) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="logger (v1.7.0, rbenv: 3.3.10) [gem]" level="application" />
26
+ <orderEntry type="library" scope="PROVIDED" name="murmurhash3 (v0.1.7, rbenv: 3.3.10) [gem]" level="application" />
27
+ <orderEntry type="library" scope="PROVIDED" name="mustermann (v2.0.2, rbenv: 3.3.10) [gem]" level="application" />
28
+ <orderEntry type="library" scope="PROVIDED" name="net-http (v0.9.1, rbenv: 3.3.10) [gem]" level="application" />
29
+ <orderEntry type="library" scope="PROVIDED" name="public_suffix (v7.0.5, rbenv: 3.3.10) [gem]" level="application" />
30
+ <orderEntry type="library" scope="PROVIDED" name="rack (v2.2.6.4, rbenv: 3.3.10) [gem]" level="application" />
31
+ <orderEntry type="library" scope="PROVIDED" name="rack-protection (v2.2.3, rbenv: 3.3.10) [gem]" level="application" />
32
+ <orderEntry type="library" scope="PROVIDED" name="rake (v13.3.1, rbenv: 3.3.10) [gem]" level="application" />
33
+ <orderEntry type="library" scope="PROVIDED" name="redis (v5.4.1, rbenv: 3.3.10) [gem]" level="application" />
34
+ <orderEntry type="library" scope="PROVIDED" name="redis-client (v0.28.0, rbenv: 3.3.10) [gem]" level="application" />
35
+ <orderEntry type="library" scope="PROVIDED" name="ruby2_keywords (v0.0.5, rbenv: 3.3.10) [gem]" level="application" />
36
+ <orderEntry type="library" scope="PROVIDED" name="sem_version (v2.0.1, rbenv: 3.3.10) [gem]" level="application" />
37
+ <orderEntry type="library" scope="PROVIDED" name="shotgun (v0.9.2, rbenv: 3.3.10) [gem]" level="application" />
38
+ <orderEntry type="library" scope="PROVIDED" name="sinatra (v2.2.3, rbenv: 3.3.10) [gem]" level="application" />
39
+ <orderEntry type="library" scope="PROVIDED" name="thin (v1.8.1, rbenv: 3.3.10) [gem]" level="application" />
40
+ <orderEntry type="library" scope="PROVIDED" name="tilt (v2.1.0, rbenv: 3.3.10) [gem]" level="application" />
41
+ <orderEntry type="library" scope="PROVIDED" name="uri (v1.1.1, rbenv: 3.3.10) [gem]" level="application" />
21
42
  </component>
22
43
  </module>
@@ -1,3 +1,5 @@
1
1
  #!/bin/sh
2
2
  RACK_ENV=development
3
+ #export FEATUREHUB_REDIS_STORE=redis://localhost:6379
4
+ export FEATUREHUB_LOCAL_YAML=feature-flags.yaml
3
5
  bundle exec thin -R thin.ru -a 0.0.0.0 -p 8099 start
@@ -36,7 +36,10 @@ Gem::Specification.new do |spec|
36
36
 
37
37
  spec.add_dependency "concurrent-ruby", "~> 1.3"
38
38
  spec.add_dependency "faraday", "~> 2"
39
- spec.add_dependency "ld-eventsource", "~> 2.3.0"
39
+ spec.add_dependency "ld-eventsource", "~> 2.5.1"
40
40
  spec.add_dependency "murmurhash3", "~> 0.1.7"
41
41
  spec.add_dependency "sem_version", "~> 2.0.0"
42
+
43
+ # we will dynamically determine if redis is available
44
+ spec.add_development_dependency "redis", "~> 5"
42
45
  end
@@ -17,9 +17,19 @@ module FeatureHub
17
17
  class ClientContext
18
18
  attr_reader :repo
19
19
 
20
- def initialize(repo)
20
+ WELL_KNOWN_KEY_METHODS = {
21
+ ContextKeys::USERKEY => :user_key,
22
+ ContextKeys::SESSION => :session_key,
23
+ ContextKeys::COUNTRY => :country,
24
+ ContextKeys::PLATFORM => :platform,
25
+ ContextKeys::DEVICE => :device,
26
+ ContextKeys::VERSION => :version
27
+ }.freeze
28
+
29
+ def initialize(repo, attrs = nil)
21
30
  @repo = repo
22
31
  @attributes = {}
32
+ assign(attrs) if attrs
23
33
  end
24
34
 
25
35
  def user_key(value)
@@ -54,7 +64,9 @@ module FeatureHub
54
64
 
55
65
  # this takes an array parameter
56
66
  def attribute_value(key, values)
57
- if values.empty?
67
+ return self if key.nil? || key.to_s.empty?
68
+
69
+ if values.nil? || values.empty?
58
70
  @attributes.delete(key.to_sym)
59
71
  else
60
72
  @attributes[key.to_sym] = if values.is_a?(Array)
@@ -67,6 +79,19 @@ module FeatureHub
67
79
  self
68
80
  end
69
81
 
82
+ def assign(attrs)
83
+ attrs.each do |key, value|
84
+ sym_key = key.to_sym
85
+ method_name = WELL_KNOWN_KEY_METHODS[sym_key]
86
+ if method_name
87
+ send(method_name, value)
88
+ else
89
+ attribute_value(sym_key, value)
90
+ end
91
+ end
92
+ self
93
+ end
94
+
70
95
  def clear
71
96
  @attributes = {}
72
97
  self
@@ -90,7 +115,7 @@ module FeatureHub
90
115
  end
91
116
 
92
117
  def feature(key)
93
- @repo.feature(key)
118
+ @repo.feature(key).with_context(self)
94
119
  end
95
120
 
96
121
  def set?(key)
@@ -153,10 +178,6 @@ module FeatureHub
153
178
  def build_sync
154
179
  self
155
180
  end
156
-
157
- def feature(key)
158
- @repo.feature(key).with_context(self)
159
- end
160
181
  end
161
182
 
162
183
  # context used when evaluating server side
@@ -4,8 +4,14 @@ module FeatureHub
4
4
  module Sdk
5
5
  # interface style definition for all edge services
6
6
  class EdgeService
7
- # abstract
8
- def initialize(repository, api_keys, edge_url, logger = nil) end
7
+ attr_reader :repository
8
+
9
+ def initialize(repository, api_keys, edge_url, logger = nil)
10
+ @repository = repository
11
+ @api_keys = api_keys
12
+ @edge_url = edge_url
13
+ @logger = logger
14
+ end
9
15
 
10
16
  # abstract
11
17
  def poll; end
@@ -21,24 +27,47 @@ module FeatureHub
21
27
  class FeatureHubConfig
22
28
  attr_reader :edge_url, :api_keys, :client_evaluated, :logger
23
29
 
24
- def initialize(edge_url, api_keys, repository = nil, edge_provider = nil, logger = nil)
25
- raise "edge_url is not set to a valid string" if edge_url.nil? || edge_url.strip.empty?
30
+ def initialize(edge_url = nil, api_keys = nil, repository = nil, edge_provider = nil, logger = nil) # rubocop:disable Metrics/ParameterLists
31
+ @logger = logger
32
+ @repository = repository || FeatureHub::Sdk::FeatureHubRepository.new(nil, @logger)
26
33
 
27
- raise "api_keys must be an array of API keys" if api_keys.nil? || !api_keys.is_a?(Array) || api_keys.empty?
34
+ resolved_url = resolve_edge_url(edge_url)
35
+ resolved_keys = resolve_api_keys(api_keys)
28
36
 
29
- detect_client_evaluated(api_keys)
30
-
31
- @edge_url = parse_edge_url(edge_url)
32
- @api_keys = api_keys
33
- @repository = repository || FeatureHub::Sdk::FeatureHubRepository.new
34
- @edge_service_provider = edge_provider || method(:create_default_provider)
35
- @logger = logger || FeatureHub::Sdk.default_logger
37
+ if resolved_url && resolved_keys && !resolved_keys.empty?
38
+ detect_client_evaluated(resolved_keys)
39
+ @edge_url = parse_edge_url(resolved_url)
40
+ @api_keys = resolved_keys
41
+ @edge_service_provider = edge_provider || method(:create_default_provider)
42
+ else
43
+ @edge_url = nil
44
+ @api_keys = []
45
+ @client_evaluated = false
46
+ @edge_service_provider = edge_provider || method(:create_null_provider)
47
+ end
36
48
  end
37
49
 
38
50
  def repository(repo = nil)
39
51
  @repository = repo || @repository
40
52
  end
41
53
 
54
+ def feature(key, attrs = nil)
55
+ @repository.feature(key, attrs)
56
+ end
57
+
58
+ def value(key, default_value = nil, attrs = nil)
59
+ @repository.value(key, default_value, attrs)
60
+ end
61
+
62
+ def register_interceptor(interceptor)
63
+ @repository.register_interceptor(interceptor)
64
+ end
65
+
66
+ def register_raw_update_listener(listener)
67
+ @repository ||= FeatureHub::Sdk::FeatureHubRepository.new(nil, @logger)
68
+ @repository.register_raw_update_listener(listener)
69
+ end
70
+
42
71
  def init
43
72
  get_or_create_edge_service.poll
44
73
  self
@@ -90,6 +119,18 @@ module FeatureHub
90
119
  end
91
120
 
92
121
  def close
122
+ unless @repository.nil?
123
+ @repository.close
124
+ @repository = nil
125
+ end
126
+
127
+ return if @edge_service.nil?
128
+
129
+ @edge_service.close
130
+ @edge_service = nil
131
+ end
132
+
133
+ def close_edge
93
134
  return if @edge_service.nil?
94
135
 
95
136
  @edge_service.close
@@ -110,6 +151,21 @@ module FeatureHub
110
151
  FeatureHub::Sdk::StreamingEdgeService.new(repo, api_keys, edge_url, logger)
111
152
  end
112
153
 
154
+ def resolve_edge_url(edge_url)
155
+ url = edge_url.nil? || edge_url.strip.empty? ? ENV.fetch("FEATUREHUB_EDGE_URL", nil) : edge_url
156
+ url&.strip&.empty? ? nil : url
157
+ end
158
+
159
+ def resolve_api_keys(api_keys)
160
+ return api_keys if api_keys.is_a?(Array) && !api_keys.empty?
161
+
162
+ [ENV.fetch("FEATUREHUB_CLIENT_API_KEY", nil), ENV.fetch("FEATUREHUB_SERVER_API_KEY", nil)].compact
163
+ end
164
+
165
+ def create_null_provider(repo, api_keys, edge_url, logger)
166
+ EdgeService.new(repo, api_keys, edge_url, logger)
167
+ end
168
+
113
169
  def detect_client_evaluated(api_keys)
114
170
  @client_evaluated = !api_keys.detect { |k| k.include?("*") }.nil?
115
171
  if api_keys.detect { |k| (@client_evaluated && !k.include?("*")) || (!@client_evaluated && k.include?("*")) }
@@ -1,24 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "concurrent-ruby"
4
+
3
5
  module FeatureHub
4
6
  module Sdk
5
7
  # the core implementation of a feature repository
6
8
  class FeatureHubRepository < InternalFeatureRepository
7
9
  attr_reader :features
8
10
 
9
- def initialize(apply_features = nil)
11
+ def initialize(apply_features = nil, logger = nil)
10
12
  super()
11
13
  @strategy_matcher = apply_features || FeatureHub::Sdk::Impl::ApplyFeature.new
12
14
  @interceptors = []
15
+ @raw_listeners = []
13
16
  @features = {}
14
17
  @ready = false
18
+ @logger = logger
15
19
  end
16
20
 
17
21
  def apply(strategies, key, feature_id, context)
18
22
  @strategy_matcher.apply(strategies, key, feature_id, context)
19
23
  end
20
24
 
21
- def notify(status, data)
25
+ def notify(status, data, source = "unknown")
22
26
  return unless status
23
27
 
24
28
  if status.to_sym == :failed
@@ -31,26 +35,58 @@ module FeatureHub
31
35
  case status.to_sym
32
36
  when :features
33
37
  update_features(data)
38
+ @logger&.debug("[featurehubsdk] became ready through updates from #{source}") unless @ready
34
39
  @ready = true
40
+ notify_raw_listeners_async { |l| l.process_updates(data, source) }
41
+ @logger&.debug("[featurehubsdk] full updates from #{source} are #{data}")
35
42
  when :feature
43
+ return if data.nil? || data["key"].nil?
44
+
36
45
  update_feature(data)
46
+ @logger&.debug("[featurehubsdk] became ready through updates from #{source}") unless @ready
37
47
  @ready = true
48
+ notify_raw_listeners_async { |l| l.process_update(data, source) }
49
+ @logger&.debug("[featurehubsdk] single feature update from #{source} are #{data}")
38
50
  when :delete_feature
51
+ return unless data && data["key"]
52
+
39
53
  delete_feature(data)
54
+ notify_raw_listeners_async { |l| l.delete_feature(data, source) }
55
+ @logger&.debug("[featurehubsdk] delete from #{source} are #{data}")
40
56
  end
41
57
  end
42
58
 
43
- def feature(key)
44
- sym_key = key.to_sym
45
- @features[sym_key] || make_feature_holder(sym_key)
59
+ def feature(key, attrs = nil)
60
+ holder = @features[key.to_sym] || make_feature_holder(key.to_sym)
61
+ return holder unless attrs
62
+
63
+ ClientContext.new(self, attrs).feature(key)
64
+ end
65
+
66
+ def value(key, default_value = nil, attrs = nil)
67
+ f = feature(key, attrs)
68
+ f.present? ? f.value : default_value
46
69
  end
47
70
 
48
71
  def register_interceptor(interceptor)
49
72
  @interceptors.push(interceptor)
50
73
  end
51
74
 
52
- def find_interceptor(feature_value)
53
- @interceptors.filter_map { |interceptor| interceptor.intercepted_value(feature_value) }.first
75
+ def register_raw_update_listener(listener)
76
+ @raw_listeners.push(listener)
77
+ end
78
+
79
+ def find_interceptor(feature_key, feature_state = nil)
80
+ @interceptors.each do |interceptor|
81
+ matched, value = interceptor.intercepted_value(feature_key, self, feature_state)
82
+ return [true, value] if matched
83
+ end
84
+ [false, nil]
85
+ end
86
+
87
+ def close
88
+ @interceptors.each(&:close)
89
+ @raw_listeners.each(&:close)
54
90
  end
55
91
 
56
92
  def ready?
@@ -70,15 +106,13 @@ module FeatureHub
70
106
  private
71
107
 
72
108
  def delete_feature(data)
73
- return unless data && data["key"]
74
-
75
109
  feat = @features[data["key"].to_sym]
76
110
 
77
111
  feat&.update_feature_state(nil)
78
112
  end
79
113
 
80
114
  def make_feature_holder(key)
81
- fs = FeatureHub::Sdk::FeatureState.new(key, self)
115
+ fs = FeatureHub::Sdk::FeatureStateHolder.new(key, self)
82
116
  @features[key.to_sym] = fs
83
117
  fs
84
118
  end
@@ -89,13 +123,18 @@ module FeatureHub
89
123
  end
90
124
  end
91
125
 
92
- def update_feature(feature_state)
93
- return if feature_state.nil? || feature_state["key"].nil?
126
+ def notify_raw_listeners_async(&block)
127
+ return if @raw_listeners.empty?
128
+
129
+ listeners = @raw_listeners.dup
130
+ Concurrent::Future.execute { listeners.each(&block) }
131
+ end
94
132
 
133
+ def update_feature(feature_state)
95
134
  key = feature_state["key"].to_sym
96
135
  holder = @features[key]
97
136
  if !holder
98
- @features[key] = FeatureHub::Sdk::FeatureState.new(key, self, feature_state)
137
+ @features[key] = FeatureHub::Sdk::FeatureStateHolder.new(key, self, feature_state)
99
138
  return
100
139
  elsif feature_state["version"] < holder.version
101
140
  return