-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Docs: Add structure to existing content (#86)
* docs: split getting started document into separate topics, reformat and fix small errors * update topic structure
- Loading branch information
Showing
11 changed files
with
988 additions
and
881 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
- Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
--> | ||
|
||
<!DOCTYPE topic | ||
SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd"> | ||
<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd" | ||
title="Configuration" id="configuration"> | ||
<p><code>RPCConfig</code> is a class used to configure <code>KRPCClient</code> and <code>KRPCServer</code> | ||
(not to be confused with <code>RPCClient</code> and <code>RPCServer</code>). | ||
It has two children: <code>RPCConfig.Client</code> and <code>RPCConfig.Server</code>. | ||
<code>Client</code> and <code>Server</code> may have shared properties as well as distinct ones. | ||
To create instances of these configurations, DSL builders are provided | ||
(<code>RPCConfigBuilder.Client</code> class with <code>rpcClientConfig</code> function | ||
and <code>RPCConfigBuilder.Server</code> class with <code>rpcServerConfig</code> function respectively): | ||
</p> | ||
|
||
<code-block lang="kotlin"> | ||
val config: RPCConfig.Client = rpcClientConfig { // same for RPCConfig.Server with rpcServerConfig | ||
waitForServices = true // default parameter | ||
} | ||
</code-block> | ||
<p>The following configuration options are available:</p> | ||
<chapter id="serialization-dsl"> | ||
<title> | ||
<code>serialization</code> DSL | ||
</title> | ||
<p>This parameter defines how serialization should work in RPC services | ||
and is present in both client and server configurations.</p> | ||
<p>The serialization process is used to encode and decode data in RPC requests, | ||
so that the data can be transferred through the network.</p> | ||
<p>Currently only <code>StringFormat</code> and <code>BinaryFormat</code> from | ||
<a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a> are supported, | ||
and by default you can choose from Json, Protobuf or Cbor formats:</p> | ||
|
||
<code-block lang="kotlin"> | ||
rpcClientConfig { | ||
serialization { | ||
json { /* this: JsonBuilder from kotlinx.serialization */ } | ||
cbor { /* this: CborBuilder from kotlinx.serialization */ } | ||
protobuf { /* this: ProtobufBuilder from kotlinx.serialization */ } | ||
} | ||
} | ||
</code-block> | ||
<p>Only last defined format will be used to serialize requests. | ||
If no format is specified, the error will be thrown. | ||
You can also define a custom format.</p> | ||
</chapter> | ||
<chapter id="sharedflowparameters-dsl"> | ||
<title> | ||
<code>sharedFlowParameters</code> DSL | ||
</title> | ||
|
||
<code-block lang="kotlin"> | ||
rpcClientConfig { | ||
sharedFlowParameters { | ||
replay = 1 // default parameter | ||
extraBufferCapacity = 10 // default parameter | ||
onBufferOverflow = BufferOverflow.SUSPEND // default parameter | ||
} | ||
} | ||
</code-block> | ||
<p>This parameter is needed to decode <code>SharedFlow</code> parameters received from a peer. | ||
<code>MutableSharedFlow</code>, the default function to create a <code>SharedFlow</code> instance, | ||
has the following signature:</p> | ||
|
||
<code-block lang="kotlin"> | ||
fun <T> MutableSharedFlow( | ||
replay: Int = 0, | ||
extraBufferCapacity: Int = 0, | ||
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND | ||
): MutableSharedFlow<T> { /* ... */ | ||
} | ||
</code-block> | ||
<p>It creates a <code>SharedFlowImpl</code> class that contains these parameters as properties, | ||
but this class in internal in <code>kotlinx.coroutines</code> and neither <code>SharedFlow</code>, | ||
nor <code>MutableShatedFlow</code> interfaces define these properties, | ||
which makes it impossible (at least for now) to send these properties from one endpoint to another. | ||
But instances of these flows when deserialized should be created somehow, | ||
so to overcome this - configuration parameter is created. | ||
Configuration builder allows defining these parameters | ||
and produces a builder function that is then placed into the <code>RPCConfig</code>.</p> | ||
</chapter> | ||
<chapter id="waitforservices-dsl"> | ||
<title> | ||
<code>waitForServices</code> DSL | ||
</title> | ||
<p><code>waitForServices</code> parameter is available for both client and server. | ||
It specifies the behavior for an endpoint in situations | ||
when the message for a service is received, | ||
but the service is not present in <code>RPCClient</code> or <code>RPCServer</code>. | ||
If set to <code>true</code>, the message will be stored in memory, | ||
otherwise, the error will be sent to a peer endpoint, | ||
saying that the message was not handled. | ||
Default value is <code>true</code>.</p> | ||
</chapter> | ||
</topic> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,174 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!-- | ||
- Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
--> | ||
|
||
<!DOCTYPE topic | ||
SYSTEM "https://resources.jetbrains.com/writerside/1.0/xhtml-entities.dtd"> | ||
<topic xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd" | ||
title="Features" id="features"> | ||
<chapter title="Send and receive Flows" id="send-and-receive-flows"> | ||
<p>You can send and receive <a href="https://kotlinlang.org/docs/flow.html">Kotlin Flows</a> in RPC | ||
methods. | ||
That includes <code>Flow</code>, <code>SharedFlow</code>, and <code>StateFlow</code>, but | ||
<control>NOT</control> | ||
their mutable versions. | ||
Flows can be nested and can be included in other classes, like this: | ||
</p> | ||
|
||
<code-block lang="kotlin"> | ||
@Serializable | ||
data class StreamResult { | ||
@Contextual | ||
val innerFlow: StateFlow<Int> | ||
} | ||
|
||
interface MyService : RPC { | ||
suspend fun sendStream(stream: Flow<Flow<Int>>): Flow<StreamResult> | ||
} | ||
</code-block> | ||
<p>Note that flows that are declared in classes (like in <code>StreamResult</code>) require | ||
<a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serializers.md#contextual-serialization">Contextual</a> | ||
annotation.</p> | ||
<p>To use flows in your code - you need to use special <code>streamScoped</code> function | ||
that will provide your flows with their lifetime:</p> | ||
|
||
<code-block lang="kotlin"> | ||
interface MyService { | ||
suspend fun sendFlow(flow: Flow<Int>) | ||
} | ||
|
||
val myService = rpcClient.withService<MyService>() | ||
|
||
streamScoped { | ||
val flow = flow { | ||
repeat(10) { i -> | ||
emit(i) | ||
} | ||
} | ||
|
||
myService.sendFlow(flow) | ||
} | ||
</code-block> | ||
<p>In that case all your flows, including incoming and outgoing, | ||
will work until the <code>streamScoped</code> function completes. | ||
After that, all streams that are still live will be closed.</p> | ||
<p>You can have multiple RPC call and flows inside the <code>streamScoped</code> function, even from | ||
different services.</p> | ||
<p>On server side, you can use <code>invokeOnStreamScopeCompletion</code> handler inside your methods | ||
to execute code after <code>streamScoped</code> on client side has closed. | ||
It might be useful to clean resources, for example:</p> | ||
|
||
<code-block lang="kotlin"> | ||
override suspend fun hotFlow(): StateFlow<Int> { | ||
val state = MutableStateFlow(-1) | ||
|
||
incomingHotFlowJob = launch { | ||
repeat(Int.MAX_VALUE) { value -> | ||
state.value = value | ||
|
||
hotFlowMirror.first { it == value } | ||
} | ||
} | ||
|
||
invokeOnStreamScopeCompletion { | ||
incomingHotFlowJob.cancel() | ||
} | ||
|
||
return state | ||
} | ||
</code-block> | ||
<p>Note that this API is experimental and may be removed in future releases.</p> | ||
</chapter> | ||
<chapter title="Service fields" id="service-fields"> | ||
<p>Our protocol provides you with an ability to declare service fields:</p> | ||
|
||
<code-block lang="kotlin"> | ||
interface MyService : RPC { | ||
val plainFlow: Flow<Int> | ||
val sharedFlow: SharedFlow<Int> | ||
val stateFlow: StateFlow<Int> | ||
} | ||
|
||
// ### Server code ### | ||
|
||
class MyServiceImpl(override val coroutineContext: CoroutineContext) : MyService { | ||
override val plainFlow: Flow<Int> = flow { | ||
emit(1) | ||
} | ||
|
||
override val sharedFlow: SharedFlow<Int> = MutableSharedFlow(replay = 1) | ||
override val stateFlow: StateFlow<Int> = MutableStateFlow(value = 1) | ||
} | ||
</code-block> | ||
<p>Field declarations are only supported for these three types: <code>Flow</code>, | ||
<code>SharedFlow</code> and <code>StateFlow</code>.</p> | ||
<p>You don't need to use <code>streamScoped</code> function to work with streams in fields.</p> | ||
<p>To learn more about the limitations of such declarations, | ||
see <a anchor="field-declarations-in-services">Field declarations in services</a>.</p> | ||
</chapter> | ||
<chapter title="Field declarations in services" id="field-declarations-in-services"> | ||
<p>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 <code>Flow</code>, <code>SharedFlow</code> and | ||
<code>StateFlow</code> | ||
(excluding mutable versions of them). | ||
To work with these fields, you may use additional provided APIs:</p> | ||
<p>Firstly, we define two possible states of a flow field: | ||
<emphasis>uninitialized</emphasis> | ||
and | ||
<emphasis>initialized</emphasis> | ||
. | ||
Before the first information about this flow has arrived from a server, | ||
the flow is in | ||
<emphasis>uninitialized</emphasis> | ||
state. | ||
In this state, if you access any of its | ||
<control>fields</control> | ||
(<code>replayCache</code> for <code>SharedFlow</code> and <code>StateFlow</code>, and <code>value</code> | ||
for <code>StateFlow</code>) | ||
you will get a <code>UninitializedRPCFieldException</code>. | ||
If you call a suspend <code>collect</code> method on them, | ||
execution will suspend until the state is | ||
<emphasis>initialized</emphasis> | ||
and then the actual <code>collect</code> method will be executed. | ||
The same ability to suspend execution until the state is | ||
<emphasis>initialized</emphasis> | ||
can be achieved by using <code>awaitFieldInitialization</code> function: | ||
</p> | ||
|
||
<code-block lang="kotlin"> | ||
interface MyService : RPC { | ||
val flow: StateFlow<Int> | ||
} | ||
|
||
// ### Somewhere in client code ### | ||
val myService: 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 | ||
</code-block> | ||
<p>Secondly, we provide you with an instrument to make initialization faster. | ||
By default, all fields are lazy. | ||
By adding <code>@RPCEagerField</code> annotation, you can change this behavior, | ||
so that fields will be initialized when the service in created | ||
(when <code>withService</code> method is called):</p> | ||
|
||
<code-block lang="kotlin"> | ||
interface MyService : RPC { | ||
val lazyFlow: Flow<Int> // initialized on first access | ||
|
||
@RPCEagerField | ||
val eagerFlow: Flow<Int> // initialized on service creation | ||
} | ||
</code-block> | ||
</chapter> | ||
</topic> |
Oops, something went wrong.