mirror of
https://github.com/InsanusMokrassar/MicroUtils.git
synced 2026-09-26 02:15:31 +00:00
Compare commits
13 Commits
renovate/j
...
0.30.2
| Author | SHA1 | Date | |
|---|---|---|---|
| 9516abef15 | |||
| cb1eb40b4b | |||
| 9d20b9bf5d | |||
| b00e93372d | |||
| 8f51cd4d60 | |||
| f618ea05f7 | |||
| 2b053dfedb | |||
| f6ab406ffa | |||
| fe635835a5 | |||
| 795bc4e09e | |||
| d431446da0 | |||
| 362821e4d7 | |||
| 2045db9cd0 |
2
.github/workflows/dokka_push.yml
vendored
2
.github/workflows/dokka_push.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
java-version: 17
|
java-version: 17
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./gradlew build && ./gradlew dokkaHtml
|
run: ./gradlew build && ./gradlew :micro_utils.dokka:dokkaGenerate
|
||||||
- name: Publish KDocs
|
- name: Publish KDocs
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
33
CHANGELOG.md
33
CHANGELOG.md
@@ -1,5 +1,38 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.30.2
|
||||||
|
|
||||||
|
* `Coroutines`:
|
||||||
|
* `SmartRWLocker`:
|
||||||
|
* Fix of `unlockWrite`, `lockWrite` and `releaseRead` calls to pass correct number of permits
|
||||||
|
* `SmartMutex`:
|
||||||
|
* Fix `unlock` call
|
||||||
|
* `SmartSemaphore`:
|
||||||
|
* Fix same issues to avoid cancellation exceptions handling errors and several other problems
|
||||||
|
|
||||||
|
## 0.30.1
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `KSLog`: `1.6.1` -> `1.7.0`
|
||||||
|
* `SQLite`: `3.53.2.0` -> `3.53.2.1`
|
||||||
|
* `Ktor`: `3.5.1` -> `3.5.2`
|
||||||
|
* `Okio`: `3.17.0` -> `3.18.1`
|
||||||
|
|
||||||
|
## 0.30.0
|
||||||
|
|
||||||
|
* `Versions`:
|
||||||
|
* `Compose`: `1.11.0` -> `1.11.1`
|
||||||
|
* `KSP`: `2.3.8` -> `2.3.9`
|
||||||
|
* `Ktor`: `3.5.0` -> `3.5.1`
|
||||||
|
* `Koin`: `4.2.1` -> `4.2.2`
|
||||||
|
* `SQLite`: `3.53.1.0` -> `3.53.2.0`
|
||||||
|
* `AndroidX Core KTX`: `1.18.0` -> `1.19.0`
|
||||||
|
|
||||||
|
## 0.29.4
|
||||||
|
|
||||||
|
* `Meta`:
|
||||||
|
* Inited
|
||||||
|
|
||||||
## 0.29.3
|
## 0.29.3
|
||||||
|
|
||||||
* `Versions`:
|
* `Versions`:
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ allprojects {
|
|||||||
mavenCentral()
|
mavenCentral()
|
||||||
google()
|
google()
|
||||||
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
maven { url "https://maven.pkg.jetbrains.space/public/p/compose/dev" }
|
||||||
maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
// maven { url "https://nexus.inmo.dev/repository/maven-releases/" }
|
||||||
mavenLocal()
|
mavenLocal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -7,6 +8,7 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@@ -92,8 +94,8 @@ sealed interface SmartMutex {
|
|||||||
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
|
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
|
||||||
* [SmartMutex] - false
|
* [SmartMutex] - false
|
||||||
*/
|
*/
|
||||||
suspend fun unlock(): Boolean {
|
suspend fun unlock(): Boolean = withContext(NonCancellable) {
|
||||||
return if (_lockStateFlow.value) {
|
if (_lockStateFlow.value) {
|
||||||
internalChangesMutex.withLock {
|
internalChangesMutex.withLock {
|
||||||
if (_lockStateFlow.value) {
|
if (_lockStateFlow.value) {
|
||||||
_lockStateFlow.value = false
|
_lockStateFlow.value = false
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@@ -21,6 +23,7 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
|
|||||||
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
val readSemaphore: SmartSemaphore.Immutable = _readSemaphore.immutable()
|
||||||
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
|
val writeMutex: SmartMutex.Immutable = _writeMutex.immutable()
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do lock in [readSemaphore] inside of [writeMutex] locking
|
* Do lock in [readSemaphore] inside of [writeMutex] locking
|
||||||
*/
|
*/
|
||||||
@@ -32,8 +35,8 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
|
|||||||
/**
|
/**
|
||||||
* Release one read permit in [readSemaphore]
|
* Release one read permit in [readSemaphore]
|
||||||
*/
|
*/
|
||||||
suspend fun releaseRead(): Boolean {
|
suspend fun releaseRead(): Boolean = withContext(NonCancellable) {
|
||||||
return _readSemaphore.release()
|
_readSemaphore.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +47,9 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
|
|||||||
try {
|
try {
|
||||||
_readSemaphore.acquire(readPermits)
|
_readSemaphore.acquire(readPermits)
|
||||||
} catch (e: CancellationException) {
|
} catch (e: CancellationException) {
|
||||||
_writeMutex.unlock()
|
withContext(NonCancellable) {
|
||||||
|
_writeMutex.unlock()
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,9 +57,9 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
|
|||||||
/**
|
/**
|
||||||
* Unlock [writeMutex]
|
* Unlock [writeMutex]
|
||||||
*/
|
*/
|
||||||
suspend fun unlockWrite(): Boolean {
|
suspend fun unlockWrite(): Boolean = withContext(NonCancellable) {
|
||||||
return _writeMutex.unlock().also {
|
_writeMutex.unlock().also { unlocked ->
|
||||||
if (it) {
|
if (unlocked) {
|
||||||
_readSemaphore.release(readPermits)
|
_readSemaphore.release(readPermits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package dev.inmo.micro_utils.coroutines
|
package dev.inmo.micro_utils.coroutines
|
||||||
|
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -8,6 +9,7 @@ import kotlinx.coroutines.isActive
|
|||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.contracts.ExperimentalContracts
|
import kotlin.contracts.ExperimentalContracts
|
||||||
import kotlin.contracts.InvocationKind
|
import kotlin.contracts.InvocationKind
|
||||||
import kotlin.contracts.contract
|
import kotlin.contracts.contract
|
||||||
@@ -76,7 +78,9 @@ sealed interface SmartSemaphore {
|
|||||||
}
|
}
|
||||||
} while (shouldContinue && currentCoroutineContext().isActive)
|
} while (shouldContinue && currentCoroutineContext().isActive)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
release(acquiredPermits)
|
if (acquiredPermits > 0) {
|
||||||
|
release(acquiredPermits)
|
||||||
|
}
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,9 +111,9 @@ sealed interface SmartSemaphore {
|
|||||||
*/
|
*/
|
||||||
suspend fun tryAcquire(permits: Int = 1): Boolean {
|
suspend fun tryAcquire(permits: Int = 1): Boolean {
|
||||||
val checkedPermits = checkedPermits(permits)
|
val checkedPermits = checkedPermits(permits)
|
||||||
return if (_freePermitsStateFlow.value < checkedPermits) {
|
return if (_freePermitsStateFlow.value >= checkedPermits) {
|
||||||
internalChangesMutex.withLock {
|
internalChangesMutex.withLock {
|
||||||
if (_freePermitsStateFlow.value < checkedPermits) {
|
if (_freePermitsStateFlow.value >= checkedPermits) {
|
||||||
_freePermitsStateFlow.value -= checkedPermits
|
_freePermitsStateFlow.value -= checkedPermits
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -125,12 +129,12 @@ sealed interface SmartSemaphore {
|
|||||||
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
|
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
|
||||||
* [SmartSemaphore] - false
|
* [SmartSemaphore] - false
|
||||||
*/
|
*/
|
||||||
suspend fun release(permits: Int = 1): Boolean {
|
suspend fun release(permits: Int = 1): Boolean = withContext(NonCancellable) {
|
||||||
val checkedPermits = checkedPermits(permits)
|
val checkedPermits = checkedPermits(permits)
|
||||||
return if (_freePermitsStateFlow.value < this.maxPermits) {
|
if (_freePermitsStateFlow.value < maxPermits) {
|
||||||
internalChangesMutex.withLock {
|
internalChangesMutex.withLock {
|
||||||
if (_freePermitsStateFlow.value < this.maxPermits) {
|
if (_freePermitsStateFlow.value < maxPermits) {
|
||||||
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, this.maxPermits)
|
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, maxPermits)
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
|||||||
73
coroutines/src/commonTest/kotlin/SmartMutexTests.kt
Normal file
73
coroutines/src/commonTest/kotlin/SmartMutexTests.kt
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.SmartMutex
|
||||||
|
import dev.inmo.micro_utils.coroutines.withLock
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.cancel
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class SmartMutexTests {
|
||||||
|
@Test
|
||||||
|
fun cancelledUnlockCompletesUnderContention() = runTest(timeout = 5.seconds) {
|
||||||
|
val mutex = SmartMutex.Mutable()
|
||||||
|
|
||||||
|
// Delegate this acquisition's release to another coroutine. The
|
||||||
|
// unconfined collector runs while lock() still holds its internal mutex.
|
||||||
|
val releaser = launch(Dispatchers.Unconfined) {
|
||||||
|
mutex.lockStateFlow.first { it }
|
||||||
|
currentCoroutineContext().cancel()
|
||||||
|
mutex.unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep acquisition on the normal test dispatcher so the collector
|
||||||
|
// attempts cancelled cleanup before the internal mutex is released.
|
||||||
|
mutex.lock()
|
||||||
|
releaser.join()
|
||||||
|
|
||||||
|
assertTrue(releaser.isCancelled)
|
||||||
|
assertFalse(mutex.isLocked, "Cancellation must not prevent the delegated unlock")
|
||||||
|
assertTrue(mutex.tryLock())
|
||||||
|
assertTrue(mutex.unlock())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancellingWithLockBodyReleasesMutex() = runTest(timeout = 5.seconds) {
|
||||||
|
val mutex = SmartMutex.Mutable()
|
||||||
|
val holder = launch(Dispatchers.Unconfined) {
|
||||||
|
mutex.withLock {
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertTrue(mutex.isLocked)
|
||||||
|
|
||||||
|
holder.cancelAndJoin()
|
||||||
|
|
||||||
|
assertFalse(mutex.isLocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelledWaiterDoesNotEnterOrReleaseHeldMutex() = runTest(timeout = 5.seconds) {
|
||||||
|
val mutex = SmartMutex.Mutable()
|
||||||
|
var entered = false
|
||||||
|
mutex.withLock {
|
||||||
|
val waiter = launch(Dispatchers.Unconfined) {
|
||||||
|
mutex.withLock {
|
||||||
|
entered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
waiter.cancelAndJoin()
|
||||||
|
|
||||||
|
assertFalse(entered)
|
||||||
|
assertTrue(mutex.isLocked)
|
||||||
|
}
|
||||||
|
assertFalse(mutex.isLocked)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import kotlin.test.assertEquals
|
|||||||
import kotlin.test.assertFails
|
import kotlin.test.assertFails
|
||||||
import kotlin.test.assertFalse
|
import kotlin.test.assertFalse
|
||||||
import kotlin.test.assertTrue
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
class SmartRWLockerTests {
|
class SmartRWLockerTests {
|
||||||
@@ -109,6 +110,59 @@ class SmartRWLockerTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun failureOnReadFreeingRead() = runTest {
|
||||||
|
val locker = SmartRWLocker()
|
||||||
|
val job = launch {
|
||||||
|
locker.withReadAcquire {
|
||||||
|
while (isActive) {
|
||||||
|
delay(1.days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
locker.readSemaphore.permitsStateFlow.first {
|
||||||
|
it == locker.readSemaphore.maxPermits - 1
|
||||||
|
}
|
||||||
|
job.cancelAndJoin()
|
||||||
|
locker.readSemaphore.permitsStateFlow.first {
|
||||||
|
it == locker.readSemaphore.maxPermits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelledReaderReleasesPermitUnderContention() = runTest(timeout = 5.seconds) {
|
||||||
|
val locker = SmartRWLocker(readPermits = 2)
|
||||||
|
val reader = launch(Dispatchers.Unconfined) {
|
||||||
|
locker.withReadAcquire {
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(1, locker.readSemaphore.freePermits)
|
||||||
|
|
||||||
|
// Observe the second acquisition synchronously while it still holds the
|
||||||
|
// semaphore's internal mutex. Cancelling the unconfined reader makes its
|
||||||
|
// cleanup contend for that mutex before the acquisition can release it.
|
||||||
|
val cancellation = launch(Dispatchers.Unconfined) {
|
||||||
|
locker.readSemaphore.permitsStateFlow.first { it == 0 }
|
||||||
|
reader.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep this acquisition on the normal test dispatcher: making it
|
||||||
|
// unconfined would change the ordering that forces cleanup contention.
|
||||||
|
locker.withReadAcquire {
|
||||||
|
cancellation.join()
|
||||||
|
reader.join()
|
||||||
|
assertTrue(reader.isCancelled)
|
||||||
|
assertEquals(
|
||||||
|
1,
|
||||||
|
locker.readSemaphore.freePermits,
|
||||||
|
"The cancelled reader must release its permit while the other reader still holds one"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertEquals(2, locker.readSemaphore.freePermits)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun simpleWithReadAcquireTest() {
|
fun simpleWithReadAcquireTest() {
|
||||||
val locker = SmartRWLocker()
|
val locker = SmartRWLocker()
|
||||||
|
|||||||
104
coroutines/src/commonTest/kotlin/SmartSemaphoreTests.kt
Normal file
104
coroutines/src/commonTest/kotlin/SmartSemaphoreTests.kt
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import dev.inmo.micro_utils.coroutines.SmartSemaphore
|
||||||
|
import dev.inmo.micro_utils.coroutines.withAcquire
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.cancelAndJoin
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertFalse
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
class SmartSemaphoreTests {
|
||||||
|
@Test
|
||||||
|
fun cancelledHolderReleasesPermitUnderContention() = runTest(timeout = 5.seconds) {
|
||||||
|
val semaphore = SmartSemaphore.Mutable(permits = 2)
|
||||||
|
val holder = launch(Dispatchers.Unconfined) {
|
||||||
|
semaphore.withAcquire {
|
||||||
|
awaitCancellation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assertEquals(1, semaphore.freePermits)
|
||||||
|
|
||||||
|
// The synchronous observer cancels the holder while the second
|
||||||
|
// acquisition still owns the semaphore's internal changes mutex.
|
||||||
|
val cancellation = launch(Dispatchers.Unconfined) {
|
||||||
|
semaphore.permitsStateFlow.first { it == 0 }
|
||||||
|
holder.cancel()
|
||||||
|
}
|
||||||
|
semaphore.withAcquire {
|
||||||
|
cancellation.join()
|
||||||
|
holder.join()
|
||||||
|
assertTrue(holder.isCancelled)
|
||||||
|
assertEquals(1, semaphore.freePermits, "The cancelled holder must return its permit")
|
||||||
|
}
|
||||||
|
assertEquals(2, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelledAcquireReturnsPartialPermitsUnderContention() = runTest(timeout = 5.seconds) {
|
||||||
|
// One permit belongs to another holder; the waiter can acquire two
|
||||||
|
// permits immediately, but must wait for the third.
|
||||||
|
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 1)
|
||||||
|
lateinit var waiter: kotlinx.coroutines.Job
|
||||||
|
val cancellation = launch(Dispatchers.Unconfined) {
|
||||||
|
semaphore.permitsStateFlow.first { it == 1 }
|
||||||
|
waiter.cancel()
|
||||||
|
}
|
||||||
|
waiter = launch(Dispatchers.Unconfined) {
|
||||||
|
semaphore.acquire(3)
|
||||||
|
}
|
||||||
|
assertEquals(0, semaphore.freePermits)
|
||||||
|
assertFalse(waiter.isCompleted)
|
||||||
|
|
||||||
|
// Publishing this release resumes the observer while the internal
|
||||||
|
// mutex is held. The cancelled acquire must wait to roll back safely.
|
||||||
|
semaphore.release()
|
||||||
|
cancellation.join()
|
||||||
|
waiter.join()
|
||||||
|
assertTrue(waiter.isCancelled)
|
||||||
|
assertEquals(3, semaphore.freePermits, "Cancellation must return both partially acquired permits")
|
||||||
|
semaphore.withAcquire(3) {
|
||||||
|
assertEquals(0, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
assertEquals(3, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun cancelledAcquireWithoutPermitsDoesNotReleaseAnotherHoldersPermit() = runTest(timeout = 5.seconds) {
|
||||||
|
val semaphore = SmartSemaphore.Mutable(permits = 1, acquiredPermits = 1)
|
||||||
|
val waiter = launch(Dispatchers.Unconfined) {
|
||||||
|
semaphore.acquire()
|
||||||
|
}
|
||||||
|
assertFalse(waiter.isCompleted)
|
||||||
|
|
||||||
|
waiter.cancelAndJoin()
|
||||||
|
assertEquals(0, semaphore.freePermits, "A cancelled waiter that acquired nothing must release nothing")
|
||||||
|
assertTrue(semaphore.release())
|
||||||
|
assertEquals(1, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tryAcquireUsesAvailablePermits() = runTest {
|
||||||
|
val semaphore = SmartSemaphore.Mutable(permits = 3)
|
||||||
|
assertTrue(semaphore.tryAcquire(2))
|
||||||
|
assertEquals(1, semaphore.freePermits)
|
||||||
|
assertTrue(semaphore.tryAcquire())
|
||||||
|
assertEquals(0, semaphore.freePermits)
|
||||||
|
assertTrue(semaphore.release(3))
|
||||||
|
assertEquals(3, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tryAcquireWithInsufficientPermitsLeavesStateUnchanged() = runTest {
|
||||||
|
val semaphore = SmartSemaphore.Mutable(permits = 3, acquiredPermits = 2)
|
||||||
|
assertFalse(semaphore.tryAcquire(2))
|
||||||
|
assertEquals(1, semaphore.freePermits)
|
||||||
|
semaphore.acquire()
|
||||||
|
assertFalse(semaphore.tryAcquire())
|
||||||
|
assertEquals(0, semaphore.freePermits)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ kotlin {
|
|||||||
browser()
|
browser()
|
||||||
nodejs()
|
nodejs()
|
||||||
}
|
}
|
||||||
android {}
|
androidTarget {}
|
||||||
|
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
@@ -91,7 +91,7 @@ kotlin {
|
|||||||
|
|
||||||
private List<SourceDirectorySet> findSourcesWithName(String... approximateNames) {
|
private List<SourceDirectorySet> findSourcesWithName(String... approximateNames) {
|
||||||
return parent.subprojects
|
return parent.subprojects
|
||||||
.findAll { it != project && it.hasProperty("kotlin") }
|
.findAll { it != project && it.hasProperty("kotlin") && (it.name.contains("dokka") == false) }
|
||||||
.collectMany { it.kotlin.sourceSets }
|
.collectMany { it.kotlin.sourceSets }
|
||||||
.findAll { sourceSet ->
|
.findAll { sourceSet ->
|
||||||
approximateNames.any { nameToFilter ->
|
approximateNames.any { nameToFilter ->
|
||||||
@@ -100,14 +100,14 @@ private List<SourceDirectorySet> findSourcesWithName(String... approximateNames)
|
|||||||
}.collect { it.kotlin }
|
}.collect { it.kotlin }
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.dokkaHtml {
|
dokka {
|
||||||
dokkaSourceSets {
|
dokkaSourceSets {
|
||||||
configureEach {
|
configureEach {
|
||||||
skipDeprecated.set(true)
|
skipDeprecated.set(true)
|
||||||
|
|
||||||
sourceLink {
|
sourceLink {
|
||||||
localDirectory.set(file("../"))
|
localDirectory.set(file("../"))
|
||||||
remoteUrl.set(new URL("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
remoteUrl.set(new URI("https://github.com/InsanusMokrassar/MicroUtils/blob/master/"))
|
||||||
remoteLineSuffix.set("#L")
|
remoteLineSuffix.set("#L")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,4 +130,12 @@ tasks.dokkaHtml {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//dependencies {
|
||||||
|
// project.parent.subprojects.forEach {
|
||||||
|
// if (it != project) {
|
||||||
|
// dokka(it)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
apply from: "$defaultAndroidSettings"
|
apply from: "$defaultAndroidSettings"
|
||||||
|
|||||||
@@ -18,5 +18,5 @@ crypto_js_version=4.1.1
|
|||||||
# Project data
|
# Project data
|
||||||
|
|
||||||
group=dev.inmo
|
group=dev.inmo
|
||||||
version=0.29.3
|
version=0.30.2
|
||||||
android_code_version=313
|
android_code_version=316
|
||||||
|
|||||||
@@ -6,28 +6,28 @@ kt-coroutines = "1.11.0"
|
|||||||
|
|
||||||
kotlinx-browser = "0.5.0"
|
kotlinx-browser = "0.5.0"
|
||||||
|
|
||||||
kslog = "1.6.1"
|
kslog = "1.7.0"
|
||||||
|
|
||||||
jb-compose = "1.11.0"
|
jb-compose = "1.11.1"
|
||||||
jb-compose-material3 = "1.11.0-alpha07"
|
jb-compose-material3 = "1.11.0-alpha07"
|
||||||
jb-compose-icons = "1.7.8"
|
jb-compose-icons = "1.7.8"
|
||||||
jb-exposed = "1.3.0"
|
jb-exposed = "1.3.0"
|
||||||
jb-dokka = "2.2.0"
|
jb-dokka = "2.2.0"
|
||||||
|
|
||||||
sqlite = "3.53.1.0"
|
sqlite = "3.53.2.1"
|
||||||
|
|
||||||
korlibs = "5.4.0"
|
korlibs = "5.4.0"
|
||||||
uuid = "0.8.4"
|
uuid = "0.8.4"
|
||||||
|
|
||||||
ktor = "3.5.0"
|
ktor = "3.5.2"
|
||||||
|
|
||||||
gh-release = "2.5.2"
|
gh-release = "2.5.2"
|
||||||
|
|
||||||
koin = "4.2.1"
|
koin = "4.2.2"
|
||||||
|
|
||||||
okio = "3.17.0"
|
okio = "3.18.1"
|
||||||
|
|
||||||
ksp = "2.3.8"
|
ksp = "2.3.9"
|
||||||
kotlin-poet = "2.3.0"
|
kotlin-poet = "2.3.0"
|
||||||
|
|
||||||
versions = "0.54.0"
|
versions = "0.54.0"
|
||||||
@@ -36,7 +36,7 @@ nmcp = "1.5.0"
|
|||||||
android-gradle = "8.12.+"
|
android-gradle = "8.12.+"
|
||||||
dexcount = "4.0.0"
|
dexcount = "4.0.0"
|
||||||
|
|
||||||
android-coreKtx = "1.18.0"
|
android-coreKtx = "1.19.0"
|
||||||
android-recyclerView = "1.4.0"
|
android-recyclerView = "1.4.0"
|
||||||
android-appCompat = "1.7.1"
|
android-appCompat = "1.7.1"
|
||||||
android-fragment = "1.8.9"
|
android-fragment = "1.8.9"
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
project.version = "$version"
|
project.version = "$version"
|
||||||
project.group = "$group"
|
project.group = "$group"
|
||||||
|
|
||||||
|
apply plugin: 'org.jetbrains.dokka'
|
||||||
|
|
||||||
kotlin {
|
kotlin {
|
||||||
sourceSets {
|
sourceSets {
|
||||||
commonMain {
|
commonMain {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class Processor(
|
|||||||
val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation
|
val annotation = ksClassDeclaration.getGenerateSealedWorkaroundAnnotation
|
||||||
val subClasses = ksClassDeclaration.resolveSubclasses(
|
val subClasses = ksClassDeclaration.resolveSubclasses(
|
||||||
searchIn = resolver.getAllFiles(),
|
searchIn = resolver.getAllFiles(),
|
||||||
allowNonSealed = annotation ?.includeNonSealedSubTypes ?: false
|
allowNonSealed = withNoSuchElementWorkaround(null) { annotation ?.includeNonSealedSubTypes } ?: false
|
||||||
).distinct()
|
).distinct()
|
||||||
val subClassesNames = subClasses.filter {
|
val subClassesNames = subClasses.filter {
|
||||||
when (it.classKind) {
|
when (it.classKind) {
|
||||||
@@ -165,15 +165,7 @@ class Processor(
|
|||||||
@OptIn(KspExperimental::class)
|
@OptIn(KspExperimental::class)
|
||||||
override fun process(resolver: Resolver): List<KSAnnotated> {
|
override fun process(resolver: Resolver): List<KSAnnotated> {
|
||||||
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
(resolver.getSymbolsWithAnnotation(GenerateSealedWorkaround::class.qualifiedName!!)).filterIsInstance<KSClassDeclaration>().forEach {
|
||||||
val prefix = runCatching {
|
val prefix = withNoSuchElementWorkaround(null) { (it.getGenerateSealedWorkaroundAnnotation) ?.prefix } ?.takeIf {
|
||||||
(it.getGenerateSealedWorkaroundAnnotation) ?.prefix
|
|
||||||
}.getOrElse {
|
|
||||||
if (it is NoSuchElementException) {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
throw it
|
|
||||||
}
|
|
||||||
} ?.takeIf {
|
|
||||||
it.isNotEmpty()
|
it.isNotEmpty()
|
||||||
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
|
} ?: it.buildSubFileName.replaceFirst(it.simpleName.asString(), "")
|
||||||
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
|
it.writeFile(prefix = prefix, suffix = "SealedWorkaround") {
|
||||||
|
|||||||
12
meta/build.gradle
Normal file
12
meta/build.gradle
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
plugins {
|
||||||
|
id "org.jetbrains.kotlin.multiplatform"
|
||||||
|
id "org.jetbrains.kotlin.plugin.serialization"
|
||||||
|
id "com.android.library"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
sourceSets {
|
||||||
|
}
|
||||||
|
}
|
||||||
94
meta/src/commonMain/kotlin/MetaContainer.kt
Normal file
94
meta/src/commonMain/kotlin/MetaContainer.kt
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
package dev.inmo.micro_utils.meta
|
||||||
|
|
||||||
|
import kotlinx.serialization.Polymorphic
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A polymorphic container for storing heterogeneous key-value pairs with type-safe retrieval.
|
||||||
|
* Each key is bound to a specific type, enabling type-safe access to stored values.
|
||||||
|
*
|
||||||
|
* @property map The underlying map storing key-value pairs with polymorphic values.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class MetaContainer(
|
||||||
|
@MetaContainerRootMapWarning
|
||||||
|
val map: Map<Key<*>, @Polymorphic Any>
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* A marker interface for type-safe keys in [MetaContainer].
|
||||||
|
*
|
||||||
|
* @param T The type of value associated with this key.
|
||||||
|
*/
|
||||||
|
interface Key<T : Any>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a value from the container by its key.
|
||||||
|
*
|
||||||
|
* @param key The type-safe key to look up.
|
||||||
|
* @return The value associated with the key, or null if not present.
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST", "OPT_IN_USAGE")
|
||||||
|
operator fun <T : Any> get(key: Key<T>): T? = map[key] as? T?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a value exists for the given key.
|
||||||
|
*
|
||||||
|
* @param key The type-safe key to check.
|
||||||
|
* @return true if the key exists and has a non-null value, false otherwise.
|
||||||
|
*/
|
||||||
|
operator fun <T : Any> contains(key: Key<T>): Boolean = get(key) != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for constructing [MetaContainer] instances with a fluent API.
|
||||||
|
*/
|
||||||
|
class Builder(
|
||||||
|
@MetaContainerRootMapWarning
|
||||||
|
private val map: MutableMap<Key<*>, Any> = mutableMapOf<Key<*>, Any>()
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Puts a value associated with the given key into the builder.
|
||||||
|
*
|
||||||
|
* @param k The type-safe key.
|
||||||
|
* @param v The value to store.
|
||||||
|
*/
|
||||||
|
fun <T : Any> put(k: Key<T>, v: T) {
|
||||||
|
map[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a value from the builder by its key.
|
||||||
|
*
|
||||||
|
* @param key The type-safe key to look up.
|
||||||
|
* @return The value associated with the key, or null if not present.
|
||||||
|
*/
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
operator fun <T : Any> get(key: Key<T>): T? = map[key] as T?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a value exists for the given key in the builder.
|
||||||
|
*
|
||||||
|
* @param key The type-safe key to check.
|
||||||
|
* @return true if the key exists and has a non-null value, false otherwise.
|
||||||
|
*/
|
||||||
|
operator fun <T : Any> contains(key: Key<T>): Boolean = get(key) != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds and returns the immutable [MetaContainer] instance.
|
||||||
|
*
|
||||||
|
* @return A new [MetaContainer] with the accumulated key-value pairs.
|
||||||
|
*/
|
||||||
|
fun build(): MetaContainer = MetaContainer(map.toMap())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* An empty [MetaContainer] instance with no entries.
|
||||||
|
*/
|
||||||
|
val EMPTY = MetaContainer(emptyMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
17
meta/src/commonMain/kotlin/MetaContainerRootMapWarning.kt
Normal file
17
meta/src/commonMain/kotlin/MetaContainerRootMapWarning.kt
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package dev.inmo.micro_utils.meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the direct use of [MetaContainer.map] as requiring explicit opt-in.
|
||||||
|
*
|
||||||
|
* This annotation warns against direct manipulation of the internal map without using
|
||||||
|
* the type-safe accessors, which could break type safety guarantees.
|
||||||
|
*/
|
||||||
|
@RequiresOptIn(
|
||||||
|
"Do not use this directly without any special reason",
|
||||||
|
RequiresOptIn.Level.WARNING
|
||||||
|
)
|
||||||
|
@Target(
|
||||||
|
AnnotationTarget.FIELD,
|
||||||
|
)
|
||||||
|
@Retention(AnnotationRetention.BINARY)
|
||||||
|
annotation class MetaContainerRootMapWarning
|
||||||
13
meta/src/commonMain/kotlin/buildMetaContainer.kt
Normal file
13
meta/src/commonMain/kotlin/buildMetaContainer.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package dev.inmo.micro_utils.meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL builder function for creating a [MetaContainer] with a lambda block.
|
||||||
|
*
|
||||||
|
* @param block A lambda with receiver ([MetaContainer.Builder]) to configure the container.
|
||||||
|
* @return A new [MetaContainer] instance built from the DSL block.
|
||||||
|
*/
|
||||||
|
fun buildMetaContainer(block: MetaContainer.Builder.() -> Unit): MetaContainer {
|
||||||
|
val builder = MetaContainer.Builder()
|
||||||
|
builder.block()
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
rootProject.name='micro_utils'
|
rootProject.name='micro_utils'
|
||||||
|
|
||||||
String[] includes = [
|
String[] includes = [
|
||||||
|
":meta",
|
||||||
":common",
|
":common",
|
||||||
":common:compose",
|
":common:compose",
|
||||||
":transactions",
|
":transactions",
|
||||||
|
|||||||
Reference in New Issue
Block a user