diff --git a/buildSrc/src/main/kotlin/P.kt b/buildSrc/src/main/kotlin/P.kt index ac545646b..1ee4231ab 100644 --- a/buildSrc/src/main/kotlin/P.kt +++ b/buildSrc/src/main/kotlin/P.kt @@ -77,7 +77,7 @@ sealed class P(override val group: String) : ProjectDetail() { object SimbotLogger : P(GROUP_LOGGER) object SimbotGradle : P(GROUP_GRADLE) object SimbotQuantcat : P(GROUP_QUANTCAT) - object SimbotExtension : P(GROUP_QUANTCAT) + object SimbotExtension : P(GROUP_EXTENSION) final override val version: Version val versionWithoutSnapshot: Version diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c938355f2..3210284a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,6 +16,8 @@ reactor = "3.6.2" suspendTransform = "0.7.0-beta1" suspendReversal = "0.2.0" gradleCommon = "0.2.0" +# tests +mockk = "1.13.9" [libraries] @@ -120,6 +122,10 @@ gradle-common-core = { group = "love.forte.gradle.common", name = "gradle-common gradle-common-multiplatform = { group = "love.forte.gradle.common", name = "gradle-common-kotlin-multiplatform", version.ref = "gradleCommon" } gradle-common-publication = { group = "love.forte.gradle.common", name = "gradle-common-publication", version.ref = "gradleCommon" } +# mockk +## https://mockk.io/ +mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" } + [plugins] ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } diff --git a/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.kt b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.kt index d5448a364..f31408bf3 100644 --- a/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.kt +++ b/simbot-commons/simbot-common-core/src/commonMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.kt @@ -21,10 +21,14 @@ * */ +@file:JvmName("DispatchersUtil") +@file:JvmMultifileClass package love.forte.simbot.common.coroutines import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlin.jvm.JvmMultifileClass +import kotlin.jvm.JvmName /** * 在 JVM 和 native 平台下,得到 `Dispatchers.IO`; @@ -32,3 +36,4 @@ import kotlinx.coroutines.Dispatchers * */ public expect val Dispatchers.IOOrDefault: CoroutineDispatcher + diff --git a/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.jvm.kt b/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.jvm.kt index 03a631533..ff85371d8 100644 --- a/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.jvm.kt +++ b/simbot-commons/simbot-common-core/src/jvmMain/kotlin/love/forte/simbot/common/coroutines/Dispatchers.jvm.kt @@ -20,11 +20,21 @@ * along with this program. If not, see . * */ +@file:JvmName("DispatchersUtil") +@file:JvmMultifileClass package love.forte.simbot.common.coroutines import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.asCoroutineDispatcher +import love.forte.simbot.annotations.Api4J +import java.lang.invoke.MethodHandles +import java.lang.invoke.MethodType +import java.util.concurrent.Executor +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors /** * 得到 [Dispatchers.IO]。 @@ -33,3 +43,40 @@ import kotlinx.coroutines.Dispatchers */ public actual inline val Dispatchers.IOOrDefault: CoroutineDispatcher get() = IO + +/** + * 在支持虚拟线程调度器时使用虚拟线程调度器 (`Executors.newVirtualThreadPerTaskExecutor`) + * 作为 [CoroutineDispatcher], + * 否则得到 `null`。 + * + */ +public val Dispatchers.Virtual: CoroutineDispatcher? by lazy { + runCatching { + val handle = MethodHandles.publicLookup().findStatic( + Executors::class.java, + "newVirtualThreadPerTaskExecutor", + MethodType.methodType(ExecutorService::class.java) + ) + (handle.invoke() as Executor).asCoroutineDispatcher() + }.getOrNull() +} + +/** + * Friendly API for Java. + */ +@Api4J +public val VirtualDispatcher: CoroutineDispatcher? get() = Dispatchers.Virtual + +/** + * 在支持虚拟线程调度器时使用虚拟线程调度器 (`Executors.newVirtualThreadPerTaskExecutor`) + * 作为 [CoroutineDispatcher], + * 否则得到 [Dispatchers.IO]。 + * + */ +public val Dispatchers.VirtualOrIO: CoroutineDispatcher by lazy { Dispatchers.Virtual ?: IO } + +/** + * Friendly API for Java. + */ +@Api4J +public val VirtualOrIODispatcher: CoroutineDispatcher? get() = Dispatchers.VirtualOrIO diff --git a/simbot-extensions/simbot-extension-continuous-session/README.md b/simbot-extensions/simbot-extension-continuous-session/README.md index 1724fe67c..3814fb7e6 100644 --- a/simbot-extensions/simbot-extension-continuous-session/README.md +++ b/simbot-extensions/simbot-extension-continuous-session/README.md @@ -1,5 +1,8 @@ # 持续会话扩展 +> [!warning] +> 尚在试验阶段,随时可能删除或被调整 + 仍在考虑中。 **持续会话需要解决什么?** @@ -36,7 +39,7 @@ suspend fun inSession(event: Event, sessionContext: ContinuousSessionContext): E val sessionProvider = sessionContext.session(key) { // this: sessionReceiver // 异步中、可挂起 - val e = await() // event + val e = await { it.toResult() } // event } // 向会话推送,然后得到一个结果? diff --git a/simbot-extensions/simbot-extension-continuous-session/build.gradle.kts b/simbot-extensions/simbot-extension-continuous-session/build.gradle.kts index 3d1b99ec8..a90916887 100644 --- a/simbot-extensions/simbot-extension-continuous-session/build.gradle.kts +++ b/simbot-extensions/simbot-extension-continuous-session/build.gradle.kts @@ -38,7 +38,7 @@ plugins { setup(P.SimbotExtension) configJavaCompileWithModule("simbot.extension.continuous.session") -// apply(plugin = "simbot-multiplatform-maven-publish") +apply(plugin = "simbot-multiplatform-maven-publish") kotlin { explicitApi() @@ -74,6 +74,8 @@ kotlin { implementation(libs.kotlinx.coroutines.test) // implementation(libs.kotlinx.coroutines.debug) implementation(kotlin("test")) + implementation(project(":simbot-cores:simbot-core")) + implementation(project(":simbot-test")) } } @@ -81,6 +83,7 @@ kotlin { jvmMain { dependencies { + compileOnly(project(":simbot-commons:simbot-common-annotations")) compileOnly(libs.kotlinx.coroutines.reactive) compileOnly(libs.kotlinx.coroutines.reactor) compileOnly(libs.kotlinx.coroutines.rx2) @@ -99,6 +102,7 @@ kotlin { implementation(kotlin("test-junit5")) implementation(kotlin("reflect")) implementation(libs.ktor.client.cio) + implementation(libs.mockk) } } diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt index 0784e3f5c..9786e5873 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/AbstractContinuousSessionContext.kt @@ -44,6 +44,7 @@ import kotlin.jvm.JvmName /** + * 针对 [ContinuousSessionContext] 的基础抽象实现类。 * * @author ForteScarlet */ diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt index 4c58e85dd..f17011edd 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/ContinuousSessionContext.kt @@ -28,11 +28,23 @@ package love.forte.simbot.extension.continuous.session import kotlin.jvm.JvmMultifileClass import kotlin.jvm.JvmName +import kotlin.jvm.JvmSynthetic /** * 使用于 [ContinuousSessionContext.session] 中的 `receiver` 逻辑函数。 + * + * 在 Java 中,可以使用 `InSessions` 中提供的各种静态工厂函数构建它,例如 + * `InSessions.async`、`InSessions.mono` 等。 + * + * 在 `ContinuousSession` 中使用时,我们强烈建议使用非阻塞的 [InSession] 实现, + * 或者为 `ContinuousSession` 的调度器配置为 **虚拟线程调度器** 。 + * + * ```java + * var dispatcher = ExecutorsKt.from(Executors.newVirtualThreadPerTaskExecutor()); + * ``` */ public fun interface InSession { + @JvmSynthetic public suspend fun ContinuousSessionReceiver.invoke() } @@ -94,10 +106,7 @@ public fun interface InSession { * |--------------- | --------| * ↓ | * return session.push(handleEvent) // 推送 '事件', 得到 '结果' - * * // 直接返回这个结果 - * return result - * * } * ``` * @@ -115,7 +124,12 @@ public interface ContinuousSessionContext { * @param key session 会话的标识。[key] 的类型应当是一个可以保证能够作为一个 hash key 的类型, * 例如基础数据类型(例如 [Int]、[String])、数据类类型(data class)、object 类型等。 * @param strategy 当 [key] 出现冲突时的处理策略 - * @param inSession 在**异步**中进行 + * @param inSession 在**异步**中进行会话逻辑的函数实例。 + * 在 Java 中可使用 `InSessions` 中提供的静态工厂函数构建实例, + * 例如 `InSessions.async`、`InSessions.mono` 等。 + * 在 `ContinuousSession` 中使用时,我们强烈建议使用非阻塞的 [InSession] 实现, + * 或者为 `ContinuousSession` 的调度器配置为 **虚拟线程调度器** 。 + * * @throws ConflictSessionKeyException 如果 [strategy] 为 [ConflictStrategy.FAILURE] 并且出现了冲突 */ public fun session( @@ -125,21 +139,28 @@ public interface ContinuousSessionContext { ): ContinuousSessionProvider /** - * 尝试创建一组 `ContinuousSession`, 并在出现 [key] 冲突时基于 [] + * 尝试创建一组 `ContinuousSession`, 并在出现 [key] 冲突时使用 [ConflictStrategy.FAILURE] 作为冲突解决策略。 */ public fun session( key: Any, inSession: InSession ): ContinuousSessionProvider = session(key, ConflictStrategy.FAILURE, inSession) - /** * 根据 [key] 获取指定的 [ContinuousSessionProvider] 并在找不到时返回 `null`。 */ public operator fun get(key: Any): ContinuousSessionProvider? + /** + * 判断是否包含某个 [key] 对应的会话。 + */ public operator fun contains(key: Any): Boolean + /** + * 移除某个指定 [key] 的会话。 + * [remove] 仅会从记录中移除,不会使用 [ContinuousSessionProvider.cancel], + * 需要由调用者主动使用。 + */ public fun remove(key: Any): ContinuousSessionProvider? /** @@ -164,3 +185,4 @@ public interface ContinuousSessionContext { } } + diff --git a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/EventContinuousSessionContext.kt b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/EventContinuousSessionContext.kt index d8e7738f0..ad1d24fc3 100644 --- a/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/EventContinuousSessionContext.kt +++ b/simbot-extensions/simbot-extension-continuous-session/src/commonMain/kotlin/love/forte/simbot/extension/continuous/session/EventContinuousSessionContext.kt @@ -26,6 +26,7 @@ package love.forte.simbot.extension.continuous.session +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import love.forte.simbot.application.Application import love.forte.simbot.application.ApplicationConfiguration @@ -77,7 +78,8 @@ public interface EventContinuousSessionContext : ContinuousSessionContext. + * + */ + +import kotlinx.coroutines.test.runTest +import love.forte.simbot.core.application.launchSimpleApplication +import love.forte.simbot.extension.continuous.session.EventContinuousSessionContext +import love.forte.simbot.plugin.find +import kotlin.test.Test +import kotlin.test.assertNotNull + +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * This file is part of the Simple Robot Library (Alias: simple-robot, simbot, etc.). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Lesser GNU General Public License for more details. + * + * You should have received a copy of the Lesser GNU General Public License + * along with this program. If not, see . + * + */ + +/** + * + * @author ForteScarlet + */ +class InAppTests { + + @Test + fun installSessionContextTest() = runTest { + val app = launchSimpleApplication { + install(EventContinuousSessionContext) + } + + assertNotNull(app.plugins.find()) + } + +} diff --git a/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/java/module-info.java b/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/java/module-info.java new file mode 100644 index 000000000..82f6a3d5f --- /dev/null +++ b/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/java/module-info.java @@ -0,0 +1,10 @@ +module simbot.extension.continuous.session { + requires kotlin.stdlib; + requires simbot.api; + requires kotlinx.coroutines.core; + requires static simbot.common.annotations; + requires static kotlinx.coroutines.reactor; + requires static reactor.core; + + exports love.forte.simbot.extension.continuous.session; +} diff --git a/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/kotlin/love/forte/simbot/extension/continuous/session/InSession.jvm.kt b/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/kotlin/love/forte/simbot/extension/continuous/session/InSession.jvm.kt new file mode 100644 index 000000000..485293fad --- /dev/null +++ b/simbot-extensions/simbot-extension-continuous-session/src/jvmMain/kotlin/love/forte/simbot/extension/continuous/session/InSession.jvm.kt @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2024. ForteScarlet. + * + * Project https://github.com/simple-robot/simpler-robot + * Email ForteScarlet@163.com + * + * This file is part of the Simple Robot Library (Alias: simple-robot, simbot, etc.). + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Lesser GNU General Public License for more details. + * + * You should have received a copy of the Lesser GNU General Public License + * along with this program. If not, see . + * + */ + +@file:JvmName("InSessions") +@file:JvmMultifileClass + +package love.forte.simbot.extension.continuous.session + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.future.await +import kotlinx.coroutines.reactor.awaitSingleOrNull +import kotlinx.coroutines.runInterruptible +import love.forte.simbot.annotations.Api4J +import reactor.core.publisher.Mono +import java.util.concurrent.CompletionStage +import kotlin.coroutines.CoroutineContext + +/** + * 以阻塞的API构造 [InSession] 实例。 + * 可通过 [InSessions.block][blockInSession] 构造。 + * + * @see blockInSession + */ +public fun interface BlockInSession : InSession { + override suspend fun ContinuousSessionReceiver.invoke() { + runInterruptible(Dispatchers.IO) { block(this) } + } + + public fun block(receiver: ContinuousSessionReceiver) +} + +/** + * Java 友好 API,用于构造一个阻塞风格的 [InSession] 实例。 + * + * @param context 应用在 [runInterruptible] 中用于执行阻塞逻辑的协程上下文。如果为 `null` 则会默认使用 [Dispatchers.IO]。 + */ +@JvmName("block") +@JvmOverloads +@Api4J +public fun blockInSession(context: CoroutineContext? = null, function: BlockInSession): InSession { + if (context == null) return function + + return InSession { runInterruptible(context) { function.block(this) } } +} + +/** + * 以异步的API构造 [InSession] 实例。 + * 可通过 [InSessions.async][asyncInSession] 构造。 + * + * @see asyncInSession + */ +public fun interface AsyncInSession : InSession { + override suspend fun ContinuousSessionReceiver.invoke() { + async(this).await() + } + + public fun async(receiver: ContinuousSessionReceiver): CompletionStage +} + +/** + * Java 友好 API,用于构造一个异步风格的 [InSession] 实例。 + */ +@JvmName("async") +@Api4J +public fun asyncInSession(function: AsyncInSession): InSession = function + +/** + * 以响应式风格 ([Mono]) 的API构造 [InSession] 实例。 + * 可通过 [InSessions.mono][monoInSession] 构造。 + * + * 注意:如果要使用 [MonoInSession], 需要确保 runtime 环境中存在 + * [`kotlinx-coroutines-reactor`](https://github.com/Kotlin/kotlinx.coroutines/tree/master/reactive) + * 依赖。 + * + * @see Mono + * @see monoInSession + */ +public fun interface MonoInSession : InSession { + override suspend fun ContinuousSessionReceiver.invoke() { + mono(this).awaitSingleOrNull() + } + + public fun mono(receiver: ContinuousSessionReceiver): Mono +} + +/** + * Java 友好 API,用于构造一个响应式风格 ([Mono]) 的 [InSession] 实例。 + */ +@JvmName("mono") +@Api4J +public fun monoInSession(function: MonoInSession): InSession = function diff --git a/website b/website index b01be7a52..7f3e63ec1 160000 --- a/website +++ b/website @@ -1 +1 @@ -Subproject commit b01be7a52265eeaa139e41314e78b112a7a225dd +Subproject commit 7f3e63ec17e9b5ddf654b8194b32d935fcf738b8