-
Notifications
You must be signed in to change notification settings - Fork 29
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #181 from icerockdev/develop
Release 0.18.0
- Loading branch information
Showing
30 changed files
with
627 additions
and
38 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
network/src/commonMain/kotlin/dev/icerock/moko/network/features/DynamicUserAgent.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
152
network/src/iosMain/kotlin/dev/icerock/moko/network/IosWebSocket.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
9 changes: 9 additions & 0 deletions
9
network/src/iosMain/kotlin/dev/icerock/moko/network/ReceiveMessageException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
9 changes: 9 additions & 0 deletions
9
network/src/iosMain/kotlin/dev/icerock/moko/network/SendMessageException.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
76 changes: 76 additions & 0 deletions
76
network/src/iosMain/kotlin/dev/icerock/moko/network/WSIosHttpClientEngine.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.