diff --git a/README-zh.md b/README-zh.md new file mode 100644 index 0000000..907827d --- /dev/null +++ b/README-zh.md @@ -0,0 +1,160 @@ +# Kotlin cloudflare worker + +纯Kotlin编写的库,可以在Cloudflare worker上运行Kotlin/JS + +# 快速开始 + +## 配置 + +> 项目依赖于Kotlin协程库, 请确保协程库版本号大于`1.10.2` + +```kotlin +plugins { + kotlin("multiplatform") version "2.2.21" +} + +repositories { + mavenCentral() + maven("https://repo.maven.rtast.cn/releases") +} +``` + +```kotlin +// 最新版本请前往下方链接查看 +// https://next.pkg.rtast.cn/#/releases/cn/rtast/kotlin-cfworker/kotlin-cloudflare-worker/ +implementation("cn.rtast.kotlin-cfworker:kotlin-cloudflare-worker:1.0.2") +``` + +## Hello world程序 + +```kotlin +kotlin { + js(IR) { + nodejs { + outputModuleName = "kotlin-cloudflare-worker" + binaries.executable() + } + } +} +``` + +> Minimal hello world app + +```kotlin +/** + * 不要修改这个函数的任何内容 + */ +fun main() { + @Suppress("unused_expression") + val eventListener = EventListener { event -> + val dyn = event.asDynamic() + event.asDynamic().respondWith(handleRequest(dyn.request as Request)) + Unit + } + js("addEventListener('fetch', eventListener)") +} + +/** + * 这里是处理http请求的函数 + */ +@JsExport +fun handleRequest(request: Request): Promise = GlobalScope.promise { + val server = WorkerApplication().apply { + route("/") { + respondText("Hello kotlin cloudflare worker") + } + } + return@promise server.handle(request) +} + +``` + +> 在本地运行之前需要先安装wrangler + +在项目根目录创建一个名为`wrangler.toml`的文件,然后将下面的内容复制进去并修改为你自己的信息 + +```toml +name = "kotlin-cloudflare-worker" # 可以是任何名字 +account_id = "" +workers_dev = false +preview_urls = false +compatibility_date = "2022-08-11" +main = "kotlin-cloudflare-worker.js" # 必须和`outputModuleName`一致 +``` + +运行gradle任务编译出js `gradlew compileProductionExecutableKotlinJs`, +将wrangler.toml复制到 `build/compileSync/js/main/productionExecutable/kotlin`, +并且进入该目录下运行`wrangler dev` + +## 部署 + +使用`wrangler deploy`来部署到cloudflare worker, 在此之前你需要先使用`wrangler login`来登录 + +# 注意事项 + +Cloudflare worker 使用V8引擎来驱动js脚本,线程模型为单线程,所有时间均有event loop(事件循环)驱动,在此之外的所有任务 +都会被drop, 所以不要使用协程库中的launch或任何可以凭空创建出一个挂起作用域的函数来调用suspend函数, 取而代之的是你需要从 +`handleRequest`方法中传播`CoroutineScope`(suspend), 就像下面这样 + +```kotlin +// 不要使用这种方法 +val scope = CoroutineScope(Dispatcher.DEFAULT) + +fun blockingFunction() { + scope.launch { + // 这里的代码永远不会被执行 + } +} + +// 使用下面的代码来正确的调用suspend函数 + +fun main() { + @Suppress("unused_expression") + val eventListener = EventListener { event -> + val dyn = event.asDynamic() + event.asDynamic().respondWith(handleRequest(dyn.request as Request)) + Unit + } + js("addEventListener('fetch', eventListener)") +} + +@JsExport +fun handleRequest(request: Request): Promise = GlobalScope.promise { + return Promise.resolve(Response("Hello")) +} +``` + +# Http 客户端 + +不要使用ktor来作为http客户端, 因为cloudflare worker不是一个标准的nodejs环境, 你需要使用 fetch +`cn.rtast.cfworker.client.fetch` 来发送http请求, 注意不是`window.fetch`, `window.fetch`是浏览器API + +# ByteArray 和 ByteBuffer 转换 + +```kotlin +import cn.rtast.cfworker.util.toByteArray +import cn.rtast.cfworker.util.toArrayBuffer + +val bb: ByteBuffer = ... +// 将ByteBuffer转换为ByteArray +val ba: ByteArray = bb.toByteArray() + +// 将ByteArray转换为ByteBuffer +val bb2: ByteBuffer = ba.toArrayBuffer() +``` + +# CPU时间限制 + +cloudflare worker 免费版本限制处理每个请求的CPU时间为10ms(不包括IO等待时间), 如果你需要执行CPU密集型任务或需要更高的配额, +请考虑升级到Workers paid或使用JS来减少标准库和协程库的加载时间. + +即使使用此库注册了超过15个api端点,并且都具备文件处理功能,每个请求占用的cpu时间平均值不超过10ms+-2ms, +在冷启动时处理首次请求时间会大幅增加,最高可达50ms,但worker仍然可以处理并返回请求. + +![avg-cpu-time](./images/avg-cpu-time.png) + +> 上图反应了过去24小时每个请求的平均cpu时间(P50) + +# 已经使用此项目的实例 + +https://repo.maven.rtast.cn \ No newline at end of file diff --git a/README.md b/README.md index bd428a4..9a355dc 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ A library to running kotlin/js on cloudflare worker. +
+English +中文 +
+ # Get started ## Setup @@ -22,7 +27,7 @@ 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.1") +implementation("cn.rtast.kotlin-cfworker:kotlin-cloudflare-worker:1.0.2") ``` ## Run app @@ -148,6 +153,20 @@ val ba: ByteArray = bb.toByteArray() val bb2: ByteBuffer = ba.toArrayBuffer() ``` +# CPU Time limit + +Cloudflare worker free tier have a limit of worker cpu limit (10ms, exclude IO waiting time), `kotlin cloudflare worker` +depends on `kotlin-stdlib` and `kotlinx.coroutines`, which are quite heavy*, When performing CPU-bound task, +please carefully consider whether to use JavaScript instead of Kotlin. + +But it works for my maven repository, I registered over 15 api endpoints. +The avg cpu time for each endpoint does not exceed 10ms, but it may increase significantly during +cold start (Over 50-200ms, but still responsible) + +![avg-cpu-time](./images/avg-cpu-time.png) + +> This image shows last 24 hours avg cpu time, see `P50` + # Real instance https://repo.maven.rtast.cn diff --git a/build.gradle.kts b/build.gradle.kts index c6f434a..22daa89 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "cn.rtast.kotlin-cfworker" -version = "1.0.1" +version = "1.0.2" repositories { mavenCentral() diff --git a/images/avg-cpu-time.png b/images/avg-cpu-time.png new file mode 100644 index 0000000..936659e Binary files /dev/null and b/images/avg-cpu-time.png differ diff --git a/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt b/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt index 031ee78..27ec8bf 100644 --- a/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt +++ b/src/jsMain/kotlin/cn/rtast/cfworker/client/fetch.kt @@ -7,11 +7,29 @@ package cn.rtast.cfworker.client +import cn.rtast.cfworker.util.toByteArray +import kotlinx.coroutines.await +import org.w3c.dom.url.URL +import org.w3c.fetch.Request import org.w3c.fetch.RequestInit import org.w3c.fetch.Response import kotlin.js.Promise +/** + * define fetch function externally + */ public external fun fetch( input: String, init: RequestInit = definedExternally, -): Promise \ No newline at end of file +): Promise + +/** + * Get request raw body, http POST + */ +public suspend fun Request.rawBody(): ByteArray = + this.arrayBuffer().await().toByteArray() + +/** + * Get [URL] object + */ +public val Request.Url: URL get() = URL(this.url) \ No newline at end of file