diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 964fdbf98..c4cf8b1cd 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.5.1' + CONFIG_JSON_VERSION: '0.6.0' jobs: build: diff --git a/README.md b/README.md index 03faef104..eade1ca6f 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ Example of a setup in a project's `build.gradle.kts`: plugins { kotlin("multiplatform") version "2.1.20" kotlin("plugin.serialization") version "2.1.20" - id("org.jetbrains.kotlinx.rpc.plugin") version "0.5.1" + id("org.jetbrains.kotlinx.rpc.plugin") version "0.6.0" } ``` @@ -131,15 +131,15 @@ And now you can add dependencies to your project: ```kotlin dependencies { // Client API - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-client:0.6.0") // Server API - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-server:0.6.0") // Serialization module. Also, protobuf and cbor are provided - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-serialization-json:0.6.0") // Transport implementation for Ktor - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.5.1") - implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.5.1") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-client:0.6.0") + implementation("org.jetbrains.kotlinx:kotlinx-rpc-krpc-ktor-server:0.6.0") // Ktor API implementation("io.ktor:ktor-client-cio-jvm:$ktor_version") diff --git a/docs/pages/kotlinx-rpc/help-versions.json b/docs/pages/kotlinx-rpc/help-versions.json index bc1ee7380..6db88babb 100644 --- a/docs/pages/kotlinx-rpc/help-versions.json +++ b/docs/pages/kotlinx-rpc/help-versions.json @@ -1,3 +1,3 @@ [ - {"version":"0.5.1","url":"/kotlinx-rpc/0.5.1/","isCurrent":true} + {"version":"0.6.0","url":"/kotlinx-rpc/0.6.0/","isCurrent":true} ] diff --git a/docs/pages/kotlinx-rpc/rpc.tree b/docs/pages/kotlinx-rpc/rpc.tree index aef7ccde8..aaf3fa079 100644 --- a/docs/pages/kotlinx-rpc/rpc.tree +++ b/docs/pages/kotlinx-rpc/rpc.tree @@ -1,6 +1,6 @@ + - \ No newline at end of file + diff --git a/docs/pages/kotlinx-rpc/topics/0-6-0.topic b/docs/pages/kotlinx-rpc/topics/0-6-0.topic new file mode 100644 index 000000000..5b1935087 --- /dev/null +++ b/docs/pages/kotlinx-rpc/topics/0-6-0.topic @@ -0,0 +1,24 @@ + + + + + + +

+ Version 0.6.0 introduces non-breaking changes that require migration. +

+ + +

+ Non-suspending server flows are now supported. + That begins the deprecation cycle for the suspending server flows and stream scopes. + For more details, refer to the Non-suspending server flows and Stream scopes management + sections in the topic. +

+
+
diff --git a/docs/pages/kotlinx-rpc/topics/features.topic b/docs/pages/kotlinx-rpc/topics/features.topic index 54b5830ac..8a11a73f9 100644 --- a/docs/pages/kotlinx-rpc/topics/features.topic +++ b/docs/pages/kotlinx-rpc/topics/features.topic @@ -1,6 +1,6 @@ Another requirement is that server-side steaming (flows that are returned from a function), - must be the top-level type: + must be the top-level type and the function must be non-suspending:

@@ -45,7 +45,8 @@ @Rpc interface MyService : RemoteService { - suspend fun serverStream(): Flow<String> // ok + fun serverStream(): Flow<String> // ok + suspend fun serverStream(): Flow<String> // not ok suspend fun serverStream(): StreamResult // not ok } @@ -55,57 +56,5 @@
Contextual annotation. - -

- To use flows in your code, use the 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 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: -

- - val streamScope = StreamScope(myJob) - withStreamScope(streamScope) { - // use streams here - } - diff --git a/docs/pages/kotlinx-rpc/topics/strict-mode.topic b/docs/pages/kotlinx-rpc/topics/strict-mode.topic index 18e8c46a1..9053e22d5 100644 --- a/docs/pages/kotlinx-rpc/topics/strict-mode.topic +++ b/docs/pages/kotlinx-rpc/topics/strict-mode.topic @@ -1,4 +1,8 @@ + + 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 + +

    Deprecation level: WARNING

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

    Deprecation level: WARNING

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

    Deprecation level: WARNING

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

    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 level: WARNING

    + + + data class SpotifyWrapped(val extra: Data) + + @Rpc + interface Service : RemoteService { + suspend fun old(): Flow<SpotifyWrapped> // deprecated + + fun new(): Flow<SpotifyWrapped> + } + +
    + +

    Deprecation level: WARNING

    + +

    + The next stream scope management structures are deprecated due to the introduction of + non-suspending server flows: +

    + +
  • StreamScoped class and function
  • +
  • streamScoped function
  • +
  • invokeOnStreamScopeCompletion function
  • +
  • withStreamScope function
  • +
    +

    + Stream collection and completion is now bound to the CoroutineScope in which the flow was + collected (server-side flows) or produced (client-side flows). +

    +

    + 0.5.x: +

    + + @Rpc + interface Service : RemoteService { + suspend fun oldClient(flow: Flow<Int>) + suspend fun oldServer(): Flow<Int> + } + + suspend fun consumer(service: Service) { + streamScoped { + service.oldClient(flow { /* ... */ }) + + service.oldServer().collect { + // ... + } } - - -
  • - Nested Flows -

    Deprecation level: WARNING

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

    + 0.6.x: +

    + + @Rpc + interface Service : RemoteService { + suspend fun newClient(flow: Flow<Int>) + fun newServer(): Flow<Int> + } + + fun consumer(service: Service, scope: CoroutineScope) { + val flow = service.new() + scope.launch { + service.newClient(flow { /* ... */ }) + + flow.collect { + // ... + } } - -
  • -
  • - 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 + } + + // or + suspend fun consumer(service: Service) { + service.newClient(flow { /* ... */ }) + + service.new().collect { + // ... } - -
  • - + } + + -

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

    - + +

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

    + // build.gradle.kts plugins { id("org.jetbrains.kotlinx.rpc.plugin") @@ -81,16 +163,19 @@ nestedFlow = RpcStrictMode.WARNING notTopLevelServerFlow = RpcStrictMode.WARNING fields = RpcStrictMode.WARNING + suspendingServerStreaming = RpcStrictMode.WARNING + streamScopedFunctions = RpcStrictMode.WARNING } } -

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

    +

    + 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. - + + Note that setting RpcStrictMode.NONE should not be done permanently. + All deprecated APIs will become errors in the future without an option to suppress them. + Consider your migration path in advance. + +
    diff --git a/docs/pages/kotlinx-rpc/v.list b/docs/pages/kotlinx-rpc/v.list index 7addb534a..0263aede8 100644 --- a/docs/pages/kotlinx-rpc/v.list +++ b/docs/pages/kotlinx-rpc/v.list @@ -1,6 +1,6 @@ @@ -14,6 +14,6 @@ - + diff --git a/docs/pages/kotlinx-rpc/writerside.cfg b/docs/pages/kotlinx-rpc/writerside.cfg index b007eb914..c103bbe98 100644 --- a/docs/pages/kotlinx-rpc/writerside.cfg +++ b/docs/pages/kotlinx-rpc/writerside.cfg @@ -1,6 +1,6 @@ @@ -12,5 +12,5 @@ - + diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt index cd65fabcb..f159371fa 100644 --- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt +++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/KrpcClient.kt @@ -355,6 +355,7 @@ public abstract class KrpcClient( } catch (e: CancellationException) { // sendCancellation is not suspending, so no need for NonCancellable sendCancellation(CancellationType.REQUEST, call.serviceId.toString(), callId) + connector.unsubscribeFromMessages(call.descriptor.fqName, callId) throw e } diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/StreamScope.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/StreamScope.kt index 8cfb1968f..a3f2843bc 100644 --- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/StreamScope.kt +++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/StreamScope.kt @@ -26,6 +26,7 @@ import kotlin.js.JsName * Failure of one request will not cancel all streams in the others. */ @OptIn(InternalCoroutinesApi::class) +@Deprecated("StreamScope is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-6-0.html", level = DeprecationLevel.WARNING) public class StreamScope internal constructor( parentContext: CoroutineContext, internal val role: Role, @@ -170,6 +171,7 @@ public suspend fun callScoped(callId: String, block: suspend CoroutineScope. * } * ``` */ +@Deprecated("streamScoped is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-6-0.html", level = DeprecationLevel.WARNING) @OptIn(ExperimentalContracts::class) public suspend fun streamScoped(block: suspend CoroutineScope.() -> T): T { contract { @@ -205,6 +207,7 @@ private fun CoroutineContext.checkContextForStreamScope() { */ @JsName("StreamScope_fun") @ExperimentalRpcApi +@Deprecated("StreamScoped is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-6-0.html", level = DeprecationLevel.WARNING) public fun StreamScope(parent: CoroutineContext): StreamScope { parent.checkContextForStreamScope() @@ -216,6 +219,10 @@ public fun StreamScope(parent: CoroutineContext): StreamScope { */ @OptIn(ExperimentalContracts::class) @ExperimentalRpcApi +@Deprecated( + "withStreamScope is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-6-0.html", + level = DeprecationLevel.WARNING +) public suspend fun withStreamScope(scope: StreamScope, block: suspend CoroutineScope.() -> T): T { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) @@ -252,6 +259,10 @@ public suspend fun withStreamScope(scope: StreamScope, block: suspend Corout * ``` */ @ExperimentalRpcApi +@Deprecated( + "invokeOnStreamScopeCompletion is deprecated, see https://kotlin.github.io/kotlinx-rpc/0-6-0.html", + level = DeprecationLevel.WARNING +) public suspend fun invokeOnStreamScopeCompletion(throwIfNoScope: Boolean = true, block: (Throwable?) -> Unit) { val streamScope = streamScopeOrNull() ?: noStreamScopeError()