diff --git a/README.md b/README.md index 2c1fce9ff..4b1ef178f 100644 --- a/README.md +++ b/README.md @@ -174,11 +174,10 @@ embeddedServer(CIO) { }.start(true) ``` -### More examples: +### More samples: -- [multiplatform-chat](examples/multiplatform-chat) - chat implementation with JVM server and JS/JVM client with shared classes and +- [multiplatform-chat](samples/chat) - chat implementation with JVM server and JS/JVM client with shared classes and serializing data using [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) -- [nodejs-tcp-transport](examples/nodejs-tcp-transport) - implementation of TCP transport for nodejs ## Reactive Streams Semantics diff --git a/examples/multiplatform-chat/build.gradle.kts b/examples/multiplatform-chat/build.gradle.kts deleted file mode 100644 index e229c3c1e..000000000 --- a/examples/multiplatform-chat/build.gradle.kts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import org.jetbrains.kotlin.konan.target.* - -plugins { - kotlin("multiplatform") - kotlin("plugin.serialization") - id("kotlinx-atomicfu") -} - -val ktorVersion: String by rootProject -val kotlinxSerializationVersion: String by rootProject - -kotlin { - jvm("serverJvm") - jvm("clientJvm") - js("clientJs", IR) { - browser { - binaries.executable() - } - nodejs { - binaries.executable() - } - } - when { - HostManager.hostIsLinux -> linuxX64("clientNative") - HostManager.hostIsMingw -> null //no native support for TCP mingwX64("clientNative") - HostManager.hostIsMac -> macosX64("clientNative") - else -> null - }?.binaries { - executable { - entryPoint = "main" - } - } - - sourceSets { - val commonMain by getting { - dependencies { - implementation(project(":rsocket-core")) - - implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinxSerializationVersion") - } - } - - val clientMain by creating { - dependsOn(commonMain) - dependencies { - implementation(project(":rsocket-transport-ktor-client")) - } - } - - val serverJvmMain by getting { - dependencies { - implementation(project(":rsocket-transport-ktor-server")) - implementation("io.ktor:ktor-server-cio:$ktorVersion") - } - } - - val clientJvmMain by getting { - dependsOn(clientMain) - dependencies { - implementation("io.ktor:ktor-client-cio:$ktorVersion") - } - } - - val clientJsMain by getting { - dependsOn(clientMain) - dependencies { - implementation("io.ktor:ktor-client-core:$ktorVersion") - } - } - - if (!HostManager.hostIsMingw) { - val clientNativeMain by getting { - dependsOn(clientMain) - } - } - } -} diff --git a/examples/multiplatform-chat/src/clientJsMain/kotlin/App.kt b/examples/multiplatform-chat/src/clientJsMain/kotlin/App.kt deleted file mode 100644 index 9cf47fe4a..000000000 --- a/examples/multiplatform-chat/src/clientJsMain/kotlin/App.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.flow.* - -suspend fun main() { - val api = connectToApiUsingWS("Yuri") - - api.users.all().forEach { - println(it) - } - - val chat = api.chats.all().firstOrNull() ?: api.chats.new("rsocket-kotlin chat") - - val sentMessage = api.messages.send(chat.id, "RSocket is awesome! (from JS)") - println(sentMessage) - - api.messages.messages(chat.id, -1).collect { - println("Received: $it") - } -} diff --git a/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppTCP.kt b/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppTCP.kt deleted file mode 100644 index f781cea39..000000000 --- a/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppTCP.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.flow.* - -suspend fun main() { - val api = connectToApiUsingTCP("Hanna") - - api.users.all().forEach { - println(it) - } - - val chat = api.chats.all().firstOrNull() ?: api.chats.new("rsocket-kotlin chat") - - val sentMessage = api.messages.send(chat.id, "RSocket is awesome! (from JVM TCP)") - println(sentMessage) - - api.messages.messages(chat.id, -1).collect { - println("Received: $it") - } -} diff --git a/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppWS.kt b/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppWS.kt deleted file mode 100644 index 7ecfd1ce5..000000000 --- a/examples/multiplatform-chat/src/clientJvmMain/kotlin/AppWS.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.flow.* - -suspend fun main() { - val api = connectToApiUsingWS("Oleg") - - api.users.all().forEach { - println(it) - } - - val chat = api.chats.all().firstOrNull() ?: api.chats.new("rsocket-kotlin chat") - - val sentMessage = api.messages.send(chat.id, "RSocket is awesome! (from JVM WS)") - println(sentMessage) - - api.messages.messages(chat.id, -1).collect { - println("Received: $it") - } -} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt b/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt deleted file mode 100644 index b8a440e02..000000000 --- a/examples/multiplatform-chat/src/clientMain/kotlin/Api.kt +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.ktor.client.* -import io.ktor.client.features.websocket.* -import io.rsocket.kotlin.* -import io.rsocket.kotlin.core.* -import io.rsocket.kotlin.payload.* -import io.rsocket.kotlin.transport.ktor.* -import io.rsocket.kotlin.transport.ktor.client.* -import kotlinx.coroutines.* - -class Api(rSocket: RSocket) { - private val proto = ConfiguredProtoBuf - val users = UserApi(rSocket, proto) - val chats = ChatApi(rSocket, proto) - val messages = MessageApi(rSocket, proto) -} - -suspend fun connectToApiUsingWS(name: String): Api { - val client = HttpClient { - install(WebSockets) - install(RSocketSupport) { - connector = connector(name) - } - } - - return Api(client.rSocket(port = 9000)) -} - -suspend fun connectToApiUsingTCP(name: String): Api { - val transport = TcpClientTransport("0.0.0.0", 8000, CoroutineExceptionHandler { coroutineContext, throwable -> - println("FAIL: $coroutineContext, $throwable") - }) - return Api(connector(name).connect(transport)) -} - -private fun connector(name: String): RSocketConnector = RSocketConnector { - connectionConfig { - setupPayload { buildPayload { data(name) } } - } -} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/ChatApi.kt b/examples/multiplatform-chat/src/clientMain/kotlin/ChatApi.kt deleted file mode 100644 index f2492f758..000000000 --- a/examples/multiplatform-chat/src/clientMain/kotlin/ChatApi.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.ktor.utils.io.core.* -import io.rsocket.kotlin.* -import kotlinx.serialization.* -import kotlinx.serialization.protobuf.* - -@OptIn(ExperimentalSerializationApi::class) -actual class ChatApi(private val rSocket: RSocket, private val proto: ProtoBuf) { - actual suspend fun all(): List = proto.decodeFromPayload( - rSocket.requestResponse(Payload(route = "chats.all", ByteReadPacket.Empty)) - ) - - actual suspend fun new(name: String): Chat = proto.decodeFromPayload( - rSocket.requestResponse( - proto.encodeToPayload(route = "chats.new", NewChat(name)) - ) - ) - - actual suspend fun delete(id: Int) { - rSocket.requestResponse( - proto.encodeToPayload(route = "chats.delete", DeleteChat(id)) - ).close() - } -} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/MessageApi.kt b/examples/multiplatform-chat/src/clientMain/kotlin/MessageApi.kt deleted file mode 100644 index 3983604e4..000000000 --- a/examples/multiplatform-chat/src/clientMain/kotlin/MessageApi.kt +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.rsocket.kotlin.* -import kotlinx.coroutines.flow.* -import kotlinx.serialization.* -import kotlinx.serialization.protobuf.* - -@OptIn(ExperimentalSerializationApi::class) -actual class MessageApi(private val rSocket: RSocket, private val proto: ProtoBuf) { - actual suspend fun send(chatId: Int, content: String): Message = proto.decodeFromPayload( - rSocket.requestResponse( - proto.encodeToPayload(route = "messages.send", SendMessage(chatId, content)) - ) - ) - - actual suspend fun history(chatId: Int, limit: Int): List = proto.decodeFromPayload( - rSocket.requestResponse( - proto.encodeToPayload(route = "messages.history", HistoryMessages(chatId, limit)) - ) - ) - - actual fun messages(chatId: Int, fromMessageId: Int): Flow = rSocket.requestStream( - proto.encodeToPayload(route = "messages.stream", StreamMessages(chatId, fromMessageId)) - ).map { - proto.decodeFromPayload(it) - } -} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt b/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt deleted file mode 100644 index 75597e68a..000000000 --- a/examples/multiplatform-chat/src/clientMain/kotlin/PayloadWithRoute.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.ktor.utils.io.core.* -import io.rsocket.kotlin.metadata.* -import io.rsocket.kotlin.payload.* - -fun Payload(route: String, packet: ByteReadPacket): Payload = buildPayload { - data(packet) - metadata(RoutingMetadata(route)) -} diff --git a/examples/multiplatform-chat/src/clientMain/kotlin/UserApi.kt b/examples/multiplatform-chat/src/clientMain/kotlin/UserApi.kt deleted file mode 100644 index 80d491ec2..000000000 --- a/examples/multiplatform-chat/src/clientMain/kotlin/UserApi.kt +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import io.ktor.utils.io.core.* -import io.rsocket.kotlin.* -import kotlinx.serialization.* -import kotlinx.serialization.protobuf.* - -@OptIn(ExperimentalSerializationApi::class) -actual class UserApi(private val rSocket: RSocket, private val proto: ProtoBuf) { - - actual suspend fun getMe(): User = proto.decodeFromPayload( - rSocket.requestResponse(Payload(route = "users.getMe", ByteReadPacket.Empty)) - ) - - actual suspend fun deleteMe() { - rSocket.fireAndForget(Payload(route = "users.deleteMe", ByteReadPacket.Empty)) - } - - actual suspend fun all(): List = proto.decodeFromPayload( - rSocket.requestResponse(Payload(route = "users.all", ByteReadPacket.Empty)) - ) -} diff --git a/examples/multiplatform-chat/src/clientNativeMain/kotlin/AppTCP.kt b/examples/multiplatform-chat/src/clientNativeMain/kotlin/AppTCP.kt deleted file mode 100644 index 77b342847..000000000 --- a/examples/multiplatform-chat/src/clientNativeMain/kotlin/AppTCP.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* - -fun main(): Unit = runBlocking { - val api = connectToApiUsingTCP("Gloria") - - api.users.all().forEach { - println(it) - } - - val chat = api.chats.all().firstOrNull() ?: api.chats.new("rsocket-kotlin chat") - - val sentMessage = api.messages.send(chat.id, "RSocket is awesome! (from Native)") - println(sentMessage) - - api.messages.messages(chat.id, -1).collect { - println("Received: $it") - } -} diff --git a/examples/multiplatform-chat/src/commonMain/kotlin/ChatApi.kt b/examples/multiplatform-chat/src/commonMain/kotlin/ChatApi.kt deleted file mode 100644 index 2c7458650..000000000 --- a/examples/multiplatform-chat/src/commonMain/kotlin/ChatApi.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.serialization.* - -@Serializable -data class Chat( - val id: Int, - val name: String, -) - -expect class ChatApi { - suspend fun all(): List - suspend fun new(name: String): Chat - suspend fun delete(id: Int) -} - -@Serializable -data class NewChat(val name: String) - -@Serializable -data class DeleteChat(val id: Int) diff --git a/examples/multiplatform-chat/src/commonMain/kotlin/MessageApi.kt b/examples/multiplatform-chat/src/commonMain/kotlin/MessageApi.kt deleted file mode 100644 index 1dd52fda9..000000000 --- a/examples/multiplatform-chat/src/commonMain/kotlin/MessageApi.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.flow.* -import kotlinx.serialization.* - -@Serializable -data class Message( - val id: Int, - val chatId: Int, - val senderId: Int, - val timestamp: Long, - val content: String, -) - -expect class MessageApi { - suspend fun send(chatId: Int, content: String): Message - suspend fun history(chatId: Int, limit: Int = 10): List - fun messages(chatId: Int, fromMessageId: Int): Flow -} - -@Serializable -data class SendMessage(val chatId: Int, val content: String) - -@Serializable -data class HistoryMessages(val chatId: Int, val limit: Int) - -@Serializable -data class StreamMessages(val chatId: Int, val fromMessageId: Int) diff --git a/examples/multiplatform-chat/src/commonMain/kotlin/UserApi.kt b/examples/multiplatform-chat/src/commonMain/kotlin/UserApi.kt deleted file mode 100644 index 31aa0e6c9..000000000 --- a/examples/multiplatform-chat/src/commonMain/kotlin/UserApi.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.serialization.* - -@Serializable -data class User( - val id: Int, - val name: String, -) - -expect class UserApi { - suspend fun getMe(): User - suspend fun deleteMe() - suspend fun all(): List -} diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/ChatApi.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/ChatApi.kt deleted file mode 100644 index 3233148b2..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/ChatApi.kt +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -actual class ChatApi( - private val chats: Chats, - private val messages: Messages, -) { - - actual suspend fun all(): List = chats.values.toList() - - actual suspend fun new(name: String): Chat = chats.create(name) - - actual suspend fun delete(id: Int) { - messages.deleteForChat(id) - chats -= id - } -} - diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Chats.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/Chats.kt deleted file mode 100644 index dce10f5a7..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Chats.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.atomicfu.* -import java.util.concurrent.* - -class Chats { - private val chats: MutableMap = ConcurrentHashMap() - private val chatsId = atomic(0) - - val values: List get() = chats.values.toList() - - fun getOrNull(id: Int): Chat? = chats[id] - - fun delete(id: Int) { - chats -= id - } - - fun create(name: String): Chat { - if (chats.values.any { it.name == name }) error("Chat with such name already exists") - val chatId = chatsId.incrementAndGet() - val chat = Chat(chatId, name) - chats[chatId] = chat - return chat - } - - fun exists(id: Int): Boolean = id in chats -} - -operator fun Chats.get(id: Int): Chat = getOrNull(id) ?: error("No user with id '$id' exists") -operator fun Chats.minusAssign(id: Int): Unit = delete(id) -operator fun Chats.contains(id: Int): Boolean = exists(id) diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/MessageApi.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/MessageApi.kt deleted file mode 100644 index bdde7f513..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/MessageApi.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.* -import kotlinx.coroutines.flow.* - -@OptIn(ExperimentalCoroutinesApi::class) -actual class MessageApi( - private val messages: Messages, - private val chats: Chats, -) { - - private val listeners = mutableListOf>() - - actual suspend fun send(chatId: Int, content: String): Message { - if (chatId !in chats) error("No chat with id '$chatId'") - val userId = currentSession().userId - val message = messages.create(userId, chatId, content) - listeners.forEach { it.send(message) } - return message - } - - actual suspend fun history(chatId: Int, limit: Int): List { - if (chatId !in chats) error("No chat with id '$chatId'") - return messages.takeLast(chatId, limit) - } - - actual fun messages(chatId: Int, fromMessageId: Int): Flow = flow { - messages.takeAfter(chatId, fromMessageId).forEach { emit(it) } - emitAll(channelFlow { - listeners += channel - awaitClose { - listeners -= channel - } - }.buffer()) - } -} - diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Messages.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/Messages.kt deleted file mode 100644 index 0bddd5ff3..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Messages.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.atomicfu.* -import java.time.* -import java.util.concurrent.* - -class Messages { - private val messages: MutableMap = ConcurrentHashMap() - private val messagesId = atomic(0) - - val values: List get() = messages.values.toList() - - fun getOrNull(id: Int): Message? = messages[id] - - fun delete(id: Int) { - messages -= id - } - - fun deleteForChat(chatId: Int) { - messages -= messages.filterValues { it.chatId == chatId }.keys - } - - fun create(userId: Int, chatId: Int, content: String): Message { - val messageId = messagesId.incrementAndGet() - val message = Message(messageId, chatId, userId, Instant.now().epochSecond, content) - messages[messageId] = message - return message - } - - private fun byChatSorted(chatId: Int): List = values.filter { it.chatId == chatId }.sortedByDescending { it.timestamp } - - fun takeLast(chatId: Int, limit: Int): List = byChatSorted(chatId).take(limit) - - fun takeAfter(chatId: Int, messageId: Int): List { - val messages = byChatSorted(chatId) - if (messageId == -1) return messages.take(1) - - val index = messages.indexOfFirst { it.id == messageId } - if (index == -1) error("No message with id '$messageId'") - return messages.drop(index) - } -} - -operator fun Messages.get(id: Int): Message = getOrNull(id) ?: error("No user with id '$id' exists") -operator fun Messages.minusAssign(id: Int): Unit = delete(id) diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Session.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/Session.kt deleted file mode 100644 index 70947ff13..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Session.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlin.coroutines.* - -data class Session(val userId: Int) : CoroutineContext.Element { - override val key: CoroutineContext.Key<*> get() = Session - - companion object : CoroutineContext.Key -} - -suspend fun currentSession(): Session = coroutineContext[Session]!! diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/UserApi.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/UserApi.kt deleted file mode 100644 index 7020e8ec0..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/UserApi.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -actual class UserApi( - private val users: Users, -) { - - actual suspend fun getMe(): User { - val userId = currentSession().userId - return users[userId] - } - - actual suspend fun deleteMe() { - val userId = currentSession().userId - users -= userId - } - - actual suspend fun all(): List = users.values -} - diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Users.kt b/examples/multiplatform-chat/src/serverJvmMain/kotlin/Users.kt deleted file mode 100644 index 542d9773e..000000000 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/Users.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import kotlinx.atomicfu.* -import java.util.concurrent.* - -class Users { - private val users: MutableMap = ConcurrentHashMap() - private val usersId = atomic(0) - - val values: List get() = users.values.toList() - - fun getOrCreate(name: String): User = - users.values.find { it.name == name } ?: run { - val userId = usersId.incrementAndGet() - User(userId, name).also { users[userId] = it } - } - - fun getOrNull(id: Int): User? = users[id] - - fun delete(id: Int) { - users -= id - } -} - -operator fun Users.get(id: Int): User = getOrNull(id) ?: error("No user with id '$id' exists") -operator fun Users.minusAssign(id: Int): Unit = delete(id) diff --git a/samples/chat/README.md b/samples/chat/README.md new file mode 100644 index 000000000..f83991ef5 --- /dev/null +++ b/samples/chat/README.md @@ -0,0 +1,13 @@ +# chat + +* api - shared chat API for both client and server +* client - client API implementation as requests to RSocket with Protobuf serialization. Works on JVM(TCP/WS), JS(WS), + Native(TCP). Tasks for running sample clients: + * JVM: `run` + * Native: `runDebugExecutableNative` / `runReleaseExecutableNative` + * NodeJs: `jsNodeRun` + * Browser: `jsBrowserRun` +* server - server API implementation with storage in ordinary concurrent map and exposing it through RSocket with + Protobuf serialization. Can be started on JVM(TCP+WS) and Native(TCP). Tasks for running sample servers: + * JVM: `run` + * Native: `runDebugExecutableNative` / `runReleaseExecutableNative` diff --git a/samples/chat/api/build.gradle.kts b/samples/chat/api/build.gradle.kts new file mode 100644 index 000000000..22eb57613 --- /dev/null +++ b/samples/chat/api/build.gradle.kts @@ -0,0 +1,32 @@ +import org.jetbrains.kotlin.konan.target.* + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") +} + +val rsocketVersion: String by rootProject +val kotlinxSerializationVersion: String by rootProject + +kotlin { + jvm() + js(IR) { + browser() + nodejs() + } + when { + HostManager.hostIsLinux -> linuxX64("native") + HostManager.hostIsMingw -> null //no native support for TCP in ktor mingwX64("native") + HostManager.hostIsMac -> macosX64("native") + else -> null + } + + sourceSets { + commonMain { + dependencies { + api("io.rsocket.kotlin:rsocket-core:$rsocketVersion") + api("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinxSerializationVersion") + } + } + } +} diff --git a/samples/chat/api/src/commonMain/kotlin/ChatApi.kt b/samples/chat/api/src/commonMain/kotlin/ChatApi.kt new file mode 100644 index 000000000..2ed70347a --- /dev/null +++ b/samples/chat/api/src/commonMain/kotlin/ChatApi.kt @@ -0,0 +1,21 @@ +package io.rsocket.kotlin.samples.chat.api + +import kotlinx.serialization.* + +interface ChatApi { + suspend fun all(): List + suspend fun new(name: String): Chat + suspend fun delete(id: Int) +} + +@Serializable +data class Chat( + val id: Int, + val name: String, +) + +@Serializable +data class NewChat(val name: String) + +@Serializable +data class DeleteChat(val id: Int) diff --git a/samples/chat/api/src/commonMain/kotlin/MessageApi.kt b/samples/chat/api/src/commonMain/kotlin/MessageApi.kt new file mode 100644 index 000000000..c69709709 --- /dev/null +++ b/samples/chat/api/src/commonMain/kotlin/MessageApi.kt @@ -0,0 +1,28 @@ +package io.rsocket.kotlin.samples.chat.api + +import kotlinx.coroutines.flow.* +import kotlinx.serialization.* + +interface MessageApi { + suspend fun send(chatId: Int, content: String): Message + suspend fun history(chatId: Int, limit: Int = 10): List + fun messages(chatId: Int, fromMessageId: Int): Flow +} + +@Serializable +data class Message( + val id: Int, + val chatId: Int, + val senderId: Int, + val timestamp: Long, + val content: String, +) + +@Serializable +data class SendMessage(val chatId: Int, val content: String) + +@Serializable +data class HistoryMessages(val chatId: Int, val limit: Int) + +@Serializable +data class StreamMessages(val chatId: Int, val fromMessageId: Int) diff --git a/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt b/samples/chat/api/src/commonMain/kotlin/Serialization.kt similarity index 65% rename from examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt rename to samples/chat/api/src/commonMain/kotlin/Serialization.kt index 88aae2f1f..8515da20d 100644 --- a/examples/multiplatform-chat/src/commonMain/kotlin/Serialization.kt +++ b/samples/chat/api/src/commonMain/kotlin/Serialization.kt @@ -1,20 +1,7 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +package io.rsocket.kotlin.samples.chat.api import io.ktor.utils.io.core.* +import io.rsocket.kotlin.* import io.rsocket.kotlin.metadata.* import io.rsocket.kotlin.payload.* import kotlinx.serialization.* @@ -22,12 +9,14 @@ import kotlinx.serialization.protobuf.* import kotlin.jvm.* //just stub +@ExperimentalSerializationApi val ConfiguredProtoBuf = ProtoBuf @ExperimentalSerializationApi inline fun ProtoBuf.decodeFromPayload(payload: Payload): T = decodeFromByteArray(payload.data.readBytes()) @ExperimentalSerializationApi +@OptIn(ExperimentalMetadataApi::class) inline fun ProtoBuf.encodeToPayload(route: String, value: T): Payload = buildPayload { data(encodeToByteArray(value)) metadata(RoutingMetadata(route)) @@ -48,3 +37,12 @@ inline fun ProtoBuf.decoding(payload: Payload, block: (I) -> Unit): decodeFromPayload(payload).let(block) return Payload.Empty } + +@OptIn(ExperimentalMetadataApi::class) +fun Payload(route: String, packet: ByteReadPacket = ByteReadPacket.Empty): Payload = buildPayload { + data(packet) + metadata(RoutingMetadata(route)) +} + +@OptIn(ExperimentalMetadataApi::class) +fun Payload.route(): String = metadata?.read(RoutingMetadata)?.tags?.first() ?: error("No route provided") diff --git a/samples/chat/api/src/commonMain/kotlin/UserApi.kt b/samples/chat/api/src/commonMain/kotlin/UserApi.kt new file mode 100644 index 000000000..05ba6c64f --- /dev/null +++ b/samples/chat/api/src/commonMain/kotlin/UserApi.kt @@ -0,0 +1,15 @@ +package io.rsocket.kotlin.samples.chat.api + +import kotlinx.serialization.* + +interface UserApi { + suspend fun getMe(): User + suspend fun deleteMe() + suspend fun all(): List +} + +@Serializable +data class User( + val id: Int, + val name: String, +) diff --git a/samples/chat/client/build.gradle.kts b/samples/chat/client/build.gradle.kts new file mode 100644 index 000000000..d96f28e3d --- /dev/null +++ b/samples/chat/client/build.gradle.kts @@ -0,0 +1,57 @@ +import org.jetbrains.kotlin.konan.target.* + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + application +} + +val rsocketVersion: String by rootProject +val ktorVersion: String by rootProject + +application { + mainClass.set("io.rsocket.kotlin.samples.chat.client.AppKt") +} + +kotlin { + jvm { + withJava() + } + js(IR) { + browser { + binaries.executable() + } + nodejs { + binaries.executable() + } + } + when { + HostManager.hostIsLinux -> linuxX64("native") + HostManager.hostIsMingw -> null //no native support for TCP in ktor mingwX64("clientNative") + HostManager.hostIsMac -> macosX64("native") + else -> null + }?.binaries { + executable { + entryPoint = "io.rsocket.kotlin.samples.chat.client.main" + } + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":api")) + implementation("io.rsocket.kotlin:rsocket-transport-ktor-client:$rsocketVersion") + } + } + val jvmMain by getting { + dependencies { + implementation("io.ktor:ktor-client-cio:$ktorVersion") + } + } + val jsMain by getting { + dependencies { + implementation("io.ktor:ktor-client-core:$ktorVersion") + } + } + } +} diff --git a/samples/chat/client/src/commonMain/kotlin/ApiClient.kt b/samples/chat/client/src/commonMain/kotlin/ApiClient.kt new file mode 100644 index 000000000..52a2b5793 --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/ApiClient.kt @@ -0,0 +1,13 @@ +package io.rsocket.kotlin.samples.chat.client + +import io.rsocket.kotlin.* +import io.rsocket.kotlin.samples.chat.api.* +import kotlinx.serialization.* + +@OptIn(ExperimentalSerializationApi::class) +class ApiClient(rSocket: RSocket) { + private val proto = ConfiguredProtoBuf + val users = UserApiClient(rSocket, proto) + val chats = ChatApiClient(rSocket, proto) + val messages = MessageApiClient(rSocket, proto) +} diff --git a/samples/chat/client/src/commonMain/kotlin/ChatApiClient.kt b/samples/chat/client/src/commonMain/kotlin/ChatApiClient.kt new file mode 100644 index 000000000..b815fba44 --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/ChatApiClient.kt @@ -0,0 +1,25 @@ +package io.rsocket.kotlin.samples.chat.client + +import io.rsocket.kotlin.* +import io.rsocket.kotlin.samples.chat.api.* +import kotlinx.serialization.* +import kotlinx.serialization.protobuf.* + +@OptIn(ExperimentalSerializationApi::class) +class ChatApiClient(private val rSocket: RSocket, private val proto: ProtoBuf) : ChatApi { + override suspend fun all(): List = proto.decodeFromPayload( + rSocket.requestResponse(Payload(route = "chats.all")) + ) + + override suspend fun new(name: String): Chat = proto.decodeFromPayload( + rSocket.requestResponse( + proto.encodeToPayload(route = "chats.new", NewChat(name)) + ) + ) + + override suspend fun delete(id: Int) { + rSocket.requestResponse( + proto.encodeToPayload(route = "chats.delete", DeleteChat(id)) + ).close() + } +} diff --git a/samples/chat/client/src/commonMain/kotlin/MessageApiClient.kt b/samples/chat/client/src/commonMain/kotlin/MessageApiClient.kt new file mode 100644 index 000000000..832685601 --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/MessageApiClient.kt @@ -0,0 +1,28 @@ +package io.rsocket.kotlin.samples.chat.client + +import io.rsocket.kotlin.* +import io.rsocket.kotlin.samples.chat.api.* +import kotlinx.coroutines.flow.* +import kotlinx.serialization.* +import kotlinx.serialization.protobuf.* + +@OptIn(ExperimentalSerializationApi::class) +class MessageApiClient(private val rSocket: RSocket, private val proto: ProtoBuf) : MessageApi { + override suspend fun send(chatId: Int, content: String): Message = proto.decodeFromPayload( + rSocket.requestResponse( + proto.encodeToPayload(route = "messages.send", SendMessage(chatId, content)) + ) + ) + + override suspend fun history(chatId: Int, limit: Int): List = proto.decodeFromPayload( + rSocket.requestResponse( + proto.encodeToPayload(route = "messages.history", HistoryMessages(chatId, limit)) + ) + ) + + override fun messages(chatId: Int, fromMessageId: Int): Flow = rSocket.requestStream( + proto.encodeToPayload(route = "messages.stream", StreamMessages(chatId, fromMessageId)) + ).map { + proto.decodeFromPayload(it) + } +} diff --git a/samples/chat/client/src/commonMain/kotlin/UserApiClient.kt b/samples/chat/client/src/commonMain/kotlin/UserApiClient.kt new file mode 100644 index 000000000..cf9b74b7f --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/UserApiClient.kt @@ -0,0 +1,22 @@ +package io.rsocket.kotlin.samples.chat.client + +import io.rsocket.kotlin.* +import io.rsocket.kotlin.samples.chat.api.* +import kotlinx.serialization.* +import kotlinx.serialization.protobuf.* + +@OptIn(ExperimentalSerializationApi::class) +class UserApiClient(private val rSocket: RSocket, private val proto: ProtoBuf) : UserApi { + + override suspend fun getMe(): User = proto.decodeFromPayload( + rSocket.requestResponse(Payload(route = "users.getMe")) + ) + + override suspend fun deleteMe() { + rSocket.fireAndForget(Payload(route = "users.deleteMe")) + } + + override suspend fun all(): List = proto.decodeFromPayload( + rSocket.requestResponse(Payload(route = "users.all")) + ) +} diff --git a/samples/chat/client/src/commonMain/kotlin/connect.kt b/samples/chat/client/src/commonMain/kotlin/connect.kt new file mode 100644 index 000000000..488e7685c --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/connect.kt @@ -0,0 +1,32 @@ +package io.rsocket.kotlin.samples.chat.client + +import io.ktor.client.* +import io.ktor.client.features.websocket.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.payload.* +import io.rsocket.kotlin.transport.ktor.* +import io.rsocket.kotlin.transport.ktor.client.* + +private fun connector(name: String): RSocketConnector = RSocketConnector { + connectionConfig { + setupPayload { buildPayload { data(name) } } + } +} + +//not supported on native +suspend fun connectToApiUsingWS(name: String, port: Int = 9000): ApiClient { + val client = HttpClient { + install(WebSockets) + install(RSocketSupport) { + connector = connector(name) + } + } + + return ApiClient(client.rSocket(port = port)) +} + +//not supported on JS +suspend fun connectToApiUsingTCP(name: String, port: Int = 8000): ApiClient { + val transport = TcpClientTransport("0.0.0.0", port) + return ApiClient(connector(name).connect(transport)) +} diff --git a/samples/chat/client/src/commonMain/kotlin/usage.kt b/samples/chat/client/src/commonMain/kotlin/usage.kt new file mode 100644 index 000000000..9fbc72a1d --- /dev/null +++ b/samples/chat/client/src/commonMain/kotlin/usage.kt @@ -0,0 +1,19 @@ +package io.rsocket.kotlin.samples.chat.client + +import kotlinx.coroutines.flow.* + +suspend fun ApiClient.use(message: String) { + + users.all().forEach { + println(it) + } + + val chat = chats.all().firstOrNull() ?: chats.new("rsocket-kotlin chat") + + val sentMessage = messages.send(chat.id, message) + println(sentMessage) + + messages.messages(chat.id, -1).collect { + println("Received: $it") + } +} diff --git a/samples/chat/client/src/jsMain/kotlin/App.kt b/samples/chat/client/src/jsMain/kotlin/App.kt new file mode 100644 index 000000000..0fb473e7b --- /dev/null +++ b/samples/chat/client/src/jsMain/kotlin/App.kt @@ -0,0 +1,5 @@ +package io.rsocket.kotlin.samples.chat.client + +suspend fun main() { + connectToApiUsingWS("Yuri").use("RSocket is awesome! (from JS WS)") +} diff --git a/examples/multiplatform-chat/src/clientJsMain/resources/index.html b/samples/chat/client/src/jsMain/resources/index.html similarity index 91% rename from examples/multiplatform-chat/src/clientJsMain/resources/index.html rename to samples/chat/client/src/jsMain/resources/index.html index 3c89e463b..6b9fe7dff 100644 --- a/examples/multiplatform-chat/src/clientJsMain/resources/index.html +++ b/samples/chat/client/src/jsMain/resources/index.html @@ -18,10 +18,10 @@ - Title + Chat - + diff --git a/samples/chat/client/src/jvmMain/kotlin/App.kt b/samples/chat/client/src/jvmMain/kotlin/App.kt new file mode 100644 index 000000000..485e66e8f --- /dev/null +++ b/samples/chat/client/src/jvmMain/kotlin/App.kt @@ -0,0 +1,20 @@ +package io.rsocket.kotlin.samples.chat.client + +import kotlin.random.* + +suspend fun main() { + when (Random.nextInt(3)) { + 0 -> { + println("Connect WS") + connectToApiUsingWS("Oleg").use("RSocket is awesome! (from JVM WS)") + } + 1 -> { + println("Connect TCP") + connectToApiUsingTCP("Hanna").use("RSocket is awesome! (from JVM TCP)") + } + 2 -> { + println("Connect TCP native") + connectToApiUsingTCP("Someone", 7000).use("RSocket is awesome! (from JVM TCP)") + } + } +} diff --git a/samples/chat/client/src/nativeMain/kotlin/App.kt b/samples/chat/client/src/nativeMain/kotlin/App.kt new file mode 100644 index 000000000..546821bdd --- /dev/null +++ b/samples/chat/client/src/nativeMain/kotlin/App.kt @@ -0,0 +1,17 @@ +package io.rsocket.kotlin.samples.chat.client + +import kotlinx.coroutines.* +import kotlin.random.* + +fun main() = runBlocking { + when (Random.nextInt(2)) { + 0 -> { + println("Connect TCP") + connectToApiUsingTCP("Gloria").use("RSocket is awesome! (from Native TCP)") + } + 1 -> { + println("Connect TCP native") + connectToApiUsingTCP("Kolya", 7000).use("RSocket is awesome! (from Native TCP)") + } + } +} diff --git a/samples/chat/gradle.properties b/samples/chat/gradle.properties new file mode 100644 index 000000000..1d8064c5c --- /dev/null +++ b/samples/chat/gradle.properties @@ -0,0 +1,10 @@ +group=io.rsocket.kotlin.sample.chat +version=1.0.0 +kotlinVersion=1.6.10 +ktorVersion=1.6.7 +kotlinxSerializationVersion=1.3.1 +rsocketVersion=0.14.3 +kotlin.js.compiler=ir +kotlin.mpp.stability.nowarn=true +kotlin.native.enableDependencyPropagation=false +kotlin.mpp.enableGranularSourceSetsMetadata=true diff --git a/samples/chat/gradle/wrapper/gradle-wrapper.jar b/samples/chat/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..7454180f2 Binary files /dev/null and b/samples/chat/gradle/wrapper/gradle-wrapper.jar differ diff --git a/samples/chat/gradle/wrapper/gradle-wrapper.properties b/samples/chat/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..d2880ba80 --- /dev/null +++ b/samples/chat/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/samples/chat/gradlew b/samples/chat/gradlew new file mode 100755 index 000000000..1b6c78733 --- /dev/null +++ b/samples/chat/gradlew @@ -0,0 +1,234 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/samples/chat/gradlew.bat b/samples/chat/gradlew.bat new file mode 100644 index 000000000..107acd32c --- /dev/null +++ b/samples/chat/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/chat/server/build.gradle.kts b/samples/chat/server/build.gradle.kts new file mode 100644 index 000000000..a5648cc98 --- /dev/null +++ b/samples/chat/server/build.gradle.kts @@ -0,0 +1,46 @@ +import org.jetbrains.kotlin.konan.target.* + +plugins { + kotlin("multiplatform") + kotlin("plugin.serialization") + application +} + +val rsocketVersion: String by rootProject +val ktorVersion: String by rootProject + +application { + mainClass.set("io.rsocket.kotlin.samples.chat.server.AppKt") +} + +kotlin { + jvm { + withJava() + } + when { + HostManager.hostIsLinux -> linuxX64("native") + HostManager.hostIsMingw -> null //no native support for TCP in ktor mingwX64("clientNative") + HostManager.hostIsMac -> macosX64("native") + else -> null + }?.binaries { + executable { + entryPoint = "io.rsocket.kotlin.samples.chat.server.main" + } + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":api")) + implementation("io.rsocket.kotlin:rsocket-transport-ktor:$rsocketVersion") + implementation("io.ktor:ktor-utils:$ktorVersion") //for concurrent map implementation and shared list + } + } + val jvmMain by getting { + dependencies { + implementation("io.rsocket.kotlin:rsocket-transport-ktor-server:$rsocketVersion") + implementation("io.ktor:ktor-server-cio:$ktorVersion") + } + } + } +} diff --git a/samples/chat/server/src/commonMain/kotlin/ChatApi.kt b/samples/chat/server/src/commonMain/kotlin/ChatApi.kt new file mode 100644 index 000000000..b169f0848 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/ChatApi.kt @@ -0,0 +1,19 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.samples.chat.api.* + +class ChatApiImpl( + private val chats: Chats, + private val messages: Messages, +) : ChatApi { + + override suspend fun all(): List = chats.values.toList() + + override suspend fun new(name: String): Chat = chats.create(name) + + override suspend fun delete(id: Int) { + messages.deleteForChat(id) + chats -= id + } +} + diff --git a/samples/chat/server/src/commonMain/kotlin/Chats.kt b/samples/chat/server/src/commonMain/kotlin/Chats.kt new file mode 100644 index 000000000..72475c256 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/Chats.kt @@ -0,0 +1,29 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.samples.chat.api.* + +class Chats { + private val storage = Storage() + + val values: List get() = storage.values() + + fun getOrNull(id: Int): Chat? = storage.getOrNull(id) + + fun delete(id: Int) { + storage.remove(id) + } + + fun create(name: String): Chat { + if (storage.values().any { it.name == name }) error("Chat with such name already exists") + val chatId = storage.nextId() + val chat = Chat(chatId, name) + storage.save(chatId, chat) + return chat + } + + fun exists(id: Int): Boolean = storage.contains(id) +} + +operator fun Chats.get(id: Int): Chat = getOrNull(id) ?: error("No user with id '$id' exists") +operator fun Chats.minusAssign(id: Int): Unit = delete(id) +operator fun Chats.contains(id: Int): Boolean = exists(id) diff --git a/samples/chat/server/src/commonMain/kotlin/MessageApi.kt b/samples/chat/server/src/commonMain/kotlin/MessageApi.kt new file mode 100644 index 000000000..f00a64cdb --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/MessageApi.kt @@ -0,0 +1,40 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.ktor.util.collections.* +import io.rsocket.kotlin.samples.chat.api.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.* +import kotlinx.coroutines.flow.* + +@OptIn(ExperimentalCoroutinesApi::class) +class MessageApiImpl( + private val messages: Messages, + private val chats: Chats, +) : MessageApi { + + private val listeners = ConcurrentList>() + + override suspend fun send(chatId: Int, content: String): Message { + if (chatId !in chats) error("No chat with id '$chatId'") + val userId = currentSession().userId + val message = messages.create(userId, chatId, content) + listeners.forEach { it.send(message) } + return message + } + + override suspend fun history(chatId: Int, limit: Int): List { + if (chatId !in chats) error("No chat with id '$chatId'") + return messages.takeLast(chatId, limit) + } + + override fun messages(chatId: Int, fromMessageId: Int): Flow = flow { + messages.takeAfter(chatId, fromMessageId).forEach { emit(it) } + emitAll(channelFlow { + listeners += channel + awaitClose { + listeners -= channel + } + }.buffer()) + } +} + diff --git a/samples/chat/server/src/commonMain/kotlin/Messages.kt b/samples/chat/server/src/commonMain/kotlin/Messages.kt new file mode 100644 index 000000000..0fe931bd3 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/Messages.kt @@ -0,0 +1,45 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.samples.chat.api.* + +class Messages { + private val storage = Storage() + + val values: List get() = storage.values() + + fun getOrNull(id: Int): Message? = storage.getOrNull(id) + + fun delete(id: Int) { + storage.remove(id) + } + + fun deleteForChat(chatId: Int) { + storage.values().forEach { + if (it.chatId == chatId) storage.remove(it.id) + } + } + + fun create(userId: Int, chatId: Int, content: String): Message { + val messageId = storage.nextId() + val message = Message(messageId, chatId, userId, currentMillis(), content) + storage.save(messageId, message) + return message + } + + private fun byChatSorted(chatId: Int): List = + storage.values().filter { it.chatId == chatId }.sortedByDescending { it.timestamp } + + fun takeLast(chatId: Int, limit: Int): List = byChatSorted(chatId).take(limit) + + fun takeAfter(chatId: Int, messageId: Int): List { + val messages = byChatSorted(chatId) + if (messageId == -1) return messages.take(1) + + val index = messages.indexOfFirst { it.id == messageId } + if (index == -1) error("No message with id '$messageId'") + return messages.drop(index) + } +} + +operator fun Messages.get(id: Int): Message = getOrNull(id) ?: error("No user with id '$id' exists") +operator fun Messages.minusAssign(id: Int): Unit = delete(id) diff --git a/samples/chat/server/src/commonMain/kotlin/Session.kt b/samples/chat/server/src/commonMain/kotlin/Session.kt new file mode 100644 index 000000000..922c44338 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/Session.kt @@ -0,0 +1,11 @@ +package io.rsocket.kotlin.samples.chat.server + +import kotlin.coroutines.* + +data class Session(val userId: Int) : CoroutineContext.Element { + override val key: CoroutineContext.Key<*> get() = Session + + companion object : CoroutineContext.Key +} + +suspend fun currentSession(): Session = coroutineContext[Session]!! diff --git a/samples/chat/server/src/commonMain/kotlin/UserApi.kt b/samples/chat/server/src/commonMain/kotlin/UserApi.kt new file mode 100644 index 000000000..04b140ea8 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/UserApi.kt @@ -0,0 +1,21 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.samples.chat.api.* + +class UserApiImpl( + private val users: Users, +) : UserApi { + + override suspend fun getMe(): User { + val userId = currentSession().userId + return users[userId] + } + + override suspend fun deleteMe() { + val userId = currentSession().userId + users -= userId + } + + override suspend fun all(): List = users.values +} + diff --git a/samples/chat/server/src/commonMain/kotlin/Users.kt b/samples/chat/server/src/commonMain/kotlin/Users.kt new file mode 100644 index 000000000..498e02fc2 --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/Users.kt @@ -0,0 +1,24 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.samples.chat.api.* + +class Users { + private val storage = Storage() + + val values: List get() = storage.values() + + fun getOrCreate(name: String): User = + storage.values().find { it.name == name } ?: run { + val userId = storage.nextId() + User(userId, name).also { storage.save(userId, it) } + } + + fun getOrNull(id: Int): User? = storage.getOrNull(id) + + fun delete(id: Int) { + storage.remove(id) + } +} + +operator fun Users.get(id: Int): User = getOrNull(id) ?: error("No user with id '$id' exists") +operator fun Users.minusAssign(id: Int): Unit = delete(id) diff --git a/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt b/samples/chat/server/src/commonMain/kotlin/acceptor.kt similarity index 57% rename from examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt rename to samples/chat/server/src/commonMain/kotlin/acceptor.kt index 8a44f4cbd..bc3033918 100644 --- a/examples/multiplatform-chat/src/serverJvmMain/kotlin/App.kt +++ b/samples/chat/server/src/commonMain/kotlin/acceptor.kt @@ -1,51 +1,23 @@ -/* - * Copyright 2015-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +package io.rsocket.kotlin.samples.chat.server -import io.ktor.application.* -import io.ktor.routing.* -import io.ktor.server.cio.* -import io.ktor.server.engine.* -import io.ktor.websocket.* import io.rsocket.kotlin.* -import io.rsocket.kotlin.core.* -import io.rsocket.kotlin.metadata.* -import io.rsocket.kotlin.payload.* -import io.rsocket.kotlin.transport.ktor.* -import io.rsocket.kotlin.transport.ktor.server.* +import io.rsocket.kotlin.samples.chat.api.* import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlinx.serialization.* -@OptIn(ExperimentalSerializationApi::class, ExperimentalMetadataApi::class, DelicateCoroutinesApi::class) -fun main() { +@OptIn(ExperimentalSerializationApi::class) +fun acceptor(): ConnectionAcceptor { val proto = ConfiguredProtoBuf val users = Users() val chats = Chats() val messages = Messages() - val userApi = UserApi(users) - val chatsApi = ChatApi(chats, messages) - val messagesApi = MessageApi(messages, chats) + val userApi = UserApiImpl(users) + val chatsApi = ChatApiImpl(chats, messages) + val messagesApi = MessageApiImpl(messages, chats) - val rSocketServer = RSocketServer() - - fun Payload.route(): String = metadata?.read(RoutingMetadata)?.tags?.first() ?: error("No route provided") - - //create acceptor - val acceptor = ConnectionAcceptor { + return ConnectionAcceptor { val userName = config.setupPayload.data.readText() val user = users.getOrCreate(userName) val session = Session(user.id) @@ -93,19 +65,4 @@ fun main() { } } } - - //start TCP server - rSocketServer.bind(TcpServerTransport(port = 8000), acceptor) - - //start WS server - embeddedServer(CIO, port = 9000) { - install(WebSockets) - install(RSocketSupport) { - server = rSocketServer - } - - routing { - rSocket(acceptor = acceptor) - } - }.start(true) } diff --git a/samples/chat/server/src/commonMain/kotlin/expects.kt b/samples/chat/server/src/commonMain/kotlin/expects.kt new file mode 100644 index 000000000..2999a7acc --- /dev/null +++ b/samples/chat/server/src/commonMain/kotlin/expects.kt @@ -0,0 +1,33 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.ktor.util.* +import io.ktor.util.collections.* + +expect class Counter() { + fun next(): Int +} + +expect fun currentMillis(): Long + +@OptIn(InternalAPI::class) +class Storage { + private val map: MutableMap = ConcurrentMap() + private val id = Counter() + + fun nextId(): Int = id.next() + + operator fun get(id: Int): T = map.getValue(id) + fun getOrNull(id: Int): T? = map[id] + + fun save(id: Int, value: T) { + map[id] = value + } + + fun remove(id: Int) { + map.remove(id) + } + + fun contains(id: Int): Boolean = id in map + fun values(): List = map.values.toList() + +} \ No newline at end of file diff --git a/samples/chat/server/src/jvmMain/kotlin/App.kt b/samples/chat/server/src/jvmMain/kotlin/App.kt new file mode 100644 index 000000000..30ebbcb67 --- /dev/null +++ b/samples/chat/server/src/jvmMain/kotlin/App.kt @@ -0,0 +1,31 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.ktor.application.* +import io.ktor.routing.* +import io.ktor.server.cio.* +import io.ktor.server.engine.* +import io.ktor.websocket.* +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.transport.ktor.* +import io.rsocket.kotlin.transport.ktor.server.* + +fun main() { + val acceptor = acceptor() + + val rSocketServer = RSocketServer() + + //start TCP server + rSocketServer.bind(TcpServerTransport("0.0.0.0", 8000), acceptor) + + //start WS server + embeddedServer(CIO, port = 9000) { + install(WebSockets) + install(RSocketSupport) { + server = rSocketServer + } + + routing { + rSocket(acceptor = acceptor) + } + }.start(true) +} diff --git a/samples/chat/server/src/jvmMain/kotlin/expects.jvm.kt b/samples/chat/server/src/jvmMain/kotlin/expects.jvm.kt new file mode 100644 index 000000000..becb26a89 --- /dev/null +++ b/samples/chat/server/src/jvmMain/kotlin/expects.jvm.kt @@ -0,0 +1,10 @@ +package io.rsocket.kotlin.samples.chat.server + +import java.util.concurrent.atomic.* + +actual class Counter { + private val atomic = AtomicInteger(0) + actual fun next(): Int = atomic.incrementAndGet() +} + +actual fun currentMillis(): Long = System.currentTimeMillis() diff --git a/samples/chat/server/src/nativeMain/kotlin/App.kt b/samples/chat/server/src/nativeMain/kotlin/App.kt new file mode 100644 index 000000000..a3a5600be --- /dev/null +++ b/samples/chat/server/src/nativeMain/kotlin/App.kt @@ -0,0 +1,20 @@ +package io.rsocket.kotlin.samples.chat.server + +import io.rsocket.kotlin.core.* +import io.rsocket.kotlin.transport.ktor.* +import kotlinx.coroutines.* + +fun main() = runBlocking { + val acceptor = acceptor() + + val rSocketServer = RSocketServer() + + //start TCP server + rSocketServer.bindIn( + CoroutineScope(CoroutineExceptionHandler { coroutineContext, throwable -> + println("Error happened $coroutineContext: $throwable") + }), + TcpServerTransport("0.0.0.0", 7000), + acceptor + ).handlerJob.join() +} diff --git a/samples/chat/server/src/nativeMain/kotlin/expects.native.kt b/samples/chat/server/src/nativeMain/kotlin/expects.native.kt new file mode 100644 index 000000000..8e5555080 --- /dev/null +++ b/samples/chat/server/src/nativeMain/kotlin/expects.native.kt @@ -0,0 +1,11 @@ +package io.rsocket.kotlin.samples.chat.server + +import kotlin.native.concurrent.* +import kotlin.system.* + +actual class Counter { + private val atomic = AtomicInt(0) + actual fun next(): Int = atomic.addAndGet(1) +} + +actual fun currentMillis(): Long = getTimeMillis() diff --git a/samples/chat/settings.gradle.kts b/samples/chat/settings.gradle.kts new file mode 100644 index 000000000..3bbe144ff --- /dev/null +++ b/samples/chat/settings.gradle.kts @@ -0,0 +1,24 @@ +pluginManagement { + repositories { + gradlePluginPortal() + mavenCentral() + } + + val kotlinVersion: String by settings + plugins { + kotlin("multiplatform") version kotlinVersion + kotlin("plugin.serialization") version kotlinVersion + } +} + +dependencyResolutionManagement { + repositories { + mavenCentral() + } +} + +rootProject.name = "chat" + +include("api") +include("client") +include("server")