diff --git a/paging/kover/coverage.xml b/paging/kover/coverage.xml
index 13254ba4c..a23845937 100644
--- a/paging/kover/coverage.xml
+++ b/paging/kover/coverage.xml
@@ -2,20 +2,15 @@
-
-
-
-
-
-
+
-
-
+
+
@@ -278,10 +273,9 @@
-
-
+
-
+
@@ -449,10 +443,10 @@
-
+
-
-
+
+
@@ -1066,10 +1060,10 @@
-
+
-
-
+
+
diff --git a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt
index 84fd02948..d798d57fe 100644
--- a/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt
+++ b/store/src/commonMain/kotlin/org/mobilenativefoundation/store/store5/impl/RealMutableStore.kt
@@ -201,16 +201,11 @@ internal class RealMutableStore withThreadSafety(
key: Key,
block: suspend ThreadSafety.() -> Output,
- ): Output {
- storeLock.lock()
- try {
+ ): Output =
+ storeLock.withLock {
val threadSafety = requireNotNull(keyToThreadSafety[key])
- val output = threadSafety.block()
- return output
- } finally {
- storeLock.unlock()
+ threadSafety.block()
}
- }
private suspend fun conflictsMightExist(key: Key): Boolean {
val lastFailedSync = bookkeeper?.getLastFailedSync(key)
diff --git a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt
index b50ec9703..119ccd3a0 100644
--- a/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt
+++ b/store/src/commonTest/kotlin/org/mobilenativefoundation/store/store5/StoreWithInMemoryCacheTests.kt
@@ -2,13 +2,20 @@ package org.mobilenativefoundation.store.store5
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runTest
+import org.mobilenativefoundation.store.core5.ExperimentalStoreApi
import org.mobilenativefoundation.store.store5.impl.extensions.get
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.time.Duration.Companion.hours
+@OptIn(ExperimentalStoreApi::class)
@FlowPreview
@ExperimentalCoroutinesApi
class StoreWithInMemoryCacheTests {
@@ -39,4 +46,91 @@ class StoreWithInMemoryCacheTests {
assertEquals("result", c)
assertEquals("result", d)
}
+
+ @Test
+ fun storeDeadlock() =
+ runTest {
+ repeat(100) {
+ val store: MutableStore =
+ StoreBuilder
+ .from(
+ fetcher = Fetcher.of { key: Int -> "fetcher_$key" },
+ sourceOfTruth =
+ SourceOfTruth.of(
+ reader = { key: Int ->
+ flowOf("source_of_truth_$key")
+ },
+ writer = { key: Int, local: String -> },
+ ),
+ )
+ .disableCache()
+ .toMutableStoreBuilder(
+ converter =
+ object : Converter {
+ override fun fromNetworkToLocal(network: String): String = network
+
+ override fun fromOutputToLocal(output: String): String = output
+ },
+ )
+ .build(
+ updater =
+ object : Updater {
+ var callCount = -1
+
+ override suspend fun post(
+ key: Int,
+ value: String,
+ ): UpdaterResult {
+ callCount += 1
+ return if (callCount % 2 == 0) {
+ throw IllegalArgumentException("$key value: $value")
+ } else {
+ UpdaterResult.Success.Untyped("")
+ }
+ }
+
+ override val onCompletion: OnUpdaterCompletion? = null
+ },
+ )
+
+ val jobs = mutableListOf()
+ jobs.add(
+ store.stream(StoreReadRequest.cached(1, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .launchIn(this),
+ )
+ val job1 =
+ store.stream(StoreReadRequest.cached(0, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .launchIn(this)
+ jobs.add(
+ store.stream(StoreReadRequest.cached(2, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .launchIn(this),
+ )
+ jobs.add(
+ store.stream(StoreReadRequest.cached(3, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .launchIn(this),
+ )
+ job1.cancel()
+ assertEquals(
+ expected = "source_of_truth_0",
+ actual =
+ store.stream(StoreReadRequest.cached(0, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .first(),
+ )
+ jobs.forEach {
+ it.cancel()
+ assertEquals(
+ expected = "source_of_truth_0",
+ actual =
+ store.stream(StoreReadRequest.cached(0, refresh = true))
+ .mapNotNull { it.dataOrNull() }
+ .first(),
+ )
+ }
+ }
+ }
}