diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 9cc8b7380..8a3e63011 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -20,7 +20,7 @@ env: ALGOLIA_INDEX_NAME: 'prod_kotlin_rpc' ALGOLIA_KEY: '${{ secrets.ALGOLIA_KEY }}' CONFIG_JSON_PRODUCT: 'kotlinx-rpc' - CONFIG_JSON_VERSION: '0.4.0' + CONFIG_JSON_VERSION: '0.5.0' jobs: build: diff --git a/README.md b/README.md index e046720bb..ad7a96be6 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,16 @@ import kotlinx.rpc.annotations.Rpc @Rpc interface AwesomeService : RemoteService { suspend fun getNews(city: String): Flow + + suspend fun daysUntilStableRelese(): Int } ``` In your server code define how to respond by simply implementing the service: ```kotlin -class AwesomeServiceImpl(override val coroutineContext: CoroutineContext) : AwesomeService { +class AwesomeServiceImpl( + val parameters: AwesomeParameters, + override val coroutineContext: CoroutineContext, +) : AwesomeService { override suspend fun getNews(city: String): Flow { return flow { emit("Today is 23 degrees!") @@ -37,11 +42,19 @@ class AwesomeServiceImpl(override val coroutineContext: CoroutineContext) : Awes emit("New dogs cafe has opened doors to all fluffy customers!") } } + + override suspend fun daysUntilStableRelese(): Int { + retuen if (parameters.stable) 0 else { + parameters.daysUntilStable ?: error("Who says it will be stable?") + } + } } ``` Then, choose how do you want your service to communicate. For example, you can use integration with [Ktor](https://ktor.io/): ```kotlin +data class AwesomeParameters(val stable: Boolean, val daysUntilStable: Int?) + fun main() { embeddedServer(Netty, 8080) { install(Krpc) @@ -53,7 +66,9 @@ fun main() { } } - registerService { ctx -> AwesomeServiceImpl(ctx) } + registerService { ctx -> + AwesomeServiceImpl(AwesomeParameters(false, null), ctx) + } } } }.start(wait = true) @@ -71,8 +86,12 @@ val rpcClient = HttpClient { installKrpc() }.rpc { } } +val service = rpcClient.withService() + +service.daysUntilStableRelese() + streamScoped { - rpcClient.withService().getNews("KotlinBurg").collect { article -> + service.getNews("KotlinBurg").collect { article -> println(article) } } @@ -92,7 +111,7 @@ Example of a setup in a project's `build.gradle.kts`: plugins { kotlin("multiplatform") version "2.1.0" kotlin("plugin.serialization") version "2.1.0" - id("org.jetbrains.kotlinx.rpc.plugin") version "0.4.0" + id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.0" } ``` @@ -107,15 +126,15 @@ And now you can add dependencies to your project: ```kotlin dependencies { // Client API - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.0") // Server API - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.0") // Serialization module. Also, protobuf and cbor are provided - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.0") // Transport implementation for Ktor - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.4.0") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.4.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.5.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.5.0") // Ktor API implementation("io.ktor:ktor-client-cio-jvm:$ktor_version") diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt index 4d3e313fa..9f68337c7 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc @@ -8,6 +8,10 @@ package kotlinx.rpc * The field marked with this annotation will be initialized with the service creation. */ @Target(AnnotationTarget.PROPERTY) +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public annotation class RpcEagerField @Deprecated("Use RpcEagerField instead", ReplaceWith("RpcEagerField"), level = DeprecationLevel.ERROR) diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt b/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt index f27cffd49..927c1cc48 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc @@ -18,6 +18,10 @@ public typealias UninitializedRPCFieldException = UninitializedRpcFieldException * * Use [awaitFieldInitialization] to await for the field initialization */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public class UninitializedRpcFieldException(serviceName: String, property: KProperty<*>) : Exception() { override val message: String = "${property.name} field of RPC service \"$serviceName\" in not initialized" } diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt b/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt index 9491d0cec..3a93aef0d 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc @@ -27,6 +27,10 @@ import kotlin.reflect.KClass * @param getter function that returns the field of the context service to wait for. * @return service filed after it was initialized. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public suspend fun <@Rpc T : Any, R> T.awaitFieldInitialization(getter: T.() -> R): R { val field = getter() @@ -56,6 +60,10 @@ public suspend fun <@Rpc T : Any, R> T.awaitFieldInitialization(getter: T.() -> * @param T service type * @return specified service, after all of it's field were initialized. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public suspend inline fun <@Rpc reified T : Any> T.awaitFieldInitialization(): T { return awaitFieldInitialization(T::class) } @@ -79,6 +87,10 @@ public suspend inline fun <@Rpc reified T : Any> T.awaitFieldInitialization(): T * @param kClass [KClass] of the [T] type. * @return specified service, after all of it's field were initialized. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public suspend fun <@Rpc T : Any> T.awaitFieldInitialization(kClass: KClass): T { serviceDescriptorOf(kClass) .getFields(this) diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt index 288f17481..c361a4921 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.descriptor @@ -44,6 +44,10 @@ public interface RpcServiceDescriptor<@Rpc T : Any> { public val fqName: String @InternalRpcApi + @Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) public fun getFields(service: T): List> public fun getCallable(name: String): RpcCallable? @@ -68,6 +72,10 @@ public sealed interface RpcInvokator<@Rpc T : Any> { } @ExperimentalRpcApi + @Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) public fun interface Field<@Rpc T : Any> : RpcInvokator { public fun call(service: T): Any? } diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt b/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt index 59ad6cdcc..74f5b3987 100644 --- a/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt +++ b/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc @@ -25,6 +25,10 @@ import kotlinx.rpc.internal.RpcFlow * @param serviceId id of the service, that made the call * @return Flow instance to be consumed. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public fun RpcClient.registerPlainFlowField( serviceScope: CoroutineScope, descriptor: RpcServiceDescriptor<*>, @@ -46,6 +50,10 @@ public fun RpcClient.registerPlainFlowField( * @param serviceId id of the service, that made the call * @return SharedFlow instance to be consumed. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public fun RpcClient.registerSharedFlowField( serviceScope: CoroutineScope, descriptor: RpcServiceDescriptor<*>, @@ -67,6 +75,10 @@ public fun RpcClient.registerSharedFlowField( * @param serviceId id of the service, that made the call * @return StateFlow instance to be consumed. */ +@Deprecated( + "Fields are deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, +) public fun RpcClient.registerStateFlowField( serviceScope: CoroutineScope, descriptor: RpcServiceDescriptor<*>, diff --git a/docs/pages/kotlinx-rpc/.idea/kotlinx-rpc.iml b/docs/pages/kotlinx-rpc/.idea/kotlinx-rpc.iml new file mode 100644 index 000000000..610219404 --- /dev/null +++ b/docs/pages/kotlinx-rpc/.idea/kotlinx-rpc.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/pages/kotlinx-rpc/.idea/modules.xml b/docs/pages/kotlinx-rpc/.idea/modules.xml new file mode 100644 index 000000000..6a8e3f943 --- /dev/null +++ b/docs/pages/kotlinx-rpc/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/docs/pages/kotlinx-rpc/help-versions.json b/docs/pages/kotlinx-rpc/help-versions.json index a5e4e23e9..445d8a961 100644 --- a/docs/pages/kotlinx-rpc/help-versions.json +++ b/docs/pages/kotlinx-rpc/help-versions.json @@ -1,3 +1,3 @@ [ - {"version":"0.4.0","url":"/kotlinx-rpc/0.4.0/","isCurrent":true} + {"version":"0.5.0","url":"/kotlinx-rpc/0.5.0/","isCurrent":true} ] diff --git a/docs/pages/kotlinx-rpc/rpc.tree b/docs/pages/kotlinx-rpc/rpc.tree index 53f20105a..0d5f9bc64 100644 --- a/docs/pages/kotlinx-rpc/rpc.tree +++ b/docs/pages/kotlinx-rpc/rpc.tree @@ -14,8 +14,10 @@ + + @@ -24,8 +26,10 @@ + + diff --git a/docs/pages/kotlinx-rpc/topics/0-5-0.topic b/docs/pages/kotlinx-rpc/topics/0-5-0.topic new file mode 100644 index 000000000..0e480eb37 --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/0-5-0.topic @@ -0,0 +1,167 @@ + + + + +

+ Version 0.5.0 introduces breaking changes. +

+ + +

+ This release introduces annotation type-safety. + As a result, some code that previously compiled successfully may now produce errors: +

+ + @Rpc + interface MyService : RemoteService + + class MyServiceImpl : MyService + + fun <@Rpc T : Any> withService() {} + + + + withService<MyService>() // ok + withService<MyServiceImpl>() // ok in 0.4.0, error in 0.5.0 + +

+ This should not cause any major issues, as the affected code was already incorrect + in previous releases and would have failed at runtime in 0.4.0. +

+
+ + +

+ This release introduces Strict Mode. + Some service declarations are now deprecated with a warning. + In upcoming releases, these warnings will be replaced with errors. +

+

+ Deprecated service APIs: +

+ +
  • StateFlow and SharedFlow usage
  • +
  • Fields in services
  • +
  • Nested Flows
  • +
  • Not top-level server side flows
  • +
    +

    + For more information, see the Strict Mode documentation. +

    +
    + + + The following APIs were renamed to satisfy Kotlin style guides and bring even more consistency to the library: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    OldNew
    kotlinx.rpc.RPCClientkotlinx.rpc.RpcClient
    kotlinx.rpc.RPCServerkotlinx.rpc.RpcServer
    kotlinx.rpc.UninitializedRPCFieldExceptionkotlinx.rpc.UninitializedRpcFieldException
    kotlinx.rpc.RPCEagerFieldkotlinx.rpc.RpcEagerField
    kotlinx.rpc.RPCEagerFieldkotlinx.rpc.RpcEagerField
    kotlinx.rpc.internal.utils.InternalRPCApikotlinx.rpc.internal.utils.InternalRpcApi
    kotlinx.rpc.internal.utils.ExperimentalRPCApikotlinx.rpc.internal.utils.ExperimentalRpcApi
    kotlinx.rpc.krpc.RPCConfigkotlinx.rpc.krpc.KrpcConfig
    kotlinx.rpc.krpc.RPCConfigBuilderkotlinx.rpc.krpc.KrpcConfigBuilder
    kotlinx.rpc.krpc.RPCTransportkotlinx.rpc.krpc.KrpcTransport
    kotlinx.rpc.krpc.RPCTransportMessagekotlinx.rpc.krpc.KrpcTransportMessage
    kotlinx.rpc.krpc.client.KRPCClientkotlinx.rpc.krpc.client.KrpcClient
    kotlinx.rpc.krpc.server.KRPCServerkotlinx.rpc.krpc.server.KrpcServer
    kotlinx.rpc.krpc.ktor.client.installRPCkotlinx.rpc.krpc.ktor.client.installKrpc
    kotlinx.rpc.krpc.ktor.client.RPCkotlinx.rpc.krpc.ktor.client.Krpc
    kotlinx.rpc.krpc.ktor.client.KtorRPCClientkotlinx.rpc.krpc.ktor.client.KtorRpcClient
    kotlinx.rpc.krpc.ktor.server.RPCkotlinx.rpc.krpc.ktor.server.Krpc
    kotlinx.rpc.krpc.ktor.server.RPCRoutekotlinx.rpc.krpc.ktor.server.KrpcRoute
    kotlinx.rpc.krpc.serialization.RPCSerialFormatkotlinx.rpc.krpc.serialization.KrpcSerialFormat
    kotlinx.rpc.krpc.serialization.RPCSerialFormatBuilderkotlinx.rpc.krpc.serialization.KrpcSerialFormatBuilder
    kotlinx.rpc.krpc.serialization.RPCSerialFormatConfigurationkotlinx.rpc.krpc.serialization.KrpcSerialFormatConfiguration
    kotlinx.rpc.krpc.serialization.cbor.RPCCborSerialFormatkotlinx.rpc.krpc.serialization.cbor.KrpcCborSerialFormat
    kotlinx.rpc.krpc.serialization.json.RPCJsonSerialFormatkotlinx.rpc.krpc.serialization.json.KrpcJsonSerialFormat
    kotlinx.rpc.krpc.serialization.protobuf.RPCProtobufSerialFormatkotlinx.rpc.krpc.serialization.protobuf.KrpcProtobufSerialFormat
    +
    + + +

    + New API is introduced to access compiler generated code - . +

    +
    +
    diff --git a/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic new file mode 100644 index 000000000..135146a7b --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic @@ -0,0 +1,73 @@ + + + + +

    + The library introduces a concept of annotation type-safety. Consider the following example: +

    + + @Rpc + interface MyService : RemoteService + + class MyServiceImpl : MyService + + fun <T : RemoteService> withService() {} + +

    + The compiler can't guarantee that the passed type parameter is the one for which the code generation was run: +

    + + withService<MyService>() // ok + withService<MyServiceImpl>() // compile time ok, runtime throws + +

    + The compiler plugin enforces annotation type-safety by requiring type parameters to have specific annotations, + like @Rpc. +

    + + @Rpc + interface MyService : RemoteService + + class MyServiceImpl : MyService + + fun <@Rpc T : Any> withService() {} + + + + withService<MyService>() // ok + withService<MyServiceImpl>() // compile time error + + + + Annotation type safety only ensures that the resolved type parameters are annotated with the required + annotation. + The actual type of the passed argument may differ: + + fun <@Rpc T : Any> registerService(body: () -> T) {} + + // T is resolved to MyService, + // but 'body' returns MyServiceImpl + registerService<MyService> { ctx -> MyServiceImpl(ctx) } + + // Error: T is resolved to MyServiceImpl + registerService { ctx -> MyServiceImpl(ctx) } + + + + + This feature is highly experimental and may lead to unexpected behaviour. If you encounter any issues, + please
    report them. + + To prevent critical bugs from affecting your app, you can disable this feature using the following + configuration: + + // build.gradle.kts + rpc { + annotationTypeSafetyEnabled = false // true by default + } + + + diff --git a/docs/pages/kotlinx-rpc/topics/configuration.topic b/docs/pages/kotlinx-rpc/topics/configuration.topic index 49d7864df..0b382bd18 100644 --- a/docs/pages/kotlinx-rpc/topics/configuration.topic +++ b/docs/pages/kotlinx-rpc/topics/configuration.topic @@ -53,6 +53,11 @@ sharedFlowParameters DSL + + These parameters are deprecated since 0.5.0. For more information, + see the migration guide. + + rpcClientConfig { sharedFlowParameters { diff --git a/docs/pages/kotlinx-rpc/topics/features.topic b/docs/pages/kotlinx-rpc/topics/features.topic index 707d88795..54b5830ac 100644 --- a/docs/pages/kotlinx-rpc/topics/features.topic +++ b/docs/pages/kotlinx-rpc/topics/features.topic @@ -8,182 +8,103 @@ - -

    You can send and receive Kotlin Flows in RPC - methods. - That includes Flow, SharedFlow, and StateFlow, but - NOT - their mutable versions. - Flows can be nested and can be included in other classes, like this: -

    - - - @Serializable - data class StreamResult { - @Contextual - val innerFlow: StateFlow<Int> - } - - @Rpc - interface MyService : RemoteService { - suspend fun sendStream(stream: Flow<Flow<Int>>): Flow<StreamResult> - } - -

    Note that flows that are declared in classes (like in StreamResult) require - Contextual - annotation.

    -

    To use flows in your code - you need to use special streamScoped function - that will provide your flows with their lifetime:

    - - - @Rpc - interface MyService : RemoteService { - suspend fun sendFlow(flow: Flow<Int>) - } - - val myService = rpcClient.withService<MyService>() - - streamScoped { - val flow = flow { - repeat(10) { i -> - emit(i) - } - } - - myService.sendFlow(flow) - } - -

    In that case all your flows, including incoming and outgoing, - will work until the streamScoped function completes. - After that, all streams that are still live will be closed.

    -

    You can have multiple RPC call and flows inside the streamScoped function, even from - different services.

    -

    On server side, you can use invokeOnStreamScopeCompletion handler inside your methods - to execute code after streamScoped on client side has closed. - It might be useful to clean resources, for example:

    - - - override suspend fun hotFlow(): StateFlow<Int> { - val state = MutableStateFlow(-1) + +

    + You can send and receive Kotlin Flows in RPC + methods. + However, this only applies to the Flow type. StateFlow and SharedFlow + are not supported, and there are no plans to add support for them. +

    - incomingHotFlowJob = launch { - repeat(Int.MAX_VALUE) { value -> - state.value = value + + @Serializable + data class StreamRequest( + @Contextual + val innerFlow: Flow<Int> + ) - hotFlowMirror.first { it == value } - } - } + @Rpc + interface MyService : RemoteService { + suspend fun sendStream(stream: Flow<Int>): Flow<String> - invokeOnStreamScopeCompletion { - incomingHotFlowJob.cancel() - } + suspend fun streamRequest(request: StreamRequest) + } + - return state - } -
    -

    Note that this API is experimental and may be removed in future releases.

    -

    - Another way of managing streams is to do it manually. - For this, you can use the StreamScope constructor function together with - withStreamScope: -

    - - val streamScope = StreamScope(myJob) - withStreamScope(streamScope) { - // use streams here - } - -
    - -

    Our protocol provides you with an ability to declare service fields:

    +

    + Another requirement is that server-side steaming (flows that are returned from a function), + must be the top-level type: +

    - - @Rpc - interface MyService : RemoteService { - val plainFlow: Flow<Int> - val sharedFlow: SharedFlow<Int> - val stateFlow: StateFlow<Int> - } + + @Serializable + data class StreamResult( + @Contextual + val innerFlow: Flow<Int> + ) - // ### Server code ### + @Rpc + interface MyService : RemoteService { + suspend fun serverStream(): Flow<String> // ok + suspend fun serverStream(): StreamResult // not ok + } + - class MyServiceImpl(override val coroutineContext: CoroutineContext) : MyService { - override val plainFlow: Flow<Int> = flow { - emit(1) - } + + Note that flows that are declared in classes (like in StreamResult) require a + Contextual + annotation. + - override val sharedFlow: SharedFlow<Int> = MutableSharedFlow(replay = 1) - override val stateFlow: StateFlow<Int> = MutableStateFlow(value = 1) - } - -

    Field declarations are only supported for these three types: Flow, - SharedFlow and StateFlow.

    -

    You don't need to use streamScoped function to work with streams in fields.

    -

    To learn more about the limitations of such declarations, - see Field declarations in services.

    -
    - -

    Fields are supported in the in-house RPC protocol, - but the support comes with its limitations. - There always will be a considerable time gap between the - initial access to a field and the moment information about this field arrives from a server. - This makes it hard to provide good uniform API for all possible field types, - so now will limit supported types to Flow, SharedFlow and - StateFlow - (excluding mutable versions of them). - To work with these fields, you may use additional provided APIs:

    -

    Firstly, we define two possible states of a flow field: - uninitialized - and - initialized - . - Before the first information about this flow has arrived from a server, - the flow is in - uninitialized - state. - In this state, if you access any of its - fields - (replayCache for SharedFlow and StateFlow, and value - for StateFlow) - you will get a UninitializedRpcFieldException. - If you call a suspend collect method on them, - execution will suspend until the state is - initialized - and then the actual collect method will be executed. - The same ability to suspend execution until the state is - initialized - can be achieved by using awaitFieldInitialization function: +

    + To use flows in your code, use the streamScoped function + that will provide your flows with their lifetime:

    @Rpc interface MyService : RemoteService { - val flow: StateFlow<Int> + suspend fun sendFlow(flow: Flow<Int>) } - // ### Somewhere in client code ### - val myService: MyService = rpcClient.withService<MyService>() + val myService = rpcClient.withService<MyService>() - val value = myService.flow.value // throws UninitializedRpcFieldException - val value = myService.awaitFieldInitialization { flow }.value // OK - // or - val value = myService.awaitFieldInitialization().flow.value // OK - // or - val firstFive = myService.flow.take(5).toList() // OK - -

    Secondly, we provide you with an instrument to make initialization faster. - By default, all fields are lazy. - By adding @RpcEagerField annotation, you can change this behavior, - so that fields will be initialized when the service in created - (when withService method is called):

    + streamScoped { + val flow = flow { + repeat(10) { i -> + emit(i) + } + } + myService.sendFlow(flow) + } +
    +

    + In that case all your flows, including incoming and outgoing, + will work until the streamScoped function completes. + After that, all streams that are still live will be closed. +

    +

    + You can have multiple RPC calls and flows inside the streamScoped function, including those from + different services. +

    +

    + On the server side, you can use the invokeOnStreamScopeCompletion handler inside your methods + to execute code after streamScoped on the client side has closed. + It might be useful to clean resources, for example. +

    + + Note that this API is experimental and may be removed in future releases. + +

    + Another way of managing streams is to do it manually. + For this, you can use the StreamScope constructor function together with + withStreamScope: +

    - @Rpc - interface MyService : RemoteService { - val lazyFlow: Flow<Int> // initialized on first access - - @RpcEagerField - val eagerFlow: Flow<Int> // initialized on service creation + val streamScope = StreamScope(myJob) + withStreamScope(streamScope) { + // use streams here } diff --git a/docs/pages/kotlinx-rpc/topics/service-descriptors.topic b/docs/pages/kotlinx-rpc/topics/service-descriptors.topic new file mode 100644 index 000000000..421c1b9c8 --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/service-descriptors.topic @@ -0,0 +1,21 @@ + + + + + + This API is experimental and may be changed at any time. + +

    + Service Descriptors allow you to access entities generated by the compiler plugin. + You can access them by using the following code: +

    + + serviceDescriptorOf<MyService>() + +

    + For the list of available entities, refer to the source code. +

    +
    diff --git a/docs/pages/kotlinx-rpc/topics/services.topic b/docs/pages/kotlinx-rpc/topics/services.topic index b10a1dee0..0cfa5ecb2 100644 --- a/docs/pages/kotlinx-rpc/topics/services.topic +++ b/docs/pages/kotlinx-rpc/topics/services.topic @@ -41,7 +41,9 @@

    Now we can implement the service, so server knows how to process incoming requests.

    - class MyServiceImpl(override val coroutineContext: CoroutineContext) : MyService { + class MyServiceImpl( + override val coroutineContext: CoroutineContext, + ) : MyService { override suspend fun hello(name: String): String { return "Hello, $name! I'm server!" } diff --git a/docs/pages/kotlinx-rpc/topics/strict-mode.topic b/docs/pages/kotlinx-rpc/topics/strict-mode.topic new file mode 100644 index 000000000..18e8c46a1 --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/strict-mode.topic @@ -0,0 +1,96 @@ + + + + +

    + Starting with version 0.5.0, the library introduces major changes to the service APIs. + The following declarations will be gradually restricted: +

    + +
  • + StateFlow and SharedFlow +

    Deprecation level: WARNING

    + + @Rpc + interface Service : RemoteService { + suspend fun old(): StateFlow<Int> // deprecated + + suspend fun new(): Flow<Int> // use .stateIn on the client side + } + +
  • +
  • + Fields +

    Deprecation level: WARNING

    + + @Rpc + interface Service : RemoteService { + val old: Flow<Int> // deprecated + + suspend fun new(): Flow<Int> // store flow locally + } + +
  • +
  • + Nested Flows +

    Deprecation level: WARNING

    + + @Rpc + interface Service : RemoteService { + suspend fun old(): Flow<Flow<Int>> // deprecated + + // no particular alternative, depends on the use case + } + +
  • +
  • + Not top-level server flows +

    Deprecation level: WARNING

    + + + data class SpotifyWrapped(val myMusicFlow: Flow<Rap>, val extra: Data) + + @Rpc + interface Service : RemoteService { + suspend fun old(): SpotifyWrapped // deprecated + + // one should consider message delivery order when calling these + suspend fun new(): Flow<Rap> + suspend fun getData(): Data + } + +
  • +
    + +

    + Deprecation levels are controlled by the Gradle rpc extension: +

    + + // build.gradle.kts + plugins { + id("org.jetbrains.kotlinx.rpc.plugin") + } + + rpc { + strict { + stateFlow = RpcStrictMode.WARNING + sharedFlow = RpcStrictMode.WARNING + nestedFlow = RpcStrictMode.WARNING + notTopLevelServerFlow = RpcStrictMode.WARNING + fields = RpcStrictMode.WARNING + } + } + +

    + Modes RpcStrictMode.NONE and RpcStrictMode.ERROR are available. +

    + + + Note that setting RpcStrictMode.NONE should not be done permanently. + All deprecated APIs will become errors in future without an option to suppress it. + Consider your migration path in advance. + +
    diff --git a/docs/pages/kotlinx-rpc/v.list b/docs/pages/kotlinx-rpc/v.list index 21b24f30b..873005992 100644 --- a/docs/pages/kotlinx-rpc/v.list +++ b/docs/pages/kotlinx-rpc/v.list @@ -14,6 +14,6 @@ - + diff --git a/docs/pages/kotlinx-rpc/writerside.cfg b/docs/pages/kotlinx-rpc/writerside.cfg index 0a0f6d429..14e4f4b76 100644 --- a/docs/pages/kotlinx-rpc/writerside.cfg +++ b/docs/pages/kotlinx-rpc/writerside.cfg @@ -12,5 +12,5 @@ - + diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt index 76c349243..962e965e1 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. */ package kotlinx.rpc.krpc @@ -27,11 +27,19 @@ public sealed class KrpcConfigBuilder private constructor() { * parameters, and thus they cannot be encoded and transferred. * So then creating their instance on an endpoint, the library should know which parameters to use. */ + @Deprecated( + "SharedFlow support is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) @Suppress("MemberVisibilityCanBePrivate") public class SharedFlowParametersBuilder internal constructor() { /** * The number of values replayed to new subscribers (cannot be negative, defaults to zero). */ + @Deprecated( + "SharedFlow support is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) public var replay: Int = DEFAULT_REPLAY /** @@ -39,6 +47,10 @@ public sealed class KrpcConfigBuilder private constructor() { * emit does not suspend while there is a buffer space remaining * (optional, cannot be negative, defaults to zero). */ + @Deprecated( + "SharedFlow support is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) public var extraBufferCapacity: Int = DEFAULT_EXTRA_BUFFER_CAPACITY /** @@ -50,10 +62,15 @@ public sealed class KrpcConfigBuilder private constructor() { * In the absence of subscribers only the most recent replay values are stored * and the buffer overflow behavior is never triggered and has no effect. */ + @Deprecated( + "SharedFlow support is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) public var onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND @InternalRpcApi public fun builder(): () -> MutableSharedFlow = { + @Suppress("DEPRECATION") MutableSharedFlow(replay, extraBufferCapacity, onBufferOverflow) } @@ -69,12 +86,18 @@ public sealed class KrpcConfigBuilder private constructor() { } } + @Suppress("DEPRECATION") protected var sharedFlowBuilder: () -> MutableSharedFlow = SharedFlowParametersBuilder().builder() /** * @see SharedFlowParametersBuilder */ - public fun sharedFlowParameters(builder: SharedFlowParametersBuilder.() -> Unit) { + @Deprecated( + "SharedFlow support is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-5-0.html", + level = DeprecationLevel.WARNING, + ) + public fun sharedFlowParameters(builder: @Suppress("DEPRECATION") SharedFlowParametersBuilder.() -> Unit) { + @Suppress("DEPRECATION") sharedFlowBuilder = SharedFlowParametersBuilder().apply(builder).builder() }