Skip to content

Commit

Permalink
Merge pull request #181 from icerockdev/develop
Browse files Browse the repository at this point in the history
Release 0.18.0
  • Loading branch information
Alex009 authored Jul 16, 2022
2 parents 0f8459f + d6a3e50 commit a05813a
Show file tree
Hide file tree
Showing 30 changed files with 627 additions and 38 deletions.
6 changes: 6 additions & 0 deletions .idea/copyright/IceRock.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions .idea/copyright/profiles_settings.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ root build.gradle
```groovy
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath "dev.icerock.moko:network-generator:0.17.0"
classpath "dev.icerock.moko:network-generator:0.18.0"
}
}
Expand All @@ -53,9 +53,9 @@ project build.gradle
apply plugin: "dev.icerock.mobile.multiplatform-network-generator"
dependencies {
commonMainApi("dev.icerock.moko:network:0.17.0")
commonMainApi("dev.icerock.moko:network-bignum:0.17.0") // kbignum serializer
commonMainApi("dev.icerock.moko:network-errors:0.17.0") // moko-errors integration
commonMainApi("dev.icerock.moko:network:0.18.0")
commonMainApi("dev.icerock.moko:network-bignum:0.18.0") // kbignum serializer
commonMainApi("dev.icerock.moko:network-errors:0.18.0") // moko-errors integration
}
```

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ mokoResourcesVersion = "0.18.0"
mokoMvvmVersion = "0.12.0"
mokoErrorsVersion = "0.6.0"
mokoTestVersion = "0.6.1"
mokoNetworkVersion = "0.17.0"
mokoNetworkVersion = "0.18.0"

