diff --git a/collections/api/collections.api b/collections/api/collections.api index a77aa564..6fb6209c 100644 --- a/collections/api/collections.api +++ b/collections/api/collections.api @@ -1,7 +1,4 @@ -public final class com/juul/tuulbox/collections/FlowConcurrentMap : java/util/concurrent/ConcurrentMap { - public fun ()V - public fun (Ljava/util/concurrent/ConcurrentMap;)V - public synthetic fun (Ljava/util/concurrent/ConcurrentMap;ILkotlin/jvm/internal/DefaultConstructorMarker;)V +public final class com/juul/tuulbox/collections/FlowMutableMap : java/util/Map, kotlin/jvm/internal/markers/KMutableMap { public fun clear ()V public fun containsKey (Ljava/lang/Object;)Z public fun containsValue (Ljava/lang/Object;)Z @@ -25,7 +22,7 @@ public final class com/juul/tuulbox/collections/FlowConcurrentMap : java/util/co public final fun values ()Ljava/util/Collection; } -public final class com/juul/tuulbox/collections/FlowConcurrentMapKt { - public static final fun withFlow (Ljava/util/concurrent/ConcurrentMap;)Lcom/juul/tuulbox/collections/FlowConcurrentMap; +public final class com/juul/tuulbox/collections/FlowMutableMapKt { + public static final fun withFlow (Ljava/util/Map;)Lcom/juul/tuulbox/collections/FlowMutableMap; } diff --git a/collections/build.gradle b/collections/build.gradle index e8177d18..75050da4 100644 --- a/collections/build.gradle +++ b/collections/build.gradle @@ -11,6 +11,7 @@ apply from: rootProject.file('gradle/publish.gradle') dependencies { api deps.kotlin.stdlib - api deps.kotlin.coroutines + api deps.kotlin.coroutines.core + testImplementation deps.kotlin.coroutines.debug testImplementation deps.kotlin.junit } diff --git a/collections/src/main/kotlin/FlowConcurrentMap.kt b/collections/src/main/kotlin/FlowMutableMap.kt similarity index 73% rename from collections/src/main/kotlin/FlowConcurrentMap.kt rename to collections/src/main/kotlin/FlowMutableMap.kt index 452b3c0a..ce02ca9f 100644 --- a/collections/src/main/kotlin/FlowConcurrentMap.kt +++ b/collections/src/main/kotlin/FlowMutableMap.kt @@ -1,31 +1,29 @@ package com.juul.tuulbox.collections -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull -fun ConcurrentMap.withFlow() = FlowConcurrentMap(this) +fun MutableMap.withFlow() = FlowMutableMap(this) /** - * Wraps a [ConcurrentMap] to provide a [Flow] of [onChanged] events. The [onChanged] events emit - * a copy of the [ConcurrentMap] at the time of the change event. + * Wraps a [MutableMap] to provide a [Flow] of [onChanged] events. The [onChanged] events emit a + * copy of the [MutableMap] at the time of the change event. */ -class FlowConcurrentMap( - private val map: ConcurrentMap = ConcurrentHashMap() -) : ConcurrentMap by map { +class FlowMutableMap internal constructor( + private val map: MutableMap +) : MutableMap by map { private val _onChanged = MutableStateFlow?>(null) val onChanged: Flow> = _onChanged.filterNotNull() - /** @see ConcurrentMap.put */ + /** @see MutableMap.put */ override fun put( key: K, value: V ): V? = map.emitChangedAfter { put(key, value) } - /** @see ConcurrentMap.remove */ + /** @see MutableMap.remove */ override fun remove( key: K ): V? = map.emitChangedAfter { remove(key) } @@ -33,7 +31,7 @@ class FlowConcurrentMap( /** * Emits `onChanged` event when [remove] returns `true`. * - * @see ConcurrentMap.remove + * @see MutableMap.remove */ override fun remove( key: K, @@ -42,10 +40,10 @@ class FlowConcurrentMap( if (didRemove) _onChanged.value = map.toMap() } - /** @see ConcurrentMap.clear */ + /** @see MutableMap.clear */ override fun clear() = map.emitChangedAfter { clear() } - /** @see ConcurrentMap.putAll */ + /** @see MutableMap.putAll */ override fun putAll( from: Map ) = map.emitChangedAfter { putAll(from) } @@ -53,7 +51,7 @@ class FlowConcurrentMap( /** * Emits `onChanged` event when [putIfAbsent] returns `null`. * - * @see ConcurrentMap.putIfAbsent + * @see MutableMap.putIfAbsent */ override fun putIfAbsent( key: K, @@ -65,7 +63,7 @@ class FlowConcurrentMap( /** * Emits `onChanged` event when [replace] returns `true`. * - * @see ConcurrentMap.replace + * @see MutableMap.replace */ override fun replace( key: K, @@ -78,7 +76,7 @@ class FlowConcurrentMap( /** * Emits `onChanged` event when [replace] returns non-`null`. * - * @see ConcurrentMap.replace + * @see MutableMap.replace */ override fun replace( key: K, @@ -99,8 +97,8 @@ class FlowConcurrentMap( override val values: MutableCollection get() = throw UnsupportedOperationException() - private inline fun ConcurrentMap.emitChangedAfter( - action: ConcurrentMap.() -> T + private inline fun MutableMap.emitChangedAfter( + action: MutableMap.() -> T ): T { val result = action.invoke(this) _onChanged.value = toMap() diff --git a/collections/src/test/kotlin/FlowConcurrentMapTest.kt b/collections/src/test/kotlin/FlowMutableMapTest.kt similarity index 73% rename from collections/src/test/kotlin/FlowConcurrentMapTest.kt rename to collections/src/test/kotlin/FlowMutableMapTest.kt index 56bf8ba3..271fc657 100644 --- a/collections/src/test/kotlin/FlowConcurrentMapTest.kt +++ b/collections/src/test/kotlin/FlowMutableMapTest.kt @@ -1,26 +1,30 @@ package com.juul.tuulbox.collections.test -import com.juul.tuulbox.collections.FlowConcurrentMap -import java.util.concurrent.ConcurrentHashMap +import com.juul.tuulbox.collections.withFlow +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.debug.junit4.CoroutinesTimeout +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking +import org.junit.Rule import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.runBlocking -class FlowConcurrentMapTest { +class FlowMutableMapTest { + + @get:Rule + val timeoutRule = CoroutinesTimeout.seconds(1) @Test fun `Adding produces onChanged events`() = runBlocking { - val map = FlowConcurrentMap() + val map = mutableMapOf().withFlow() val events = Channel>() - map.onChanged.onEach(events::send).launchIn(GlobalScope) + val job = map.onChanged.onEach(events::send).launchIn(this) assertEquals( expected = 0, @@ -40,13 +44,15 @@ class FlowConcurrentMapTest { expected = 2, actual = map.size ) + + job.cancelAndJoin() } @Test fun `Replacing produces onChanged events`() = runBlocking { - val map = FlowConcurrentMap() + val map = mutableMapOf().withFlow() val events = Channel>() - map.onChanged.onEach(events::send).launchIn(GlobalScope) + val job = map.onChanged.onEach(events::send).launchIn(this) map.putAll(mapOf("1" to 1, "2" to 2)) events.receiveAndAssert(mapOf("1" to 1, "2" to 2)) @@ -68,17 +74,15 @@ class FlowConcurrentMapTest { assertTrue(map.replace("1", 100, 250)) events.receiveAndAssert(mapOf("1" to 250, "2" to -200)) + + job.cancelAndJoin() } @Test fun `Removing produces onChanged events`() = runBlocking { - val map = FlowConcurrentMap( - ConcurrentHashMap().apply { - putAll(mapOf("A" to 123, "B" to 987)) - } - ) + val map = mutableMapOf("A" to 123, "B" to 987).withFlow() val events = Channel>() - map.onChanged.onEach(events::send).launchIn(GlobalScope) + val job = map.onChanged.onEach(events::send).launchIn(this) assertFalse(map.remove("B", -1)) // Skip `receiveAndAssert` because the `remove` failed (didn't trigger onChanged). @@ -88,17 +92,15 @@ class FlowConcurrentMapTest { map.remove("A") events.receiveAndAssert(emptyMap()) + + job.cancelAndJoin() } @Test fun `putIfAbsent produces onChanged events`() = runBlocking { - val map = FlowConcurrentMap( - ConcurrentHashMap().apply { - putAll(mapOf("A" to 123, "B" to 987)) - } - ) + val map = mutableMapOf("A" to 123, "B" to 987).withFlow() val events = Channel>() - map.onChanged.onEach(events::send).launchIn(GlobalScope) + val job = map.onChanged.onEach(events::send).launchIn(this) assertEquals( expected = 987, // Previous value (indicates we didn't perform a "put"). @@ -108,43 +110,43 @@ class FlowConcurrentMapTest { assertNull(map.putIfAbsent("C", -128)) events.receiveAndAssert(mapOf("A" to 123, "B" to 987, "C" to -128)) + + job.cancelAndJoin() } @Test fun `Clearing produces onChanged event`() = runBlocking { - val map = FlowConcurrentMap( - ConcurrentHashMap().apply { - putAll(mapOf("A" to 123, "B" to 987)) - } - ) + val map = mutableMapOf("A" to 123, "B" to 987).withFlow() val events = Channel>() - map.onChanged.onEach(events::send).launchIn(GlobalScope) + val job = map.onChanged.onEach(events::send).launchIn(this) map["C"] = 1337 events.receiveAndAssert(mapOf("A" to 123, "B" to 987, "C" to 1337)) map.clear() events.receiveAndAssert(emptyMap()) + + job.cancelAndJoin() } @Test fun `FlowConcurrentMap 'entries' throws UnsupportedOperationException`() { assertFailsWith { - FlowConcurrentMap().entries + mutableMapOf().withFlow().entries } } @Test fun `FlowConcurrentMap 'keys' throws UnsupportedOperationException`() { assertFailsWith { - FlowConcurrentMap().keys + mutableMapOf().withFlow().keys } } @Test fun `FlowConcurrentMap 'values' throws UnsupportedOperationException`() { assertFailsWith { - FlowConcurrentMap().values + mutableMapOf().withFlow().values } } } diff --git a/coroutines/build.gradle b/coroutines/build.gradle index e8177d18..75050da4 100644 --- a/coroutines/build.gradle +++ b/coroutines/build.gradle @@ -11,6 +11,7 @@ apply from: rootProject.file('gradle/publish.gradle') dependencies { api deps.kotlin.stdlib - api deps.kotlin.coroutines + api deps.kotlin.coroutines.core + testImplementation deps.kotlin.coroutines.debug testImplementation deps.kotlin.junit } diff --git a/coroutines/src/test/kotlin/CombineParametersTest.kt b/coroutines/src/test/kotlin/CombineParametersTest.kt index 24114346..7cb384ca 100644 --- a/coroutines/src/test/kotlin/CombineParametersTest.kt +++ b/coroutines/src/test/kotlin/CombineParametersTest.kt @@ -1,13 +1,18 @@ package com.juul.tuulbox.coroutines.flow +import kotlinx.coroutines.debug.junit4.CoroutinesTimeout import kotlin.test.Test import kotlin.test.assertEquals import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.single import kotlinx.coroutines.runBlocking +import org.junit.Rule class CombineParametersTest { + @get:Rule + val timeoutRule = CoroutinesTimeout.seconds(1) + @Test fun testSixParameters() = runBlocking { val flow = combine( diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 3f77a5f9..8a8511f8 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -1,7 +1,10 @@ ext.deps = [ kotlin: [ stdlib: "org.jetbrains.kotlin:kotlin-stdlib", - coroutines: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7", + coroutines: [ + core: "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7", + debug: "org.jetbrains.kotlinx:kotlinx-coroutines-debug:1.3.7", + ], junit: "org.jetbrains.kotlin:kotlin-test-junit", ], ]