From 8a1635ca863aa3a89fce38412966386a94c8b0f1 Mon Sep 17 00:00:00 2001 From: RTAkland Date: Sun, 4 Jan 2026 00:33:42 +0800 Subject: [PATCH] Websocket server supported --- .gitignore | 4 +- README.md | 4 +- build.gradle.kts | 39 ++++---- core/build.gradle.kts | 67 +++++++++++++ .../jsMain/kotlin/cn/rtast/cfworker/CORS.kt | 0 .../kotlin/cn/rtast/cfworker/HttpMethod.kt | 0 .../cn/rtast/cfworker/WorkerApplication.kt | 12 +++ .../cn/rtast/cfworker/auth/AuthResult.kt | 0 .../kotlin/cn/rtast/cfworker/auth/AuthUtil.kt | 0 .../auth/credentials/BasicCredential.kt | 0 .../auth/credentials/BearerCredential.kt | 0 .../auth/credentials/HttpCredential.kt | 0 .../cfworker/auth/provider/Authenticator.kt | 0 .../auth/provider/BasicAuthenticator.kt | 0 .../auth/provider/BearerAuthenticator.kt | 0 .../kotlin/cn/rtast/cfworker/client/fetch.kt | 0 .../cn/rtast/cfworker/config/CORSConfig.kt | 0 .../jsMain/kotlin/cn/rtast/cfworker/main.kt | 11 +++ .../rtast/cfworker/response/ResponseUtil.kt | 0 .../cn/rtast/cfworker/route/RouteHandler.kt | 0 .../cn/rtast/cfworker/route/RouteUtil.kt | 0 .../cfworker/route/type/AbstractRoute.kt | 0 .../rtast/cfworker/route/type/RegexRoute.kt | 0 .../cn/rtast/cfworker/route/type/RouteType.kt | 0 .../rtast/cfworker/route/type/StringRoute.kt | 0 .../kotlin/cn/rtast/cfworker/util/Base64.kt | 0 .../cn/rtast/cfworker/util/ByteArray.kt | 0 .../websocket/WebsocketEventHandler.kt | 94 +++++++++++++++++++ .../cfworker/websocket/WebsocketRoute.kt | 26 +++++ .../response/WebsocketResponseUtil.kt | 17 ++++ {src => core/src}/jsTest/kotlin/test/Test.kt | 0 core/wrangler.template.toml | 6 ++ serialization/build.gradle.kts | 5 + settings.gradle.kts | 2 + 34 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 core/build.gradle.kts rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/CORS.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/HttpMethod.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt (82%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/AuthResult.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/AuthUtil.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BasicCredential.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BearerCredential.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/HttpCredential.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/provider/Authenticator.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BasicAuthenticator.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BearerAuthenticator.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/config/CORSConfig.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/main.kt (82%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/response/ResponseUtil.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/RouteHandler.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/RouteUtil.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/type/AbstractRoute.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/type/RegexRoute.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/type/RouteType.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/route/type/StringRoute.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/util/Base64.kt (100%) rename {src => core/src}/jsMain/kotlin/cn/rtast/cfworker/util/ByteArray.kt (100%) create mode 100644 core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketEventHandler.kt create mode 100644 core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketRoute.kt create mode 100644 core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/response/WebsocketResponseUtil.kt rename {src => core/src}/jsTest/kotlin/test/Test.kt (100%) create mode 100644 core/wrangler.template.toml create mode 100644 serialization/build.gradle.kts diff --git a/.gitignore b/.gitignore index 74fdfe0..d915166 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ .idea/ .kotlin/ build/ -.gradle/ \ No newline at end of file +.gradle/ +/core/.dev.vars +/core/wrangler.toml diff --git a/README.md b/README.md index 9a355dc..daa5431 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ repositories { ```kotlin // to get latest version of this lib, go to -// https://next.pkg.rtast.cn/#/releases/cn/rtast/kotlin-cfworker/kotlin-cloudflare-worker/ -implementation("cn.rtast.kotlin-cfworker:kotlin-cloudflare-worker:1.0.2") +// https://next.pkg.rtast.cn/#/releases/cn/rtast/kotlin-cfworker/core/ +implementation("cn.rtast.kotlin-cfworker:core:1.0.2") ``` ## Run app diff --git a/build.gradle.kts b/build.gradle.kts index 22daa89..c83027c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,36 +1,31 @@ plugins { - kotlin("multiplatform") version "2.2.21" + kotlin("multiplatform") version "2.2.21" apply false id("maven-publish") } -group = "cn.rtast.kotlin-cfworker" -version = "1.0.2" -repositories { - mavenCentral() -} -kotlin { - explicitApi() - js(IR) { nodejs { binaries.executable() } } - sourceSets { - jsMain.dependencies { - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") - } +allprojects { + group = "cn.rtast.kotlin-cfworker" + version = "1.0.2" - commonTest.dependencies { - implementation(kotlin("test")) - } + repositories { + mavenCentral() } } -publishing { - repositories { - maven("https://repo.maven.rtast.cn/releases") { - credentials { - username = "RTAkland" - password = System.getenv("PUBLISH_TOKEN") +subprojects { + apply(plugin = "org.jetbrains.kotlin.multiplatform") + apply(plugin = "maven-publish") + + publishing { + repositories { + maven("https://repo.maven.rtast.cn/releases") { + credentials { + username = "RTAkland" + password = System.getenv("PUBLISH_TOKEN") + } } } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts new file mode 100644 index 0000000..444ff87 --- /dev/null +++ b/core/build.gradle.kts @@ -0,0 +1,67 @@ +kotlin { + explicitApi() + js(IR) { nodejs { binaries.executable() } } + + sourceSets { + jsMain.dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2") + } + + commonTest.dependencies { + implementation(kotlin("test")) + } + } +} + + +val wranglerRunDir: Provider = layout.buildDirectory.dir("wrangler-run") +tasks.register("prepareWranglerRun") { + group = "wrangler" + dependsOn("compileDevelopmentExecutableKotlinJs") + val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/developmentExecutable/kotlin") + from(buildOutputDir) + into(wranglerRunDir) +} + +val wranglerDev by tasks.registering(Exec::class) { + group = "wrangler" + workingDir = layout.buildDirectory.dir("wrangler-run").get().asFile.apply { mkdirs() } + doFirst { + val sourceDir = project.layout.projectDirectory + mapOf( + sourceDir.file("wrangler.toml").asFile to File(workingDir, "wrangler.toml"), + sourceDir.file(".dev.vars").asFile to File(workingDir, ".dev.vars"), + ).forEach { (s, d) -> s.copyTo(d, overwrite = true) } + } + commandLine( + if (System.getProperty("os.name").lowercase().contains("windows")) listOf( + "cmd", "/c", "wrangler dev --port 7071" + ) + else listOf("sh", "-c", "wrangler dev --port 7071") + ) + standardInput = System.`in` + isIgnoreExitValue = false +} + +val wranglerDeployDir: Provider = layout.buildDirectory.dir("wrangler-deploy") + .apply { get().asFile.deleteRecursively() } +val prepareProductionDeploy by tasks.registering(Copy::class) { + group = "wrangler" + dependsOn("compileProductionExecutableKotlinJs") + val buildOutputDir = layout.buildDirectory.dir("compileSync/js/main/productionExecutable/kotlin") + from(buildOutputDir) { exclude("*.map") } + into(wranglerDeployDir) + from(layout.projectDirectory.file("wrangler.toml")) + into(wranglerDeployDir) +} + +val wranglerDeploy by tasks.registering(Exec::class) { + group = "wrangler" + dependsOn(prepareProductionDeploy) + workingDir = layout.buildDirectory.dir("wrangler-deploy").get().asFile.apply { mkdirs() } + commandLine( + if (System.getProperty("os.name").lowercase().contains("windows")) listOf("cmd", "/c", "wrangler deploy") + else listOf("sh", "-c", "wrangler deploy") + ) + standardInput = System.`in` +} \ No newline at end of file diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/CORS.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/CORS.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/CORS.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/CORS.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/HttpMethod.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/HttpMethod.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/HttpMethod.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/HttpMethod.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt similarity index 82% rename from src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt index 2f50bec..8554847 100644 --- a/src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt +++ b/core/src/jsMain/kotlin/cn/rtast/cfworker/WorkerApplication.kt @@ -20,9 +20,12 @@ import cn.rtast.cfworker.route.type.AbstractRoute import cn.rtast.cfworker.route.type.RegexRoute import cn.rtast.cfworker.route.type.StringRoute import cn.rtast.cfworker.util.decodeBase64String +import cn.rtast.cfworker.websocket.WebsocketEventHandler +import cn.rtast.cfworker.websocket.WebsocketRoute import org.w3c.dom.url.URL import org.w3c.fetch.Request import org.w3c.fetch.Response +import org.w3c.fetch.ResponseInit /** * Kotlin cloudflare worker logic entrypoint class @@ -31,12 +34,21 @@ public class WorkerApplication( public val corsConfig: CORSConfig = CORSConfig(), ) { internal val routes: MutableList = mutableListOf() + internal val websocketRoutes: MutableList = mutableListOf() public suspend fun handle(request: Request): Response { val url = URL(request.url) val path = url.pathname val method = HttpMethod.fromString(request.method) ?: HttpMethod.GET if (method == HttpMethod.OPTIONS && corsConfig.enabled) return respondEmpty() + if (request.headers.get("upgrade") != null) { + val route = websocketRoutes.firstOrNull { r -> + r.stringPath?.let { it == url.pathname } ?: r.regexPath?.matches(url.pathname) ?: false + } ?: return Response("Not Found", ResponseInit(404)) + val handler = WebsocketEventHandler(request = request) + route.block(handler) + return handler.handle() + } val matchedRoutes = routes.filter { when (it) { is StringRoute -> it.path == path diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthResult.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthResult.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthResult.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthResult.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthUtil.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthUtil.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthUtil.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/AuthUtil.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BasicCredential.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BasicCredential.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BasicCredential.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BasicCredential.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BearerCredential.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BearerCredential.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BearerCredential.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/BearerCredential.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/HttpCredential.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/HttpCredential.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/HttpCredential.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/credentials/HttpCredential.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/Authenticator.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/Authenticator.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/Authenticator.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/Authenticator.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BasicAuthenticator.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BasicAuthenticator.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BasicAuthenticator.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BasicAuthenticator.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BearerAuthenticator.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BearerAuthenticator.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BearerAuthenticator.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/auth/provider/BearerAuthenticator.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/config/CORSConfig.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/config/CORSConfig.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/config/CORSConfig.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/config/CORSConfig.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/main.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/main.kt similarity index 82% rename from src/jsMain/kotlin/cn/rtast/cfworker/main.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/main.kt index dea095e..3b5595d 100644 --- a/src/jsMain/kotlin/cn/rtast/cfworker/main.kt +++ b/core/src/jsMain/kotlin/cn/rtast/cfworker/main.kt @@ -15,6 +15,8 @@ package cn.rtast.cfworker import cn.rtast.cfworker.response.respondText import cn.rtast.cfworker.route.route +import cn.rtast.cfworker.websocket.readText +import cn.rtast.cfworker.websocket.webSocket import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.promise @@ -39,6 +41,15 @@ public fun handleRequest(request: Request): Promise = GlobalScope.prom route("/") { respondText("Hello kotlin cloudflare worker") } + + webSocket("/ws") { + onMessage { + println(it.readText()) + } + + onClose { + } + } } return@promise server.handle(request) } diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/response/ResponseUtil.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/response/ResponseUtil.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/response/ResponseUtil.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/response/ResponseUtil.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteHandler.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteHandler.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/RouteHandler.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteHandler.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteUtil.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteUtil.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/RouteUtil.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/RouteUtil.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/type/AbstractRoute.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/AbstractRoute.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/type/AbstractRoute.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/AbstractRoute.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RegexRoute.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RegexRoute.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/type/RegexRoute.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RegexRoute.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RouteType.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RouteType.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/type/RouteType.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/RouteType.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/route/type/StringRoute.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/StringRoute.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/route/type/StringRoute.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/route/type/StringRoute.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/util/Base64.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/util/Base64.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/util/Base64.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/util/Base64.kt diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/util/ByteArray.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/util/ByteArray.kt similarity index 100% rename from src/jsMain/kotlin/cn/rtast/cfworker/util/ByteArray.kt rename to core/src/jsMain/kotlin/cn/rtast/cfworker/util/ByteArray.kt diff --git a/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketEventHandler.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketEventHandler.kt new file mode 100644 index 0000000..3649b1b --- /dev/null +++ b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketEventHandler.kt @@ -0,0 +1,94 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/1/3 + */ + + +@file:OptIn(DelicateCoroutinesApi::class) + +package cn.rtast.cfworker.websocket + +import cn.rtast.cfworker.util.toByteArray +import cn.rtast.cfworker.websocket.response.respondSwitchingProtocol +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.promise +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Uint8Array +import org.w3c.dom.CloseEvent +import org.w3c.dom.MessageEvent +import org.w3c.dom.WebSocket +import org.w3c.dom.events.Event +import org.w3c.fetch.Request +import org.w3c.fetch.Response +import org.w3c.fetch.ResponseInit +import org.w3c.files.Blob + +public class WebsocketEventHandler(public val request: Request) { + private var onMessageBlock: (suspend (MessageEvent) -> Unit)? = null + private var onCloseBlock: (suspend (CloseEvent) -> Unit?)? = null + private var onOpenBlock: (suspend (Event) -> Unit)? = null + private var onErrorBlock: (suspend (Event) -> Unit)? = null + private var requireUpgradeHeaderBlock: (suspend (Request) -> Response)? = + { Response("Expected Upgrade: websocket", ResponseInit(426)) } + public val clients: MutableList = mutableListOf() + + public fun upgradeHeaderRequired(block: suspend (Request) -> Response) { + requireUpgradeHeaderBlock = block + } + + public fun onMessage(block: suspend (MessageEvent) -> Unit) { + onMessageBlock = block + } + + public fun onClose(block: suspend (CloseEvent) -> Unit?) { + onCloseBlock = block + } + + public fun onOpen(block: suspend (Event) -> Unit) { + onOpenBlock = block + } + + public fun onError(block: suspend (Event) -> Unit) { + onErrorBlock = block + } + + public fun close(code: Short = 1000, reason: String? = null): Unit = + this.clients.forEach { it.close(code, reason ?: "Closed") } + + internal suspend fun handle(): Response { + val upgradeHeader = request.headers.get("upgrade") + if (upgradeHeader == null || upgradeHeader != "websocket") + return requireUpgradeHeaderBlock!!.invoke(request) + val pair = js("new WebSocketPair()") + val client = pair[0] + val server = pair[1] + server.accept() + clients.add(server) + server.addEventListener("message", { e: MessageEvent -> GlobalScope.promise { onMessageBlock?.invoke(e) } }) + server.addEventListener("error", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } }) + server.addEventListener("open", { e: Event -> GlobalScope.promise { onErrorBlock?.invoke(e) } }) + server.addEventListener("close", { e: CloseEvent -> + GlobalScope.promise { + onCloseBlock?.invoke(e) + clients.remove(client) + } + }) + return respondSwitchingProtocol(client) + } +} + +public typealias WebsocketHandler = suspend WebsocketEventHandler.() -> Unit + +public fun MessageEvent.readText(): String = + this.data.unsafeCast() + +public fun MessageEvent.readByteArray(): ByteArray = + this.data.unsafeCast().toByteArray() + +public fun MessageEvent.readBlob(): Blob = + this.data.unsafeCast() + +public fun MessageEvent.readUint8Array(): Uint8Array = + this.data.unsafeCast() \ No newline at end of file diff --git a/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketRoute.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketRoute.kt new file mode 100644 index 0000000..eb0053b --- /dev/null +++ b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/WebsocketRoute.kt @@ -0,0 +1,26 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/1/3 + */ + + +package cn.rtast.cfworker.websocket + +import cn.rtast.cfworker.WorkerApplication + +internal data class WebsocketRoute( + val stringPath: String? = null, + val regexPath: Regex? = null, + val block: WebsocketHandler, +) + +public fun WorkerApplication.webSocket( + path: Regex, + block: WebsocketHandler, +): Unit = run { websocketRoutes.add(WebsocketRoute(regexPath = path, block = block)) } + +public fun WorkerApplication.webSocket( + path: String, + block: WebsocketHandler, +): Unit = run { websocketRoutes.add(WebsocketRoute(stringPath = path, block = block)) } diff --git a/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/response/WebsocketResponseUtil.kt b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/response/WebsocketResponseUtil.kt new file mode 100644 index 0000000..a9c429c --- /dev/null +++ b/core/src/jsMain/kotlin/cn/rtast/cfworker/websocket/response/WebsocketResponseUtil.kt @@ -0,0 +1,17 @@ +/* + * Copyright © 2026 RTAkland + * Author: RTAkland + * Date: 2026/1/3 + */ + + +package cn.rtast.cfworker.websocket.response + +import org.w3c.fetch.Response + +/** + * Respond switching protocol + * Websocket + */ +internal fun respondSwitchingProtocol(client: dynamic): Response = + Response(null, js("{ status: 101, webSocket: client }")) \ No newline at end of file diff --git a/src/jsTest/kotlin/test/Test.kt b/core/src/jsTest/kotlin/test/Test.kt similarity index 100% rename from src/jsTest/kotlin/test/Test.kt rename to core/src/jsTest/kotlin/test/Test.kt diff --git a/core/wrangler.template.toml b/core/wrangler.template.toml new file mode 100644 index 0000000..4c50702 --- /dev/null +++ b/core/wrangler.template.toml @@ -0,0 +1,6 @@ +name = "kotlin-cloudflare-worker" # any name you want +account_id = "" +workers_dev = false +preview_urls = false +compatibility_date = "2022-08-11" +main = "kotlin-cloudflare-worker.js" # Set this value same as `outputModuleName` diff --git a/serialization/build.gradle.kts b/serialization/build.gradle.kts new file mode 100644 index 0000000..c9dd371 --- /dev/null +++ b/serialization/build.gradle.kts @@ -0,0 +1,5 @@ +kotlin { + js(IR) { + nodejs { binaries.executable() } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 95c22d3..0d65c7c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,3 @@ +include(":core") +include(":serialization") rootProject.name = "kotlin-cloudflare-worker" \ No newline at end of file