sdk-reforge 1.9.0

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 (103) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc.sample +3 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/pull_request_template.md +8 -0
  5. data/.github/workflows/ruby.yml +48 -0
  6. data/.gitmodules +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.tool-versions +1 -0
  9. data/CHANGELOG.md +257 -0
  10. data/CODEOWNERS +1 -0
  11. data/Gemfile +29 -0
  12. data/Gemfile.lock +182 -0
  13. data/LICENSE.txt +20 -0
  14. data/README.md +105 -0
  15. data/Rakefile +63 -0
  16. data/VERSION +1 -0
  17. data/compile_protos.sh +20 -0
  18. data/dev/allocation_stats +60 -0
  19. data/dev/benchmark +40 -0
  20. data/dev/console +12 -0
  21. data/dev/script_setup.rb +18 -0
  22. data/lib/prefab_pb.rb +77 -0
  23. data/lib/reforge/caching_http_connection.rb +95 -0
  24. data/lib/reforge/client.rb +133 -0
  25. data/lib/reforge/config_client.rb +275 -0
  26. data/lib/reforge/config_client_presenter.rb +18 -0
  27. data/lib/reforge/config_loader.rb +67 -0
  28. data/lib/reforge/config_resolver.rb +84 -0
  29. data/lib/reforge/config_value_unwrapper.rb +123 -0
  30. data/lib/reforge/config_value_wrapper.rb +18 -0
  31. data/lib/reforge/context.rb +241 -0
  32. data/lib/reforge/context_shape.rb +20 -0
  33. data/lib/reforge/context_shape_aggregator.rb +70 -0
  34. data/lib/reforge/criteria_evaluator.rb +345 -0
  35. data/lib/reforge/duration.rb +58 -0
  36. data/lib/reforge/encryption.rb +65 -0
  37. data/lib/reforge/error.rb +6 -0
  38. data/lib/reforge/errors/env_var_parse_error.rb +11 -0
  39. data/lib/reforge/errors/initialization_timeout_error.rb +12 -0
  40. data/lib/reforge/errors/invalid_sdk_key_error.rb +19 -0
  41. data/lib/reforge/errors/missing_default_error.rb +13 -0
  42. data/lib/reforge/errors/missing_env_var_error.rb +11 -0
  43. data/lib/reforge/errors/uninitialized_error.rb +13 -0
  44. data/lib/reforge/evaluation.rb +53 -0
  45. data/lib/reforge/evaluation_summary_aggregator.rb +86 -0
  46. data/lib/reforge/example_contexts_aggregator.rb +77 -0
  47. data/lib/reforge/exponential_backoff.rb +21 -0
  48. data/lib/reforge/feature_flag_client.rb +43 -0
  49. data/lib/reforge/fixed_size_hash.rb +14 -0
  50. data/lib/reforge/http_connection.rb +45 -0
  51. data/lib/reforge/internal_logger.rb +43 -0
  52. data/lib/reforge/javascript_stub.rb +99 -0
  53. data/lib/reforge/local_config_parser.rb +151 -0
  54. data/lib/reforge/murmer3.rb +50 -0
  55. data/lib/reforge/options.rb +191 -0
  56. data/lib/reforge/periodic_sync.rb +74 -0
  57. data/lib/reforge/prefab.rb +120 -0
  58. data/lib/reforge/rate_limit_cache.rb +41 -0
  59. data/lib/reforge/resolved_config_presenter.rb +86 -0
  60. data/lib/reforge/semver.rb +132 -0
  61. data/lib/reforge/sse_config_client.rb +112 -0
  62. data/lib/reforge/time_helpers.rb +7 -0
  63. data/lib/reforge/weighted_value_resolver.rb +42 -0
  64. data/lib/reforge/yaml_config_parser.rb +34 -0
  65. data/lib/reforge-sdk.rb +57 -0
  66. data/test/fixtures/datafile.json +87 -0
  67. data/test/integration_test.rb +171 -0
  68. data/test/integration_test_helpers.rb +114 -0
  69. data/test/support/common_helpers.rb +201 -0
  70. data/test/support/mock_base_client.rb +41 -0
  71. data/test/support/mock_config_client.rb +19 -0
  72. data/test/support/mock_config_loader.rb +1 -0
  73. data/test/test_caching_http_connection.rb +218 -0
  74. data/test/test_client.rb +351 -0
  75. data/test/test_config_client.rb +84 -0
  76. data/test/test_config_loader.rb +82 -0
  77. data/test/test_config_resolver.rb +502 -0
  78. data/test/test_config_value_unwrapper.rb +270 -0
  79. data/test/test_config_value_wrapper.rb +42 -0
  80. data/test/test_context.rb +271 -0
  81. data/test/test_context_shape.rb +50 -0
  82. data/test/test_context_shape_aggregator.rb +150 -0
  83. data/test/test_criteria_evaluator.rb +1180 -0
  84. data/test/test_duration.rb +37 -0
  85. data/test/test_encryption.rb +16 -0
  86. data/test/test_evaluation_summary_aggregator.rb +162 -0
  87. data/test/test_example_contexts_aggregator.rb +233 -0
  88. data/test/test_exponential_backoff.rb +18 -0
  89. data/test/test_feature_flag_client.rb +16 -0
  90. data/test/test_fixed_size_hash.rb +119 -0
  91. data/test/test_helper.rb +17 -0
  92. data/test/test_integration.rb +75 -0
  93. data/test/test_internal_logger.rb +25 -0
  94. data/test/test_javascript_stub.rb +176 -0
  95. data/test/test_local_config_parser.rb +147 -0
  96. data/test/test_logger_initialization.rb +12 -0
  97. data/test/test_options.rb +93 -0
  98. data/test/test_prefab.rb +16 -0
  99. data/test/test_rate_limit_cache.rb +44 -0
  100. data/test/test_semver.rb +108 -0
  101. data/test/test_sse_config_client.rb +211 -0
  102. data/test/test_weighted_value_resolver.rb +71 -0
  103. metadata +345 -0
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class JavaScriptStub
5
+ LOG = Reforge::InternalLogger.new(self)
6
+ CAMELS = {}
7
+
8
+ def initialize(client = nil)
9
+ @client = client || Prefab.instance
10
+ end
11
+
12
+ def bootstrap(context)
13
+ configs, warnings = data(context, :bootstrap)
14
+ <<~JS
15
+ window._prefabBootstrap = {
16
+ evaluations: #{JSON.dump(configs)},
17
+ context: #{JSON.dump(context)}
18
+ }
19
+ #{log_warnings(warnings)}
20
+ JS
21
+ end
22
+
23
+ def generate_stub(context, callback = nil)
24
+ configs, warnings = data(context, :stub)
25
+ <<~JS
26
+ window.prefab = window.prefab || {};
27
+ window.prefab.config = #{JSON.dump(configs)};
28
+ window.prefab.get = function(key) {
29
+ var value = window.prefab.config[key];
30
+ #{callback && " #{callback}(key, value);"}
31
+ return value;
32
+ };
33
+ window.prefab.isEnabled = function(key) {
34
+ var value = window.prefab.config[key] === true;
35
+ #{callback && " #{callback}(key, value);"}
36
+ return value;
37
+ };
38
+ #{log_warnings(warnings)}
39
+ JS
40
+ end
41
+
42
+ private
43
+
44
+ def underlying_value(value)
45
+ v = Reforge::ConfigValueUnwrapper.new(value, @client.resolver).unwrap(raw_json: true)
46
+ case v
47
+ when Google::Protobuf::RepeatedField
48
+ v.to_a
49
+ when Reforge::Duration
50
+ v.as_json
51
+ else
52
+ v
53
+ end
54
+ end
55
+
56
+ def log_warnings(warnings)
57
+ return '' if warnings.empty?
58
+
59
+ <<~JS
60
+ console.warn('The following keys could not be resolved:', #{JSON.dump(warnings)});
61
+ JS
62
+ end
63
+
64
+ def data(context, mode)
65
+ permitted = {}
66
+ warnings = []
67
+ resolver_keys = @client.resolver.keys
68
+
69
+ resolver_keys.each do |key|
70
+ begin
71
+ config = @client.resolver.raw(key)
72
+
73
+ if config.config_type == :FEATURE_FLAG || config.send_to_client_sdk || config.config_type == :LOG_LEVEL
74
+ value = @client.resolver.get(key, context).value
75
+ if mode == :bootstrap
76
+ permitted[key] = { value: { to_camel_case(value.type) => underlying_value(value) } }
77
+ else
78
+ permitted[key] = underlying_value(value)
79
+ end
80
+ end
81
+ rescue StandardError => e
82
+ LOG.warn("Could not resolve key #{key}: #{e}")
83
+
84
+ warnings << key
85
+ end
86
+ end
87
+
88
+ [permitted, warnings]
89
+ end
90
+
91
+ def to_camel_case(str)
92
+ CAMELS[str] ||= begin
93
+ str.to_s.split('_').map.with_index { |word, index|
94
+ index == 0 ? word : word.capitalize
95
+ }.join
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ class LocalConfigParser
5
+ class << self
6
+ def parse(key, value, config, file)
7
+ if value.instance_of?(Hash)
8
+ if value['feature_flag']
9
+ config[key] = feature_flag_config(file, key, value)
10
+ elsif value['type'] == 'provided'
11
+ config[key] = provided_config(file, key, value)
12
+ elsif value['decrypt_with'] || value['confidential']
13
+ config[key] = complex_string(file, key, value)
14
+ else
15
+ value.each do |nest_key, nest_value|
16
+ nested_key = "#{key}.#{nest_key}"
17
+ nested_key = key if nest_key == '_'
18
+ parse(nested_key, nest_value, config, file)
19
+ end
20
+ end
21
+ else
22
+ config[key] = {
23
+ source: file,
24
+ match: 'default',
25
+ config: PrefabProto::Config.new(
26
+ config_type: :CONFIG,
27
+ key: key,
28
+ rows: [
29
+ PrefabProto::ConfigRow.new(values: [
30
+ PrefabProto::ConditionalValue.new(value: value_from(key, value))
31
+ ])
32
+ ]
33
+ )
34
+ }
35
+ end
36
+
37
+ config
38
+ end
39
+
40
+ def value_from(key, raw)
41
+ case raw
42
+ when String
43
+ if key.to_s.start_with? 'log-level'
44
+ prefab_log_level_resolve = PrefabProto::LogLevel.resolve(raw.upcase.to_sym) || PrefabProto::LogLevel::NOT_SET_LOG_LEVEL
45
+ { log_level: prefab_log_level_resolve }
46
+ else
47
+ { string: raw }
48
+ end
49
+ when Integer
50
+ { int: raw }
51
+ when TrueClass, FalseClass
52
+ { bool: raw }
53
+ when Float
54
+ { double: raw }
55
+ end
56
+ end
57
+
58
+ def feature_flag_config(file, key, value)
59
+ criterion = (parse_criterion(value['criterion']) if value['criterion'])
60
+
61
+ variant = PrefabProto::ConfigValue.new(value_from(key, value['value']))
62
+
63
+ row = PrefabProto::ConfigRow.new(
64
+ values: [
65
+ PrefabProto::ConditionalValue.new(
66
+ criteria: [criterion].compact,
67
+ value: variant
68
+ )
69
+ ]
70
+ )
71
+
72
+ raise Reforge::Error, "Feature flag config `#{key}` #{file} must have a `value`" unless value.key?('value')
73
+
74
+ {
75
+ source: file,
76
+ match: key,
77
+ config: PrefabProto::Config.new(
78
+ config_type: :FEATURE_FLAG,
79
+ key: key,
80
+ allowable_values: [variant],
81
+ rows: [row]
82
+ )
83
+ }
84
+ end
85
+
86
+ def provided_config(file, key, value_hash)
87
+ value = PrefabProto::ConfigValue.new(provided: PrefabProto::Provided.new(
88
+ source: :ENV_VAR,
89
+ lookup: value_hash["lookup"],
90
+ ),
91
+ confidential: value_hash["confidential"],
92
+ )
93
+
94
+ row = PrefabProto::ConfigRow.new(
95
+ values: [
96
+ PrefabProto::ConditionalValue.new(
97
+ value: value
98
+ )
99
+ ]
100
+ )
101
+
102
+ {
103
+ source: file,
104
+ match: value.provided.lookup,
105
+ config: PrefabProto::Config.new(
106
+ config_type: :CONFIG,
107
+ key: key,
108
+ rows: [row]
109
+ )
110
+ }
111
+ end
112
+
113
+ def complex_string(file, key, value_hash)
114
+ value = PrefabProto::ConfigValue.new(
115
+ string: value_hash["value"],
116
+ confidential: value_hash["confidential"],
117
+ decrypt_with: value_hash["decrypt_with"],
118
+ )
119
+
120
+ row = PrefabProto::ConfigRow.new(
121
+ values: [
122
+ PrefabProto::ConditionalValue.new(
123
+ value: value
124
+ )
125
+ ]
126
+ )
127
+
128
+ {
129
+ source: file,
130
+ config: PrefabProto::Config.new(
131
+ config_type: :CONFIG,
132
+ key: key,
133
+ rows: [row]
134
+ )
135
+ }
136
+ end
137
+
138
+ def parse_criterion(criterion)
139
+ PrefabProto::Criterion.new(operator: criterion['operator'],
140
+ property_name: criterion['property'],
141
+ value_to_match: parse_value_to_match(criterion['values']))
142
+ end
143
+
144
+ def parse_value_to_match(values)
145
+ raise "Can't handle #{values}" unless values.instance_of?(Array)
146
+
147
+ PrefabProto::ConfigValue.new(string_list: PrefabProto::StringList.new(values: values))
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Murmur3
4
+ ## MurmurHash3 was written by Austin Appleby, and is placed in the public
5
+ ## domain. The author hereby disclaims copyright to this source code.
6
+
7
+ MASK32 = 0xffffffff
8
+
9
+ def self.murmur3_32_rotl(x, r)
10
+ ((x << r) | (x >> (32 - r))) & MASK32
11
+ end
12
+
13
+ def self.murmur3_32_fmix(h)
14
+ h &= MASK32
15
+ h ^= h >> 16
16
+ h = (h * 0x85ebca6b) & MASK32
17
+ h ^= h >> 13
18
+ h = (h * 0xc2b2ae35) & MASK32
19
+ h ^ (h >> 16)
20
+ end
21
+
22
+ def self.murmur3_32__mmix(k1)
23
+ k1 = (k1 * 0xcc9e2d51) & MASK32
24
+ k1 = murmur3_32_rotl(k1, 15)
25
+ (k1 * 0x1b873593) & MASK32
26
+ end
27
+
28
+ def self.murmur3_32(str, seed = 0)
29
+ h1 = seed
30
+ numbers = str.unpack('V*C*')
31
+ tailn = str.length % 4
32
+ tail = numbers.slice!(numbers.size - tailn, tailn)
33
+ for k1 in numbers
34
+ h1 ^= murmur3_32__mmix(k1)
35
+ h1 = murmur3_32_rotl(h1, 13)
36
+ h1 = (h1 * 5 + 0xe6546b64) & MASK32
37
+ end
38
+
39
+ unless tail.empty?
40
+ k1 = 0
41
+ tail.reverse_each do |c1|
42
+ k1 = (k1 << 8) | c1
43
+ end
44
+ h1 ^= murmur3_32__mmix(k1)
45
+ end
46
+
47
+ h1 ^= str.length
48
+ murmur3_32_fmix(h1)
49
+ end
50
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ # This class contains all the options that can be passed to the Prefab client.
5
+ class Options
6
+ attr_reader :sdk_key
7
+ attr_reader :namespace
8
+ attr_reader :sources
9
+ attr_reader :sse_sources
10
+ attr_reader :telemetry_destination
11
+ attr_reader :config_sources
12
+ attr_reader :on_no_default
13
+ attr_reader :initialization_timeout_sec
14
+ attr_reader :on_init_failure
15
+ attr_reader :collect_sync_interval
16
+ attr_reader :use_local_cache
17
+ attr_reader :datafile
18
+ attr_reader :global_context
19
+ attr_accessor :is_fork
20
+ attr_reader :symbolize_json_names
21
+
22
+ module ON_INITIALIZATION_FAILURE
23
+ RAISE = :raise
24
+ RETURN = :return
25
+ end
26
+
27
+ module ON_NO_DEFAULT
28
+ RAISE = :raise
29
+ RETURN_NIL = :return_nil
30
+ end
31
+
32
+ module DATASOURCES
33
+ ALL = :all
34
+ LOCAL_ONLY = :local_only
35
+ end
36
+
37
+ DEFAULT_MAX_PATHS = 1_000
38
+ DEFAULT_MAX_KEYS = 100_000
39
+ DEFAULT_MAX_EXAMPLE_CONTEXTS = 100_000
40
+ DEFAULT_MAX_EVAL_SUMMARIES = 100_000
41
+
42
+ DEFAULT_SOURCES = [
43
+ "https://primary.reforge.com",
44
+ "https://secondary.reforge.com",
45
+ ].freeze
46
+
47
+ private def init(
48
+ sources: nil,
49
+ sdk_key: ENV['SDK_API_KEY'] || ENV['PREFAB_API_KEY'],
50
+ namespace: '',
51
+ reforge_api_url: nil,
52
+ on_no_default: ON_NO_DEFAULT::RAISE, # options :raise, :warn_and_return_nil,
53
+ initialization_timeout_sec: 10, # how long to wait before on_init_failure
54
+ on_init_failure: ON_INITIALIZATION_FAILURE::RAISE,
55
+ prefab_datasources: (ENV['REFORGE_DATASOURCES'] || ENV['PREFAB_DATASOURCES']) == 'LOCAL_ONLY' ? DATASOURCES::LOCAL_ONLY : DATASOURCES::ALL,
56
+ collect_logger_counts: true,
57
+ collect_max_paths: DEFAULT_MAX_PATHS,
58
+ collect_sync_interval: nil,
59
+ context_upload_mode: :periodic_example, # :periodic_example, :shape_only, :none
60
+ context_max_size: DEFAULT_MAX_EVAL_SUMMARIES,
61
+ collect_evaluation_summaries: true,
62
+ collect_max_evaluation_summaries: DEFAULT_MAX_EVAL_SUMMARIES,
63
+ allow_telemetry_in_local_mode: false,
64
+ datafile: ENV['PREFAB_DATAFILE'],
65
+ x_datafile: nil, # DEPRECATED in favor of `datafile`
66
+ x_use_local_cache: false,
67
+ symbolize_json_names: false,
68
+ global_context: {}
69
+ )
70
+ @sdk_key = sdk_key
71
+ @namespace = namespace
72
+ @on_no_default = on_no_default
73
+ @initialization_timeout_sec = initialization_timeout_sec
74
+ @on_init_failure = on_init_failure
75
+ @prefab_datasources = prefab_datasources
76
+
77
+ @datafile = datafile || x_datafile
78
+
79
+ if !x_datafile.nil?
80
+ warn '[DEPRECATION] x_datafile is deprecated. Please provide `datafile` instead'
81
+ end
82
+
83
+ @collect_logger_counts = collect_logger_counts
84
+ @collect_max_paths = collect_max_paths
85
+ @collect_sync_interval = collect_sync_interval
86
+ @collect_evaluation_summaries = collect_evaluation_summaries
87
+ @collect_max_evaluation_summaries = collect_max_evaluation_summaries
88
+ @allow_telemetry_in_local_mode = allow_telemetry_in_local_mode
89
+ @use_local_cache = x_use_local_cache
90
+ @is_fork = false
91
+ @global_context = global_context
92
+ @symbolize_json_names = symbolize_json_names
93
+
94
+ # defaults that may be overridden by context_upload_mode
95
+ @collect_shapes = false
96
+ @collect_max_shapes = 0
97
+ @collect_example_contexts = false
98
+ @collect_max_example_contexts = 0
99
+
100
+ if ENV['PREFAB_API_URL_OVERRIDE'] && ENV['PREFAB_API_URL_OVERRIDE'].length > 0
101
+ sources = ENV['PREFAB_API_URL_OVERRIDE']
102
+ end
103
+
104
+ @sources = Array(sources || DEFAULT_SOURCES).map {|source| remove_trailing_slash(source) }
105
+
106
+ @sse_sources = @sources
107
+ @config_sources = @sources
108
+
109
+ @telemetry_destination = @sources.select do |source|
110
+ source.start_with?('https://') && (source.include?("primary") || source.include?("secondary") || source.include?("belt") || source.include?("suspenders"))
111
+ end.map do |source|
112
+ source.sub(/(primary|secondary)\./, 'telemetry.').sub(/(belt|suspenders)\./, 'telemetry.')
113
+ end[0]
114
+
115
+ if reforge_api_url
116
+ warn '[DEPRECATION] reforge_api_url is deprecated. Please provide `sources` if you need to override the default sources'
117
+ end
118
+
119
+ case context_upload_mode
120
+ when :none
121
+ # do nothing
122
+ when :periodic_example
123
+ @collect_example_contexts = true
124
+ @collect_max_example_contexts = context_max_size
125
+ @collect_shapes = true
126
+ @collect_max_shapes = context_max_size
127
+ when :shape_only
128
+ @collect_shapes = true
129
+ @collect_max_shapes = context_max_size
130
+ else
131
+ raise "Unknown context_upload_mode #{context_upload_mode}. Please provide :periodic_example, :shape_only, or :none."
132
+ end
133
+ end
134
+
135
+ def initialize(options = {})
136
+ init(**options)
137
+ end
138
+
139
+ def local_only?
140
+ @prefab_datasources == DATASOURCES::LOCAL_ONLY
141
+ end
142
+
143
+ def datafile?
144
+ !@datafile.nil?
145
+ end
146
+
147
+ def collect_max_paths
148
+ return 0 unless telemetry_allowed?(@collect_logger_counts)
149
+
150
+ @collect_max_paths
151
+ end
152
+
153
+ def collect_max_shapes
154
+ return 0 unless telemetry_allowed?(@collect_shapes)
155
+
156
+ @collect_max_shapes
157
+ end
158
+
159
+ def collect_max_example_contexts
160
+ return 0 unless telemetry_allowed?(@collect_example_contexts)
161
+
162
+ @collect_max_example_contexts
163
+ end
164
+
165
+ def collect_max_evaluation_summaries
166
+ return 0 unless telemetry_allowed?(@collect_evaluation_summaries)
167
+
168
+ @collect_max_evaluation_summaries
169
+ end
170
+
171
+ def sdk_key_id
172
+ @sdk_key&.split("-")&.first
173
+ end
174
+
175
+ def for_fork
176
+ clone = self.clone
177
+ clone.is_fork = true
178
+ clone
179
+ end
180
+
181
+ private
182
+
183
+ def telemetry_allowed?(option)
184
+ option && (!local_only? || @allow_telemetry_in_local_mode)
185
+ end
186
+
187
+ def remove_trailing_slash(url)
188
+ url.end_with?('/') ? url[0..-2] : url
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ module PeriodicSync
5
+ LOG = Reforge::InternalLogger.new(self)
6
+
7
+ def sync
8
+ return if @data.size.zero?
9
+
10
+ LOG.debug "Syncing #{@data.size} items"
11
+
12
+ start_at_was = @start_at
13
+ @start_at = Reforge::TimeHelpers.now_in_ms
14
+
15
+ flush(prepare_data, start_at_was)
16
+ end
17
+
18
+ def prepare_data
19
+ to_ship = @data.dup
20
+ @data.clear
21
+
22
+ on_prepare_data
23
+
24
+ to_ship
25
+ end
26
+
27
+ def on_prepare_data
28
+ # noop -- override as you wish
29
+ end
30
+
31
+ def post(url, data)
32
+ @client.post(url, data)
33
+ end
34
+
35
+ def instance_hash
36
+ @client.instance_hash
37
+ end
38
+
39
+ def start_periodic_sync(sync_interval)
40
+ @start_at = Reforge::TimeHelpers.now_in_ms
41
+
42
+ @sync_interval = calculate_sync_interval(sync_interval)
43
+
44
+ Thread.new do
45
+ LOG.debug "Initialized #{@name} instance_hash=#{@client.instance_hash}"
46
+
47
+ loop do
48
+ sleep @sync_interval.call
49
+ sync
50
+ end
51
+ end
52
+ end
53
+
54
+ def pool
55
+ @pool ||= Concurrent::ThreadPoolExecutor.new(
56
+ fallback_policy: :discard,
57
+ max_queue: 5,
58
+ max_threads: 4,
59
+ min_threads: 1,
60
+ name: @name
61
+ )
62
+ end
63
+
64
+ private
65
+
66
+ def calculate_sync_interval(sync_interval)
67
+ if sync_interval.is_a?(Numeric)
68
+ proc { sync_interval }
69
+ else
70
+ sync_interval || ExponentialBackoff.new(initial_delay: 8, max_delay: 60 * 5)
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Reforge
4
+ LOG = Reforge::InternalLogger.new(self)
5
+ @@lock = Concurrent::ReadWriteLock.new
6
+ @config_has_loaded = false
7
+
8
+ def self.init(options = Reforge::Options.new)
9
+ unless @singleton.nil?
10
+ LOG.warn 'Prefab already initialized.'
11
+ return @singleton
12
+ end
13
+
14
+ @@lock.with_write_lock {
15
+ @singleton = Reforge::Client.new(options)
16
+ }
17
+ end
18
+
19
+ def self.fork
20
+ ensure_initialized
21
+ @@lock.with_write_lock {
22
+ @singleton = @singleton.fork
23
+ }
24
+ end
25
+
26
+ def self.set_rails_loggers
27
+ ensure_initialized
28
+ @singleton.set_rails_loggers
29
+ end
30
+
31
+ def self.get(key, default = NO_DEFAULT_PROVIDED, jit_context = NO_DEFAULT_PROVIDED)
32
+ ensure_initialized key
33
+ @singleton.get(key, default, jit_context)
34
+ end
35
+
36
+ def self.enabled?(feature_name, jit_context = NO_DEFAULT_PROVIDED)
37
+ ensure_initialized feature_name
38
+ @singleton.enabled?(feature_name, jit_context)
39
+ end
40
+
41
+ def self.with_context(properties, &block)
42
+ ensure_initialized
43
+ @singleton.with_context(properties, &block)
44
+ end
45
+
46
+ def self.instance
47
+ ensure_initialized
48
+ @singleton
49
+ end
50
+
51
+ def self.log_filter
52
+ InternalLogger.using_reforge_log_filter!
53
+ return Proc.new do |log|
54
+ bootstrap_log_level(log)
55
+ end
56
+ end
57
+
58
+ def self.finish_init!
59
+ @config_has_loaded = true
60
+ end
61
+
62
+ def self.bootstrap_log_level(log)
63
+ level = ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'] ? ENV['REFORGE_LOG_CLIENT_BOOTSTRAP_LOG_LEVEL'].downcase.to_sym : :warn
64
+ SemanticLogger::Levels.index(level) <= SemanticLogger::Levels.index(log.level)
65
+ end
66
+
67
+ def self.defined?(key)
68
+ ensure_initialized key
69
+ @singleton.defined?(key)
70
+ end
71
+
72
+ def self.is_ff?(key)
73
+ ensure_initialized key
74
+ @singleton.is_ff?(key)
75
+ end
76
+
77
+ # Generate the JavaScript snippet to bootstrap the client SDK. This will
78
+ # include the configuration values that are permitted to be sent to the
79
+ # client SDK.
80
+ #
81
+ # If the context provided to the client SDK is not the same as the context
82
+ # used to generate the configuration values, the client SDK will still
83
+ # generate a fetch to get the correct values for the context.
84
+ #
85
+ # Any keys that could not be resolved will be logged as a warning to the
86
+ # console.
87
+ def self.bootstrap_javascript(context)
88
+ ensure_initialized
89
+ Reforge::JavaScriptStub.new(@singleton).bootstrap(context)
90
+ end
91
+
92
+ # Generate the JavaScript snippet to *replace* the client SDK. Use this to
93
+ # get `prefab.get` and `prefab.isEnabled` functions on the window object.
94
+ #
95
+ # Only use this if you are not using the client SDK and do not need
96
+ # client-side context.
97
+ #
98
+ # Any keys that could not be resolved will be logged as a warning to the
99
+ # console.
100
+ #
101
+ # You can pass an optional callback function to be called with the key and
102
+ # value of each configuration value. This can be useful for logging,
103
+ # tracking experiment exposure, etc.
104
+ #
105
+ # e.g.
106
+ # - `Prefab.generate_javascript_stub(context, "reportExperimentExposure")`
107
+ # - `Prefab.generate_javascript_stub(context, "(key,value)=>{console.log({eval: 'eval', key,value})}")`
108
+ def self.generate_javascript_stub(context, callback = nil)
109
+ ensure_initialized
110
+ Reforge::JavaScriptStub.new(@singleton).generate_stub(context, callback)
111
+ end
112
+
113
+ private
114
+
115
+ def self.ensure_initialized(key = nil)
116
+ if not defined? @singleton or @singleton.nil?
117
+ raise Reforge::Errors::UninitializedError.new(key)
118
+ end
119
+ end
120
+ end