openfeature-sdk 0.6.7 → 0.7.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: 93f85fdb9eb64b634a3267d0987415f3e4b9033a9d864e80478dfde83074fa7d
4
- data.tar.gz: 14e94971fdae29a56d31db9d8ee44716b805b6783e0d381ac04e6348168ccf67
3
+ metadata.gz: bf1041c34e962f832b25f1be3e9eeab7fee3fd4272dc9e1e0e8dabe85b6d21e8
4
+ data.tar.gz: f45667a6a898d3521ea87c31aea62491ace0682c72ef2cfa2a16d5f8a0f3f0b5
5
5
  SHA512:
6
- metadata.gz: c1e726bae4617d91ed82f676270bb51b7355e8694fac68a76af5f8ea046ed60c33d4c31b0a716827ad61edffe2849c3eb444c614975ac8a09707dcf620286c03
7
- data.tar.gz: 47d740d86cac2c493058c0f35aa6c2489d89f0502e8df3eb9ba850585777934478fe2dcf500b7eac9b71c96cfb2c857bc6026e9d1214e7876bc385788fcf6f18
6
+ metadata.gz: 393adb8b637763e5b397f6ee702017705bf355c8785ceb1280aa4f95b585a061f8da6e4a6d67ed9e8ab20de1f341b36d4dc3febb38dbbcc7a41a68cfb8002b8c
7
+ data.tar.gz: d145cc98fb609aa75ad06485d4486cc6aec19ea953cd627f9f71b33e991fafd3a57fa139fbe07f878e5bc105885ce35a74c0ab12485e50649d63b79a100eec98
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.6.7"
2
+ ".": "0.7.1"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.7.1](https://github.com/open-feature/ruby-sdk/compare/v0.7.0...v0.7.1) (2026-10-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * isolated API instances ([#354](https://github.com/open-feature/ruby-sdk/issues/354)) ([1c92b4a](https://github.com/open-feature/ruby-sdk/commit/1c92b4ad7550a6b345e7471f84cd5a7ff445591e))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * key provider state by identity, default the NoOp reason, freeze client metadata ([#355](https://github.com/open-feature/ruby-sdk/issues/355)) ([d15bd84](https://github.com/open-feature/ruby-sdk/commit/d15bd84e1456b6200e29c12c3463c4f8ddfe693f))
14
+
15
+ ## [0.7.0](https://github.com/open-feature/ruby-sdk/compare/v0.6.7...v0.7.0) (2026-10-23)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * resolve a client's provider by domain on every call ([#352](https://github.com/open-feature/ruby-sdk/issues/352))
21
+
22
+ ### Features
23
+
24
+ * let providers own their lifecycle events ([#353](https://github.com/open-feature/ruby-sdk/issues/353)) ([15faf98](https://github.com/open-feature/ruby-sdk/commit/15faf98d1c900118da362f6b8d76ccdec1b3b99e))
25
+
26
+
27
+ ### Bug Fixes
28
+
29
+ * catch NotImplementedError in track and the hook lifecycle ([#349](https://github.com/open-feature/ruby-sdk/issues/349)) ([62759f4](https://github.com/open-feature/ruby-sdk/commit/62759f443fa8cb2f7a1b67720397240063623da1))
30
+ * deliver provider events in order, outside handler locks ([#351](https://github.com/open-feature/ruby-sdk/issues/351)) ([16bf89c](https://github.com/open-feature/ruby-sdk/commit/16bf89cf72541c22de9565f0dcdbfcf9dfe097ad))
31
+ * release init waiters on replacement and same-thread re-entry ([#348](https://github.com/open-feature/ruby-sdk/issues/348)) ([9e82b15](https://github.com/open-feature/ruby-sdk/commit/9e82b15e9730f0c53f2186aeb39cc53275d5f929))
32
+ * resolve a client's provider by domain on every call ([#352](https://github.com/open-feature/ruby-sdk/issues/352)) ([fa0076f](https://github.com/open-feature/ruby-sdk/commit/fa0076fb8a159a180af408ee0a335e9f8d43afc9))
33
+
3
34
  ## [0.6.7](https://github.com/open-feature/ruby-sdk/compare/v0.6.6...v0.6.7) (2026-10-23)
4
35
 
5
36
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- openfeature-sdk (0.6.7)
4
+ openfeature-sdk (0.7.1)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -17,8 +17,8 @@
17
17
  </a>
18
18
  <!-- x-release-please-start-version -->
19
19
 
20
- <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.6.7">
21
- <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.6.7&color=blue&style=for-the-badge" />
20
+ <a href="https://github.com/open-feature/ruby-sdk/releases/tag/v0.7.1">
21
+ <img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v0.7.1&color=blue&style=for-the-badge" />
22
22
  </a>
23
23
 
24
24
  <!-- x-release-please-end -->
@@ -287,6 +287,7 @@ Log output uses a structured key=value format:
287
287
 
288
288
  Clients can be assigned to a domain. A domain is a logical identifier which can be used to associate clients with a particular provider.
289
289
  If a domain has no associated provider, the default provider is used.
290
+ Clients look up their domain's provider on every call, so a client created before `set_provider` follows the new binding.
290
291
 
291
292
  ```ruby
292
293
  OpenFeature::SDK.configure do |config|
@@ -300,7 +301,11 @@ legacy_flag_client = OpenFeature::SDK.build_client(domain: "legacy_flags")
300
301
  ### Eventing
301
302
 
302
303
  Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
303
- Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
304
+ Providers emit initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) from `init`.
305
+ A provider without `init` is `READY` as soon as it's registered, and its `PROVIDER_READY` handlers run before `set_provider` returns.
306
+ For providers that define `init` but not `emits_lifecycle_events?` returning `true`, the SDK emits `PROVIDER_READY` after `init` returns and `PROVIDER_ERROR` if it raises.
307
+ That path is deprecated (spec Appendix E), and the SDK logs a deprecation warning once per provider class to its configured logger.
308
+ Handlers for events a provider emits from `init` run while `init` is still in progress, so they can't call `set_provider_and_wait` for that same provider.
304
309
  Some providers emit additional events: `PROVIDER_CONFIGURATION_CHANGED`, `PROVIDER_STALE`, `PROVIDER_RECONCILING` and `PROVIDER_CONTEXT_CHANGED`.
305
310
  Handlers receive a hash with the `provider_name` and any details the provider sent, such as `message` or `error_code`.
306
311
 
@@ -314,15 +319,17 @@ end
314
319
 
315
320
  OpenFeature::SDK.add_handler(OpenFeature::SDK::ProviderEvent::PROVIDER_READY, ready_handler)
316
321
 
317
- # The SDK automatically emits lifecycle events. Providers can emit additional spontaneous events
318
- # using the EventEmitter mixin to signal internal state changes like configuration updates.
322
+ # Providers emit events using the EventEmitter mixin: lifecycle events from init, and spontaneous
323
+ # events to signal internal state changes like configuration updates.
319
324
  class MyEventAwareProvider
320
325
  include OpenFeature::SDK::Provider::EventEmitter
321
326
 
327
+ def emits_lifecycle_events? = true
328
+
322
329
  def init(evaluation_context)
323
330
  # Start background process to monitor for configuration changes
324
- # Note: SDK automatically emits PROVIDER_READY when init completes successfully
325
331
  start_background_process
332
+ emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
326
333
  end
327
334
 
328
335
  def start_background_process
@@ -436,6 +443,29 @@ class MyRequestScopedPropagator
436
443
  end
437
444
  ```
438
445
 
446
+ ### Isolated API instances
447
+
448
+ `OpenFeature::SDK` is a global singleton. Some applications need more than one independent API in a process, for example separately developed modules that each need their own provider, or tests that run in parallel. Require the factory explicitly to create one:
449
+
450
+ ```ruby
451
+ require "open_feature/sdk/isolated"
452
+
453
+ api = OpenFeature::SDK::Isolated.create_api
454
+ api.set_provider_and_wait(MyProvider.new)
455
+ client = api.build_client
456
+ client.fetch_boolean_value(flag_key: "my-flag", default_value: false)
457
+ ```
458
+
459
+ Each instance has its own providers, hooks, evaluation context, event handlers, transaction context propagator and `shutdown`, and shares none of them with `OpenFeature::SDK` or other instances.
460
+ Build its clients with `api.build_client`; `OpenFeature::SDK::Client.new` binds to the global API.
461
+ Prefer the global API unless you need this.
462
+
463
+ A provider instance should be registered with only one API at a time.
464
+ Registering a provider that includes `OpenFeature::SDK::Provider::EventEmitter` with a second API raises `ArgumentError`; other providers can't be detected.
465
+ Once the first API replaces the provider or shuts down, it can be registered with another.
466
+
467
+ A `LoggingHook` created without `logger:` logs to the global API's logger, even when added to an isolated API. Pass `logger:` to use another.
468
+
439
469
  ## Extending
440
470
 
441
471
  ### Develop a provider
@@ -446,13 +476,23 @@ You’ll then need to write the provider by implementing the `Provider` duck.
446
476
 
447
477
  ```ruby
448
478
  class MyProvider
479
+ include OpenFeature::SDK::Provider::EventEmitter
480
+
481
+ # Tells the SDK that init emits PROVIDER_READY/PROVIDER_ERROR itself.
482
+ def emits_lifecycle_events? = true
483
+
484
+ # Optional: a provider without init is READY as soon as it's registered.
449
485
  # Both arguments are optional; declare only the ones you need.
450
486
  # `domain` is the domain the provider was bound to, or nil for the default provider.
451
487
  def init(evaluation_context = nil, domain: nil)
452
- # Perform any initialization steps with flag management system here
488
+ # Perform any initialization steps with flag management system here, then emit PROVIDER_READY.
453
489
  # Return value is ignored
454
490
  # **Note** The OpenFeature spec defines a lifecycle method called `initialize` to be called when a new provider is set.
455
491
  # To avoid conflicting with the Ruby `initialize` method, this method should be named `init` when creating a provider.
492
+ emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_READY)
493
+ rescue => e
494
+ emit_event(OpenFeature::SDK::ProviderEvent::PROVIDER_ERROR, error_code: OpenFeature::SDK::Provider::ErrorCode::GENERAL, message: e.message)
495
+ raise
456
496
  end
457
497
 
458
498
  # Optional: return true if this provider keeps state for a single domain.
@@ -32,13 +32,15 @@ module OpenFeature
32
32
  #
33
33
  # client = OpenFeature::SDK::API.instance.build_client(name: 'my-open-feature-client')
34
34
  class API
35
- include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1
35
+ include Singleton # Satisfies Flag Evaluation API Requirement 1.1.1; Isolated.create_api builds others (1.8.1)
36
36
  extend Forwardable
37
37
 
38
38
  def_delegators :configuration, :provider, :set_provider, :set_provider_and_wait, :hooks, :add_hooks, :evaluation_context
39
39
 
40
- def configuration
41
- @configuration ||= Configuration.new
40
+ attr_reader :configuration
41
+
42
+ def initialize
43
+ @configuration = Configuration.new
42
44
  end
43
45
 
44
46
  def configure(&block)
@@ -48,11 +50,7 @@ module OpenFeature
48
50
  end
49
51
 
50
52
  def build_client(domain: nil, evaluation_context: nil)
51
- active_provider = provider(domain:).nil? ? Provider::NoOpProvider.new : provider(domain:)
52
-
53
- Client.new(provider: active_provider, domain:, evaluation_context:)
54
- rescue
55
- Client.new(provider: Provider::NoOpProvider.new, evaluation_context:)
53
+ Client.new(domain:, evaluation_context:, configuration:)
56
54
  end
57
55
 
58
56
  def provider_metadata(domain: nil)
@@ -17,12 +17,13 @@ module OpenFeature
17
17
  SUFFIXES = %i[value details].freeze
18
18
  EMPTY_HINTS = Hooks::Hints.new.freeze
19
19
 
20
- attr_reader :metadata, :evaluation_context
20
+ attr_reader :metadata
21
21
 
22
- attr_accessor :hooks
22
+ attr_accessor :hooks, :evaluation_context
23
23
 
24
- def initialize(provider:, domain: nil, evaluation_context: nil)
24
+ def initialize(provider: nil, domain: nil, evaluation_context: nil, configuration: API.instance.configuration)
25
25
  @provider = provider
26
+ @configuration = configuration
26
27
  @metadata = ClientMetadata.new(domain:)
27
28
  @evaluation_context = evaluation_context
28
29
  @hooks = []
@@ -33,17 +34,17 @@ module OpenFeature
33
34
  end
34
35
 
35
36
  def provider_status
36
- OpenFeature::SDK.configuration.provider_state(@provider)
37
+ @configuration.provider_state(resolve_provider)
37
38
  end
38
39
 
39
40
  def add_handler(event_type, handler = nil, &block)
40
41
  actual_handler = handler || block
41
- OpenFeature::SDK.configuration.add_client_handler(self, event_type, actual_handler)
42
+ @configuration.add_client_handler(self, event_type, actual_handler)
42
43
  end
43
44
 
44
45
  def remove_handler(event_type, handler = nil, &block)
45
46
  actual_handler = handler || block
46
- OpenFeature::SDK.configuration.remove_client_handler(self, event_type, actual_handler)
47
+ @configuration.remove_client_handler(self, event_type, actual_handler)
47
48
  end
48
49
 
49
50
  # Tracking API (spec 6.1.1.1) — dynamic-context paradigm
@@ -51,12 +52,13 @@ module OpenFeature
51
52
  # Records a tracking event. If the provider does not implement
52
53
  # tracking, this is a no-op (spec 6.1.4).
53
54
  def track(tracking_event_name, evaluation_context: nil, tracking_event_details: nil)
54
- return unless @provider.respond_to?(:track)
55
+ provider = resolve_provider
56
+ return unless provider.respond_to?(:track)
55
57
 
56
58
  built_context = build_evaluation_context(evaluation_context)
57
59
 
58
- @provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
59
- rescue
60
+ provider.track(tracking_event_name, evaluation_context: built_context, tracking_event_details: tracking_event_details)
61
+ rescue StandardError, NotImplementedError
60
62
  nil
61
63
  end
62
64
 
@@ -75,16 +77,18 @@ module OpenFeature
75
77
  private
76
78
 
77
79
  def fetch_details(type:, flag_key:, default_value:, evaluation_context: nil, invocation_hooks: [], hook_hints: nil)
80
+ provider = resolve_provider
78
81
  built_context = build_evaluation_context(evaluation_context)
79
82
 
80
83
  # Assemble ordered hooks: API → Client → Invocation → Provider (spec 4.4.2)
81
- provider_hooks = @provider.respond_to?(:hooks) ? Array(@provider.hooks) : []
82
- ordered_hooks = [*OpenFeature::SDK.hooks, *@hooks, *invocation_hooks, *provider_hooks]
84
+ provider_hooks = provider.respond_to?(:hooks) ? Array(provider.hooks) : []
85
+ ordered_hooks = [*@configuration.hooks, *@hooks, *invocation_hooks, *provider_hooks]
83
86
 
84
- # Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature)
87
+ # Short-circuit NOT_READY/FATAL providers (required by evaluation_v2.feature). A provider resolved from the
88
+ # configuration is tracked while bound, so one that isn't was replaced mid-call and reads NOT_READY.
85
89
  short_circuit_code = nil
86
- if OpenFeature::SDK.configuration.provider_tracked?(@provider)
87
- short_circuit_code = short_circuit_error_code(provider_status)
90
+ if @provider ? @configuration.provider_tracked?(provider) : !provider.equal?(@fallback_provider)
91
+ short_circuit_code = short_circuit_error_code(@configuration.provider_state(provider))
88
92
  end
89
93
 
90
94
  # Fast path: skip hook ceremony when no hooks are registered
@@ -98,7 +102,7 @@ module OpenFeature
98
102
  return EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
99
103
  end
100
104
 
101
- return evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
105
+ return evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: built_context)
102
106
  end
103
107
 
104
108
  hook_context = Hooks::HookContext.new(
@@ -107,7 +111,7 @@ module OpenFeature
107
111
  default_value: default_value,
108
112
  evaluation_context: built_context,
109
113
  client_metadata: @metadata,
110
- provider_metadata: @provider.respond_to?(:metadata) ? @provider.metadata : nil
114
+ provider_metadata: provider.respond_to?(:metadata) ? provider.metadata : nil
111
115
  )
112
116
 
113
117
  hints = if hook_hints.is_a?(Hooks::Hints)
@@ -118,7 +122,7 @@ module OpenFeature
118
122
  EMPTY_HINTS
119
123
  end
120
124
 
121
- executor = Hooks::HookExecutor.new(logger: OpenFeature::SDK.configuration.logger)
125
+ executor = Hooks::HookExecutor.new(logger: @configuration.logger)
122
126
 
123
127
  if short_circuit_code
124
128
  resolution = Provider::ResolutionDetails.new(
@@ -131,7 +135,7 @@ module OpenFeature
131
135
  evaluation_details
132
136
  else
133
137
  executor.execute(ordered_hooks: ordered_hooks, hook_context: hook_context, hints: hints) do |hctx|
134
- evaluate_flag(type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
138
+ evaluate_flag(provider: provider, type: type, flag_key: flag_key, default_value: default_value, evaluation_context: hctx.evaluation_context)
135
139
  end
136
140
  end
137
141
  rescue StandardError, NotImplementedError => e
@@ -145,8 +149,8 @@ module OpenFeature
145
149
  EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution)
146
150
  end
147
151
 
148
- def evaluate_flag(type:, flag_key:, default_value:, evaluation_context:)
149
- resolution_details = @provider.send(
152
+ def evaluate_flag(provider:, type:, flag_key:, default_value:, evaluation_context:)
153
+ resolution_details = provider.send(
150
154
  :"fetch_#{type}_value",
151
155
  flag_key: flag_key,
152
156
  default_value: default_value,
@@ -163,6 +167,12 @@ module OpenFeature
163
167
  EvaluationDetails.new(flag_key: flag_key, resolution_details: resolution_details)
164
168
  end
165
169
 
170
+ # Callers resolve once per operation, so a rebinding mid-evaluation can't mix two providers.
171
+ # The fallback NoOp is never registered: untracked, so not short-circuited, and its status reads NOT_READY.
172
+ def resolve_provider
173
+ @provider || @configuration.provider(domain: @metadata.domain) || (@fallback_provider ||= Provider::NoOpProvider.new)
174
+ end
175
+
166
176
  def short_circuit_error_code(state)
167
177
  case state
168
178
  when ProviderState::NOT_READY then Provider::ErrorCode::PROVIDER_NOT_READY
@@ -171,11 +181,11 @@ module OpenFeature
171
181
  end
172
182
 
173
183
  def build_evaluation_context(invocation_context)
174
- propagator = OpenFeature::SDK.configuration.transaction_context_propagator
184
+ propagator = @configuration.transaction_context_propagator
175
185
  transaction_context = propagator&.get_transaction_context
176
186
 
177
187
  EvaluationContextBuilder.new.call(
178
- api_context: OpenFeature::SDK.evaluation_context,
188
+ api_context: @configuration.evaluation_context,
179
189
  transaction_context: transaction_context,
180
190
  client_context: evaluation_context,
181
191
  invocation_context: invocation_context
@@ -4,6 +4,11 @@ module OpenFeature
4
4
  module SDK
5
5
  ClientMetadata = Struct.new(:domain, keyword_init: true) do
6
6
  alias_method :name, :domain
7
+
8
+ def initialize(domain: nil)
9
+ super(domain: domain&.dup&.freeze)
10
+ freeze
11
+ end
7
12
  end
8
13
  end
9
14
  end
@@ -21,19 +21,21 @@ module OpenFeature
21
21
  attr_reader :logger
22
22
 
23
23
  # One per registration; +done+ is closed once init finishes, so waiters can pop on it
24
- InitAttempt = Struct.new(:state, :error, :done)
24
+ InitAttempt = Struct.new(:state, :error, :done, :thread)
25
25
  private_constant :InitAttempt
26
26
 
27
27
  def initialize
28
28
  @hooks = []
29
29
  @providers = {}
30
30
  @init_attempts = {}.compare_by_identity
31
+ @legacy_warned = Set.new.compare_by_identity
31
32
  @provider_mutex = Mutex.new
32
33
  @logger = nil
33
34
  @event_dispatcher = EventDispatcher.new(@logger)
34
35
  @provider_state_registry = ProviderStateRegistry.new
35
36
  @client_handlers = {}
36
37
  @client_handlers_mutex = Mutex.new
38
+ @dispatch_key = :"openfeature_dispatch_#{object_id}"
37
39
  end
38
40
 
39
41
  def add_hooks(*new_hooks)
@@ -60,6 +62,8 @@ module OpenFeature
60
62
 
61
63
  # @api private
62
64
  def add_client_handler(client, event_type, handler)
65
+ @event_dispatcher.validate_handler!(event_type, handler)
66
+
63
67
  @client_handlers_mutex.synchronize do
64
68
  @client_handlers[client] ||= Hash.new { |h, k| h[k] = [] }
65
69
  @client_handlers[client][event_type] << handler
@@ -123,9 +127,9 @@ module OpenFeature
123
127
  @provider_state_registry.clear
124
128
  @provider_mutex.synchronize do
125
129
  @providers.clear
126
- # Release anyone waiting on an init that shutdown cut short
127
- @init_attempts.each_value { |attempt| attempt.done.close }
130
+ @init_attempts.each_value { |attempt| abandon_init_attempt(attempt) }
128
131
  @init_attempts.clear
132
+ @legacy_warned.clear
129
133
  end
130
134
  @hooks.clear
131
135
  @evaluation_context = nil
@@ -144,31 +148,36 @@ module OpenFeature
144
148
 
145
149
  # Provider code runs before taking the lock: it may call back into the SDK
146
150
  scoped = domain_scoped?(provider)
151
+ has_init = provider.respond_to?(:init)
152
+ legacy = has_init && !emits_lifecycle_events?(provider)
153
+ warn_legacy_provider(provider) if legacy
147
154
 
148
155
  @provider_mutex.synchronize do
149
156
  validate_domain_scoped_binding!(provider, domain, scoped)
150
157
 
151
158
  old_provider = @providers[domain]
152
159
 
153
- new_providers = @providers.dup
154
- new_providers[domain] = provider
155
- @providers = new_providers
156
-
157
160
  # Spec 1.1.2.2: initialize once per registration; a failed init is retried on the next set
158
161
  attempt = @init_attempts[provider]
159
162
  needs_init = attempt.nil? || attempt.state == :failed
160
163
 
161
164
  if needs_init
165
+ # First, so a provider registered with another API (1.8.4) leaves no state behind
166
+ provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
162
167
  attempt = @init_attempts[provider] = InitAttempt.new(:running, nil, Thread::Queue.new)
163
168
  @provider_state_registry.set_initial_state(provider)
164
- provider.send(:attach, self) if provider.is_a?(Provider::EventEmitter)
165
169
  end
166
170
 
171
+ # Publish last: clients read @providers without the lock, and must not see an untracked provider
172
+ new_providers = @providers.dup
173
+ new_providers[domain] = provider
174
+ @providers = new_providers
175
+
167
176
  # Spec 1.1.2.3: Only shutdown old provider if it's no longer bound to any domain
168
177
  if old_provider && !old_provider.equal?(provider)
169
178
  needs_shutdown = !provider_bound?(old_provider)
170
179
  if needs_shutdown
171
- @init_attempts.delete(old_provider)
180
+ abandon_init_attempt(@init_attempts.delete(old_provider))
172
181
  @provider_state_registry.remove_provider(old_provider)
173
182
  old_provider.send(:detach) if old_provider.is_a?(Provider::EventEmitter)
174
183
  end
@@ -186,37 +195,43 @@ module OpenFeature
186
195
 
187
196
  # Initialize provider outside the mutex to avoid blocking other operations
188
197
  if needs_init
189
- if wait_for_init
190
- init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: true)
198
+ # Spec 2.8.5.1: a provider without init is READY, and its handlers have run, when set_provider returns
199
+ if wait_for_init || !has_init
200
+ init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: wait_for_init)
201
+ # Replaced or shut down while init ran: fail like the waiters on this attempt did
202
+ raise incomplete_init_error(provider, "initialization did not complete") if wait_for_init && attempt.state != :succeeded
191
203
  else
192
204
  init_thread = Thread.new do
193
205
  init_provider(provider, context_for_init, domain:, attempt:, raise_on_error: false)
194
206
  end
195
207
  end
208
+ elsif !has_init
209
+ # Spec 2.8.5.1: each new binding of a provider without init runs its domain's READY handlers
210
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) unless old_provider.equal?(provider)
196
211
  elsif wait_for_init
197
- # Already registered: wait for an init still in flight, then report its outcome
212
+ # Already registered: wait for an init still in flight, then report its outcome.
213
+ # From inside that init (e.g. a handler of an event it emits) the wait could never end.
214
+ raise incomplete_init_error(provider, "is still initializing on this thread") if running_on_this_thread?(attempt)
215
+
198
216
  attempt.done.pop
199
- unless attempt.state == :succeeded
200
- raise attempt.error || ProviderInitializationError.new(
201
- "Provider #{provider.class.name} initialization did not complete",
202
- provider:,
203
- error_code: Provider::ErrorCode::GENERAL
204
- )
205
- end
217
+ raise attempt.error || incomplete_init_error(provider, "initialization did not complete") unless attempt.state == :succeeded
206
218
 
207
- dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
219
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) if legacy
208
220
  end
209
221
  ensure
210
222
  # Fail an attempt no init took over (e.g. a Timeout hit first), or it would stay running forever
211
- finish_init_attempt(attempt, :failed) if needs_init && !init_thread
223
+ finish_init_attempt(attempt, :failed) if needs_init && attempt && !init_thread
212
224
  end
213
225
 
214
226
  def init_provider(provider, context, domain:, attempt:, raise_on_error: false)
227
+ emits_lifecycle_events = emits_lifecycle_events?(provider)
228
+ attempt.thread = Thread.current
215
229
  call_init(provider, context, domain)
216
230
 
217
- # Settle the attempt before handlers run, so a handler can register this provider again
218
- finish_init_attempt(attempt, :succeeded)
219
- dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY)
231
+ # Settle the attempt before handlers run, so a handler can register this provider again.
232
+ # An abandoned attempt (provider replaced or shut down meanwhile) reports no READY.
233
+ # Spec 2.8.2/2.8.3: a provider that emits its own lifecycle events gets no synthetic READY or ERROR
234
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_READY, token: attempt) if finish_init_attempt(attempt, :succeeded) && !emits_lifecycle_events
220
235
  rescue => e
221
236
  # Spec 1.7.5: PROVIDER_FATAL from init makes the provider FATAL
222
237
  error_code = EvaluationError.code_for(e)
@@ -228,9 +243,10 @@ module OpenFeature
228
243
  )
229
244
  finish_init_attempt(attempt, :failed, error)
230
245
 
231
- dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
232
- error_code: error_code,
233
- message: e.message)
246
+ unless emits_lifecycle_events
247
+ dispatch_provider_event(provider, ProviderEvent::PROVIDER_ERROR,
248
+ {error_code: error_code, message: e.message}, token: attempt)
249
+ end
234
250
 
235
251
  raise error if raise_on_error
236
252
  ensure
@@ -238,35 +254,80 @@ module OpenFeature
238
254
  finish_init_attempt(attempt, :failed)
239
255
  end
240
256
 
257
+ # Caller holds @provider_mutex: the provider was unbound or the API reset before its init settled
258
+ def abandon_init_attempt(attempt)
259
+ return unless attempt
260
+
261
+ attempt.state = :failed if attempt.state == :running
262
+ attempt.done.close
263
+ end
264
+
265
+ def incomplete_init_error(provider, reason)
266
+ ProviderInitializationError.new("Provider #{provider.class.name} #{reason}", provider:, error_code: Provider::ErrorCode::GENERAL)
267
+ end
268
+
241
269
  def finish_init_attempt(attempt, state, error = nil)
242
270
  @provider_mutex.synchronize do
243
- return unless attempt.state == :running
271
+ return false unless attempt.state == :running
244
272
 
245
273
  attempt.state = state
246
274
  attempt.error = error
247
275
  end
248
276
  attempt.done.close
277
+ true
249
278
  end
250
279
 
251
- def dispatch_provider_event(provider, event_type, details = {})
252
- # Under the replacement lock, so a late event can't re-track a replaced or shut-down provider
253
- @provider_mutex.synchronize do
254
- return unless provider_bound?(provider)
255
-
256
- @provider_state_registry.update_state_from_event(provider, event_type, details)
280
+ # An event for a provider already dispatching on this call stack is queued behind the current one
281
+ def dispatch_provider_event(provider, event_type, details = {}, token: nil)
282
+ # Lifecycle events (the SDK's own, or the provider's from its init thread) record status when sent, so
283
+ # set_provider_and_wait returns with it even from the provider's own handler (1.1.2.4); others record it when delivered (5.3.5)
284
+ token ||= running_init_attempt_on_this_thread(provider)
285
+ return if token && !accept_provider_event(provider, event_type, details, token)
286
+
287
+ queues = (Thread.current[@dispatch_key] ||= {}.compare_by_identity)
288
+ if (queue = queues[provider])
289
+ queue << [event_type, details.dup, token]
290
+ return
257
291
  end
258
292
 
259
- provider_name = extract_provider_name(provider)
293
+ queue = queues[provider] = [[event_type, details, token]]
294
+ begin
295
+ deliver_provider_event(provider, *queue.shift)
296
+ until queue.empty?
297
+ begin
298
+ deliver_provider_event(provider, *queue.shift)
299
+ rescue => e
300
+ # Its own emitter already returned, so log rather than fail the event that was dispatching
301
+ @logger&.error("Queued provider event failed: #{e.message}")
302
+ end
303
+ end
304
+ ensure
305
+ queues.delete(provider)
306
+ Thread.current[@dispatch_key] = nil if queues.empty?
307
+ end
308
+ end
260
309
 
261
- event_details = {
262
- provider_name: provider_name
263
- }.merge(details)
310
+ def deliver_provider_event(provider, event_type, details, token)
311
+ return unless accept_provider_event(provider, event_type, details, token, record_status: token.nil?)
264
312
 
313
+ event_details = {provider_name: extract_provider_name(provider)}.merge(details)
265
314
  run_handlers_for_provider(provider, event_type, event_details)
266
315
  end
267
316
 
268
317
  private
269
318
 
319
+ # Under the replacement lock, so a late event can't re-track a replaced or shut-down provider
320
+ def accept_provider_event(provider, event_type, details, token, record_status: true)
321
+ @provider_mutex.synchronize do
322
+ return false unless provider_bound?(provider)
323
+ # An SDK READY/ERROR from an earlier registration of this provider is stale
324
+ return false unless token.nil? || @init_attempts[provider].equal?(token)
325
+
326
+ @provider_state_registry.update_state_from_event(provider, event_type, details) if record_status
327
+ true
328
+ end
329
+ end
330
+
270
331
  def provider_bound?(provider)
271
332
  @providers.each_value.any? { |p| p.equal?(provider) }
272
333
  end
@@ -279,6 +340,26 @@ module OpenFeature
279
340
  provider.respond_to?(:domain_scoped?) && provider.domain_scoped?
280
341
  end
281
342
 
343
+ # Spec 2.8.5.1: without init there's nothing to report, so the marker only counts alongside init
344
+ def emits_lifecycle_events?(provider)
345
+ provider.respond_to?(:init) && provider.respond_to?(:emits_lifecycle_events?) && provider.emits_lifecycle_events?
346
+ end
347
+
348
+ def running_init_attempt_on_this_thread(provider)
349
+ attempt = @provider_mutex.synchronize { @init_attempts[provider] }
350
+ attempt if attempt && running_on_this_thread?(attempt)
351
+ end
352
+
353
+ def running_on_this_thread?(attempt)
354
+ attempt.state == :running && attempt.thread.equal?(Thread.current)
355
+ end
356
+
357
+ def warn_legacy_provider(provider)
358
+ return if @logger.nil? || !@provider_mutex.synchronize { @legacy_warned.add?(provider.class) }
359
+ @logger.warn("Provider #{extract_provider_name(provider)} relies on SDK-emitted lifecycle events, which are deprecated; " \
360
+ "emit PROVIDER_READY/PROVIDER_ERROR from init and define emits_lifecycle_events? returning true (spec Appendix E)")
361
+ end
362
+
282
363
  def validate_domain_scoped_binding!(provider, domain, scoped)
283
364
  return unless scoped && provider_bound?(provider) && !@providers[domain].equal?(provider)
284
365
 
@@ -313,22 +394,20 @@ module OpenFeature
313
394
  # Run global handlers (API-level, no domain filtering)
314
395
  @event_dispatcher.trigger_event(event_type, event_details)
315
396
 
316
- # Run client handlers (domain-scoped)
317
- @client_handlers_mutex.synchronize do
318
- @client_handlers.each do |client, handlers_by_event|
397
+ # Run client handlers (domain-scoped) outside the lock, so they can add or remove handlers
398
+ handlers = @client_handlers_mutex.synchronize do
399
+ @client_handlers.flat_map do |client, handlers_by_event|
319
400
  # Check if this client should receive events from this provider
320
401
  client_provider = provider(domain: client.metadata.domain)
321
- next unless client_provider&.equal?(provider)
322
-
323
- # Trigger handlers for this client
324
- handlers = handlers_by_event[event_type]
325
- handlers.each do |handler|
326
- handler.call(event_details)
327
- rescue => e
328
- @logger&.error("Client event handler failed: #{e.message}")
329
- end
402
+ client_provider&.equal?(provider) ? handlers_by_event[event_type] : []
330
403
  end
331
404
  end
405
+
406
+ handlers.each do |handler|
407
+ handler.call(event_details)
408
+ rescue => e
409
+ @logger&.error("Client event handler failed: #{e.message}")
410
+ end
332
411
  end
333
412
 
334
413
  def run_immediate_handler(event_type, handler, client)
@@ -340,8 +419,8 @@ module OpenFeature
340
419
  }
341
420
 
342
421
  if client.nil?
343
- # API-level handler: check all providers
344
- @providers.each do |domain, provider|
422
+ # API-level handler: check each bound provider once, however many domains it's bound to
423
+ @providers.values.uniq(&:object_id).each do |provider|
345
424
  next unless provider
346
425
 
347
426
  provider_state = @provider_state_registry.get_state(provider)
@@ -16,14 +16,18 @@ module OpenFeature
16
16
  end
17
17
 
18
18
  def add_handler(event_type, handler)
19
- raise ArgumentError, "Invalid event type: #{event_type}" unless valid_event?(event_type)
20
- raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
19
+ validate_handler!(event_type, handler)
21
20
 
22
21
  @mutex.synchronize do
23
22
  @handlers[event_type] << handler
24
23
  end
25
24
  end
26
25
 
26
+ def validate_handler!(event_type, handler)
27
+ raise ArgumentError, "Invalid event type: #{event_type}" unless valid_event?(event_type)
28
+ raise ArgumentError, "Handler must respond to call" unless handler.respond_to?(:call)
29
+ end
30
+
27
31
  def remove_handler(event_type, handler)
28
32
  return unless valid_event?(event_type)
29
33
 
@@ -54,7 +54,7 @@ module OpenFeature
54
54
  else
55
55
  run_after_hooks(ordered_hooks, hook_context, evaluation_details, hints)
56
56
  end
57
- rescue => e
57
+ rescue StandardError, NotImplementedError => e
58
58
  run_error_hooks(ordered_hooks, hook_context, e, hints)
59
59
 
60
60
  evaluation_details = EvaluationDetails.new(
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../sdk"
4
+
5
+ module OpenFeature
6
+ module SDK
7
+ # Spec 1.8.3: a separate require, so isolated instances are an explicit opt-in
8
+ module Isolated
9
+ # Spec 1.8.1: a new API that shares no state with the global one or other isolated APIs
10
+ def self.create_api
11
+ API.send(:new)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -8,7 +8,7 @@ module OpenFeature
8
8
  # Mixin for providers that emit lifecycle events
9
9
  module EventEmitter
10
10
  def emit_event(event_type, details = {})
11
- config = @configuration
11
+ config = @openfeature_configuration
12
12
  return unless config
13
13
 
14
14
  unless ::OpenFeature::SDK::ProviderEvent::ALL_EVENTS.include?(event_type)
@@ -19,17 +19,22 @@ module OpenFeature
19
19
  end
20
20
 
21
21
  def configuration_attached?
22
- !@configuration.nil?
22
+ !@openfeature_configuration.nil?
23
23
  end
24
24
 
25
25
  private
26
26
 
27
27
  def attach(configuration)
28
- @configuration = configuration
28
+ # Spec 1.8.4: only providers using this mixin know which API they're registered with
29
+ if @openfeature_configuration && !@openfeature_configuration.equal?(configuration)
30
+ raise ArgumentError, "Provider is already registered with another OpenFeature API instance"
31
+ end
32
+
33
+ @openfeature_configuration = configuration
29
34
  end
30
35
 
31
36
  def detach
32
- @configuration = nil
37
+ @openfeature_configuration = nil
33
38
  end
34
39
  end
35
40
  end
@@ -19,10 +19,6 @@ module OpenFeature
19
19
  @flags = flags
20
20
  end
21
21
 
22
- def init(evaluation_context = nil)
23
- # Intentional no-op, used for testing
24
- end
25
-
26
22
  def shutdown
27
23
  # Intentional no-op, used for testing
28
24
  end
@@ -23,7 +23,7 @@ module OpenFeature
23
23
  # * <tt>fetch_object_value</tt> - Retrieve feature flag object value
24
24
  #
25
25
  class NoOpProvider
26
- REASON_NO_OP = "No-op"
26
+ REASON_NO_OP = Reason::DEFAULT
27
27
  NAME = "No-op Provider"
28
28
 
29
29
  attr_reader :metadata
@@ -21,8 +21,8 @@ module OpenFeature
21
21
  # @param message [String] the error message
22
22
  # @param provider [Object] the provider that failed to initialize
23
23
  # @param original_error [Exception] the original error that caused the failure
24
- # @param error_code [String] the OpenFeature error code (defaults to PROVIDER_FATAL)
25
- def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::PROVIDER_FATAL)
24
+ # @param error_code [String] the OpenFeature error code (defaults to GENERAL)
25
+ def initialize(message, provider: nil, original_error: nil, error_code: Provider::ErrorCode::GENERAL)
26
26
  super(message)
27
27
  @provider = provider
28
28
  @original_error = original_error
@@ -9,7 +9,7 @@ module OpenFeature
9
9
  # Tracks provider states
10
10
  class ProviderStateRegistry
11
11
  def initialize
12
- @states = {}
12
+ @states = {}.compare_by_identity
13
13
  @mutex = Mutex.new
14
14
  end
15
15
 
@@ -17,7 +17,7 @@ module OpenFeature
17
17
  return unless provider
18
18
 
19
19
  @mutex.synchronize do
20
- @states[provider.object_id] = {state: state, details: {}}
20
+ @states[provider] = {state: state, details: {}}
21
21
  end
22
22
  end
23
23
 
@@ -29,7 +29,7 @@ module OpenFeature
29
29
  # Only update state if the event should cause a state change
30
30
  if new_state
31
31
  @mutex.synchronize do
32
- @states[provider.object_id] = {state: new_state, details: event_details || {}}
32
+ @states[provider] = {state: new_state, details: event_details || {}}
33
33
  end
34
34
  new_state
35
35
  else
@@ -42,7 +42,7 @@ module OpenFeature
42
42
  return ProviderState::NOT_READY unless provider
43
43
 
44
44
  @mutex.synchronize do
45
- entry = @states[provider.object_id]
45
+ entry = @states[provider]
46
46
  entry ? entry[:state] : ProviderState::NOT_READY
47
47
  end
48
48
  end
@@ -51,7 +51,7 @@ module OpenFeature
51
51
  return {} unless provider
52
52
 
53
53
  @mutex.synchronize do
54
- entry = @states[provider.object_id]
54
+ entry = @states[provider]
55
55
  entry ? entry[:details] : {}
56
56
  end
57
57
  end
@@ -60,7 +60,7 @@ module OpenFeature
60
60
  return unless provider
61
61
 
62
62
  @mutex.synchronize do
63
- @states.delete(provider.object_id)
63
+ @states.delete(provider)
64
64
  end
65
65
  end
66
66
 
@@ -68,7 +68,7 @@ module OpenFeature
68
68
  return false unless provider
69
69
 
70
70
  @mutex.synchronize do
71
- @states.key?(provider.object_id)
71
+ @states.key?(provider)
72
72
  end
73
73
  end
74
74
 
@@ -5,14 +5,19 @@ module OpenFeature
5
5
  class ThreadLocalTransactionContextPropagator
6
6
  include TransactionContextPropagator
7
7
 
8
+ # One context per propagator under this key, so separate APIs never share one (spec 1.8.2)
9
+ # while resetting the key still clears them all. Weak keys let discarded propagators be collected.
8
10
  THREAD_KEY = :openfeature_transaction_context
9
11
 
10
12
  def set_transaction_context(evaluation_context)
11
- Thread.current[THREAD_KEY] = evaluation_context
13
+ contexts = Thread.current[THREAD_KEY]
14
+ contexts = Thread.current[THREAD_KEY] = ObjectSpace::WeakKeyMap.new unless contexts.is_a?(ObjectSpace::WeakKeyMap)
15
+ evaluation_context.nil? ? contexts.delete(self) : contexts[self] = evaluation_context
12
16
  end
13
17
 
14
18
  def get_transaction_context
15
- Thread.current[THREAD_KEY]
19
+ contexts = Thread.current[THREAD_KEY]
20
+ contexts[self] if contexts.is_a?(ObjectSpace::WeakKeyMap)
16
21
  end
17
22
  end
18
23
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module OpenFeature
4
4
  module SDK
5
- VERSION = "0.6.7"
5
+ VERSION = "0.7.1"
6
6
  end
7
7
  end
@@ -1,10 +1,13 @@
1
1
  module OpenFeature
2
2
  module SDK
3
3
  class API
4
- include Singleton
5
4
  extend Forwardable
6
5
 
7
- def configuration: () -> Configuration
6
+ include Singleton
7
+
8
+ attr_reader configuration: Configuration
9
+
10
+ def initialize: () -> void
8
11
 
9
12
  def configure: () { (Configuration) -> void } -> void
10
13
 
@@ -7,10 +7,10 @@ module OpenFeature
7
7
  EMPTY_HINTS: Hooks::Hints
8
8
 
9
9
  attr_reader metadata: ClientMetadata
10
- attr_reader evaluation_context: EvaluationContext?
10
+ attr_accessor evaluation_context: EvaluationContext?
11
11
  attr_accessor hooks: Array[untyped]
12
12
 
13
- def initialize: (provider: untyped, ?domain: String?, ?evaluation_context: EvaluationContext?) -> void
13
+ def initialize: (?provider: untyped, ?domain: String?, ?evaluation_context: EvaluationContext?, ?configuration: Configuration) -> void
14
14
 
15
15
  def add_hooks: (*Array[untyped] new_hooks) -> void
16
16
 
@@ -40,7 +40,8 @@ module OpenFeature
40
40
  private
41
41
 
42
42
  def fetch_details: (type: Symbol, flag_key: String, default_value: untyped, ?evaluation_context: EvaluationContext?, ?invocation_hooks: Array[untyped], ?hook_hints: Hooks::Hints?) -> EvaluationDetails?
43
- def evaluate_flag: (type: Symbol, flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> EvaluationDetails
43
+ def evaluate_flag: (provider: untyped, type: Symbol, flag_key: String, default_value: untyped, evaluation_context: EvaluationContext?) -> EvaluationDetails
44
+ def resolve_provider: () -> untyped
44
45
  def short_circuit_error_code: (String state) -> String?
45
46
  def build_evaluation_context: (EvaluationContext? invocation_context) -> EvaluationContext?
46
47
  def validate_default_value_type: (Symbol type, untyped default_value) -> void
@@ -33,10 +33,18 @@ module OpenFeature
33
33
  def reset: () -> void
34
34
  def set_provider_internal: (untyped provider, domain: String?, wait_for_init: bool) -> void
35
35
  def init_provider: (untyped provider, EvaluationContext? context, domain: String?, attempt: untyped, ?raise_on_error: bool) -> void
36
- def finish_init_attempt: (untyped attempt, Symbol state, ?ProviderInitializationError? error) -> void
37
- def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details) -> void
36
+ def finish_init_attempt: (untyped attempt, Symbol state, ?ProviderInitializationError? error) -> bool
37
+ def abandon_init_attempt: (untyped attempt) -> void
38
+ def incomplete_init_error: (untyped provider, String reason) -> ProviderInitializationError
39
+ def dispatch_provider_event: (untyped provider, String event_type, ?Hash[Symbol, untyped] details, ?token: untyped) -> void
40
+ def deliver_provider_event: (untyped provider, String event_type, Hash[Symbol, untyped]? details, untyped token) -> void
41
+ def accept_provider_event: (untyped provider, String event_type, Hash[Symbol, untyped]? details, untyped token, ?record_status: bool) -> bool
38
42
  def provider_bound?: (untyped provider) -> bool
39
43
  def domain_scoped?: (untyped provider) -> bool
44
+ def emits_lifecycle_events?: (untyped provider) -> bool
45
+ def warn_legacy_provider: (untyped provider) -> void
46
+ def running_init_attempt_on_this_thread: (untyped provider) -> untyped
47
+ def running_on_this_thread?: (untyped attempt) -> bool
40
48
  def validate_domain_scoped_binding!: (untyped provider, String? domain, bool scoped) -> void
41
49
  def call_init: (untyped provider, EvaluationContext? context, String? domain) -> void
42
50
  def init_accepts_domain?: (Array[[Symbol, Symbol?]] parameters) -> bool
@@ -6,6 +6,7 @@ module OpenFeature
6
6
  def initialize: (?untyped? logger) -> void
7
7
 
8
8
  def add_handler: (String event_type, _ToProc handler) -> void
9
+ def validate_handler!: (String event_type, untyped handler) -> void
9
10
  def remove_handler: (String event_type, _ToProc handler) -> void
10
11
  def remove_all_handlers: (String event_type) -> void
11
12
  def trigger_event: (String event_type, ?Hash[Symbol, untyped] event_details) -> void
@@ -0,0 +1,7 @@
1
+ module OpenFeature
2
+ module SDK
3
+ module Isolated
4
+ def self.create_api: () -> API
5
+ end
6
+ end
7
+ end
@@ -20,7 +20,6 @@ module OpenFeature
20
20
 
21
21
  def initialize: (?Hash[String, untyped] flags) -> void
22
22
 
23
- def init: (?EvaluationContext? evaluation_context) -> void
24
23
  def shutdown: () -> void
25
24
 
26
25
  def add_flag: (flag_key: String, value: untyped) -> void
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openfeature-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.7
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - OpenFeature Authors
@@ -55,6 +55,7 @@ files:
55
55
  - lib/open_feature/sdk/hooks/hook_context.rb
56
56
  - lib/open_feature/sdk/hooks/hook_executor.rb
57
57
  - lib/open_feature/sdk/hooks/logging_hook.rb
58
+ - lib/open_feature/sdk/isolated.rb
58
59
  - lib/open_feature/sdk/provider.rb
59
60
  - lib/open_feature/sdk/provider/error_code.rb
60
61
  - lib/open_feature/sdk/provider/event_emitter.rb
@@ -92,6 +93,7 @@ files:
92
93
  - sig/open_feature/sdk/hooks/hook_context.rbs
93
94
  - sig/open_feature/sdk/hooks/hook_executor.rbs
94
95
  - sig/open_feature/sdk/hooks/logging_hook.rbs
96
+ - sig/open_feature/sdk/isolated.rbs
95
97
  - sig/open_feature/sdk/provider.rbs
96
98
  - sig/open_feature/sdk/provider/error_code.rbs
97
99
  - sig/open_feature/sdk/provider/event_emitter.rbs