Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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
14 changes: 14 additions & 0 deletions surf-api-core/surf-api-core/api/surf-api-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <K : Any, V : Any> noOpRemovalListener(): RemovalListener<K, V> = 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 <K : Any, V : Any> Caffeine<K, V>.withAutoCloseOnRemoval(
beforeClose: RemovalListener<K, V> = noOpRemovalListener()
): Caffeine<K, V> = 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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}