Compare commits

..

3 Commits

Author SHA1 Message Date
9516abef15 update changelog 2026-09-23 22:54:38 +06:00
cb1eb40b4b fix of smart lockers 2026-09-23 22:54:13 +06:00
9d20b9bf5d start 0.30.2 2026-09-23 22:54:02 +06:00
66 changed files with 223 additions and 441 deletions

View File

@@ -1,45 +1,6 @@
# Changelog
## 0.32.1
* `Coroutines`:
* `SmartSemaphore`:
* Optimize permit checks, acquisition, uncontended releases and already satisfied waits to reduce allocations while preserving cancellation handling.
* `SmartMutex`:
* Optimize waits for unlocked mutexes and uncontended unlocks to reduce allocations while preserving cancellation handling.
* `SmartRWLocker`:
* Remove redundant coroutine contexts during read releases, write unlocks and cancellation cleanup.
* `SmartKeyRWLocker`:
* Reduce allocations during keyed operations by inlining the locker allocation helper and using a hash map without insertion order tracking.
## 0.32.0
* `Versions`:
* `crypto-js`: `4.1.1` -> `4.2.0`
* `AndroidX Fragment`: `1.9.0` -> `1.9.1`
* `AndroidX Core KTX`: `1.19.0` -> `1.19.1`
* `NMCP`: `1.6.1` -> `1.6.2`
* `Gradle Versions`: `0.61.0` -> `0.65.0`
* `KotlinPoet`: `2.3.0` -> `2.4.0`
* `Okio`: `3.18.1` -> `3.18.2`
* `Ktor`: `3.5.2` -> `3.6.0`
* `Exposed`: `1.5.0` -> `1.5.1`
* `Compose Material3`: `1.11.0-alpha07` -> `1.9.0`
* `KSLog`: `1.7.0` -> `2.1.0`
* `Kotlin`: `2.4.10` -> `2.4.21`
* `Compose`: `1.12.0` -> `1.12.1`
* `Gradle wrapper`: `9.3.1` -> `9.7.0`
* `Coroutines`:
* `SmartSemaphore`:
* Rewrite docs for `freePermits` property.
* Add constructor arguments requirements to fail on invalid `permits` and `acquiredPermits` values.
* `SmartRWLocker`:
* Add constructor arguments requirements to fail on invalid `readPermits` values.
* `SmartKeyRWLocker`:
* Add constructor arguments requirements to fail on invalid `globalLockerReadPermits` and `perKeyReadPermits` values.
## 0.31.1
## 0.30.2
* `Coroutines`:
* `SmartRWLocker`:
@@ -49,17 +10,6 @@
* `SmartSemaphore`:
* Fix same issues to avoid cancellation exceptions handling errors and several other problems
## 0.31.0
* `Versions`:
* `Kotlin`: `2.3.21` -> `2.4.10`
* `SQLite`: `3.53.2.1` -> `3.53.4.0`
* `KSP`: `2.3.9` -> `2.3.11`
* `Exposed`: `1.3.0` -> `1.5.0`
* `NMCP`: `1.5.0` -> `1.6.1`
* `AndroidX AppCompat`: `1.7.1` -> `1.8.0`
* `AndroidX Fragment`: `1.8.9` -> `1.9.0`
## 0.30.1
* `Versions`:

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppAndroidProject"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppAndroidProject"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppAndroidProject"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
@@ -13,7 +13,7 @@ kotlin {
api libs.klock
}
}
jvmAndAndroidMain {
jvmMain {
dependencies {
api project(":micro_utils.coroutines")
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}

View File

@@ -28,16 +28,11 @@ class SmartKeyRWLocker<T>(
globalLockerWriteIsLocked: Boolean = false,
private val perKeyReadPermits: Int = Int.MAX_VALUE
) {
init {
require(globalLockerReadPermits > 0) { "The maximal number 'globalLockerReadPermits' of global permits on reading ($globalLockerReadPermits) must be positive." }
require(perKeyReadPermits > 0) { "The maximal number 'perKeyReadPermits' of permits on reading per key ($perKeyReadPermits) must be positive." }
}
private val globalRWLocker: SmartRWLocker = SmartRWLocker(
readPermits = globalLockerReadPermits,
writeIsLocked = globalLockerWriteIsLocked
)
private val lockers = hashMapOf<T, SmartRWLocker>()
private val lockers = mutableMapOf<T, SmartRWLocker>()
private val lockersMutex = Mutex()
private val lockersWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
private val globalWritingLocker = SmartSemaphore.Mutable(Int.MAX_VALUE)
@@ -45,7 +40,7 @@ class SmartKeyRWLocker<T>(
private fun allocateLockerWithoutLock(key: T) = lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits)
}
private suspend inline fun allocateLocker(key: T) = lockersMutex.withLock {
private suspend fun allocateLocker(key: T) = lockersMutex.withLock {
lockers.getOrPut(key) {
SmartRWLocker(perKeyReadPermits)
}

View File

@@ -2,7 +2,6 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -50,31 +49,64 @@ sealed interface SmartMutex {
override val lockStateFlow: StateFlow<Boolean> = _lockStateFlow.asStateFlow()
private val internalChangesMutex = Mutex()
private val backingMutex = Mutex(locked = locked)
fun immutable() = Immutable(lockStateFlow)
/**
* Holds call until this [SmartMutex] will be re-locked. That means that while [isLocked] == true, [holds] will
* wait for [isLocked] == false and then try to lock
*/
suspend fun lock() {
backingMutex.lock()
internalChangesMutex.withLock {
_lockStateFlow.value = true
}
do {
waitUnlock()
val shouldContinue = internalChangesMutex.withLock {
if (_lockStateFlow.value) {
true
} else {
_lockStateFlow.value = true
false
}
}
} while (shouldContinue && currentCoroutineContext().isActive)
}
/**
* Will try to lock this [SmartMutex] immediataly
*
* @return True if lock was successful. False otherwise
*/
suspend fun tryLock(): Boolean {
val result = backingMutex.tryLock()
if (result) internalChangesMutex.withLock {
_lockStateFlow.value = true
return if (!_lockStateFlow.value) {
internalChangesMutex.withLock {
if (!_lockStateFlow.value) {
_lockStateFlow.value = true
true
} else {
false
}
}
} else {
false
}
return result
}
suspend fun unlock(): Boolean {
val tryUnlock = internalChangesMutex.withLock {
_lockStateFlow.value.also { if (it) _lockStateFlow.value = false }
/**
* If [isLocked] == true - will change it to false and return true. If current call will not unlock this
* [SmartMutex] - false
*/
suspend fun unlock(): Boolean = withContext(NonCancellable) {
if (_lockStateFlow.value) {
internalChangesMutex.withLock {
if (_lockStateFlow.value) {
_lockStateFlow.value = false
true
} else {
false
}
}
} else {
false
}
if (tryUnlock) backingMutex.unlock()
return tryUnlock
}
}
}
@@ -102,11 +134,4 @@ suspend inline fun <T> SmartMutex.Mutable.withLock(action: () -> T): T {
* Anyway, after the end of this block there are no any guaranties that [SmartMutex.isLocked] == false due to the fact
* that some other parties may lock it again
*/
suspend fun SmartMutex.waitUnlock(): Boolean {
currentCoroutineContext().ensureActive()
return if (!lockStateFlow.value) {
false
} else {
lockStateFlow.first { !it }
}
}
suspend fun SmartMutex.waitUnlock() = lockStateFlow.first { !it }

View File

@@ -1,6 +1,8 @@
package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
@@ -15,10 +17,6 @@ import kotlin.contracts.contract
* * [unlockWrite] will just unlock [writeMutex]
*/
class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked: Boolean = false) {
init {
require(readPermits > 0) { "The maximal number 'readPermits' of permits on reading ($readPermits) must be positive." }
}
private val _readSemaphore = SmartSemaphore.Mutable(permits = readPermits, acquiredPermits = if (writeIsLocked) readPermits else 0)
private val _writeMutex = SmartMutex.Mutable(locked = writeIsLocked)
@@ -37,7 +35,9 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/**
* Release one read permit in [readSemaphore]
*/
suspend fun releaseRead(): Boolean = _readSemaphore.release()
suspend fun releaseRead(): Boolean = withContext(NonCancellable) {
_readSemaphore.release()
}
/**
* Locking [writeMutex] and wait while all [readSemaphore] permits will be freed
@@ -47,7 +47,9 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
try {
_readSemaphore.acquire(readPermits)
} catch (e: CancellationException) {
_writeMutex.unlock()
withContext(NonCancellable) {
_writeMutex.unlock()
}
throw e
}
}
@@ -55,12 +57,13 @@ class SmartRWLocker(private val readPermits: Int = Int.MAX_VALUE, writeIsLocked:
/**
* Unlock [writeMutex]
*/
suspend fun unlockWrite(): Boolean =
suspend fun unlockWrite(): Boolean = withContext(NonCancellable) {
_writeMutex.unlock().also { unlocked ->
if (unlocked) {
_readSemaphore.release(readPermits)
}
}
}
}
/**

View File

@@ -2,7 +2,6 @@ package dev.inmo.micro_utils.coroutines
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
@@ -30,10 +29,8 @@ sealed interface SmartSemaphore {
val permitsStateFlow: StateFlow<Int>
/**
* The number of the available permits.
*
* If returns `0`, it is fully acquired (locked).
* If it returns the same value as `maxPermits`, it is fully released (unlocked).
* * True - locked
* * False - unlocked
*/
val freePermits: Int
get() = permitsStateFlow.value
@@ -50,12 +47,6 @@ sealed interface SmartSemaphore {
* @param locked Preset state of [freePermits] and its internal [_freePermitsStateFlow]
*/
class Mutable(permits: Int, acquiredPermits: Int = 0) : SmartSemaphore {
init {
require(permits > 0) { "The maximal number 'permits' of permits ($permits) must be positive." }
require(acquiredPermits >= 0) { "The number 'acquiredPermits' of initially acquired permits ($acquiredPermits) must be non-negative." }
require(permits >= acquiredPermits) { "The number 'acquiredPermits' of acquired initially permits ($acquiredPermits) must not exceed the maximal number 'permits' of permits ($permits)." }
}
override val maxPermits: Int = permits
private val _freePermitsStateFlow = MutableRedeliverStateFlow<Int>(permits - acquiredPermits)
override val permitsStateFlow: StateFlow<Int> = _freePermitsStateFlow.asStateFlow()
@@ -64,7 +55,7 @@ sealed interface SmartSemaphore {
fun immutable() = Immutable(permitsStateFlow, maxPermits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1, maxPermits)
private fun checkedPermits(permits: Int) = permits.coerceIn(1 .. this.maxPermits)
/**
* Holds call until this [SmartSemaphore] will be re-locked. That means that current method will
@@ -76,8 +67,7 @@ sealed interface SmartSemaphore {
do {
val shouldContinue = internalChangesMutex.withLock {
val requiredPermits = checkedPermits - acquiredPermits
val acquiring = minOf(freePermits, requiredPermits)
if (acquiring <= 0) return@withLock true
val acquiring = minOf(freePermits, requiredPermits).takeIf { it > 0 } ?: return@withLock true
acquiredPermits += acquiring
_freePermitsStateFlow.value -= acquiring
@@ -139,32 +129,21 @@ sealed interface SmartSemaphore {
* If [freePermits] == true - will change it to false and return true. If current call will not unlock this
* [SmartSemaphore] - false
*/
suspend fun release(permits: Int = 1): Boolean {
suspend fun release(permits: Int = 1): Boolean = withContext(NonCancellable) {
val checkedPermits = checkedPermits(permits)
if (_freePermitsStateFlow.value >= maxPermits) return false
// Updating permits without suspension remains safe in a cancelled coroutine.
if (internalChangesMutex.tryLock()) {
try {
return releaseLocked(checkedPermits)
} finally {
internalChangesMutex.unlock()
}
}
return withContext(NonCancellable) {
if (_freePermitsStateFlow.value < maxPermits) {
internalChangesMutex.withLock {
releaseLocked(checkedPermits)
if (_freePermitsStateFlow.value < maxPermits) {
_freePermitsStateFlow.value = minOf(_freePermitsStateFlow.value + checkedPermits, maxPermits)
true
} else {
false
}
}
} else {
false
}
}
private fun releaseLocked(permits: Int): Boolean {
val freePermits = _freePermitsStateFlow.value
if (freePermits >= maxPermits) return false
_freePermitsStateFlow.value = minOf(freePermits + permits, maxPermits)
return true
}
}
}
@@ -191,14 +170,5 @@ suspend inline fun <T> SmartSemaphore.Mutable.withAcquire(permits: Int = 1, acti
* Anyway, after the end of this block there are no any guaranties that [SmartSemaphore.freePermits] >= [permits] due to
* the fact that some other parties may lock it again
*/
suspend fun SmartSemaphore.waitRelease(permits: Int = 1): Int {
currentCoroutineContext().ensureActive()
val availablePermits = freePermits
return if (availablePermits >= permits) availablePermits else permitsStateFlow.first { it >= permits }
}
suspend fun SmartSemaphore.waitReleaseAll(): Int {
currentCoroutineContext().ensureActive()
val availablePermits = freePermits
return if (availablePermits == maxPermits) availablePermits else permitsStateFlow.first { it == maxPermits }
}
suspend fun SmartSemaphore.waitRelease(permits: Int = 1) = permitsStateFlow.first { it >= permits }
suspend fun SmartSemaphore.waitReleaseAll() = permitsStateFlow.first { it == maxPermits }

View File

@@ -14,17 +14,6 @@ import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds
class SmartMutexTests {
@Test
fun initiallyLockedMutexCanBeReleasedAndReacquired() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable(locked = true)
assertTrue(mutex.isLocked)
assertFalse(mutex.tryLock())
assertTrue(mutex.unlock())
assertFalse(mutex.isLocked)
assertTrue(mutex.tryLock())
assertTrue(mutex.unlock())
}
@Test
fun cancelledUnlockCompletesUnderContention() = runTest(timeout = 5.seconds) {
val mutex = SmartMutex.Mutable()
@@ -43,8 +32,7 @@ class SmartMutexTests {
releaser.join()
assertTrue(releaser.isCancelled)
assertTrue(mutex.isLocked, "Cancellation must not prevent the delegated unlock")
assertTrue(mutex.unlock())
assertFalse(mutex.isLocked, "Cancellation must not prevent the delegated unlock")
assertTrue(mutex.tryLock())
assertTrue(mutex.unlock())
}

View File

@@ -13,25 +13,6 @@ import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
class SmartRWLockerTests {
@Test
fun initiallyWriteLockedLockerReleasesReadPermits() = runTest(timeout = 5.seconds) {
val locker = SmartRWLocker(readPermits = 2, writeIsLocked = true)
assertTrue(locker.writeMutex.isLocked)
assertEquals(0, locker.readSemaphore.freePermits)
assertTrue(locker.unlockWrite())
assertFalse(locker.writeMutex.isLocked)
assertEquals(2, locker.readSemaphore.freePermits)
locker.withReadAcquire {
assertEquals(1, locker.readSemaphore.freePermits)
}
locker.withWriteLock {
assertEquals(0, locker.readSemaphore.freePermits)
}
assertEquals(2, locker.readSemaphore.freePermits)
}
@Test
fun compositeTest() {
val locker = SmartRWLocker()

View File

@@ -1,72 +0,0 @@
import dev.inmo.micro_utils.coroutines.SmartSemaphore
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.coroutineScope
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 SmartSemaphoreConcurrentReleaseTests {
@Test
fun queuedReleasesRestoreEveryAcquiredPermit() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 4, acquiredPermits = 4)
val results = releaseWithQueuedContenders(semaphore, contenderCount = 3)
assertEquals(listOf(true, true, true), results)
assertEquals(4, semaphore.freePermits)
assertTrue(semaphore.tryAcquire(4))
assertFalse(semaphore.tryAcquire())
assertEquals(0, semaphore.freePermits)
}
@Test
fun queuedReleasesRecheckCapacityAfterAcquiringTheMutex() = runTest(timeout = 5.seconds) {
val semaphore = SmartSemaphore.Mutable(permits = 2, acquiredPermits = 2)
val results = releaseWithQueuedContenders(semaphore, contenderCount = 3)
assertEquals(1, results.count { released -> released })
assertEquals(2, results.count { released -> !released })
assertEquals(2, semaphore.freePermits)
assertFalse(semaphore.release())
}
private suspend fun releaseWithQueuedContenders(
semaphore: SmartSemaphore.Mutable,
contenderCount: Int
): List<Boolean> = coroutineScope {
val contenders = mutableListOf<Deferred<Boolean>>()
val observer = launch(Dispatchers.Unconfined) {
semaphore.permitsStateFlow.first { freePermits ->
if (freePermits == 1) {
// The first release still holds the changes mutex while publishing.
// Every contender observes spare capacity before waiting for the mutex.
repeat(contenderCount) {
val contender = async(Dispatchers.Unconfined, start = CoroutineStart.UNDISPATCHED) {
semaphore.release()
}
contenders.add(contender)
assertFalse(contender.isCompleted, "Competing releases must wait for the publishing release")
}
true
} else {
false
}
}
}
assertTrue(semaphore.release())
observer.join()
assertEquals(contenderCount, contenders.size)
contenders.awaitAll()
}
}

View File

@@ -1,78 +0,0 @@
package dev.inmo.micro_utils.coroutines
import java.util.concurrent.CyclicBarrier
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
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 SmartSemaphoreParallelReleaseTests {
@Test
fun parallelSinglePermitReleasesRestoreEveryPermit() = runTest(timeout = 30.seconds) {
assertParallelReleases(permits = 8, releasePermits = List(8) { 1 }, expectedSuccessfulReleases = 8)
}
@Test
fun parallelBulkReleasesRestoreEveryPermit() = runTest(timeout = 30.seconds) {
val releasePermits = (1 .. 8).toList()
assertParallelReleases(
permits = releasePermits.sum(),
releasePermits = releasePermits,
expectedSuccessfulReleases = releasePermits.size
)
}
@Test
fun parallelExcessReleasesStopAtCapacity() = runTest(timeout = 30.seconds) {
assertParallelReleases(permits = 3, releasePermits = List(8) { 1 }, expectedSuccessfulReleases = 3)
assertParallelReleases(permits = 5, releasePermits = List(8) { 3 }, expectedSuccessfulReleases = 2)
}
private suspend fun assertParallelReleases(
permits: Int,
releasePermits: List<Int>,
expectedSuccessfulReleases: Int
) = coroutineScope {
val semaphore = SmartSemaphore.Mutable(permits = permits, acquiredPermits = permits)
val barrier = CyclicBarrier(releasePermits.size)
val results = BooleanArray(releasePermits.size)
val rounds = 100
Executors.newFixedThreadPool(releasePermits.size).asCoroutineDispatcher().use { dispatcher ->
releasePermits.mapIndexed { worker, count ->
launch(dispatcher) {
repeat(rounds) { round ->
// Blocking barriers on dedicated JVM threads produce actual parallel releases.
barrier.await(5, TimeUnit.SECONDS)
results[worker] = semaphore.release(count)
barrier.await(5, TimeUnit.SECONDS)
if (worker == 0) {
assertEquals(permits, semaphore.freePermits, "Round $round must preserve released permits")
assertEquals(expectedSuccessfulReleases, results.count { released -> released })
assertFalse(semaphore.release(), "A full semaphore must reject another release")
assertEquals(permits, semaphore.freePermits)
if (round < rounds - 1) {
assertTrue(semaphore.tryAcquire(permits))
assertEquals(0, semaphore.freePermits)
}
}
// Finish validation and reset before starting the next release wave.
barrier.await(5, TimeUnit.SECONDS)
}
}
}.joinAll()
}
assertEquals(permits, semaphore.freePermits)
}
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
@@ -21,3 +21,11 @@ kotlin {
}
}
}
android {
sourceSets {
main {
java.srcDirs += [ "src/jvmMain/kotlin" ]
}
}
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "org.jetbrains.dokka"
}
@@ -11,17 +11,13 @@ repositories {
mavenCentral()
}
apply from: "$defaultAndroidSettings"
kotlin {
jvm()
js(IR) {
browser()
nodejs()
}
android {
project.ext.configureAndroidTarget(delegate)
}
androidTarget {}
sourceSets {
commonMain {
@@ -141,3 +137,5 @@ dokka {
// }
// }
//}
apply from: "$defaultAndroidSettings"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,9 +1,11 @@
kotlin.code.style=official
org.gradle.parallel=true
kotlin.js.generate.externals=true
kotlin.incremental=true
kotlin.incremental.js=true
#kotlin.experimental.tryK2=true
android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
## https://github.com/google/ksp/issues/2491
@@ -11,10 +13,10 @@ org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g
# JS NPM
crypto_js_version=4.2.0
crypto_js_version=4.1.1
# Project data
group=dev.inmo
version=0.32.1
android_code_version=320
version=0.30.2
android_code_version=316

View File

@@ -1,51 +1,51 @@
[versions]
kt = "2.4.21"
kt = "2.3.21"
kt-serialization = "1.11.0"
kt-coroutines = "1.11.0"
kotlinx-browser = "0.5.0"
kslog = "2.1.0"
kslog = "1.7.0"
jb-compose = "1.12.1"
jb-compose-material3 = "1.9.0"
jb-compose = "1.11.1"
jb-compose-material3 = "1.11.0-alpha07"
jb-compose-icons = "1.7.8"
jb-exposed = "1.5.1"
jb-exposed = "1.3.0"
jb-dokka = "2.2.0"
sqlite = "3.53.4.0"
sqlite = "3.53.2.1"
korlibs = "5.4.0"
uuid = "0.8.4"
ktor = "3.6.0"
ktor = "3.5.2"
gh-release = "2.5.2"
koin = "4.2.2"
okio = "3.18.2"
okio = "3.18.1"
ksp = "2.3.11"
kotlin-poet = "2.4.0"
ksp = "2.3.9"
kotlin-poet = "2.3.0"
versions = "0.65.0"
nmcp = "1.6.2"
versions = "0.54.0"
nmcp = "1.5.0"
android-gradle = "9.1.1"
android-gradle = "8.12.+"
dexcount = "4.0.0"
android-coreKtx = "1.19.1"
android-coreKtx = "1.19.0"
android-recyclerView = "1.4.0"
android-appCompat = "1.8.0"
android-fragment = "1.9.1"
android-appCompat = "1.7.1"
android-fragment = "1.8.9"
android-espresso = "3.7.0"
android-test = "1.3.0"
android-props-minSdk = "21"
android-props-compileSdk = "37"
android-props-buildTools = "37.0.0"
android-props-compileSdk = "36"
android-props-buildTools = "36.0.0"
[libraries]
@@ -126,5 +126,5 @@ buildscript-android-dexcount = { module = "com.getkeepsafe.dexcount:dexcount-gra
jb-compose = { id = "org.jetbrains.compose", version.ref = "jb-compose" }
kt-jb-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kt" }
versions = { id = "io.github.ben-manes.versions", version.ref = "versions" }
versions = { id = "com.github.ben-manes.versions", version.ref = "versions" }
nmcp-aggregation = { id = "com.gradleup.nmcp.aggregation", version.ref = "nmcp" }

View File

@@ -1,6 +1,6 @@
kotlin {
sourceSets {
androidHostTest {
androidUnitTest {
dependencies {
implementation compose.uiTest
}

View File

@@ -1,18 +1,34 @@
apply plugin: 'com.getkeepsafe.dexcount'
ext.configureAndroidTarget = { target ->
target.compileSdk = libs.versions.android.props.compileSdk.get().toInteger()
target.buildToolsVersion = libs.versions.android.props.buildTools.get()
target.minSdk = libs.versions.android.props.minSdk.get().toInteger()
target.namespace = "${project.group}.${project.name}"
target.packaging {
resources {
excludes.add 'META-INF/kotlinx-serialization-runtime.kotlin_module'
excludes.add 'META-INF/kotlinx-serialization-cbor.kotlin_module'
excludes.add 'META-INF/kotlinx-serialization-properties.kotlin_module'
android {
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
buildToolsVersion libs.versions.android.props.buildTools.get()
defaultConfig {
minSdkVersion libs.versions.android.props.minSdk.get().toInteger()
compileSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
targetSdkVersion libs.versions.android.props.compileSdk.get().toInteger()
versionCode "${android_code_version}".toInteger()
versionName "$version"
namespace "${project.group}.${project.name}"
}
buildTypes {
release {
minifyEnabled false
}
debug {
debuggable true
}
}
target.compilerOptions.jvmTarget.set(
target.compilerOptions.jvmTarget.get().class.fromTarget("17")
)
packagingOptions {
exclude 'META-INF/kotlinx-serialization-runtime.kotlin_module'
exclude 'META-INF/kotlinx-serialization-cbor.kotlin_module'
exclude 'META-INF/kotlinx-serialization-properties.kotlin_module'
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}

View File

@@ -1,31 +1,25 @@
apply from: "$defaultAndroidSettings"
kotlin {
android {
project.ext.configureAndroidTarget(delegate)
withHostTest {}
withDeviceTest {}
androidTarget {
publishLibraryVariants(
"release",
"debug",
)
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
sourceSets {
jvmAndAndroidMain {
dependsOn commonMain
kotlin.srcDir("src/jvmMain/kotlin")
resources.srcDir("src/jvmMain/resources")
}
jvmMain {
dependsOn jvmAndAndroidMain
kotlin.setSrcDirs([])
resources.setSrcDirs([])
}
androidHostTest {
androidUnitTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
implementation libs.android.espresso
}
}
androidDeviceTest {
androidInstrumentedTest {
dependencies {
implementation kotlin('test-junit')
implementation libs.android.test.junit
@@ -33,6 +27,8 @@ kotlin {
}
}
androidMain.dependsOn jvmAndAndroidMain
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"

Binary file not shown.

View File

@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
@@ -14,7 +14,7 @@ kotlin {
api libs.uuid
}
}
jvmAndAndroidMain {
jvmMain {
dependencies {
api libs.kt.reflect
api project(":micro_utils.coroutines")

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "com.google.devtools.ksp"
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "com.google.devtools.ksp"
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "com.google.devtools.ksp"
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "com.google.devtools.ksp"
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -11,7 +11,7 @@ package dev.inmo.micro_utils.meta
RequiresOptIn.Level.WARNING
)
@Target(
AnnotationTarget.PROPERTY,
AnnotationTarget.FIELD,
)
@Retention(AnnotationRetention.BINARY)
annotation class MetaContainerRootMapWarning

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.jb.compose)
alias(libs.plugins.kt.jb.compose)
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
@@ -17,7 +17,7 @@ kotlin {
}
}
jvmAndAndroidMain {
jvmMain {
dependencies {
api internalProject("micro_utils.common")
}

View File

@@ -1,14 +1,12 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
project.version = "$version"
project.group = "$group"
apply from: "$defaultAndroidSettings"
kotlin {
jvm {
compilations.main {
@@ -25,8 +23,12 @@ kotlin {
browser()
nodejs()
}
android {
project.ext.configureAndroidTarget(delegate)
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
linuxX64()
mingwX64()
@@ -49,11 +51,6 @@ kotlin {
implementation kotlin('test-junit')
}
}
androidMain {
dependencies {
implementation kotlin('test-junit')
}
}
jsMain {
dependencies {
implementation kotlin('test-js')
@@ -64,10 +61,13 @@ kotlin {
mingwX64Main.dependsOn nativeMain
linuxArm64Main.dependsOn nativeMain
androidMain.dependsOn jvmMain
}
}
apply from: "$defaultAndroidSettings"
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
id "com.google.devtools.ksp"
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"
@@ -20,4 +20,4 @@ kotlin {
}
}
}
}
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
alias(libs.plugins.compose)
}

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"

View File

@@ -1,7 +1,7 @@
plugins {
id "org.jetbrains.kotlin.multiplatform"
id "org.jetbrains.kotlin.plugin.serialization"
id "com.android.kotlin.multiplatform.library"
id "com.android.library"
}
apply from: "$mppJvmJsWasmJsAndroidLinuxMingwLinuxArm64Project"