Skip to content

Commit

Permalink
KTOR-7675 Support CIO client for wasm-js and js (#4441)
Browse files Browse the repository at this point in the history
* Support multiple client engines for JS/WasmJs
* CIO client Js/WasmJs support
* Enable CIO tests in client tests
* Make client engines `data object` to have `toString`
* Make ClientLoader work with multiple engines on js/wasmJs and so test CIO engine on nodejs(js+wasmJs)
  • Loading branch information
whyoleg authored Nov 8, 2024
1 parent 2466db1 commit 4a8a103
Show file tree
Hide file tree
Showing 68 changed files with 397 additions and 404 deletions.
3 changes: 3 additions & 0 deletions ktor-client/ktor-client-android/api/ktor-client-android.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
public final class io/ktor/client/engine/android/Android : io/ktor/client/engine/HttpClientEngineFactory {
public static final field INSTANCE Lio/ktor/client/engine/android/Android;
public fun create (Lkotlin/jvm/functions/Function1;)Lio/ktor/client/engine/HttpClientEngine;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/ktor/client/engine/android/AndroidClientEngine : io/ktor/client/engine/HttpClientEngineBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import io.ktor.client.engine.*
*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public object Android : HttpClientEngineFactory<AndroidEngineConfig> {
public data object Android : HttpClientEngineFactory<AndroidEngineConfig> {
override fun create(block: AndroidEngineConfig.() -> Unit): HttpClientEngine =
AndroidClientEngine(AndroidEngineConfig().apply(block))
}
Expand Down
3 changes: 3 additions & 0 deletions ktor-client/ktor-client-apache/api/ktor-client-apache.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
public final class io/ktor/client/engine/apache/Apache : io/ktor/client/engine/HttpClientEngineFactory {
public static final field INSTANCE Lio/ktor/client/engine/apache/Apache;
public fun create (Lkotlin/jvm/functions/Function1;)Lio/ktor/client/engine/HttpClientEngine;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/ktor/client/engine/apache/ApacheEngineConfig : io/ktor/client/engine/HttpClientEngineConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import io.ktor.client.engine.*
*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public object Apache : HttpClientEngineFactory<ApacheEngineConfig> {
public data object Apache : HttpClientEngineFactory<ApacheEngineConfig> {
override fun create(block: ApacheEngineConfig.() -> Unit): HttpClientEngine {
val config = ApacheEngineConfig().apply(block)
return ApacheEngine(config)
Expand Down
3 changes: 3 additions & 0 deletions ktor-client/ktor-client-apache5/api/ktor-client-apache5.api
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
public final class io/ktor/client/engine/apache5/Apache5 : io/ktor/client/engine/HttpClientEngineFactory {
public static final field INSTANCE Lio/ktor/client/engine/apache5/Apache5;
public fun create (Lkotlin/jvm/functions/Function1;)Lio/ktor/client/engine/HttpClientEngine;
public fun equals (Ljava/lang/Object;)Z
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class io/ktor/client/engine/apache5/Apache5EngineConfig : io/ktor/client/engine/HttpClientEngineConfig {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import io.ktor.client.engine.*
*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public object Apache5 : HttpClientEngineFactory<Apache5EngineConfig> {
public data object Apache5 : HttpClientEngineFactory<Apache5EngineConfig> {
override fun create(block: Apache5EngineConfig.() -> Unit): HttpClientEngine {
val config = Apache5EngineConfig().apply(block)
return Apache5Engine(config)
Expand Down
6 changes: 5 additions & 1 deletion ktor-client/ktor-client-cio/api/ktor-client-cio.klib.api
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Klib ABI Dump
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64]
// Rendering settings:
// - Signature version: 2
// - Show manifest properties: true
Expand Down Expand Up @@ -62,3 +62,7 @@ final object io.ktor.client.engine.cio/CIO : io.ktor.client.engine/HttpClientEng
}

final fun (io.ktor.client.engine.cio/CIOEngineConfig).io.ktor.client.engine.cio/endpoint(kotlin/Function1<io.ktor.client.engine.cio/EndpointConfig, kotlin/Unit>): io.ktor.client.engine.cio/EndpointConfig // io.ktor.client.engine.cio/endpoint|[email protected](kotlin.Function1<io.ktor.client.engine.cio.EndpointConfig,kotlin.Unit>){}[0]

// Targets: [js]
final val io.ktor.client.engine.cio/initHook // io.ktor.client.engine.cio/initHook|{}initHook[0]
final fun <get-initHook>(): dynamic // io.ktor.client.engine.cio/initHook.<get-initHook>|<get-initHook>(){}[0]
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ import io.ktor.client.engine.*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public data object CIO : HttpClientEngineFactory<CIOEngineConfig> {
init {
addToLoader()
}

override fun create(block: CIOEngineConfig.() -> Unit): HttpClientEngine =
CIOEngine(CIOEngineConfig().apply(block))
}
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ internal class CIOEngine(
val requestJob = requestField[Job]!!
val selector = selectorManager

@OptIn(ExperimentalCoroutinesApi::class)
GlobalScope.launch(parentContext, start = CoroutineStart.ATOMIC) {
try {
requestJob.join()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.ktor.client.tests.utils.*
import io.ktor.http.*
import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import io.ktor.test.dispatcher.*
import io.ktor.utils.io.*
import io.ktor.websocket.*
import kotlinx.coroutines.*
Expand All @@ -24,7 +25,7 @@ class CIOEngineTest {
private val selectorManager = SelectorManager()

@Test
fun testRequestTimeoutIgnoredWithWebSocket(): Unit = runBlocking {
fun testRequestTimeoutIgnoredWithWebSocket() = runTestWithRealTime {
val client = HttpClient(CIO) {
engine {
requestTimeout = 10
Expand All @@ -48,7 +49,7 @@ class CIOEngineTest {
}

@Test
fun testRequestTimeoutIgnoredWithSSE(): Unit = runBlocking {
fun testRequestTimeoutIgnoredWithSSE() = runTestWithRealTime {
val client = HttpClient(CIO) {
engine {
requestTimeout = 10
Expand All @@ -67,7 +68,7 @@ class CIOEngineTest {
}

@Test
fun testExpectHeader(): Unit = runBlocking {
fun testExpectHeader() = runTestWithRealTime {
val body = "Hello World"

withServerSocket { socket ->
Expand Down Expand Up @@ -95,7 +96,7 @@ class CIOEngineTest {
}

@Test
fun testNoExpectHeaderIfNoBody(): Unit = runBlocking {
fun testNoExpectHeaderIfNoBody() = runTestWithRealTime {
withServerSocket { socket ->
val client = HttpClient(CIO)
launch {
Expand All @@ -116,7 +117,7 @@ class CIOEngineTest {
}

@Test
fun testDontWaitForContinueResponse(): Unit = runBlocking {
fun testDontWaitForContinueResponse() = runTestWithRealTime {
withTimeout(30.seconds) {
val body = "Hello World\n"

Expand Down Expand Up @@ -148,7 +149,7 @@ class CIOEngineTest {
}

@Test
fun testRepeatRequestAfterExpectationFailed(): Unit = runBlocking {
fun testRepeatRequestAfterExpectationFailed() = runTestWithRealTime {
val body = "Hello World"

withServerSocket { socket ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package io.ktor.client.engine.cio

import io.ktor.network.selector.*
import io.ktor.network.sockets.*
import io.ktor.test.dispatcher.*
import io.ktor.utils.io.core.*
import kotlinx.coroutines.*
import kotlin.test.*

Expand All @@ -24,7 +26,7 @@ class ConnectionFactoryTest {
}

@Test
fun testLimitSemaphore() = runBlocking {
fun testLimitSemaphore() = runTestWithRealTime {
val connectionFactory = ConnectionFactory(
selectorManager,
connectionsLimit = 2,
Expand All @@ -45,7 +47,7 @@ class ConnectionFactoryTest {
}

@Test
fun testAddressSemaphore() = runBlocking {
fun testAddressSemaphore() = runTestWithRealTime {
val connectionFactory = ConnectionFactory(
selectorManager,
connectionsLimit = 2,
Expand All @@ -68,7 +70,7 @@ class ConnectionFactoryTest {
}

@Test
fun testReleaseLimitSemaphoreWhenFailed() = runBlocking {
fun testReleaseLimitSemaphoreWhenFailed() = runTestWithRealTime {
val connectionFactory = ConnectionFactory(
selectorManager,
connectionsLimit = 2,
Expand Down
5 changes: 5 additions & 0 deletions ktor-client/ktor-client-cio/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#
# Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
#
target.js.browser=false
target.wasmJs.browser=false
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.cio

import io.ktor.client.engine.*
import io.ktor.util.*
import io.ktor.utils.io.*

@Suppress("DEPRECATION")
@OptIn(ExperimentalStdlibApi::class, ExperimentalJsExport::class, InternalAPI::class)
@Deprecated("", level = DeprecationLevel.HIDDEN)
@JsExport
@EagerInitialization
public val initHook: dynamic = run {
if (PlatformUtils.IS_NODE) engines.append(CIO)
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ internal actual class ConnectionPipeline actual constructor(
actual override val coroutineContext: CoroutineContext = parentContext

init {
error("Pipelining is not supported in native CIO")
error("Pipelining is not supported in native/js/wasm CIO")
}

actual val pipelineContext: Job
get() = error("Pipelining is not supported in native CIO")
get() = error("Pipelining is not supported in native/js/wasm CIO")
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@ package io.ktor.client.engine.cio
import io.ktor.client.engine.*
import io.ktor.utils.io.*

@OptIn(ExperimentalStdlibApi::class)
@Suppress("DEPRECATION")
@OptIn(ExperimentalStdlibApi::class, InternalAPI::class)
@EagerInitialization
private val initHook = CIO

@OptIn(InternalAPI::class)
internal actual fun addToLoader() {
engines.append(CIO)
}
private val initHook = engines.append(CIO)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.cio

import io.ktor.client.engine.*
import io.ktor.util.*
import io.ktor.utils.io.*

@Suppress("DEPRECATION")
@OptIn(InternalAPI::class, ExperimentalStdlibApi::class)
@EagerInitialization
private val initHook: Unit = run {
if (PlatformUtils.IS_NODE) engines.append(CIO)
}
18 changes: 12 additions & 6 deletions ktor-client/ktor-client-core/api/ktor-client-core.klib.api
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,11 @@ final object io.ktor.client.engine/ProxyBuilder { // io.ktor.client.engine/Proxy
final fun socks(kotlin/String, kotlin/Int): io.ktor.client.engine/ProxyConfig // io.ktor.client.engine/ProxyBuilder.socks|socks(kotlin.String;kotlin.Int){}[0]
}

final object io.ktor.client.engine/engines : kotlin.collections/Iterable<io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>> { // io.ktor.client.engine/engines|null[0]
final fun append(io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>) // io.ktor.client.engine/engines.append|append(io.ktor.client.engine.HttpClientEngineFactory<io.ktor.client.engine.HttpClientEngineConfig>){}[0]
final fun iterator(): kotlin.collections/Iterator<io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>> // io.ktor.client.engine/engines.iterator|iterator(){}[0]
}

final object io.ktor.client.plugins.api/Send : io.ktor.client.plugins.api/ClientHook<kotlin.coroutines/SuspendFunction2<io.ktor.client.plugins.api/Send.Sender, io.ktor.client.request/HttpRequestBuilder, io.ktor.client.call/HttpClientCall>> { // io.ktor.client.plugins.api/Send|null[0]
final fun install(io.ktor.client/HttpClient, kotlin.coroutines/SuspendFunction2<io.ktor.client.plugins.api/Send.Sender, io.ktor.client.request/HttpRequestBuilder, io.ktor.client.call/HttpClientCall>) // io.ktor.client.plugins.api/Send.install|install(io.ktor.client.HttpClient;kotlin.coroutines.SuspendFunction2<io.ktor.client.plugins.api.Send.Sender,io.ktor.client.request.HttpRequestBuilder,io.ktor.client.call.HttpClientCall>){}[0]

Expand Down Expand Up @@ -1512,12 +1517,6 @@ final suspend inline fun <#A: reified kotlin/Any?> (io.ktor.client.plugins.webso
final suspend inline fun <#A: reified kotlin/Any?> (io.ktor.client.plugins.websocket/DefaultClientWebSocketSession).io.ktor.client.plugins.websocket/sendSerialized(#A) // io.ktor.client.plugins.websocket/sendSerialized|sendSerialized@io.ktor.client.plugins.websocket.DefaultClientWebSocketSession(0:0){0§<kotlin.Any?>}[0]
final suspend inline fun <#A: reified kotlin/Any?> (io.ktor.client.statement/HttpResponse).io.ktor.client.call/body(): #A // io.ktor.client.call/body|[email protected](){0§<kotlin.Any?>}[0]

// Targets: [native]
final object io.ktor.client.engine/engines : kotlin.collections/Iterable<io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>> { // io.ktor.client.engine/engines|null[0]
final fun append(io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>) // io.ktor.client.engine/engines.append|append(io.ktor.client.engine.HttpClientEngineFactory<io.ktor.client.engine.HttpClientEngineConfig>){}[0]
final fun iterator(): kotlin.collections/Iterator<io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine/HttpClientEngineConfig>> // io.ktor.client.engine/engines.iterator|iterator(){}[0]
}

// Targets: [js, wasmJs]
abstract interface io.ktor.client.fetch/AbortSignal : io.ktor.client.fetch/EventTarget { // io.ktor.client.fetch/AbortSignal|null[0]
abstract var aborted // io.ktor.client.fetch/AbortSignal.aborted|{}aborted[0]
Expand Down Expand Up @@ -1786,6 +1785,9 @@ open class io.ktor.client.engine.js/JsClientEngineConfig : io.ktor.client.engine
// Targets: [js, wasmJs]
final object io.ktor.client.engine.js/Js : io.ktor.client.engine/HttpClientEngineFactory<io.ktor.client.engine.js/JsClientEngineConfig> { // io.ktor.client.engine.js/Js|null[0]
final fun create(kotlin/Function1<io.ktor.client.engine.js/JsClientEngineConfig, kotlin/Unit>): io.ktor.client.engine/HttpClientEngine // io.ktor.client.engine.js/Js.create|create(kotlin.Function1<io.ktor.client.engine.js.JsClientEngineConfig,kotlin.Unit>){}[0]
final fun equals(kotlin/Any?): kotlin/Boolean // io.ktor.client.engine.js/Js.equals|equals(kotlin.Any?){}[0]
final fun hashCode(): kotlin/Int // io.ktor.client.engine.js/Js.hashCode|hashCode(){}[0]
final fun toString(): kotlin/String // io.ktor.client.engine.js/Js.toString|toString(){}[0]
}

// Targets: [js, wasmJs]
Expand Down Expand Up @@ -2179,6 +2181,10 @@ abstract interface io.ktor.client.fetch/Uint8ArrayConstructor { // io.ktor.clien
abstract fun of(kotlin/Array<out kotlin/Number>...): io.ktor.client.fetch/Uint8Array // io.ktor.client.fetch/Uint8ArrayConstructor.of|of(kotlin.Array<out|kotlin.Number>...){}[0]
}

// Targets: [js]
final val io.ktor.client.engine.js/initHook // io.ktor.client.engine.js/initHook|{}initHook[0]
final fun <get-initHook>(): dynamic // io.ktor.client.engine.js/initHook.<get-initHook>|<get-initHook>(){}[0]

// Targets: [js]
final inline fun (io.ktor.client.fetch/Uint8Array).io.ktor.client.fetch/get(kotlin/Number): kotlin/Number? // io.ktor.client.fetch/get|[email protected](kotlin.Number){}[0]

Expand Down
12 changes: 10 additions & 2 deletions ktor-client/ktor-client-core/js/src/io/ktor/client/engine/js/Js.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client.engine.js

import io.ktor.client.engine.*
import io.ktor.utils.io.*

/**
* A JavaScript client engine that uses the fetch API to execute requests.
Expand All @@ -20,7 +21,7 @@ import io.ktor.client.engine.*
*
* You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html).
*/
public actual object Js : HttpClientEngineFactory<JsClientEngineConfig> {
public actual data object Js : HttpClientEngineFactory<JsClientEngineConfig> {
override fun create(block: JsClientEngineConfig.() -> Unit): HttpClientEngine =
JsClientEngine(JsClientEngineConfig().apply(block))
}
Expand All @@ -45,3 +46,10 @@ public actual open class JsClientEngineConfig : HttpClientEngineConfig() {
*/
public var nodeOptions: dynamic = js("Object").create(null)
}

@Suppress("DEPRECATION")
@OptIn(ExperimentalStdlibApi::class, ExperimentalJsExport::class, InternalAPI::class)
@Deprecated("", level = DeprecationLevel.HIDDEN)
@JsExport
@EagerInitialization
public val initHook: dynamic = engines.append(Js)
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
/*
* Copyright 2014-2019 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
* Copyright 2014-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
*/

package io.ktor.client

import io.ktor.client.engine.*
import io.ktor.client.engine.js.*
import io.ktor.utils.io.*

Expand All @@ -16,4 +17,9 @@ import io.ktor.utils.io.*
@KtorDsl
public actual fun HttpClient(
block: HttpClientConfig<*>.() -> Unit
): HttpClient = HttpClient(JsClient(), block)
): HttpClient = HttpClient(FACTORY, block)

// we need to fall back to the default (Js) engine if there are no other engines,
// but in the presence of other engines, they're preferred.
@OptIn(InternalAPI::class)
private val FACTORY = engines.firstOrNull { it != Js } ?: Js
Loading

0 comments on commit 4a8a103

Please sign in to comment.