Skip to content
This repository has been archived by the owner on Sep 13, 2023. It is now read-only.

Commit

Permalink
Merge pull request #32 from spotify/adding-minimal-event
Browse files Browse the repository at this point in the history
Add Events and async client
  • Loading branch information
vahidlazio authored Jul 24, 2023
2 parents 8688bfa + 7e2eb20 commit fd8c5eb
Show file tree
Hide file tree
Showing 11 changed files with 427 additions and 15 deletions.
7 changes: 4 additions & 3 deletions OpenFeature/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ android {
}

dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.0.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.2")
}

afterEvaluate {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ interface FeatureProvider {
val metadata: ProviderMetadata

// Called by OpenFeatureAPI whenever the new Provider is registered
suspend fun initialize(initialContext: EvaluationContext?)
fun initialize(initialContext: EvaluationContext?)

// called when the lifecycle of the OpenFeatureClient is over
// to release resources/threads.
fun shutdown()

// Called by OpenFeatureAPI whenever a new EvaluationContext is set by the application
suspend fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext)
fun onContextSet(oldContext: EvaluationContext?, newContext: EvaluationContext)
fun getBooleanEvaluation(
key: String,
defaultValue: Boolean,
Expand Down
8 changes: 6 additions & 2 deletions OpenFeature/src/main/java/dev/openfeature/sdk/NoOpProvider.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ package dev.openfeature.sdk

class NoOpProvider : FeatureProvider {
override var metadata: ProviderMetadata = NoOpProviderMetadata("No-op provider")
override suspend fun initialize(initialContext: EvaluationContext?) {
override fun initialize(initialContext: EvaluationContext?) {
// no-op
}

override suspend fun onContextSet(
override fun shutdown() {
// no-op
}

override fun onContextSet(
oldContext: EvaluationContext?,
newContext: EvaluationContext
) {
Expand Down
10 changes: 6 additions & 4 deletions OpenFeature/src/main/java/dev/openfeature/sdk/OpenFeatureAPI.kt
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package dev.openfeature.sdk

import kotlinx.coroutines.coroutineScope

object OpenFeatureAPI {
private var provider: FeatureProvider? = null
private var context: EvaluationContext? = null
var hooks: List<Hook<*>> = listOf()
private set

suspend fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) = coroutineScope {
fun setProvider(provider: FeatureProvider, initialContext: EvaluationContext? = null) {
this@OpenFeatureAPI.provider = provider
if (initialContext != null) context = initialContext
provider.initialize(context)
Expand All @@ -22,7 +20,7 @@ object OpenFeatureAPI {
provider = null
}

suspend fun setEvaluationContext(evaluationContext: EvaluationContext) {
fun setEvaluationContext(evaluationContext: EvaluationContext) {
context = evaluationContext
getProvider()?.onContextSet(context, evaluationContext)
}
Expand All @@ -46,4 +44,8 @@ object OpenFeatureAPI {
fun clearHooks() {
this.hooks = listOf()
}

fun shutdown() {
provider?.shutdown()
}
}
43 changes: 43 additions & 0 deletions OpenFeature/src/main/java/dev/openfeature/sdk/async/AsyncClient.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package dev.openfeature.sdk.async

import dev.openfeature.sdk.OpenFeatureClient
import dev.openfeature.sdk.Value
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map

interface AsyncClient {
fun observeBooleanValue(key: String, default: Boolean): Flow<Boolean>
fun observeIntValue(key: String, default: Int): Flow<Int>
fun observeStringValue(key: String, default: String): Flow<String>
fun observeDoubleValue(key: String, default: Double): Flow<Double>
fun observeValue(key: String, default: Value): Flow<Value>
}

internal class AsyncClientImpl(
private val client: OpenFeatureClient
) : AsyncClient {
private fun <T> observeEvents(callback: () -> T) = observeProviderReady()
.map { callback() }
.distinctUntilChanged()

override fun observeBooleanValue(key: String, default: Boolean) = observeEvents {
client.getBooleanValue(key, default)
}

override fun observeIntValue(key: String, default: Int) = observeEvents {
client.getIntegerValue(key, default)
}

override fun observeStringValue(key: String, default: String) = observeEvents {
client.getStringValue(key, default)
}

override fun observeDoubleValue(key: String, default: Double): Flow<Double> = observeEvents {
client.getDoubleValue(key, default)
}

override fun observeValue(key: String, default: Value): Flow<Value> = observeEvents {
client.getObjectValue(key, default)
}
}
49 changes: 49 additions & 0 deletions OpenFeature/src/main/java/dev/openfeature/sdk/async/Extensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package dev.openfeature.sdk.async

import dev.openfeature.sdk.OpenFeatureClient
import dev.openfeature.sdk.events.EventHandler
import dev.openfeature.sdk.events.OpenFeatureEvents
import dev.openfeature.sdk.events.observe
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine

fun OpenFeatureClient.toAsync(): AsyncClient {
return AsyncClientImpl(this)
}

internal fun observeProviderReady() = EventHandler.eventsObserver()
.observe<OpenFeatureEvents.ProviderReady>()
.onStart {
if (EventHandler.providerStatus().isProviderReady()) {
this.emit(OpenFeatureEvents.ProviderReady)
}
}

suspend fun awaitProviderReady() = suspendCancellableCoroutine { continuation ->
val coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope.launch {
observeProviderReady()
.take(1)
.collect {
continuation.resumeWith(Result.success(Unit))
}
}

coroutineScope.launch {
EventHandler.eventsObserver()
.observe<OpenFeatureEvents.ProviderError>()
.take(1)
.collect {
continuation.resumeWith(Result.failure(it.error))
}
}

continuation.invokeOnCancellation {
coroutineScope.cancel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package dev.openfeature.sdk.events

import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.launch
import kotlin.reflect.KClass

interface ProviderStatus {
fun isProviderReady(): Boolean
}

interface EventObserver {
fun <T : OpenFeatureEvents> observe(kClass: KClass<T>): Flow<T>
}

interface EventsPublisher {
fun publish(event: OpenFeatureEvents)
}

inline fun <reified T : OpenFeatureEvents> EventObserver.observe() = observe(T::class)

class EventHandler(dispatcher: CoroutineDispatcher) : EventObserver, EventsPublisher, ProviderStatus {
private val sharedFlow: MutableSharedFlow<OpenFeatureEvents> = MutableSharedFlow()
private val isProviderReady = MutableStateFlow(false)
private val coroutineScope = CoroutineScope(dispatcher)

init {
coroutineScope.launch {
sharedFlow.collect {
when (it) {
is OpenFeatureEvents.ProviderReady -> isProviderReady.value = true
is OpenFeatureEvents.ProviderShutDown -> {
isProviderReady.value = false
coroutineScope.cancel()
}
else -> {
// do nothing
}
}
}
}
}

override fun publish(event: OpenFeatureEvents) {
coroutineScope.launch {
sharedFlow.emit(event)
}
}

override fun <T : OpenFeatureEvents> observe(kClass: KClass<T>): Flow<T> = sharedFlow
.filterIsInstance(kClass)

override fun isProviderReady(): Boolean {
return isProviderReady.value
}

companion object {
@Volatile
private var instance: EventHandler? = null

private fun getInstance(dispatcher: CoroutineDispatcher) =
instance ?: synchronized(this) {
instance ?: create(dispatcher).also { instance = it }
}

fun eventsObserver(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventObserver =
getInstance(dispatcher)
fun providerStatus(dispatcher: CoroutineDispatcher = Dispatchers.IO): ProviderStatus =
getInstance(dispatcher)
fun eventsPublisher(dispatcher: CoroutineDispatcher = Dispatchers.IO): EventsPublisher =
getInstance(dispatcher)

private fun create(dispatcher: CoroutineDispatcher) = EventHandler(dispatcher)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.openfeature.sdk.events

sealed class OpenFeatureEvents {
object ProviderReady : OpenFeatureEvents()
object ProviderConfigurationChanged : OpenFeatureEvents()
data class ProviderError(val error: Throwable) : OpenFeatureEvents()
object ProviderStale : OpenFeatureEvents()
object ProviderShutDown : OpenFeatureEvents()
}
Loading

0 comments on commit fd8c5eb

Please sign in to comment.