1+ package dev.openfeature.kotlin.sdk.multiprovider
2+
3+ import dev.openfeature.kotlin.sdk.EvaluationContext
4+ import dev.openfeature.kotlin.sdk.FeatureProvider
5+ import dev.openfeature.kotlin.sdk.Hook
6+ import dev.openfeature.kotlin.sdk.ProviderEvaluation
7+ import dev.openfeature.kotlin.sdk.ProviderMetadata
8+ import dev.openfeature.kotlin.sdk.Value
9+ import dev.openfeature.kotlin.sdk.events.OpenFeatureProviderEvents
10+ import dev.openfeature.kotlin.sdk.exceptions.OpenFeatureError
11+ import kotlinx.coroutines.async
12+ import kotlinx.coroutines.awaitAll
13+ import kotlinx.coroutines.coroutineScope
14+ import kotlinx.coroutines.flow.Flow
15+ import kotlinx.coroutines.flow.MutableSharedFlow
16+ import kotlinx.coroutines.flow.asSharedFlow
17+ import kotlinx.coroutines.flow.launchIn
18+ import kotlinx.coroutines.flow.onEach
19+
20+ /* *
21+ * MultiProvider is a FeatureProvider implementation that delegates flag evaluations
22+ * to multiple underlying providers using a configurable strategy.
23+ *
24+ * This class acts as a composite provider that can:
25+ * - Combine multiple feature providers into a single interface
26+ * - Apply different evaluation strategies (FirstMatch, FirstSuccessful, etc.)
27+ * - Manage lifecycle events for all underlying providers
28+ * - Forward context changes to all providers
29+ *
30+ * @param providers List of FeatureProvider instances to delegate to
31+ * @param strategy Strategy to use for combining provider results (defaults to FirstMatchStrategy)
32+ */
33+ class MultiProvider (
34+ providers : List <FeatureProvider >,
35+ private val strategy : Strategy = FirstMatchStrategy (),
36+ ) : FeatureProvider {
37+ // Metadata identifying this as a multiprovider
38+ override val metadata: ProviderMetadata = object : ProviderMetadata {
39+ override val name: String? = " multiprovider"
40+ }
41+
42+ // TODO: Support hooks
43+ override val hooks: List <Hook <* >> = emptyList()
44+ private val uniqueProviders = getUniqueSetOfProviders(providers)
45+
46+ // Shared flow because we don't want the distinct operator since it would break consecutive emits of
47+ // ProviderConfigurationChanged
48+ private val eventFlow = MutableSharedFlow <OpenFeatureProviderEvents >(replay = 1 , extraBufferCapacity = 5 ).apply {
49+ OpenFeatureProviderEvents .ProviderError (OpenFeatureError .ProviderNotReadyError ())
50+ }
51+
52+ // Track individual provider statuses
53+ private val providerStatuses = mutableMapOf<FeatureProvider , OpenFeatureProviderEvents >()
54+
55+ // Event precedence (highest to lowest priority) - based on the specifications
56+ private val eventPrecedence = mapOf (
57+ OpenFeatureProviderEvents .ProviderError ::class to 4, // FATAL/ERROR
58+ OpenFeatureProviderEvents .ProviderNotReady ::class to 3, // NOT READY, Deprecated but still supporting
59+ OpenFeatureProviderEvents .ProviderStale ::class to 2, // STALE
60+ OpenFeatureProviderEvents .ProviderReady ::class to 1 // READY
61+ // ProviderConfigurationChanged doesn't affect status, so not included
62+ )
63+
64+ private fun getUniqueSetOfProviders (providers : List <FeatureProvider >): List <FeatureProvider > {
65+ val setOfProviderNames = mutableSetOf<String >()
66+ val uniqueProviders = mutableListOf<FeatureProvider >()
67+ providers.forEach { currProvider ->
68+ val providerName = currProvider.metadata.name
69+ if (setOfProviderNames.add(providerName.orEmpty())) {
70+ uniqueProviders.add(currProvider)
71+ } else {
72+ println (" Duplicate provider with name $providerName found" ) // Log error, no logging tool
73+ }
74+ }
75+
76+ return uniqueProviders
77+ }
78+
79+ /* *
80+ * @return Number of unique providers
81+ */
82+ fun getProviderCount (): Int = uniqueProviders.size
83+
84+ override fun observe (): Flow <OpenFeatureProviderEvents > = eventFlow.asSharedFlow()
85+
86+ /* *
87+ * Initializes all underlying providers with the given context.
88+ * This ensures all providers are ready before any evaluations occur.
89+ *
90+ * @param initialContext Optional evaluation context to initialize providers with
91+ */
92+ override suspend fun initialize (initialContext : EvaluationContext ? ) {
93+ coroutineScope {
94+ // Listen to events emitted by providers to emit our own set of events
95+ // according to https://openfeature.dev/specification/appendix-a/#status-and-event-handling
96+ uniqueProviders.forEach { provider ->
97+ provider.observe()
98+ .onEach { event ->
99+ handleProviderEvent(provider, event)
100+ }
101+ .launchIn(this )
102+ }
103+
104+ // State updates captured by observing individual Feature Flag providers
105+ uniqueProviders
106+ .map { async { it.initialize(initialContext) } }
107+ .awaitAll()
108+ }
109+ }
110+
111+ private suspend fun handleProviderEvent (provider : FeatureProvider , event : OpenFeatureProviderEvents ) {
112+ val hasStatusUpdated = updateProviderStatus(provider, event)
113+
114+ // This event should be re-emitted any time it occurs from any provider.
115+ if (event is OpenFeatureProviderEvents .ProviderConfigurationChanged ) {
116+ eventFlow.emit(event)
117+ return
118+ }
119+
120+ // If the status has been updated, calculate what our new event should be
121+ if (hasStatusUpdated) {
122+ val currPrecedenceVal = eventFlow.replayCache.firstOrNull()?.run { eventPrecedence[this ::class ] } ? : 0
123+ val updatedPrecedenceVal = eventPrecedence[event::class ] ? : 0
124+
125+ if (updatedPrecedenceVal > currPrecedenceVal) {
126+ eventFlow.emit(event)
127+ }
128+ }
129+ }
130+
131+ /* *
132+ * @return true if the status has been updated to a different value, false otherwise
133+ */
134+ private fun updateProviderStatus (provider : FeatureProvider , newStatus : OpenFeatureProviderEvents ): Boolean {
135+ val oldStatus = providerStatuses[provider]
136+ providerStatuses[provider] = newStatus
137+
138+ return oldStatus != newStatus
139+ }
140+
141+ /* *
142+ * Shuts down all underlying providers.
143+ * This allows providers to clean up resources and complete any pending operations.
144+ */
145+ override fun shutdown () {
146+ uniqueProviders.forEach { it.shutdown() }
147+ }
148+
149+ override suspend fun onContextSet (
150+ oldContext : EvaluationContext ? ,
151+ newContext : EvaluationContext
152+ ) {
153+ uniqueProviders.forEach { it.onContextSet(oldContext, newContext) }
154+ }
155+
156+ override fun getBooleanEvaluation (
157+ key : String ,
158+ defaultValue : Boolean ,
159+ context : EvaluationContext ?
160+ ): ProviderEvaluation <Boolean > {
161+ return strategy.evaluate(
162+ uniqueProviders,
163+ key,
164+ defaultValue,
165+ context,
166+ FeatureProvider ::getBooleanEvaluation,
167+ )
168+ }
169+
170+ override fun getStringEvaluation (
171+ key : String ,
172+ defaultValue : String ,
173+ context : EvaluationContext ?
174+ ): ProviderEvaluation <String > {
175+ return strategy.evaluate(
176+ uniqueProviders,
177+ key,
178+ defaultValue,
179+ context,
180+ FeatureProvider ::getStringEvaluation,
181+ )
182+ }
183+
184+ override fun getIntegerEvaluation (
185+ key : String ,
186+ defaultValue : Int ,
187+ context : EvaluationContext ?
188+ ): ProviderEvaluation <Int > {
189+ return strategy.evaluate(
190+ uniqueProviders,
191+ key,
192+ defaultValue,
193+ context,
194+ FeatureProvider ::getIntegerEvaluation,
195+ )
196+ }
197+
198+ override fun getDoubleEvaluation (
199+ key : String ,
200+ defaultValue : Double ,
201+ context : EvaluationContext ?
202+ ): ProviderEvaluation <Double > {
203+ return strategy.evaluate(
204+ uniqueProviders,
205+ key,
206+ defaultValue,
207+ context,
208+ FeatureProvider ::getDoubleEvaluation,
209+ )
210+ }
211+
212+ override fun getObjectEvaluation (
213+ key : String ,
214+ defaultValue : Value ,
215+ context : EvaluationContext ?
216+ ): ProviderEvaluation <Value > {
217+ return strategy.evaluate(
218+ uniqueProviders,
219+ key,
220+ defaultValue,
221+ context,
222+ FeatureProvider ::getObjectEvaluation,
223+ )
224+ }
225+ }
0 commit comments