11package dev.openfeature.sdk
22
3- import dev.openfeature.sdk.events.OpenFeatureEvents
4- import dev.openfeature.sdk.events.awaitReadyOrError
5- import dev.openfeature.sdk.events.observe
3+ import dev.openfeature.sdk.exceptions.OpenFeatureError
64import kotlinx.coroutines.CoroutineDispatcher
7- import kotlinx.coroutines.ExperimentalCoroutinesApi
8- import kotlinx.coroutines.flow.Flow
5+ import kotlinx.coroutines.CoroutineScope
6+ import kotlinx.coroutines.Deferred
7+ import kotlinx.coroutines.Dispatchers
8+ import kotlinx.coroutines.async
99import kotlinx.coroutines.flow.MutableSharedFlow
1010import kotlinx.coroutines.flow.SharedFlow
11- import kotlinx.coroutines.flow.flatMapLatest
11+ import java.util.concurrent.CancellationException
1212
1313@Suppress(" TooManyFunctions" )
1414object OpenFeatureAPI {
15+ private var setProviderJob: Deferred <Unit >? = null
1516 private val NOOP_PROVIDER = NoOpProvider ()
1617 private var provider: FeatureProvider = NOOP_PROVIDER
1718 private var context: EvaluationContext ? = null
18- private val providersFlow: MutableSharedFlow <FeatureProvider > = MutableSharedFlow (replay = 1 )
19- internal val sharedProvidersFlow: SharedFlow <FeatureProvider > get() = providersFlow
19+
20+ private val _statusFlow : MutableSharedFlow <OpenFeatureStatus > =
21+ MutableSharedFlow <OpenFeatureStatus >(replay = 1 , extraBufferCapacity = 5 )
22+ .apply {
23+ tryEmit(OpenFeatureStatus .NotReady )
24+ }
25+
26+ /* *
27+ * A flow of [OpenFeatureStatus] that emits the current status of the SDK.
28+ */
29+ val statusFlow: SharedFlow <OpenFeatureStatus > get() = _statusFlow
2030
2131 var hooks: List <Hook <* >> = listOf ()
2232 private set
2333
24- fun setProvider (provider : FeatureProvider , initialContext : EvaluationContext ? = null) {
25- this @OpenFeatureAPI.provider = provider
26- providersFlow.tryEmit(provider)
27- if (initialContext != null ) context = initialContext
28- try {
29- provider.initialize(context)
30- } catch (e: Throwable ) {
31- // This is not allowed to happen
34+ /* *
35+ * Set the [FeatureProvider] for the SDK. This method will return immediately and initialize the provider in a coroutine scope
36+ * When the provider is successfully initialized it will set the status to Ready.
37+ * If the provider fails to initialize it will set the status to Error.
38+ *
39+ * This method requires you to manually wait for the status to be Ready before using the SDK for flag evaluations.
40+ * This can be done by using the [statusFlow] and waiting for the first Ready status or by accessing [getStatus]
41+ *
42+ * @param provider the provider to set
43+ * @param dispatcher the dispatcher to use for the provider initialization coroutine. Defaults to [Dispatchers.IO] if not set.
44+ * @param initialContext the initial [EvaluationContext] to use for the provider initialization. Defaults to an null context if not set.
45+ */
46+ fun setProvider (
47+ provider : FeatureProvider ,
48+ dispatcher : CoroutineDispatcher = Dispatchers .IO ,
49+ initialContext : EvaluationContext ? = null
50+ ) {
51+ setProviderJob?.cancel()
52+ this .setProviderJob = CoroutineScope (dispatcher).async {
53+ setProviderInternal(provider, initialContext)
3254 }
3355 }
3456
57+ /* *
58+ * Set the [FeatureProvider] for the SDK. This method will block until the provider is initialized.
59+ *
60+ * @param provider the [FeatureProvider] to set
61+ * @param initialContext the initial [EvaluationContext] to use for the provider initialization. Defaults to an null context if not set.
62+ */
3563 suspend fun setProviderAndWait (
3664 provider : FeatureProvider ,
37- dispatcher : CoroutineDispatcher ,
3865 initialContext : EvaluationContext ? = null
3966 ) {
40- setProvider(provider, initialContext)
41- provider.awaitReadyOrError(dispatcher)
67+ setProviderInternal(provider, initialContext)
68+ }
69+
70+ private suspend fun setProviderInternal (
71+ provider : FeatureProvider ,
72+ initialContext : EvaluationContext ? = null
73+ ) {
74+ this @OpenFeatureAPI.provider = provider
75+ // TODO consider if stale status should emit? _statusFlow.tryEmit(OpenFeatureStatus.Stale)
76+ if (initialContext != null ) context = initialContext
77+ try {
78+ getProvider().initialize(context)
79+ _statusFlow .tryEmit(OpenFeatureStatus .Ready )
80+ } catch (e: OpenFeatureError ) {
81+ _statusFlow .tryEmit(OpenFeatureStatus .Error (e))
82+ } catch (e: Throwable ) {
83+ _statusFlow .tryEmit(
84+ OpenFeatureStatus .Error (
85+ OpenFeatureError .GeneralError (
86+ e.message ? : e.javaClass.name
87+ )
88+ )
89+ )
90+ // TODO deal with things by setting status to Error or Fatal
91+ }
4292 }
4393
94+ /* *
95+ * Get the current [FeatureProvider] for the SDK.
96+ */
4497 fun getProvider (): FeatureProvider {
4598 return provider
4699 }
47100
101+ /* *
102+ * Clear the current [FeatureProvider] for the SDK and set it to a no-op provider.
103+ */
48104 fun clearProvider () {
49105 provider = NOOP_PROVIDER
50106 }
51107
52- fun setEvaluationContext (evaluationContext : EvaluationContext ) {
108+ /* *
109+ * Set the [EvaluationContext] for the SDK.
110+ * If the new context is different compare to the old context, this will cause the provider to reconcile with the new context.
111+ * When the provider "Reconciles" it will set the status to [OpenFeatureStatus.Reconciling].
112+ * When the provider successfully reconciles it will set the status to [OpenFeatureStatus.Ready].
113+ * If the provider fails to reconcile it will set the status to [OpenFeatureStatus.Error].
114+ *
115+ * @param evaluationContext the [EvaluationContext] to set
116+ */
117+ suspend fun setEvaluationContext (evaluationContext : EvaluationContext ) {
53118 val oldContext = context
54119 context = evaluationContext
55- getProvider().onContextSet(oldContext, evaluationContext)
120+ if (oldContext != evaluationContext) {
121+ _statusFlow .tryEmit(OpenFeatureStatus .Reconciling )
122+ try {
123+ getProvider().onContextSet(oldContext, evaluationContext)
124+ _statusFlow .tryEmit(OpenFeatureStatus .Ready )
125+ } catch (e: OpenFeatureError ) {
126+ _statusFlow .tryEmit(OpenFeatureStatus .Error (e))
127+ // TODO how do we handle fatal errors?
128+ } catch (e: Throwable ) {
129+ _statusFlow .tryEmit(
130+ OpenFeatureStatus .Error (
131+ OpenFeatureError .GeneralError (
132+ e.message ? : e.javaClass.name
133+ )
134+ )
135+ )
136+ }
137+ }
56138 }
57139
140+ /* *
141+ * Get the current [EvaluationContext] for the SDK.
142+ */
58143 fun getEvaluationContext (): EvaluationContext ? {
59144 return context
60145 }
61146
147+ /* *
148+ * Get the [ProviderMetadata] for the current [FeatureProvider].
149+ */
62150 fun getProviderMetadata (): ProviderMetadata ? {
63- return provider .metadata
151+ return getProvider() .metadata
64152 }
65153
154+ /* *
155+ * Get a [Client] for the SDK.
156+ * This client can be used to evaluate flags.
157+ */
66158 fun getClient (name : String? = null, version : String? = null): Client {
67159 return OpenFeatureClient (this , name, version)
68160 }
69161
162+ /* *
163+ * Add [Hook]s to the SDK.
164+ */
70165 fun addHooks (hooks : List <Hook <* >>) {
71166 this .hooks + = hooks
72167 }
73168
169+ /* *
170+ * Clear all [Hook]s from the SDK.
171+ */
74172 fun clearHooks () {
75173 this .hooks = listOf ()
76174 }
77175
176+ /* *
177+ * Shutdown the SDK.
178+ * This will cancel the provider set job and call the provider's shutdown method.
179+ * The SDK status will be set to [OpenFeatureStatus.NotReady].
180+ */
78181 fun shutdown () {
79- provider.shutdown()
182+ setProviderJob?.cancel(CancellationException (" Provider set job was cancelled" ))
183+ _statusFlow .tryEmit(OpenFeatureStatus .NotReady )
184+ getProvider().shutdown()
185+ clearHooks()
80186 }
81187
82- /*
83- Observe events from currently configured Provider.
84- */
85- @OptIn(ExperimentalCoroutinesApi ::class )
86- internal inline fun <reified T : OpenFeatureEvents > observe (): Flow <T > {
87- return sharedProvidersFlow.flatMapLatest { provider ->
88- provider.observe<T >()
89- }
90- }
188+ /* *
189+ * Get the current [OpenFeatureStatus] of the SDK.
190+ */
191+ fun getStatus (): OpenFeatureStatus = statusFlow.replayCache.first()
91192}
0 commit comments