From 01d9e0239e91f08ea33b3eceee89e145b08370ad Mon Sep 17 00:00:00 2001 From: sergeych Date: Thu, 25 Sep 2025 10:26:12 +0400 Subject: [PATCH] added more tools, start shifting to kotlin 2.2.0 --- build.gradle.kts | 8 ++-- gradle.properties | 2 +- .../kotlin/net.sergeych.bintools/ByteChunk.kt | 6 +-- .../kotlin/net/sergeych/tools/Expiring.kt | 24 ++++++++++ .../net/sergeych/tools/IndividualLock.kt | 29 ++++++++++++ .../kotlin/net/sergeych/tools/OncePer.kt | 44 +++++++++++++++++++ .../kotlin/net/sergeych/tools/RateLimiter.kt | 27 ++++++++++++ 7 files changed, 132 insertions(+), 8 deletions(-) create mode 100644 src/commonMain/kotlin/net/sergeych/tools/Expiring.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/IndividualLock.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/OncePer.kt create mode 100644 src/commonMain/kotlin/net/sergeych/tools/RateLimiter.kt diff --git a/build.gradle.kts b/build.gradle.kts index 873c25f..824643e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,14 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl plugins { - kotlin("multiplatform") version "2.0.21" - kotlin("plugin.serialization") version "2.0.21" + kotlin("multiplatform") version "2.2.20" + kotlin("plugin.serialization") version "2.2.20" id("org.jetbrains.dokka") version "1.9.20" `maven-publish` } group = "net.sergeych" -version = "0.1.12-SNAPSHOT" +version = "0.2.1-SNAPSHOT" repositories { mavenCentral() @@ -31,7 +31,6 @@ kotlin { // iosSimulatorArm64() linuxX64() linuxArm64() - mingwX64() @OptIn(ExperimentalWasmDsl::class) wasmJs { @@ -90,6 +89,7 @@ kotlin { // val nativeTest by getting val wasmJsMain by getting { dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-browser-wasm-js:0.5.0") } } val wasmJsTest by getting { diff --git a/gradle.properties b/gradle.properties index 5b7dc8f..88ba98e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ kotlin.code.style=official -kotlin.js.compiler=ir kotlin.mpp.applyDefaultHierarchyTemplate=false +kotlin.daemon.jvmargs=-Xmx3072M diff --git a/src/commonMain/kotlin/net.sergeych.bintools/ByteChunk.kt b/src/commonMain/kotlin/net.sergeych.bintools/ByteChunk.kt index 7fe214b..1ef8fe6 100644 --- a/src/commonMain/kotlin/net.sergeych.bintools/ByteChunk.kt +++ b/src/commonMain/kotlin/net.sergeych.bintools/ByteChunk.kt @@ -1,7 +1,7 @@ package net.sergeych.bintools import kotlinx.serialization.Serializable -import net.sergeych.mp_tools.encodeToBase64Compact +import net.sergeych.mp_tools.encodeToBase64Url import kotlin.math.min import kotlin.random.Random @@ -53,7 +53,7 @@ class ByteChunk(val data: UByteArray): Comparable { /** * hex representation of data */ - override fun toString(): String = hex + override fun toString(): String = base64 /** * Hex encoded data @@ -68,7 +68,7 @@ class ByteChunk(val data: UByteArray): Comparable { /** * Lazy encode to base64 with url alphabet, without trailing fill '=' characters. */ - val base64 by lazy { data.asByteArray().encodeToBase64Compact() } + val base64 by lazy { data.asByteArray().encodeToBase64Url() } /** * Lazy (cached) view of [data] as ByteArray diff --git a/src/commonMain/kotlin/net/sergeych/tools/Expiring.kt b/src/commonMain/kotlin/net/sergeych/tools/Expiring.kt new file mode 100644 index 0000000..d86d421 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/Expiring.kt @@ -0,0 +1,24 @@ +package net.sergecyh.diwan.tools + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration + +/** + * Value with expiration. + */ +@Suppress("unused") +class Expiring( + val value: T, + val expiresAt: Instant, +) { + constructor(value: T, expiresIn: Duration) : this(value, Clock.System.now() + expiresIn) + + /** + * @return value if not expired, null otherwise + */ + fun valueOrNull(): T? = if( isExpired ) value else null + + val isExpired: Boolean get() = expiresAt < Clock.System.now() + val isOk: Boolean get() = !isExpired +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/IndividualLock.kt b/src/commonMain/kotlin/net/sergeych/tools/IndividualLock.kt new file mode 100644 index 0000000..c739aea --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/IndividualLock.kt @@ -0,0 +1,29 @@ +@file:Suppress("unused") + +package net.sergecyh.diwan.tools + +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + +/** + * Experimental + * + * set of mutexes associated with keys `K`, so each key can have its own mutex + * used with [lock] or [withLock] + */ +class IndividualLock { + + private val access = Mutex() + private val locks = mutableMapOf() + + suspend inline fun withLock(key: K, block: ()->T): T = lock(key).use { block() } + + suspend fun lock(key: K): AutoCloseable { + val m = access.withLock { + locks.getOrPut(key) { Mutex() } + } + m.lock() + return AutoCloseable { m.unlock() } + } + +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/OncePer.kt b/src/commonMain/kotlin/net/sergeych/tools/OncePer.kt new file mode 100644 index 0000000..698e572 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/OncePer.kt @@ -0,0 +1,44 @@ +package net.sergecyh.diwan.tools + +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock + + +/** + * Coroutine-based unique "once-per-key" executor. + * + * When [invoke] is called, it checks that there is already invocation in progress + * and either wait for it to complete or start a new one. + * @param K key value, should have valid `hashCode` and `equals`, e.g., suitable to be a map key + */ +@Suppress("unused") +class OncePer { + private val access = Mutex() + + private val queue = mutableMapOf>() + + /** + * Execute [f] as unique-per-key [key]. If such invocation is in progress, it suspends than returns + * its result. If there is no invocation for such a key, start new invocation. + * + * __Important note__ all simultaneous invocation should have the same [R] type, or, at least, a type + * _castable to [R]_. + */ + suspend operator fun invoke(key: K, f: suspend ()->R ): R { + var mustStart = false + val d = access.withLock { + queue.getOrPut(key) { + CompletableDeferred().also { mustStart = true } + } + } + if( mustStart ) { + d.complete(f()) + access.withLock { + queue.remove(key) + } + } + @Suppress("UNCHECKED_CAST") + return d.await() as R + } +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net/sergeych/tools/RateLimiter.kt b/src/commonMain/kotlin/net/sergeych/tools/RateLimiter.kt new file mode 100644 index 0000000..8471123 --- /dev/null +++ b/src/commonMain/kotlin/net/sergeych/tools/RateLimiter.kt @@ -0,0 +1,27 @@ +package net.sergecyh.diwan.tools + +import kotlinx.datetime.Clock +import kotlinx.datetime.Instant +import kotlin.time.Duration + +/** + * Experimental. + * + * limit invocations rate of [invoke] to once per [minimalInterval] or less frequent. + * Note that it is not a debouncing, it just ignores too frequent calls! + */ +@Suppress("unused") +class RateLimiter(val minimalInterval: Duration) { + var lastExecutedAt = Instant.DISTANT_PAST + private set + + /** + * invoke [f] if the last invocation was earlier than now minus [minimalInterval], otherwise + * do nothing. + * @return the value returned by [f] if it was actually invoked this time, null otherwise + */ + suspend operator fun invoke(f: suspend () -> T): T? = + if (Clock.System.now() - lastExecutedAt > minimalInterval) { + f().also { lastExecutedAt = Clock.System.now() } + } else null +} \ No newline at end of file