Websocket server supported
This commit is contained in:
34 files changed
+262
-25
No files matched your search
@@ -2,3 +2,5 @@
|
||||
.kotlin/
|
||||
build/
|
||||
.gradle/
|
||||
/core/.dev.vars
|
||||
/core/wrangler.toml
|
||||
@@ -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
|
||||
|
||||
+14
-19
@@ -1,31 +1,25 @@
|
||||
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 {
|
||||
|
||||
|
||||
allprojects {
|
||||
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")
|
||||
}
|
||||
|
||||
commonTest.dependencies {
|
||||
implementation(kotlin("test"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
publishing {
|
||||
subprojects {
|
||||
apply(plugin = "org.jetbrains.kotlin.multiplatform")
|
||||
apply(plugin = "maven-publish")
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
maven("https://repo.maven.rtast.cn/releases") {
|
||||
credentials {
|
||||
@@ -34,4 +28,5 @@ publishing {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Directory> = layout.buildDirectory.dir("wrangler-run")
|
||||
tasks.register<Copy>("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<Directory> = 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`
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
+12
@@ -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<AbstractRoute> = mutableListOf()
|
||||
internal val websocketRoutes: MutableList<WebsocketRoute> = 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
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+11
@@ -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<Response> = GlobalScope.prom
|
||||
route("/") {
|
||||
respondText("Hello kotlin cloudflare worker")
|
||||
}
|
||||
|
||||
webSocket("/ws") {
|
||||
onMessage {
|
||||
println(it.readText())
|
||||
}
|
||||
|
||||
onClose {
|
||||
}
|
||||
}
|
||||
}
|
||||
return@promise server.handle(request)
|
||||
}
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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<WebSocket> = 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<String>()
|
||||
|
||||
public fun MessageEvent.readByteArray(): ByteArray =
|
||||
this.data.unsafeCast<ArrayBuffer>().toByteArray()
|
||||
|
||||
public fun MessageEvent.readBlob(): Blob =
|
||||
this.data.unsafeCast<Blob>()
|
||||
|
||||
public fun MessageEvent.readUint8Array(): Uint8Array =
|
||||
this.data.unsafeCast<Uint8Array>()
|
||||
@@ -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)) }
|
||||
@@ -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 }"))
|
||||
File renamed without changes.
@@ -0,0 +1,6 @@
|
||||
name = "kotlin-cloudflare-worker" # any name you want
|
||||
account_id = "<your account id>"
|
||||
workers_dev = false
|
||||
preview_urls = false
|
||||
compatibility_date = "2022-08-11"
|
||||
main = "kotlin-cloudflare-worker.js" # Set this value same as `outputModuleName`
|
||||
@@ -0,0 +1,5 @@
|
||||
kotlin {
|
||||
js(IR) {
|
||||
nodejs { binaries.executable() }
|
||||
}
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
include(":core")
|
||||
include(":serialization")
|
||||
rootProject.name = "kotlin-cloudflare-worker"
|
||||
Reference in New Issue
Block a user