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