Compare commits

..

11 Commits

24 changed files with 289 additions and 627 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.idea .idea
.vscode
.kotlin .kotlin
out/* out/*
*.iml *.iml

View File

@@ -1,17 +1,26 @@
# Changelog # Changelog
## 0.25.8.2 ## 0.26.1
* `Versions`:
* `Compose`: `1.8.1` -> `1.8.2`
* `Ktor`: `3.2.1` -> `3.2.2`
* `Coroutines`: * `Coroutines`:
* `runCatchingLogging` updated to rethrow `CancellationException` and log other exceptions * Add opportunity to pass logger in subscribe async
## 0.25.8.1 ## 0.26.0
* `Coroutines`: **WARNING!!! SINCE THIS VERSION IF YOU WANT TO USE SOME OF KSP MODULES, SET `ksp.useKSP2=false` IN YOUR `gradle.properties`** (see [gh issue 2491](https://github.com/google/ksp/issues/2491))
* New function `suspendPoint` to check coroutine cancellation status
* `SpecialMutableStateFlow` renamed to `MutableRedeliverStateFlow` (old name deprecated with `ReplaceWith`) * `Versions`:
* `SmartSemaphore`: * `Kotlin`: `2.1.21` -> `2.2.0`
* Fix of `waitRelease` call to pass correct number of permits * `Serialization`: `1.8.1` -> `1.9.0`
* `KSLog`: `1.4.2` -> `1.5.0`
* `Ktor`: `3.1.3` -> `3.2.1`
* `Koin`: `4.0.4` -> `4.1.0`
* `Okio`: `3.12.0` -> `3.15.0`
* `KSP`: `2.1.20-1.0.31` -> `2.2.0-2.0.2`
* `kotlin-poet`: `1.18.1` -> `2.2.0`
## 0.25.8 ## 0.25.8

View File

@@ -28,7 +28,7 @@ if ((project.hasProperty('SONATYPE_USER') || System.getenv('SONATYPE_USER') != n
centralPortal { centralPortal {
username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER') username = project.hasProperty('SONATYPE_USER') ? project.property('SONATYPE_USER') : System.getenv('SONATYPE_USER')
password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD') password = project.hasProperty('SONATYPE_PASSWORD') ? project.property('SONATYPE_PASSWORD') : System.getenv('SONATYPE_PASSWORD')
verificationTimeout = Duration.ofHours(4) validationTimeout = Duration.ofHours(4)
publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED" publishingType = System.getenv('PUBLISHING_TYPE') != "" ? System.getenv('PUBLISHING_TYPE') : "USER_MANAGED"
} }

View File

@@ -2,7 +2,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.ExperimentalTestApi
import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.test.runComposeUiTest
import dev.inmo.micro_utils.common.compose.LoadableComponent import dev.inmo.micro_utils.common.compose.LoadableComponent
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -16,8 +16,8 @@ class LoadableComponentTests {
@Test @Test
@TestOnly @TestOnly
fun testSimpleLoad() = runComposeUiTest { fun testSimpleLoad() = runComposeUiTest {
val loadingFlow = MutableRedeliverStateFlow<Int>(0) val loadingFlow = SpecialMutableStateFlow<Int>(0)
val loadedFlow = MutableRedeliverStateFlow<Int>(0) val loadedFlow = SpecialMutableStateFlow<Int>(0)
setContent { setContent {
LoadableComponent<Int>({ LoadableComponent<Int>({
loadingFlow.filter { it == 1 }.first() loadingFlow.filter { it == 1 }.first()

View File

@@ -3,7 +3,7 @@ package dev.inmo.micro_utils.coroutines.compose
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
@@ -16,7 +16,7 @@ import org.jetbrains.compose.web.css.StyleSheet
* to add `Style(stylesheet)` on every compose function call * to add `Style(stylesheet)` on every compose function call
*/ */
object StyleSheetsAggregator { object StyleSheetsAggregator {
private val _stylesFlow = MutableRedeliverStateFlow<Set<CSSRulesHolder>>(emptySet()) private val _stylesFlow = SpecialMutableStateFlow<Set<CSSRulesHolder>>(emptySet())
val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow() val stylesFlow: StateFlow<Set<CSSRulesHolder>> = _stylesFlow.asStateFlow()
@Composable @Composable

View File

@@ -2,7 +2,7 @@ import androidx.compose.material.Button
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.test.* import androidx.compose.ui.test.*
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import org.jetbrains.annotations.TestOnly import org.jetbrains.annotations.TestOnly
import kotlin.test.Test import kotlin.test.Test
@@ -11,7 +11,7 @@ class FlowStateTests {
@Test @Test
@TestOnly @TestOnly
fun simpleTest() = runComposeUiTest { fun simpleTest() = runComposeUiTest {
val flowState = MutableRedeliverStateFlow(0) val flowState = SpecialMutableStateFlow(0)
setContent { setContent {
Button({ flowState.value++ }) { Text("Click") } Button({ flowState.value++ }) { Text("Click") }
Text(flowState.collectAsState().value.toString()) Text(flowState.collectAsState().value.toString())

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.consumeAsFlow
@@ -7,13 +8,15 @@ import kotlinx.coroutines.flow.consumeAsFlow
fun <T> CoroutineScope.actorAsync( fun <T> CoroutineScope.actorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
markerFactory: suspend (T) -> Any? = { null }, markerFactory: suspend (T) -> Any? = { null },
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Channel<T> { ): Channel<T> {
val channel = Channel<T>(channelCapacity) val channel = Channel<T>(channelCapacity)
channel.consumeAsFlow().subscribeAsync(this, markerFactory, block) channel.consumeAsFlow().subscribeAsync(this, markerFactory, logger, block)
return channel return channel
} }
@Deprecated("Use standard actosAsync instead", ReplaceWith("actorAsync(channelCapacity, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.actorAsync"))
inline fun <T> CoroutineScope.safeActorAsync( inline fun <T> CoroutineScope.safeActorAsync(
channelCapacity: Int = Channel.UNLIMITED, channelCapacity: Int = Channel.UNLIMITED,
noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, noinline onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,

View File

@@ -1,5 +1,6 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.channels.* import kotlinx.coroutines.channels.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@@ -8,6 +9,7 @@ import kotlinx.coroutines.sync.withLock
private class SubscribeAsyncReceiver<T>( private class SubscribeAsyncReceiver<T>(
val scope: CoroutineScope, val scope: CoroutineScope,
val logger: KSLog,
output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit output: suspend SubscribeAsyncReceiver<T>.(T) -> Unit
) { ) {
private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED) private val dataChannel: Channel<T> = Channel(Channel.UNLIMITED)
@@ -15,7 +17,7 @@ private class SubscribeAsyncReceiver<T>(
get() = dataChannel get() = dataChannel
init { init {
scope.launchLoggingDropExceptions { scope.launchLoggingDropExceptions(logger = logger) {
for (data in dataChannel) { for (data in dataChannel) {
output(data) output(data)
} }
@@ -33,13 +35,16 @@ private data class AsyncSubscriptionCommandData<T, M>(
val scope: CoroutineScope, val scope: CoroutineScope,
val markerFactory: suspend (T) -> M, val markerFactory: suspend (T) -> M,
val block: suspend (T) -> Unit, val block: suspend (T) -> Unit,
val logger: KSLog,
val onEmpty: suspend (M) -> Unit val onEmpty: suspend (M) -> Unit
) : AsyncSubscriptionCommand<T, M> { ) : AsyncSubscriptionCommand<T, M> {
override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) { override suspend fun invoke(markersMap: MutableMap<M, SubscribeAsyncReceiver<T>>) {
val marker = markerFactory(data) val marker = markerFactory(data)
markersMap.getOrPut(marker) { markersMap.getOrPut(marker) {
SubscribeAsyncReceiver(scope.LinkedSupervisorScope()) { SubscribeAsyncReceiver(scope.LinkedSupervisorScope(), logger) {
safelyWithoutExceptions { block(it) } runCatchingLogging(logger = logger) {
block(it)
}
if (isEmpty()) { if (isEmpty()) {
onEmpty(marker) onEmpty(marker)
} }
@@ -63,6 +68,7 @@ private data class AsyncSubscriptionCommandClearReceiver<T, M>(
fun <T, M> Flow<T>.subscribeAsync( fun <T, M> Flow<T>.subscribeAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
): Job { ): Job {
val subscope = scope.LinkedSupervisorScope() val subscope = scope.LinkedSupervisorScope()
@@ -71,8 +77,14 @@ fun <T, M> Flow<T>.subscribeAsync(
it.invoke(markersMap) it.invoke(markersMap)
} }
val job = subscribeLoggingDropExceptions(subscope) { data -> val job = subscribeLoggingDropExceptions(subscope, logger = logger) { data ->
val dataCommand = AsyncSubscriptionCommandData(data, subscope, markerFactory, block) { marker -> val dataCommand = AsyncSubscriptionCommandData(
data = data,
scope = subscope,
markerFactory = markerFactory,
block = block,
logger = logger
) { marker ->
actor.send( actor.send(
AsyncSubscriptionCommandClearReceiver(marker) AsyncSubscriptionCommandClearReceiver(marker)
) )
@@ -85,17 +97,20 @@ fun <T, M> Flow<T>.subscribeAsync(
return job return job
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyAsync( fun <T, M> Flow<T>.subscribeSafelyAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler, onException: ExceptionHandler<Unit> = defaultSafelyExceptionHandler,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safely(onException) { safely(onException) {
block(it) block(it)
} }
} }
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
@@ -107,11 +122,22 @@ fun <T, M> Flow<T>.subscribeSafelyWithoutExceptionsAsync(
} }
} }
fun <T, M> Flow<T>.subscribeLoggingDropExceptionsAsync(
scope: CoroutineScope,
markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory, logger) {
block(it)
}
@Deprecated("Renamed", ReplaceWith("subscribeLoggingDropExceptionsAsync(scope, markerFactory, logger, block = block)", "dev.inmo.micro_utils.coroutines.subscribeLoggingDropExceptionsAsync"))
fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync( fun <T, M> Flow<T>.subscribeSafelySkippingExceptionsAsync(
scope: CoroutineScope, scope: CoroutineScope,
markerFactory: suspend (T) -> M, markerFactory: suspend (T) -> M,
logger: KSLog = KSLog,
block: suspend (T) -> Unit block: suspend (T) -> Unit
) = subscribeAsync(scope, markerFactory) { ) = subscribeAsync(scope, markerFactory, logger) {
safelyWithoutExceptions({ /* do nothing */}) { safelyWithoutExceptions({ /* do nothing */}) {
block(it) block(it)
} }

View File

@@ -2,27 +2,11 @@ package dev.inmo.micro_utils.coroutines
import dev.inmo.kslog.common.KSLog import dev.inmo.kslog.common.KSLog
import dev.inmo.kslog.common.e import dev.inmo.kslog.common.e
import kotlin.coroutines.cancellation.CancellationException
/**
* Executes the given [block] within a `runCatching` context and logs any exceptions that occur, excluding
* `CancellationException` which is rethrown. This method simplifies error handling by automatically logging
* the errors using the provided [logger].
*
* @param T The result type of the [block].
* @param R The receiver type on which this function operates.
* @param errorMessageBuilder A lambda to build the error log message. By default, it returns a generic error message.
* @param logger The logging instance used for logging errors. Defaults to [KSLog].
* @param block The code block to execute within the `runCatching` context.
* @return A [Result] representing the outcome of executing the [block].
*/
inline fun <T, R> R.runCatchingLogging( inline fun <T, R> R.runCatchingLogging(
noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" }, noinline errorMessageBuilder: R.(Throwable) -> Any = { "Something web wrong" },
logger: KSLog = KSLog, logger: KSLog = KSLog,
block: R.() -> T block: R.() -> T
) = runCatching(block).onFailure { ) = runCatching(block).onFailure {
when (it) { logger.e(it) { errorMessageBuilder(it) }
is CancellationException -> throw it
else -> logger.e(it) { errorMessageBuilder(it) }
}
} }

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -43,7 +44,7 @@ sealed interface SmartMutex {
* @param locked Preset state of [isLocked] and its internal [_lockStateFlow] * @param locked Preset state of [isLocked] and its internal [_lockStateFlow]
*/ */
class Mutable(locked: Boolean = false) : SmartMutex { class Mutable(locked: Boolean = false) : SmartMutex {
private val _lockStateFlow = MutableRedeliverStateFlow<Boolean>(locked) private val _lockStateFlow = SpecialMutableStateFlow<Boolean>(locked)
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow() override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex() private val internalChangesMutex = Mutex()

View File

@@ -1,6 +1,7 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@@ -46,7 +47,7 @@ sealed interface SmartSemaphore {
*/ */
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore { class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
override val maxPermits: Int = permits override val maxPermits: Int = permits
private val _freePermitsStateFlow = MutableRedeliverStateFlow<Int>(permits - acquiredPermits) private val _freePermitsStateFlow = SpecialMutableStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow() override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
private val internalChangesMutex = Mutex(false) private val internalChangesMutex = Mutex(false)
@@ -72,7 +73,7 @@ sealed interface SmartSemaphore {
acquiredPermits != checkedPermits acquiredPermits != checkedPermits
} }
if (shouldContinue) { if (shouldContinue) {
waitRelease(checkedPermits - acquiredPermits) waitRelease()
} }
} while (shouldContinue && currentCoroutineContext().isActive) } while (shouldContinue && currentCoroutineContext().isActive)
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -1,5 +1,7 @@
package dev.inmo.micro_utils.coroutines package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.InternalCoroutinesApi import kotlinx.coroutines.InternalCoroutinesApi
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
@@ -9,12 +11,13 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.internal.SynchronizedObject import kotlinx.coroutines.internal.SynchronizedObject
import kotlinx.coroutines.internal.synchronized import kotlinx.coroutines.internal.synchronized
import kotlin.coroutines.CoroutineContext
/** /**
* Works like [StateFlow], but guarantee that latest value update will always be delivered to * Works like [StateFlow], but guarantee that latest value update will always be delivered to
* each active subscriber * each active subscriber
*/ */
open class MutableRedeliverStateFlow<T>( open class SpecialMutableStateFlow<T>(
initialValue: T initialValue: T
) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> { ) : MutableStateFlow<T>, FlowCollector<T>, MutableSharedFlow<T> {
@OptIn(InternalCoroutinesApi::class) @OptIn(InternalCoroutinesApi::class)

View File

@@ -1,15 +0,0 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
/**
* Ensures that the current coroutine context is still active and throws a [kotlinx.coroutines.CancellationException]
* if the coroutine has been canceled.
*
* This function provides a convenient way to check the active status of a coroutine, which is useful
* to identify cancellation points in long-running or suspendable operations.
*
* @throws kotlinx.coroutines.CancellationException if the coroutine context is no longer active.
*/
suspend fun suspendPoint() = currentCoroutineContext().ensureActive()

View File

@@ -4,7 +4,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import kotlin.test.BeforeTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFails import kotlin.test.assertFails
@@ -14,517 +13,184 @@ import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class SmartKeyRWLockerTests { class SmartKeyRWLockerTests {
private lateinit var locker: SmartKeyRWLocker<String>
@BeforeTest
fun setup() {
locker = SmartKeyRWLocker()
}
// ==================== Global Read Tests ====================
@Test @Test
fun testGlobalReadAllowsMultipleConcurrentReads() = runTest { fun writeLockKeyFailedOnGlobalWriteLockTest() = runTest {
val results = mutableListOf<Boolean>() val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.acquireRead()
val jobs = List(5) {
launch {
locker.acquireRead()
delay(100.milliseconds)
results.add(true)
locker.releaseRead()
}
}
jobs.joinAll()
locker.releaseRead()
assertEquals(5, results.size)
}
@Test
fun testGlobalReadBlocksGlobalWrite() = runTest {
locker.acquireRead()
var writeAcquired = false
val writeJob = launch {
locker.lockWrite()
writeAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write should be blocked by global read")
locker.releaseRead()
writeJob.join()
assertTrue(writeAcquired, "Write should succeed after read released")
}
@Test
fun testGlobalReadBlocksAllKeyWrites() = runTest {
locker.acquireRead()
val writeFlags = mutableMapOf<String, Boolean>()
val keys = listOf("key1", "key2", "key3")
val jobs = keys.map { key ->
launch {
locker.lockWrite(key)
writeFlags[key] = true
locker.unlockWrite(key)
}
}
delay(200.milliseconds)
assertTrue(writeFlags.isEmpty(), "No writes should succeed while global read active")
locker.releaseRead()
jobs.joinAll()
assertEquals(keys.size, writeFlags.size, "All writes should succeed after global read released")
}
// ==================== Global Write Tests ====================
@Test
fun testGlobalWriteBlocksAllOperations() = runTest {
locker.lockWrite() locker.lockWrite()
var globalReadAcquired = false assertTrue { locker.isWriteLocked() }
var keyReadAcquired = false
var keyWriteAcquired = false
val jobs = listOf( assertFails {
launch { realWithTimeout(1.seconds) {
locker.acquireRead() locker.lockWrite(testKey)
globalReadAcquired = true
locker.releaseRead()
},
launch {
locker.acquireRead("key1")
keyReadAcquired = true
locker.releaseRead("key1")
},
launch {
locker.lockWrite("key2")
keyWriteAcquired = true
locker.unlockWrite("key2")
} }
) }
assertFalse { locker.isWriteLocked(testKey) }
delay(200.milliseconds)
assertFalse(globalReadAcquired, "Global read should be blocked")
assertFalse(keyReadAcquired, "Key read should be blocked")
assertFalse(keyWriteAcquired, "Key write should be blocked")
locker.unlockWrite() locker.unlockWrite()
jobs.joinAll() assertFalse { locker.isWriteLocked() }
assertTrue(globalReadAcquired) realWithTimeout(1.seconds) {
assertTrue(keyReadAcquired) locker.lockWrite(testKey)
assertTrue(keyWriteAcquired) }
assertTrue { locker.isWriteLocked(testKey) }
assertTrue { locker.unlockWrite(testKey) }
assertFalse { locker.isWriteLocked(testKey) }
} }
@Test @Test
fun testGlobalWriteIsExclusive() = runTest { fun writeLockKeyFailedOnGlobalReadLockTest() = runTest {
locker.lockWrite() val locker = SmartKeyRWLocker<String>()
val testKey = "test"
var secondWriteAcquired = false
val job = launch {
locker.lockWrite()
secondWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(secondWriteAcquired, "Second global write should be blocked")
locker.unlockWrite()
job.join()
assertTrue(secondWriteAcquired)
}
// ==================== Key Read Tests ====================
@Test
fun testKeyReadAllowsMultipleConcurrentReadsForSameKey() = runTest {
val key = "testKey"
val results = mutableListOf<Boolean>()
locker.acquireRead(key)
val jobs = List(5) {
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results.add(true)
locker.releaseRead(key)
}
}
jobs.joinAll()
locker.releaseRead(key)
assertEquals(5, results.size)
}
@Test
fun testKeyReadAllowsReadsForDifferentKeys() = runTest {
val results = mutableMapOf<String, Boolean>()
locker.acquireRead("key1")
val jobs = listOf("key2", "key3", "key4").map { key ->
launch {
locker.acquireRead(key)
delay(50.milliseconds)
results[key] = true
locker.releaseRead(key)
}
}
jobs.joinAll()
locker.releaseRead("key1")
assertEquals(3, results.size)
}
@Test
fun testKeyReadBlocksWriteForSameKey() = runTest {
val key = "testKey"
locker.acquireRead(key)
var writeAcquired = false
val job = launch {
locker.lockWrite(key)
writeAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds)
assertFalse(writeAcquired, "Write for same key should be blocked")
locker.releaseRead(key)
job.join()
assertTrue(writeAcquired)
}
@Test
fun testKeyReadBlocksGlobalWrite() = runTest {
locker.acquireRead("key1")
var globalWriteAcquired = false
val job = launch {
locker.lockWrite()
globalWriteAcquired = true
locker.unlockWrite()
}
delay(200.milliseconds)
assertFalse(globalWriteAcquired, "Global write should be blocked by key read")
locker.releaseRead("key1")
job.join()
assertTrue(globalWriteAcquired)
}
@Test
fun testKeyReadAllowsWriteForDifferentKey() = runTest {
locker.acquireRead("key1")
var writeAcquired = false
val job = launch {
locker.lockWrite("key2")
writeAcquired = true
locker.unlockWrite("key2")
}
job.join()
assertTrue(writeAcquired, "Write for different key should succeed")
locker.releaseRead("key1")
}
// ==================== Key Write Tests ====================
@Test
fun testKeyWriteBlocksReadForSameKey() = runTest {
val key = "testKey"
locker.lockWrite(key)
var readAcquired = false
val job = launch {
locker.acquireRead(key)
readAcquired = true
locker.releaseRead(key)
}
delay(200.milliseconds)
assertFalse(readAcquired, "Read for same key should be blocked")
locker.unlockWrite(key)
job.join()
assertTrue(readAcquired)
}
@Test
fun testKeyWriteBlocksGlobalRead() = runTest {
locker.lockWrite("key1")
var globalReadAcquired = false
val job = launch {
locker.acquireRead() locker.acquireRead()
globalReadAcquired = true
assertEquals(Int.MAX_VALUE - 1, locker.readSemaphore().freePermits)
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
}
}
assertFalse { locker.isWriteLocked(testKey) }
locker.releaseRead() locker.releaseRead()
assertEquals(Int.MAX_VALUE, locker.readSemaphore().freePermits)
realWithTimeout(1.seconds) {
locker.lockWrite(testKey)
} }
assertTrue { locker.isWriteLocked(testKey) }
delay(200.milliseconds) assertTrue { locker.unlockWrite(testKey) }
assertFalse(globalReadAcquired, "Global read should be blocked by key write") assertFalse { locker.isWriteLocked(testKey) }
locker.unlockWrite("key1")
job.join()
assertTrue(globalReadAcquired)
} }
@Test @Test
fun testKeyWriteIsExclusiveForSameKey() = runTest { fun readLockFailedOnWriteLockKeyTest() = runTest {
val key = "testKey" val locker = SmartKeyRWLocker<String>()
locker.lockWrite(key) val testKey = "test"
locker.lockWrite(testKey)
var secondWriteAcquired = false assertTrue { locker.isWriteLocked(testKey) }
val job = launch {
locker.lockWrite(key)
secondWriteAcquired = true
locker.unlockWrite(key)
}
delay(200.milliseconds) assertFails {
assertFalse(secondWriteAcquired, "Second write for same key should be blocked") realWithTimeout(1.seconds) {
locker.unlockWrite(key)
job.join()
assertTrue(secondWriteAcquired)
}
@Test
fun testKeyWriteAllowsOperationsOnDifferentKeys() = runTest {
locker.lockWrite("key1")
val results = mutableMapOf<String, Boolean>()
val jobs = listOf(
launch {
locker.acquireRead("key2")
results["read-key2"] = true
locker.releaseRead("key2")
},
launch {
locker.lockWrite("key3")
results["write-key3"] = true
locker.unlockWrite("key3")
}
)
jobs.joinAll()
assertEquals(2, results.size, "Operations on different keys should succeed")
locker.unlockWrite("key1")
}
// ==================== Complex Scenarios ====================
@Test
fun testMultipleReadersThenWriter() = runTest {
val key = "testKey"
val readCount = 5
val readers = mutableListOf<Job>()
repeat(readCount) {
readers.add(launch {
locker.acquireRead(key)
delay(100.milliseconds)
locker.releaseRead(key)
})
}
delay(50.milliseconds) // Let readers acquire
var writerExecuted = false
val writer = launch {
locker.lockWrite(key)
writerExecuted = true
locker.unlockWrite(key)
}
delay(50.milliseconds)
assertFalse(writerExecuted, "Writer should wait for all readers")
readers.joinAll()
writer.join()
assertTrue(writerExecuted, "Writer should execute after all readers done")
}
@Test
fun testWriterThenMultipleReaders() = runTest {
val key = "testKey"
locker.lockWrite(key)
val readerFlags = mutableListOf<Boolean>()
val readers = List(5) {
launch {
locker.acquireRead(key)
readerFlags.add(true)
locker.releaseRead(key)
}
}
delay(200.milliseconds)
assertTrue(readerFlags.isEmpty(), "Readers should be blocked by writer")
locker.unlockWrite(key)
readers.joinAll()
assertEquals(5, readerFlags.size, "All readers should succeed after writer")
}
@Test
fun testCascadingLocksWithDifferentKeys() = runTest {
val executed = mutableMapOf<String, Boolean>()
launch {
locker.lockWrite("key1")
executed["write-key1-start"] = true
delay(100.milliseconds)
locker.unlockWrite("key1")
executed["write-key1-end"] = true
}
delay(50.milliseconds)
launch {
locker.acquireRead("key2")
executed["read-key2"] = true
delay(100.milliseconds)
locker.releaseRead("key2")
}
delay(200.milliseconds)
assertTrue(executed["write-key1-start"] == true)
assertTrue(executed["read-key2"] == true)
assertTrue(executed["write-key1-end"] == true)
}
@Test
fun testReleaseWithoutAcquireReturnsFalse() = runTest {
assertFalse(locker.releaseRead(), "Release without acquire should return false")
assertFalse(locker.releaseRead("key1"), "Release without acquire should return false")
}
@Test
fun testUnlockWithoutLockReturnsFalse() = runTest {
assertFalse(locker.unlockWrite(), "Unlock without lock should return false")
assertFalse(locker.unlockWrite("key1"), "Unlock without lock should return false")
}
@Test
fun testProperReleaseReturnsTrue() = runTest {
locker.acquireRead() locker.acquireRead()
assertTrue(locker.releaseRead(), "Release after acquire should return true")
locker.acquireRead("key1")
assertTrue(locker.releaseRead("key1"), "Release after acquire should return true")
} }
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
locker.unlockWrite(testKey)
assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.acquireRead()
}
assertEquals(locker.readSemaphore().maxPermits - 1, locker.readSemaphore().freePermits)
assertTrue { locker.releaseRead() }
assertEquals(locker.readSemaphore().maxPermits, locker.readSemaphore().freePermits)
}
@Test @Test
fun testProperUnlockReturnsTrue() = runTest { fun writeLockFailedOnWriteLockKeyTest() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKey = "test"
locker.lockWrite(testKey)
assertTrue { locker.isWriteLocked(testKey) }
assertFails {
realWithTimeout(1.seconds) {
locker.lockWrite() locker.lockWrite()
assertTrue(locker.unlockWrite(), "Unlock after lock should return true") }
}
assertFalse(locker.isWriteLocked())
locker.lockWrite("key1") locker.unlockWrite(testKey)
assertTrue(locker.unlockWrite("key1"), "Unlock after lock should return true") assertFalse { locker.isWriteLocked(testKey) }
realWithTimeout(1.seconds) {
locker.lockWrite()
}
assertTrue(locker.isWriteLocked())
assertTrue { locker.unlockWrite() }
assertFalse(locker.isWriteLocked())
}
@Test
fun readsBlockingGlobalWrite() = runTest {
val locker = SmartKeyRWLocker<String>()
val testKeys = (0 until 100).map { "test$it" }
for (i in testKeys.indices) {
val it = testKeys[i]
locker.acquireRead(it)
val previous = testKeys.take(i)
val next = testKeys.drop(i + 1)
previous.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == Int.MAX_VALUE - 1 }
}
next.forEach {
assertTrue { locker.readSemaphoreOrNull(it) ?.freePermits == null }
}
} }
// ==================== Stress Tests ==================== for (i in testKeys.indices) {
val it = testKeys[i]
@Test assertFails {
fun stressTestWithMixedOperations() = runTest(timeout = 10.seconds) { realWithTimeout(13.milliseconds) { locker.lockWrite() }
val operations = 100 }
val keys = listOf("key1", "key2", "key3", "key4", "key5") val readPermitsBeforeLock = locker.readSemaphore().freePermits
val jobs = mutableListOf<Job>() realWithTimeout(1.seconds) { locker.acquireRead() }
repeat(operations) { i ->
val key = keys[i % keys.size]
when (i % 4) {
0 -> jobs.add(launch {
locker.acquireRead(key)
delay(10.milliseconds)
locker.releaseRead(key)
})
1 -> jobs.add(launch {
locker.lockWrite(key)
delay(10.milliseconds)
locker.unlockWrite(key)
})
2 -> jobs.add(launch {
locker.acquireRead()
delay(10.milliseconds)
locker.releaseRead() locker.releaseRead()
}) assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
3 -> jobs.add(launch {
locker.lockWrite() locker.releaseRead(it)
delay(10.milliseconds)
locker.unlockWrite()
})
}
} }
jobs.joinAll() assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
// If we reach here without deadlock or exceptions, test passes realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
} }
@Test @Test
fun testFairnessReadersDontStarveWriters() = runTest(timeout = 5.seconds) { fun writesBlockingGlobalWrite() = runTest {
val key = "testKey" val locker = SmartKeyRWLocker<String>()
var writerExecuted = false
// Start continuous readers val testKeys = (0 until 100).map { "test$it" }
val readers = List(10) {
launch { for (i in testKeys.indices) {
repeat(5) { val it = testKeys[i]
locker.acquireRead(key) locker.lockWrite(it)
delay(50.milliseconds) val previous = testKeys.take(i)
locker.releaseRead(key) val next = testKeys.drop(i + 1)
delay(10.milliseconds)
previous.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked == true }
} }
next.forEach {
assertTrue { locker.writeMutexOrNull(it) ?.isLocked != true }
} }
} }
delay(100.milliseconds) for (i in testKeys.indices) {
val it = testKeys[i]
assertFails { realWithTimeout(13.milliseconds) { locker.lockWrite() } }
// Try to acquire write lock val readPermitsBeforeLock = locker.readSemaphore().freePermits
val writer = launch { assertFails { realWithTimeout(13.milliseconds) { locker.acquireRead() } }
locker.lockWrite(key) assertEquals(readPermitsBeforeLock, locker.readSemaphore().freePermits)
writerExecuted = true
locker.unlockWrite(key) locker.unlockWrite(it)
} }
readers.joinAll() assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
writer.join() realWithTimeout(1.seconds) { locker.lockWrite() }
assertFails {
assertTrue(writerExecuted, "Writer should eventually execute") realWithTimeout(13.milliseconds) { locker.acquireRead() }
}
assertTrue { locker.unlockWrite() }
assertTrue { locker.readSemaphore().freePermits == Int.MAX_VALUE }
} }
} }

View File

@@ -1,4 +1,4 @@
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.asDeferred import dev.inmo.micro_utils.coroutines.asDeferred
import dev.inmo.micro_utils.coroutines.subscribe import dev.inmo.micro_utils.coroutines.subscribe
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -8,17 +8,17 @@ import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertTrue import kotlin.test.assertTrue
class MutableRedeliverStateFlowTests { class SpecialMutableStateFlowTests {
@Test @Test
fun simpleTest() = runTest { fun simpleTest() = runTest {
val specialMutableStateFlow = MutableRedeliverStateFlow(0) val specialMutableStateFlow = SpecialMutableStateFlow(0)
specialMutableStateFlow.value = 1 specialMutableStateFlow.value = 1
specialMutableStateFlow.first { it == 1 } specialMutableStateFlow.first { it == 1 }
assertEquals(1, specialMutableStateFlow.value) assertEquals(1, specialMutableStateFlow.value)
} }
@Test @Test
fun specialTest() = runTest { fun specialTest() = runTest {
val specialMutableStateFlow = MutableRedeliverStateFlow(0) val specialMutableStateFlow = SpecialMutableStateFlow(0)
lateinit var subscriberJob: Job lateinit var subscriberJob: Job
subscriberJob = specialMutableStateFlow.subscribe(this) { subscriberJob = specialMutableStateFlow.subscribe(this) {
when (it) { when (it) {

View File

@@ -8,6 +8,9 @@ android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# https://github.com/google/ksp/issues/2491
ksp.useKSP2=false
# JS NPM # JS NPM
crypto_js_version=4.1.1 crypto_js_version=4.1.1
@@ -15,5 +18,5 @@ crypto_js_version=4.1.1
# Project data # Project data
group=dev.inmo group=dev.inmo
version=0.25.8.2 version=0.26.1
android_code_version=298 android_code_version=300

View File

@@ -1,14 +1,14 @@
[versions] [versions]
kt = "2.1.21" kt = "2.2.0"
kt-serialization = "1.8.1" kt-serialization = "1.9.0"
kt-coroutines = "1.10.2" kt-coroutines = "1.10.2"
kotlinx-browser = "0.3" kotlinx-browser = "0.3"
kslog = "1.4.2" kslog = "1.5.0"
jb-compose = "1.8.1" jb-compose = "1.8.2"
jb-exposed = "0.61.0" jb-exposed = "0.61.0"
jb-dokka = "2.0.0" jb-dokka = "2.0.0"
@@ -17,19 +17,19 @@ sqlite = "3.50.1.0"
korlibs = "5.4.0" korlibs = "5.4.0"
uuid = "0.8.4" uuid = "0.8.4"
ktor = "3.1.3" ktor = "3.2.2"
gh-release = "2.5.2" gh-release = "2.5.2"
koin = "4.0.4" koin = "4.1.0"
okio = "3.12.0" okio = "3.15.0"
ksp = "2.1.20-1.0.31" ksp = "2.2.0-2.0.2"
kotlin-poet = "1.18.1" kotlin-poet = "2.2.0"
versions = "0.51.0" versions = "0.52.0"
nmcp = "0.1.5" nmcp = "1.0.1"
android-gradle = "8.9.+" android-gradle = "8.9.+"
dexcount = "4.0.0" dexcount = "4.0.0"

View File

@@ -10,6 +10,7 @@ repositories {
dependencies { dependencies {
api project(":micro_utils.koin") api project(":micro_utils.koin")
api project(":micro_utils.ksp.generator")
api libs.kotlin.poet api libs.kotlin.poet
api libs.ksp api libs.ksp
} }

View File

@@ -26,6 +26,7 @@ import com.squareup.kotlinpoet.asTypeName
import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo import com.squareup.kotlinpoet.ksp.writeTo
import dev.inmo.micro_ksp.generator.safeClassName
import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateGenericKoinDefinition
import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition import dev.inmo.micro_utils.koin.annotations.GenerateKoinDefinition
import org.koin.core.Koin import org.koin.core.Koin
@@ -237,15 +238,7 @@ class Processor(
""".trimIndent() """.trimIndent()
) )
ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach { ksFile.getAnnotationsByType(GenerateKoinDefinition::class).forEach {
val type = runCatching { val type = safeClassName { it.type }
it.type.asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
e.ksType.toClassName()
} else {
throw e
}
}
val targetType = runCatching { val targetType = runCatching {
type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type)) type.parameterizedBy(*(it.typeArgs.takeIf { it.isNotEmpty() } ?.map { it.asTypeName() } ?.toTypedArray() ?: return@runCatching type))
}.getOrElse { e -> }.getOrElse { e ->

View File

@@ -5,7 +5,6 @@ package dev.inmo.micro_utils.koin.generator.test
import kotlin.Any import kotlin.Any
import kotlin.Boolean import kotlin.Boolean
import kotlin.Deprecated
import kotlin.String import kotlin.String
import org.koin.core.Koin import org.koin.core.Koin
import org.koin.core.definition.Definition import org.koin.core.definition.Definition
@@ -30,95 +29,59 @@ public val Koin.sampleInfo: Test<String>
/** /**
* @return Definition by key "sampleInfo" with [parameters] * @return Definition by key "sampleInfo" with [parameters]
*/ */
public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = public inline fun Scope.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
get(named("sampleInfo"), parameters)
/** /**
* @return Definition by key "sampleInfo" with [parameters] * @return Definition by key "sampleInfo" with [parameters]
*/ */
public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = public inline fun Koin.sampleInfo(noinline parameters: ParametersDefinition): Test<String> = get(named("sampleInfo"), parameters)
get(named("sampleInfo"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo" * Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/ */
@Deprecated( public fun Module.singleSampleInfo(createdAtStart: Boolean = false, definition: Definition<Test<String>>): KoinDefinition<Test<String>> = single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
"This definition is old style and should not be used anymore. Use singleSampleInfo instead",
ReplaceWith("singleSampleInfo"),
)
public fun Module.sampleInfoSingle(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.single] and key "sampleInfo"
*/
public fun Module.singleSampleInfo(createdAtStart: Boolean = false,
definition: Definition<Test<String>>): KoinDefinition<Test<String>> =
single(named("sampleInfo"), createdAtStart = createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo" * Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/ */
@Deprecated( public fun Module.factorySampleInfo(definition: Definition<Test<String>>): KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
"This definition is old style and should not be used anymore. Use factorySampleInfo instead",
ReplaceWith("factorySampleInfo"),
)
public fun Module.sampleInfoFactory(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "sampleInfo"
*/
public fun Module.factorySampleInfo(definition: Definition<Test<String>>):
KoinDefinition<Test<String>> = factory(named("sampleInfo"), definition = definition)
/** /**
* @return Definition by key "test" with [parameters] * @return Definition by key "test" with [parameters]
*/ */
public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T public inline fun <reified T : Any> Scope.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
= get(named("test"), parameters)
/** /**
* @return Definition by key "test" with [parameters] * @return Definition by key "test" with [parameters]
*/ */
public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T public inline fun <reified T : Any> Koin.test(noinline parameters: ParametersDefinition? = null): T = get(named("test"), parameters)
= get(named("test"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "test" * Will register [definition] with [org.koin.core.module.Module.single] and key "test"
*/ */
public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline public inline fun <reified T : Any> Module.singleTest(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart = createdAtStart, definition = definition)
definition: Definition<T>): KoinDefinition<T> = single(named("test"), createdAtStart =
createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "test" * Will register [definition] with [org.koin.core.module.Module.factory] and key "test"
*/ */
public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): public inline fun <reified T : Any> Module.factoryTest(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("test"), definition = definition)
KoinDefinition<T> = factory(named("test"), definition = definition)
/** /**
* @return Definition by key "testNullable" with [parameters] * @return Definition by key "testNullable" with [parameters]
*/ */
public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = public inline fun <reified T : Any> Scope.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
null): T? = getOrNull(named("testNullable"), parameters)
/** /**
* @return Definition by key "testNullable" with [parameters] * @return Definition by key "testNullable" with [parameters]
*/ */
public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = public inline fun <reified T : Any> Koin.testNullable(noinline parameters: ParametersDefinition? = null): T? = getOrNull(named("testNullable"), parameters)
null): T? = getOrNull(named("testNullable"), parameters)
/** /**
* Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable" * Will register [definition] with [org.koin.core.module.Module.single] and key "testNullable"
*/ */
public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, public inline fun <reified T : Any> Module.singleTestNullable(createdAtStart: Boolean = false, noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"), createdAtStart = createdAtStart, definition = definition)
noinline definition: Definition<T>): KoinDefinition<T> = single(named("testNullable"),
createdAtStart = createdAtStart, definition = definition)
/** /**
* Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable" * Will register [definition] with [org.koin.core.module.Module.factory] and key "testNullable"
*/ */
public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): public inline fun <reified T : Any> Module.factoryTestNullable(noinline definition: Definition<T>): KoinDefinition<T> = factory(named("testNullable"), definition = definition)
KoinDefinition<T> = factory(named("testNullable"), definition = definition)

View File

@@ -0,0 +1,23 @@
package dev.inmo.micro_ksp.generator
import com.google.devtools.ksp.KSTypeNotPresentException
import com.google.devtools.ksp.KspExperimental
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.asTypeName
import kotlin.reflect.KClass
@Suppress("NOTHING_TO_INLINE")
@OptIn(KspExperimental::class)
inline fun safeClassName(classnameGetter: () -> KClass<*>) = runCatching {
classnameGetter().asTypeName()
}.getOrElse { e ->
if (e is KSTypeNotPresentException) {
ClassName(
e.ksType.declaration.packageName.asString(),
e.ksType.declaration.qualifiedName ?.asString() ?.replaceFirst(e.ksType.declaration.packageName.asString(), "")
?: e.ksType.declaration.simpleName.asString()
)
} else {
throw e
}
}

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.pagination.compose package dev.inmo.micro_utils.pagination.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.coroutines.runCatchingLogging import dev.inmo.micro_utils.coroutines.runCatchingLogging
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
@@ -27,8 +27,8 @@ class InfinityPagedComponentContext<T> internal constructor(
private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T> private val loader: suspend InfinityPagedComponentContext<T>.(Pagination) -> PaginationResult<T>
) { ) {
internal val startPage = SimplePagination(page, size) internal val startPage = SimplePagination(page, size)
internal val latestLoadedPage = MutableRedeliverStateFlow<PaginationResult<T>?>(null) internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null)
internal val dataState = MutableRedeliverStateFlow<List<T>?>(null) internal val dataState = SpecialMutableStateFlow<List<T>?>(null)
internal var loadingJob: Job? = null internal var loadingJob: Job? = null
internal val loadingMutex = Mutex() internal val loadingMutex = Mutex()

View File

@@ -1,7 +1,7 @@
package dev.inmo.micro_utils.pagination.compose package dev.inmo.micro_utils.pagination.compose
import androidx.compose.runtime.* import androidx.compose.runtime.*
import dev.inmo.micro_utils.coroutines.MutableRedeliverStateFlow import dev.inmo.micro_utils.coroutines.SpecialMutableStateFlow
import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions import dev.inmo.micro_utils.coroutines.launchLoggingDropExceptions
import dev.inmo.micro_utils.pagination.* import dev.inmo.micro_utils.pagination.*
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -28,8 +28,8 @@ class PagedComponentContext<T> internal constructor(
private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T> private val loader: suspend PagedComponentContext<T>.(Pagination) -> PaginationResult<T>
) { ) {
internal val startPage = SimplePagination(initialPage, size) internal val startPage = SimplePagination(initialPage, size)
internal val latestLoadedPage = MutableRedeliverStateFlow<PaginationResult<T>?>(null) internal val latestLoadedPage = SpecialMutableStateFlow<PaginationResult<T>?>(null)
internal val dataState = MutableRedeliverStateFlow<PaginationResult<T>?>(null) internal val dataState = SpecialMutableStateFlow<PaginationResult<T>?>(null)
internal var loadingJob: Job? = null internal var loadingJob: Job? = null
internal val loadingMutex = Mutex() internal val loadingMutex = Mutex()