diff --git a/gradle.properties b/gradle.properties index 6dd67bc9b..42a030991 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,6 +7,6 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled javaVersion=25 mcVersion=26.1.1 group=dev.slne.surf.api -version=3.1.1 +version=3.2.0 relocationPrefix=dev.slne.surf.api.libs snapshot=false diff --git a/surf-api-core/surf-api-core/api/surf-api-core.api b/surf-api-core/surf-api-core/api/surf-api-core.api index 3d97d7481..f37d97191 100644 --- a/surf-api-core/surf-api-core/api/surf-api-core.api +++ b/surf-api-core/surf-api-core/api/surf-api-core.api @@ -9343,6 +9343,20 @@ public final class dev/slne/surf/api/core/util/ById$Companion { public final fun build (Ljava/lang/Class;)Lit/unimi/dsi/fastutil/ints/Int2ObjectMap; } +public final class dev/slne/surf/api/core/util/Caffeine_utilKt { + public static final fun withAutoCloseOnRemoval (Lcom/github/benmanes/caffeine/cache/Caffeine;Lcom/github/benmanes/caffeine/cache/RemovalListener;)Lcom/github/benmanes/caffeine/cache/Caffeine; + public static synthetic fun withAutoCloseOnRemoval$default (Lcom/github/benmanes/caffeine/cache/Caffeine;Lcom/github/benmanes/caffeine/cache/RemovalListener;ILjava/lang/Object;)Lcom/github/benmanes/caffeine/cache/Caffeine; +} + +public final class dev/slne/surf/api/core/util/Coroutine_utilKt { + public static final fun runAtFixedRate-vLdBGDU (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; + public static synthetic fun runAtFixedRate-vLdBGDU$default (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; + public static final fun runUntil-jKevqZI (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; + public static synthetic fun runUntil-jKevqZI$default (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; + public static final fun runWithFixedDelay-vLdBGDU (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/Job; + public static synthetic fun runWithFixedDelay-vLdBGDU$default (Lkotlinx/coroutines/CoroutineScope;JJLkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lkotlinx/coroutines/Job; +} + public final class dev/slne/surf/api/core/util/Date_utilKt { public static final fun getCurrentDateTimeFormatted ()Ljava/lang/String; public static final fun getDateTimeFormatter ()Ljava/time/format/DateTimeFormatter; diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/caffeine-util.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/caffeine-util.kt new file mode 100644 index 000000000..73b54ecf0 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/caffeine-util.kt @@ -0,0 +1,51 @@ +package dev.slne.surf.api.core.util + +import com.github.benmanes.caffeine.cache.Caffeine +import com.github.benmanes.caffeine.cache.RemovalListener +import java.lang.AutoCloseable + +private val log = logger() + +private fun noOpRemovalListener(): RemovalListener = RemovalListener { _, _, _ -> } + +/** + * Configures a Caffeine cache to automatically close entries that are removed from the cache + * and invokes a specified `RemovalListener` before the auto-close operation is executed. + * + * This method ensures that if the key or value implements `AutoCloseable`, their `close` method + * is called when they are removed from the cache due to any removal cause (e.g., eviction, manual removal). + * If an exception occurs during the `close` or in the provided `beforeClose` listener, it is caught + * and logged as a warning. + * + * @param beforeClose an optional `RemovalListener` to be invoked before the automatic + * closing of the removed key and value occurs. A no-op listener is used if this parameter is not provided. + * @return a modified instance of the `Caffeine` cache configured with the automatic close-on-removal behavior. + */ +fun Caffeine.withAutoCloseOnRemoval( + beforeClose: RemovalListener = noOpRemovalListener() +): Caffeine = removalListener { key, value, cause -> + runCatching { + beforeClose.onRemoval(key, value, cause) + }.onFailure { throwable -> + log.atWarning() + .withCause(throwable) + .log( + "RemovalListener beforeClose failed for key=%s, value=%s, cause=%s", + key, + value, + cause, + ) + } + + (key as? AutoCloseable)?.closeCatching("key", key, value, cause) + (value as? AutoCloseable)?.closeCatching("value", key, value, cause) +} + +private fun AutoCloseable.closeCatching(label: String, key: Any?, value: Any?, cause: Any?) { + runCatching { close() } + .onFailure { throwable -> + log.atWarning() + .withCause(throwable) + .log("Failed to close %s on removal. key=%s, value=%s, cause=%s", label, key, value, cause) + } +} diff --git a/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/coroutine-util.kt b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/coroutine-util.kt new file mode 100644 index 000000000..0b41d3385 --- /dev/null +++ b/surf-api-core/surf-api-core/src/main/kotlin/dev/slne/surf/api/core/util/coroutine-util.kt @@ -0,0 +1,154 @@ +package dev.slne.surf.api.core.util + +import kotlinx.coroutines.* +import kotlin.time.Duration +import kotlin.time.Duration.Companion.nanoseconds + +/** + * Starts a coroutine that executes [block] at a fixed rate. + * + * The next execution time is calculated based on a fixed schedule, independent of how long + * [block] takes to execute. If [block] runs longer than [period], subsequent executions + * will attempt to "catch up" without additional delay. + * + * Execution behavior: + * - Waits for [initialDelay] before the first execution (if > 0). + * - Executes [block] immediately after the initial delay. + * - Subsequent executions are aligned to a fixed interval defined by [period]. + * + * Cancellation: + * - The returned [Job] is cancelled when the enclosing [CoroutineScope] is cancelled. + * - The loop checks for cancellation via [isActive] and [ensureActive]. + * + * Error handling: + * - If [block] throws an exception, the coroutine is cancelled and no further executions occur. + * + * @param period the interval between scheduled executions; must be > 0 + * @param initialDelay the delay before the first execution; must be >= 0 + * @param block the suspending block to execute repeatedly + * + * @return the [Job] representing the running coroutine + * + * @throws IllegalArgumentException if [period] is not positive or [initialDelay] is negative + */ +fun CoroutineScope.runAtFixedRate( + period: Duration, + initialDelay: Duration = Duration.ZERO, + block: suspend CoroutineScope.() -> Unit +): Job = launch { + require(period > Duration.ZERO) { "period must be positive" } + require(initialDelay >= Duration.ZERO) { "initialDelay must not be negative" } + + if (initialDelay.isPositive()) { + delay(initialDelay) + ensureActive() + } + + var nextRun = System.nanoTime() + while (isActive) { + nextRun += period.inWholeNanoseconds + + block() + ensureActive() + + val waitNanos = nextRun - System.nanoTime() + if (waitNanos > 0) { + delay(waitNanos.nanoseconds) + } + } +} + +/** + * Starts a coroutine that executes [block] with a fixed delay between executions. + * + * The delay is applied *after* each execution of [block]. This means that the time between + * the start of consecutive executions depends on how long [block] takes to run. + * + * Execution behavior: + * - Waits for [initialDelay] before the first execution (if > 0). + * - Executes [block]. + * - Waits for [delay] after each execution before starting the next one. + * + * Cancellation: + * - The returned [Job] is cancelled when the enclosing [CoroutineScope] is cancelled. + * - The loop checks for cancellation via [isActive] and [ensureActive]. + * + * Error handling: + * - If [block] throws an exception, the coroutine is cancelled and no further executions occur. + * + * @param delay the delay between executions; must be > 0 + * @param initialDelay the delay before the first execution; must be >= 0 + * @param block the suspending block to execute repeatedly + * + * @return the [Job] representing the running coroutine + * + * @throws IllegalArgumentException if [delay] is not positive or [initialDelay] is negative + */ +fun CoroutineScope.runWithFixedDelay( + delay: Duration, + initialDelay: Duration = Duration.ZERO, + block: suspend CoroutineScope.() -> Unit +): Job = launch { + require(delay > Duration.ZERO) { "delay must be positive" } + require(initialDelay >= Duration.ZERO) { "initialDelay must not be negative" } + + if (initialDelay.isPositive()) { + delay(initialDelay) + ensureActive() + } + + while (isActive) { + block() + ensureActive() + delay(delay) + } +} + +/** + * Starts a coroutine that repeatedly executes [block] with a fixed [delay] between executions + * as long as [predicate] returns `true`. + * + * Execution behavior: + * - Waits for [initialDelay] before the first execution (if > 0). + * - Before each iteration, [predicate] is evaluated. + * - If [predicate] returns `true`, [block] is executed. + * - After execution, waits for [delay] before the next iteration. + * - Stops when [predicate] returns `false` or the coroutine is cancelled. + * + * Cancellation: + * - The returned [Job] is cancelled when the enclosing [CoroutineScope] is cancelled. + * - The loop checks for cancellation via [isActive] and [ensureActive]. + * + * Error handling: + * - If [block] or [predicate] throws an exception, the coroutine is cancelled + * and no further executions occur. + * + * @param delay the delay between executions; must be >= 0 + * @param initialDelay the delay before the first execution; must be >= 0 + * @param predicate condition that controls whether execution should continue + * @param block the suspending block to execute repeatedly + * + * @return the [Job] representing the running coroutine + * + * @throws IllegalArgumentException if [delay] or [initialDelay] is negative + */ +fun CoroutineScope.runUntil( + delay: Duration, + initialDelay: Duration = Duration.ZERO, + predicate: suspend () -> Boolean, + block: suspend CoroutineScope.() -> Unit +): Job = launch { + require(delay >= Duration.ZERO) { "delay must not be negative" } + require(initialDelay >= Duration.ZERO) { "initialDelay must not be negative" } + + if (initialDelay.isPositive()) { + delay(initialDelay) + ensureActive() + } + + while (isActive && predicate()) { + block() + ensureActive() + delay(delay) + } +} \ No newline at end of file