# tests
espressoCoreVersion = "3.2.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ import io.ktor.http.content.TextContent
{{/hasFormParams}}
with(builder.headers) {
append("Accept", "application/json")
{{#headerParams}}
if ({{paramName}} != null)
append("{{baseName}}", {{paramName}})
{{/headerParams}}
}

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
}
{{/isMap}}
{{#isArray}}
{{#items}}{{>property_serializer}}{{/items}}.let {
{{#uniqueItems}}SetSerializer(it){{/uniqueItems}}
{{^uniqueItems}}ListSerializer(it){{/uniqueItems}}
}
{{#items}}{{>property_serializer}}{{/items}}.let {
{{#uniqueItems}}SetSerializer(it){{/uniqueItems}}
{{^uniqueItems}}ListSerializer(it){{/uniqueItems}}
}
{{/isArray}}
{{^containerType}}
{{dataType}}.serializer()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network.features

import io.ktor.client.HttpClient
import io.ktor.client.features.HttpClientFeature
import io.ktor.client.request.HttpRequestPipeline
import io.ktor.client.request.header
import io.ktor.http.HttpHeaders
import io.ktor.util.AttributeKey

class DynamicUserAgent(
val agentProvider: () -> String?
) {
class Config(var agentProvider: () -> String? = { null })

companion object Feature : HttpClientFeature<Config, DynamicUserAgent> {
override val key: AttributeKey<DynamicUserAgent> = AttributeKey("DynamicUserAgent")

override fun prepare(block: Config.() -> Unit): DynamicUserAgent =
DynamicUserAgent(Config().apply(block).agentProvider)

override fun install(feature: DynamicUserAgent, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.State) {
feature.agentProvider()?.let { context.header(HttpHeaders.UserAgent, it) }
}
}
}
}
152 changes: 152 additions & 0 deletions network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network

import io.ktor.client.features.websocket.WebSocketException
import io.ktor.http.cio.websocket.CloseReason
import io.ktor.http.cio.websocket.DefaultWebSocketSession
import io.ktor.http.cio.websocket.ExperimentalWebSocketExtensionApi
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.WebSocketExtension
import io.ktor.http.cio.websocket.readText
import io.ktor.util.InternalAPI
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.launch
import platform.Foundation.NSData
import platform.Foundation.NSError
import platform.Foundation.NSOperationQueue
import platform.Foundation.NSPOSIXErrorDomain
import platform.Foundation.NSURL
import platform.Foundation.NSURLSession
import platform.Foundation.NSURLSessionConfiguration
import platform.Foundation.NSURLSessionWebSocketCloseCode
import platform.Foundation.NSURLSessionWebSocketDelegateProtocol
import platform.Foundation.NSURLSessionWebSocketMessage
import platform.Foundation.NSURLSessionWebSocketTask
import platform.darwin.NSObject
import kotlin.coroutines.CoroutineContext

internal class IosWebSocket(
socketEndpoint: NSURL,
override val coroutineContext: CoroutineContext
) : DefaultWebSocketSession {
internal val originResponse: CompletableDeferred<String?> = CompletableDeferred()

private val webSocket: NSURLSessionWebSocketTask

private val _incoming = Channel<Frame>()
private val _outgoing = Channel<Frame>()
private val _closeReason = CompletableDeferred<CloseReason?>()

override val incoming: ReceiveChannel<Frame> = _incoming
override val outgoing: SendChannel<Frame> = _outgoing
override val closeReason: Deferred<CloseReason?> = _closeReason

@ExperimentalWebSocketExtensionApi
override val extensions: List<WebSocketExtension<*>>
get() = emptyList()

override var maxFrameSize: Long
get() = throw WebSocketException("websocket doesn't support max frame size.")
set(_) = throw WebSocketException("websocket doesn't support max frame size.")

override suspend fun flush() = Unit

@OptIn(ExperimentalWebSocketExtensionApi::class)
@InternalAPI
override fun start(negotiatedExtensions: List<WebSocketExtension<*>>) {
require(negotiatedExtensions.isEmpty()) { "Extensions are not supported." }
}

init {
val urlSession = NSURLSession.sessionWithConfiguration(
configuration = NSURLSessionConfiguration.defaultSessionConfiguration(),
delegate = object : NSObject(), NSURLSessionWebSocketDelegateProtocol {
override fun URLSession(
session: NSURLSession,
webSocketTask: NSURLSessionWebSocketTask,
didOpenWithProtocol: String?
) {
originResponse.complete(didOpenWithProtocol)
}

override fun URLSession(
session: NSURLSession,
webSocketTask: NSURLSessionWebSocketTask,
didCloseWithCode: NSURLSessionWebSocketCloseCode,
reason: NSData?
) {
val closeReason = CloseReason(
code = CloseReason.Codes.PROTOCOL_ERROR,
message = "$didCloseWithCode : ${reason.toString()}"
)
_closeReason.complete(closeReason)
}
},
delegateQueue = NSOperationQueue.currentQueue()
)
webSocket = urlSession.webSocketTaskWithURL(socketEndpoint)

CoroutineScope(coroutineContext).launch {
_outgoing.consumeEach { frame ->
if (frame is Frame.Text) {
val message = NSURLSessionWebSocketMessage(frame.readText())
webSocket.sendMessage(message) { nsError ->
if (nsError == null) return@sendMessage

nsError.closeSocketOrThrow {
throw SendMessageException(nsError.description ?: nsError.toString())
}
}
}
}
}

listenMessages()
}

fun start() {
webSocket.resume()
}

private fun listenMessages() {
webSocket.receiveMessageWithCompletionHandler { message, nsError ->
when {
nsError != null -> {
nsError.closeSocketOrThrow {
throw ReceiveMessageException(nsError.description ?: nsError.toString())
}
}
message != null -> {
message.string?.let { _incoming.trySend(Frame.Text(it)) }
}
}
listenMessages()
}
}

private fun NSError.closeSocketOrThrow(throwBlock: () -> Unit) {
if (domain !in listOf("kNWErrorDomainPOSIX", NSPOSIXErrorDomain)) return throwBlock()
if (code != 57L) return throwBlock()

val closeReason = CloseReason(
code = CloseReason.Codes.NORMAL,
message = description ?: toString()
)
_closeReason.complete(closeReason)
webSocket.cancel()
}

override fun terminate() {
coroutineContext.cancel()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network

import io.ktor.utils.io.errors.IOException

class ReceiveMessageException(message: String) : IOException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network

import io.ktor.utils.io.errors.IOException

class SendMessageException(message: String) : IOException(message)
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license.
*/

package dev.icerock.moko.network

import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.HttpClientEngineCapability
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.engine.callContext
import io.ktor.client.features.websocket.WebSocketCapability
import io.ktor.client.request.HttpRequestData
import io.ktor.client.request.HttpResponseData
import io.ktor.client.request.isUpgradeRequest
import io.ktor.http.Headers
import io.ktor.http.HttpProtocolVersion
import io.ktor.http.HttpStatusCode
import io.ktor.util.InternalAPI
import io.ktor.util.date.GMTDate
import kotlinx.coroutines.CoroutineDispatcher
import platform.Foundation.NSURL
import kotlin.coroutines.CoroutineContext

class WSIosHttpClientEngine(
private val wrappedEngine: HttpClientEngine
) : HttpClientEngine {

override val supportedCapabilities: Set<HttpClientEngineCapability<*>>
get() = wrappedEngine.supportedCapabilities + setOf(WebSocketCapability)

override val config: HttpClientEngineConfig
get() = wrappedEngine.config

override val dispatcher: CoroutineDispatcher
get() = wrappedEngine.dispatcher

override val coroutineContext: CoroutineContext
get() = wrappedEngine.coroutineContext

@InternalAPI
override suspend fun execute(data: HttpRequestData): HttpResponseData {

val callContext = callContext()
return if (data.isUpgradeRequest()) {
executeWebSocketRequest(data, callContext)
} else {
wrappedEngine.execute(data)
}
}

private suspend fun executeWebSocketRequest(
data: HttpRequestData,
callContext: CoroutineContext
): HttpResponseData {
val requestTime = GMTDate()
val url: String = data.url.toString()
val socketEndpoint = NSURL.URLWithString(url)!!

val session = IosWebSocket(socketEndpoint, callContext).apply { start() }

val originResponse = session.originResponse.await()

return HttpResponseData(
statusCode = HttpStatusCode.OK,
requestTime = requestTime,
headers = Headers.Empty,
version = HttpProtocolVersion.HTTP_1_0, // read from originResponse
body = session,
callContext = callContext
)
}

override fun close() {
wrappedEngine.close()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ actual fun createHttpClientEngine(block: HttpClientEngineConfig.() -> Unit): Htt
config.iosTimeoutIntervalForRequest?.let { setTimeoutIntervalForRequest(it) }
config.iosTimeoutIntervalForResource?.let { setTimeoutIntervalForResource(it) }
}
}
}.let { WSIosHttpClientEngine(it) }
}
3 changes: 2 additions & 1 deletion sample/android-app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
android:name=".App"
android:label="moko-network test app"
android:theme="@style/Theme.AppCompat.DayNight"
tools:ignore="GoogleAppIndexingWarning">
tools:ignore="GoogleAppIndexingWarning"
android:usesCleartextTraffic="true">

<activity android:name=".MainActivity">
<intent-filter>
Expand Down
Loading

0 comments on commit a05813a

Please sign in to comment.