diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcDeclarationScanner.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcDeclarationScanner.kt
index 3061edb0d..23a0492f0 100644
--- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcDeclarationScanner.kt
+++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcDeclarationScanner.kt
@@ -11,7 +11,6 @@ import org.jetbrains.kotlin.ir.declarations.IrClass
import org.jetbrains.kotlin.ir.declarations.IrDeclaration
import org.jetbrains.kotlin.ir.declarations.IrProperty
import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
-import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.util.dumpKotlinLike
/**
@@ -47,16 +46,10 @@ internal object RpcDeclarationScanner {
return@memoryOptimizedMap null
}
- val symbol = declaration.getter!!.returnType.classOrNull
-
- val flowType = when (symbol) {
- ctx.flow -> ServiceDeclaration.FlowField.Kind.Plain
- ctx.sharedFlow -> ServiceDeclaration.FlowField.Kind.Shared
- ctx.stateFlow -> ServiceDeclaration.FlowField.Kind.State
- else -> return@memoryOptimizedMap unsupportedDeclaration(service, declaration, logger)
- }
-
- ServiceDeclaration.FlowField(declaration, flowType)
+ error(
+ "Fields are not supported in @Rpc services, this error should be caught by frontend. " +
+ "Please report this issue to the kotlinx-rpc maintainers."
+ )
}
is IrClass -> {
@@ -80,8 +73,7 @@ internal object RpcDeclarationScanner {
return ServiceDeclaration(
service = service,
stubClass = stubClassNotNull,
- methods = declarations.filterIsInstance(),
- fields = declarations.filterIsInstance(),
+ methods = declarations.filterNotNull(),
)
}
}
@@ -89,7 +81,7 @@ internal object RpcDeclarationScanner {
private fun unsupportedDeclaration(service: IrClass, declaration: IrDeclaration, logger: MessageCollector): Nothing? {
logger.report(
severity = CompilerMessageSeverity.LOGGING,
- message = "Unsupported declaration in RemoteService interface ${service.name}: ${declaration.dumpKotlinLike()}",
+ message = "Unsupported declaration in @Rpc interface ${service.name}: ${declaration.dumpKotlinLike()}",
)
return null
diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt
index 9ba01c84f..2c16f608c 100644
--- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt
+++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcIrContext.kt
@@ -10,7 +10,6 @@ import org.jetbrains.kotlin.ir.declarations.IrSimpleFunction
import org.jetbrains.kotlin.ir.symbols.IrClassSymbol
import org.jetbrains.kotlin.ir.symbols.IrPropertySymbol
import org.jetbrains.kotlin.ir.symbols.IrSimpleFunctionSymbol
-import org.jetbrains.kotlin.ir.types.classOrNull
import org.jetbrains.kotlin.ir.types.makeNullable
import org.jetbrains.kotlin.ir.util.functions
import org.jetbrains.kotlin.ir.util.isVararg
@@ -33,34 +32,10 @@ internal class RpcIrContext(
irBuiltIns.arrayClass.typeWith(anyNullable, Variance.OUT_VARIANCE)
}
- val coroutineScope by lazy {
- getIrClassSymbol("kotlinx.coroutines", "CoroutineScope")
- }
-
- val coroutineContext by lazy {
- getIrClassSymbol("kotlin.coroutines", "CoroutineContext")
- }
-
val kTypeClass by lazy {
getIrClassSymbol("kotlin.reflect", "KType")
}
- val lazy by lazy {
- getIrClassSymbol("kotlin", "Lazy")
- }
-
- val function0 by lazy {
- getIrClassSymbol("kotlin", "Function0")
- }
-
- val function1 by lazy {
- getIrClassSymbol("kotlin", "Function1")
- }
-
- val suspendFunction0 by lazy {
- getIrClassSymbol("kotlin.coroutines", "SuspendFunction0")
- }
-
val suspendFunction2 by lazy {
getIrClassSymbol("kotlin.coroutines", "SuspendFunction2")
}
@@ -69,18 +44,6 @@ internal class RpcIrContext(
getIrClassSymbol("kotlinx.coroutines.flow", "Flow")
}
- val sharedFlow by lazy {
- getIrClassSymbol("kotlinx.coroutines.flow", "SharedFlow")
- }
-
- val stateFlow by lazy {
- getIrClassSymbol("kotlinx.coroutines.flow", "StateFlow")
- }
-
- val kProperty1 by lazy {
- getIrClassSymbol("kotlin.reflect", "KProperty1")
- }
-
val pair by lazy {
getIrClassSymbol("kotlin", "Pair")
}
@@ -97,10 +60,6 @@ internal class RpcIrContext(
getRpcIrClassSymbol("WithServiceDescriptor", "internal")
}
- val rpcEagerFieldAnnotation by lazy {
- getRpcIrClassSymbol("RpcEagerField")
- }
-
val rpcServiceDescriptor by lazy {
getRpcIrClassSymbol("RpcServiceDescriptor", "descriptor")
}
@@ -121,22 +80,10 @@ internal class RpcIrContext(
rpcInvokator.subClass("Method")
}
- val rpcInvokatorField by lazy {
- rpcInvokator.subClass("Field")
- }
-
val rpcParameter by lazy {
getRpcIrClassSymbol("RpcParameter", "descriptor")
}
- val rpcDeferredField by lazy {
- getRpcIrClassSymbol("RpcDeferredField", "internal")
- }
-
- val fieldDataObject by lazy {
- getRpcIrClassSymbol("FieldDataObject", "internal")
- }
-
val rpcMethodClass by lazy {
getRpcIrClassSymbol("RpcMethodClass", "internal")
}
@@ -156,18 +103,6 @@ internal class RpcIrContext(
val functions = Functions()
inner class Functions {
- val registerPlainFlowField by lazy {
- namedFunction("kotlinx.rpc", "registerPlainFlowField")
- }
-
- val registerSharedFlowField by lazy {
- namedFunction("kotlinx.rpc", "registerSharedFlowField")
- }
-
- val registerStateFlowField by lazy {
- namedFunction("kotlinx.rpc", "registerStateFlowField")
- }
-
val dataCast by lazy {
namedFunction("kotlinx.rpc.internal", "rpcInternalDataCast")
}
@@ -180,10 +115,6 @@ internal class RpcIrContext(
rpcClient.namedFunction("callServerStreaming")
}
- val provideStubContext by lazy {
- rpcClient.namedFunction("provideStubContext")
- }
-
val asArray by lazy {
rpcMethodClass.namedFunction("asArray")
}
@@ -196,38 +127,6 @@ internal class RpcIrContext(
namedFunction("kotlin", "emptyArray")
}
- val scopedClientCall by lazy {
- namedFunction("kotlinx.rpc.internal", "scopedClientCall")
- }
-
- val lazy by lazy {
- namedFunction("kotlin", "lazy") {
- vsApi {
- it.owner.valueParametersVS().size == 1
- }
- }
- }
-
- val lazyGetValue by lazy {
- namedFunction("kotlin", "getValue") {
- vsApi {
- it.owner.extensionReceiverParameterVS?.type?.classOrNull == this@RpcIrContext.lazy
- }
- }
- }
-
- val listOf by lazy {
- namedFunction("kotlin.collections", "listOf") {
- vsApi {
- it.owner.valueParametersVS().singleOrNull()?.isVararg ?: false
- }
- }
- }
-
- val emptyList by lazy {
- namedFunction("kotlin.collections", "emptyList")
- }
-
val mapOf by lazy {
namedFunction("kotlin.collections", "mapOf") {
vsApi {
@@ -266,10 +165,6 @@ internal class RpcIrContext(
val properties = Properties()
inner class Properties {
- val rpcClientCoroutineContext by lazy {
- rpcClient.namedProperty("coroutineContext")
- }
-
val rpcServiceDescriptorFqName by lazy {
rpcServiceDescriptor.namedProperty("fqName")
}
diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt
index fd46efe84..61a8c07a5 100644
--- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt
+++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/RpcStubGenerator.kt
@@ -27,7 +27,6 @@ import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.name.SpecialNames
import org.jetbrains.kotlin.types.Variance
import org.jetbrains.kotlin.util.OperatorNameConventions
-import org.jetbrains.kotlin.utils.memoryOptimizedPlus
import kotlin.properties.Delegates
private object Stub {
@@ -38,7 +37,6 @@ private object Stub {
private object Descriptor {
const val CALLABLE_MAP = "callableMap"
const val FQ_NAME = "fqName"
- const val GET_FIELDS = "getFields"
const val GET_CALLABLE = "getCallable"
const val CREATE_INSTANCE = "createInstance"
}
@@ -121,12 +119,6 @@ internal class RpcStubGenerator(
stubIdProperty()
clientProperty()
-
- coroutineContextProperty()
-
- declaration.fields.forEach {
- rpcFlowField(it)
- }
}
private var stubIdProperty: IrProperty by Delegates.notNull()
@@ -196,307 +188,6 @@ internal class RpcStubGenerator(
}
}
- private var coroutineContextProperty: IrProperty by Delegates.notNull()
-
- /**
- * `coroutineContext` property from `RemoteService` interface
- *
- * ```kotlin
- * final override val coroutineContext: CoroutineContext = __rpc_client.provideStubContext(__rpc_stub_id)
- * ```
- */
- private fun IrClass.coroutineContextProperty() {
- coroutineContextProperty = addProperty {
- name = Name.identifier("coroutineContext")
- visibility = DescriptorVisibilities.PUBLIC
- modality = Modality.FINAL
- }.apply {
- overriddenSymbols = listOf(ctx.properties.rpcClientCoroutineContext)
-
- addBackingFieldUtil {
- visibility = DescriptorVisibilities.PRIVATE
- type = ctx.coroutineContext.defaultType
- vsApi { isFinalVS = true }
- }.apply {
- val coroutineContextClass = ctx.coroutineContext.owner
-
- initializer = factory.createExpressionBody(
- vsApi {
- IrCallImplVS(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = coroutineContextClass.typeWith(),
- symbol = ctx.functions.provideStubContext.symbol,
- valueArgumentsCount = 1,
- typeArgumentsCount = 0,
- )
- }.apply {
- arguments {
- dispatchReceiver = irCallProperty(stubClass, clientProperty)
-
- values {
- +irCallProperty(stubClass, stubIdProperty)
- }
- }
- }
- )
- }
-
- addDefaultGetter(this@coroutineContextProperty, ctx.irBuiltIns) {
- val serviceCoroutineContext = declaration.service.getPropertyGetter("coroutineContext")
- ?: error(
- "RPC services expected to have \"coroutineContext\" property with getter: " +
- declaration.service.dump()
- )
-
- overriddenSymbols = listOf(serviceCoroutineContext)
- }
- }
- }
-
- /**
- * RPC fields.
- * Can be of two kinds: Lazy and Eager (defined by `@RpcEagerField` annotation)
- *
- * Lazy:
- * ``` kotlin
- * final override val : by lazy {
- * client.registerFlowField(
- * serviceScope = this, // CoroutineScope
- * descriptor = Companion,
- * fieldName = "",
- * serviceId = __rpc_stub_id,
- * )
- * }
- * ```
- *
- *
- * Eager:
- * ```kotlin
- * final override val : =
- * client.registerFlowField(
- * serviceScope = this, // CoroutineScope
- * descriptor = Companion,
- * fieldName = "",
- * serviceId = __rpc_stub_id,
- * )
- * ```
- *
- * Where:
- * - `` - the name of the RPC field
- * - `` - actual type of the field. Can be either Flot, SharedFlow or StateFlow
- * - `` - [ServiceDeclaration.FlowField.Kind]
- */
- @Suppress(
- "detekt.NestedBlockDepth",
- "detekt.LongMethod",
- "detekt.CyclomaticComplexMethod",
- )
- private fun IrClass.rpcFlowField(field: ServiceDeclaration.FlowField) {
- val isLazy = !field.property.hasAnnotation(ctx.rpcEagerFieldAnnotation)
-
- val servicePropertyGetter = field.property.getterOrFail
-
- addProperty {
- name = field.property.name
- visibility = field.property.visibility
- modality = Modality.FINAL
- isDelegated = isLazy
- }.apply {
- val fieldProperty = this
-
- overriddenSymbols = listOf(field.property.symbol)
-
- val fieldType = servicePropertyGetter.returnType
-
- val fieldTypeParameter = (fieldType as IrSimpleType).arguments.single()
-
- val registerFunction = when (field.flowKind) {
- ServiceDeclaration.FlowField.Kind.Plain -> ctx.functions.registerPlainFlowField
- ServiceDeclaration.FlowField.Kind.Shared -> ctx.functions.registerSharedFlowField
- ServiceDeclaration.FlowField.Kind.State -> ctx.functions.registerStateFlowField
- }
-
- val registerCall = vsApi {
- IrCallImplVS(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = fieldType,
- symbol = registerFunction,
- typeArgumentsCount = 1,
- valueArgumentsCount = 4,
- )
- }.apply {
- arguments {
- extensionReceiver = irCallProperty(stubClass, clientProperty)
-
- types {
- +fieldTypeParameter.typeOrFail
- }
-
- values {
- +IrGetValueImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = ctx.coroutineScope.defaultType,
- symbol = stubClassThisReceiver.symbol,
- )
-
- +irGetDescriptor()
-
- +stringConst(field.property.name.asString())
-
- +irCallProperty(stubClass, stubIdProperty)
- }
- }
- }
-
- if (!isLazy) {
- addBackingFieldUtil {
- type = fieldType
- visibility = DescriptorVisibilities.PRIVATE
- vsApi { isFinalVS = true }
- }.apply {
- initializer = factory.createExpressionBody(registerCall)
- }
-
- addDefaultGetter(this@rpcFlowField, ctx.irBuiltIns) {
- visibility = field.property.visibility
- overriddenSymbols = listOf(servicePropertyGetter.symbol)
- }
- } else {
- val lazyFieldType = ctx.lazy.typeWith(fieldType)
-
- val lazyField = addBackingFieldUtil {
- origin = IrDeclarationOrigin.PROPERTY_DELEGATE
- name = propertyDelegateName(this@apply.name)
- visibility = DescriptorVisibilities.PRIVATE
- type = lazyFieldType
- vsApi { isFinalVS = true }
- }.apply {
- val propertyDelegate = this
-
- // initializer for Lazy delegate with lambda
- // inside lambda - 'registerCall' expression is used
- initializer = factory.createExpressionBody(
- vsApi {
- IrCallImplVS(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = lazyFieldType,
- symbol = ctx.functions.lazy,
- typeArgumentsCount = 1,
- valueArgumentsCount = 1,
- )
- }.apply {
- val lambdaType = ctx.function0.typeWith(fieldType)
-
- val lambdaFunction = factory.buildFun {
- origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA
- name = SpecialNames.ANONYMOUS
- visibility = DescriptorVisibilities.LOCAL
- returnType = fieldType
- }.apply {
- parent = propertyDelegate
-
- body = irBuilder(symbol).irBlockBody {
- +irReturn(registerCall)
- }
- }
-
- val lambda = IrFunctionExpressionImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = lambdaType,
- origin = IrStatementOrigin.LAMBDA,
- function = lambdaFunction,
- )
-
- arguments {
- types {
- +fieldType
- }
-
- values {
- +lambda
- }
- }
- }
- )
- }
-
- // Invocation of `operator fun getValue(thisRef: Any?, property: KProperty<*>): T` for delegates
- addGetter {
- origin = IrDeclarationOrigin.DELEGATED_PROPERTY_ACCESSOR
- visibility = this@apply.visibility
- returnType = fieldType
- }.apply {
- val propertyGetter = this
-
- overriddenSymbols = listOf(servicePropertyGetter.symbol)
-
- val getterThisReceiver = vsApi {
- stubClassThisReceiver.copyToVS(propertyGetter, origin = IrDeclarationOrigin.DEFINED)
- }.also {
- vsApi {
- dispatchReceiverParameterVS = it
- }
- }
-
- body = irBuilder(symbol).irBlockBody {
- +irReturn(
- irCall(
- type = fieldType,
- callee = ctx.functions.lazyGetValue,
- typeArgumentsCount = 1,
- ).apply {
- arguments {
- types {
- +fieldType
- }
-
- extensionReceiver = IrGetFieldImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- symbol = lazyField.symbol,
- type = lazyFieldType,
- receiver = IrGetValueImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = stubClass.defaultType,
- symbol = getterThisReceiver.symbol,
- ),
- )
-
- values {
- +IrGetValueImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = stubClass.defaultType,
- symbol = getterThisReceiver.symbol,
- )
-
- +IrPropertyReferenceImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = ctx.kProperty1.typeWith(stubClass.defaultType, fieldType),
- symbol = fieldProperty.symbol,
- typeArgumentsCount = 0,
- field = null,
- getter = propertyGetter.symbol,
- setter = null,
- origin = IrStatementOrigin.PROPERTY_REFERENCE_FOR_DELEGATE,
- )
- }
- }
- }
- )
- }
- }
- }
- }
- }
-
private fun IrClass.generateMethods() {
declaration.methods.forEach {
generateRpcMethod(it)
@@ -555,8 +246,6 @@ internal class RpcStubGenerator(
}
}
- val declaredFunction = this
-
val arguments = method.arguments.memoryOptimizedMap { arg ->
addValueParameter {
name = arg.value.name
@@ -581,63 +270,19 @@ internal class RpcStubGenerator(
return@irBlockBody
}
- +irReturn(
- irCall(
- callee = ctx.functions.scopedClientCall,
- type = method.function.returnType,
- ).apply {
- // suspend lambda
- // it's type is not available at runtime, but in fact exists
- val lambdaType = ctx.suspendFunction0.typeWith(method.function.returnType)
-
- val functionLambda = factory.buildFun {
- origin = IrDeclarationOrigin.LOCAL_FUNCTION_FOR_LAMBDA
- name = SpecialNames.ANONYMOUS
- visibility = DescriptorVisibilities.LOCAL
- modality = Modality.FINAL
- returnType = method.function.returnType
- isSuspend = true
- }.apply {
- parent = declaredFunction
-
- body = irBuilder(symbol).irBlockBody {
- val call = irRpcMethodClientCall(
- method = method,
- functionThisReceiver = functionThisReceiver,
- isMethodObject = isMethodObject,
- methodClass = methodClass,
- arguments = arguments,
- )
-
- if (method.function.returnType == ctx.irBuiltIns.unitType) {
- +call
- } else {
- +irReturn(call)
- }
- }
- }
-
- val lambda = IrFunctionExpressionImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = lambdaType,
- origin = IrStatementOrigin.LAMBDA,
- function = functionLambda,
- )
-
- arguments {
- types {
- +method.function.returnType
- }
-
- values {
- +irGet(ctx.coroutineScope.defaultType, functionThisReceiver.symbol)
-
- +lambda
- }
- }
- }
+ val call = irRpcMethodClientCall(
+ method = method,
+ functionThisReceiver = functionThisReceiver,
+ isMethodObject = isMethodObject,
+ methodClass = methodClass,
+ arguments = arguments,
)
+
+ if (method.function.returnType == ctx.irBuiltIns.unitType) {
+ +call
+ } else {
+ +irReturn(call)
+ }
}
}
}
@@ -970,8 +615,6 @@ internal class RpcStubGenerator(
generateGetCallableFunction()
generateCreateInstanceFunction()
-
- generateGetFieldsFunction()
}
/**
@@ -1008,7 +651,7 @@ internal class RpcStubGenerator(
private val invokators = mutableMapOf()
private fun IrClass.generateInvokators() {
- (declaration.methods memoryOptimizedPlus declaration.fields).forEachIndexed { i, callable ->
+ declaration.methods.forEachIndexed { i, callable ->
generateInvokator(i, callable)
}
}
@@ -1055,12 +698,7 @@ internal class RpcStubGenerator(
name = Name.identifier("${callable.name}Invokator")
visibility = DescriptorVisibilities.PRIVATE
}.apply {
- val propertyTypeSymbol = when (callable) {
- is ServiceDeclaration.Method -> ctx.rpcInvokatorMethod
- is ServiceDeclaration.FlowField -> ctx.rpcInvokatorField
- }
-
- val propertyType = propertyTypeSymbol.typeWith(declaration.serviceType)
+ val propertyType = ctx.rpcInvokatorMethod.typeWith(declaration.serviceType)
addBackingFieldUtil {
visibility = DescriptorVisibilities.PRIVATE
@@ -1133,14 +771,6 @@ internal class RpcStubGenerator(
}
}
}
-
- is ServiceDeclaration.FlowField -> {
- irCall(callable.property.getterOrFail).apply {
- arguments {
- dispatchReceiver = irGet(serviceParameter)
- }
- }
- }
}
+irReturn(call)
@@ -1153,11 +783,6 @@ internal class RpcStubGenerator(
ctx.anyNullable, // data
ctx.anyNullable, // returnType
)
-
- is ServiceDeclaration.FlowField -> ctx.function1.typeWith(
- declaration.serviceType, // service
- ctx.anyNullable, // returnType
- )
}
val lambda = IrFunctionExpressionImpl(
@@ -1220,7 +845,7 @@ internal class RpcStubGenerator(
vsApi { isFinalVS = true }
visibility = DescriptorVisibilities.PRIVATE
}.apply {
- val isEmpty = declaration.methods.isEmpty() && declaration.fields.isEmpty()
+ val isEmpty = declaration.methods.isEmpty()
initializer = factory.createExpressionBody(
vsApi {
@@ -1248,7 +873,7 @@ internal class RpcStubGenerator(
val varargType = ctx.irBuiltIns.arrayClass.typeWith(pairType, Variance.OUT_VARIANCE)
- val callables = declaration.methods memoryOptimizedPlus declaration.fields
+ val callables = declaration.methods
val vararg = IrVarargImpl(
startOffset = UNDEFINED_OFFSET,
@@ -1347,28 +972,22 @@ internal class RpcStubGenerator(
val dataType = when (callable) {
is ServiceDeclaration.Method -> methodClasses[i].defaultType
- is ServiceDeclaration.FlowField -> ctx.fieldDataObject.defaultType
}
- val returnType = when (callable) {
- is ServiceDeclaration.Method -> when {
- callable.function.isNonSuspendingWithFlowReturn() -> {
- (callable.function.returnType as IrSimpleType).arguments.single().typeOrFail
- }
-
- else -> {
- callable.function.returnType
- }
+ val returnType = when {
+ callable.function.isNonSuspendingWithFlowReturn() -> {
+ (callable.function.returnType as IrSimpleType).arguments.single().typeOrFail
}
- is ServiceDeclaration.FlowField -> callable.property.getterOrFail.returnType
+ else -> {
+ callable.function.returnType
+ }
}
val invokator = invokators[callable.name]
?: error("Expected invokator for ${callable.name} in ${declaration.service.name}")
- val parameters = (callable as? ServiceDeclaration.Method)?.arguments
- ?: emptyList()
+ val parameters = callable.arguments
val callee = if (parameters.isEmpty()) {
ctx.functions.emptyArray
@@ -1449,7 +1068,7 @@ internal class RpcStubGenerator(
+arrayOfCall
- +booleanConst(callable is ServiceDeclaration.Method && !callable.function.isSuspend)
+ +booleanConst(!callable.function.isSuspend)
}
}
}
@@ -1564,87 +1183,6 @@ internal class RpcStubGenerator(
}
}
- /**
- * Function for getting a list of all RPC fields in a given service as [RpcDeferredField<*>]
- *
- * ```kotlin
- * final override fun getFields(service: MyService): List> {
- * return listOf( // or emptyList() if no fields
- * service.,
- * ...
- * service.,
- * ) as List>
- * }
- * ```
- *
- * Where:
- * - `` - the k-th field of a given service
- */
- private fun IrClass.generateGetFieldsFunction() {
- val listType = ctx.irBuiltIns.listClass.typeWith(ctx.rpcDeferredField.starProjectedType)
-
- addFunction {
- name = Name.identifier(Descriptor.GET_FIELDS)
- visibility = DescriptorVisibilities.PUBLIC
- modality = Modality.OPEN
-
- returnType = listType
- }.apply {
- overriddenSymbols = listOf(ctx.rpcServiceDescriptor.functionByName(Descriptor.GET_FIELDS))
-
- vsApi {
- dispatchReceiverParameterVS = stubCompanionObjectThisReceiver
- .copyToVS(this@apply, origin = IrDeclarationOrigin.DEFINED)
- }
-
- val service = addValueParameter {
- name = Name.identifier("service")
- type = declaration.serviceType
- }
-
- body = irBuilder(symbol).irBlockBody {
- val isEmpty = declaration.fields.isEmpty()
-
- val anyListType = ctx.irBuiltIns.listClass.typeWith(ctx.anyNullable)
-
- val listCall = irCall(
- callee = if (isEmpty) ctx.functions.emptyList else ctx.functions.listOf,
- type = anyListType,
- ).apply listApply@{
- if (isEmpty) {
- arguments {
- types { +ctx.anyNullable }
- }
-
- return@listApply
- }
-
- val vararg = IrVarargImpl(
- startOffset = UNDEFINED_OFFSET,
- endOffset = UNDEFINED_OFFSET,
- type = ctx.arrayOfAnyNullable,
- varargElementType = ctx.anyNullable,
- elements = declaration.fields.memoryOptimizedMap {
- irCallProperty(irGet(service), it.property)
- }
- )
-
- arguments {
- types {
- +ctx.anyNullable
- }
-
- values {
- +vararg
- }
- }
- }
-
- +irReturn(irAs(listCall, listType))
- }
- }
- }
-
// Associated object annotation works on JS, WASM, and Native platforms.
// See https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/find-associated-object.html
private fun addAssociatedObjectAnnotationIfPossible() {
diff --git a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt
index 3c3dce74a..0af4fd2f6 100644
--- a/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt
+++ b/compiler-plugin/compiler-plugin-backend/src/main/core/kotlinx/rpc/codegen/extension/ServiceDeclaration.kt
@@ -16,7 +16,6 @@ class ServiceDeclaration(
val service: IrClass,
val stubClass: IrClass,
val methods: List,
- val fields: List,
) {
val fqName = service.kotlinFqName.asString()
@@ -37,15 +36,4 @@ class ServiceDeclaration(
val type: IrType,
)
}
-
- class FlowField(
- val property: IrProperty,
- val flowKind: Kind,
- ) : Callable {
- override val name: String = property.name.asString()
-
- enum class Kind {
- Plain, Shared, State;
- }
- }
}
diff --git a/compiler-plugin/compiler-plugin-cli/src/main/latest/kotlinx/rpc/codegen/RpcCompilerPlugin.kt b/compiler-plugin/compiler-plugin-cli/src/main/latest/kotlinx/rpc/codegen/RpcCompilerPlugin.kt
index 441f6794a..d450e67a3 100644
--- a/compiler-plugin/compiler-plugin-cli/src/main/latest/kotlinx/rpc/codegen/RpcCompilerPlugin.kt
+++ b/compiler-plugin/compiler-plugin-cli/src/main/latest/kotlinx/rpc/codegen/RpcCompilerPlugin.kt
@@ -18,13 +18,6 @@ class RpcCommandLineProcessor : CommandLineProcessor {
override val pluginId = "kotlinx-rpc"
override val pluginOptions = listOf(
- StrictModeCliOptions.STATE_FLOW,
- StrictModeCliOptions.SHARED_FLOW,
- StrictModeCliOptions.NESTED_FLOW,
- StrictModeCliOptions.STREAM_SCOPED_FUNCTIONS,
- StrictModeCliOptions.SUSPENDING_SERVER_STREAMING,
- StrictModeCliOptions.NOT_TOP_LEVEL_SERVER_FLOW,
- StrictModeCliOptions.FIELDS,
RpcFirCliOptions.ANNOTATION_TYPE_SAFETY,
)
@@ -33,10 +26,6 @@ class RpcCommandLineProcessor : CommandLineProcessor {
value: String,
configuration: CompilerConfiguration,
) {
- if (option.processAsStrictModeOption(value, configuration)) {
- return
- }
-
when (option) {
RpcFirCliOptions.ANNOTATION_TYPE_SAFETY -> {
@Suppress("NullableBooleanElvis")
diff --git a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt
index c5da2bcaa..a60df0872 100644
--- a/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt
+++ b/compiler-plugin/compiler-plugin-common/src/main/core/kotlinx/rpc/codegen/common/Names.kt
@@ -4,13 +4,11 @@
package kotlinx.rpc.codegen.common
-import org.jetbrains.kotlin.name.CallableId
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
object RpcClassId {
- val remoteServiceInterface = ClassId(FqName("kotlinx.rpc"), Name.identifier("RemoteService"))
val rpcAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("Rpc"))
val checkedTypeAnnotation = ClassId(FqName("kotlinx.rpc.annotations"), Name.identifier("CheckedTypeAnnotation"))
@@ -22,16 +20,6 @@ object RpcClassId {
val stateFlow = ClassId(FqName("kotlinx.coroutines.flow"), Name.identifier("StateFlow"))
}
-object RpcCallableId {
- val streamScoped = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("streamScoped"))
- val withStreamScope = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("withStreamScope"))
- val StreamScope = CallableId(FqName("kotlinx.rpc.krpc"), Name.identifier("StreamScope"))
- val invokeOnStreamScopeCompletion = CallableId(
- FqName("kotlinx.rpc.krpc"),
- Name.identifier("invokeOnStreamScopeCompletion"),
- )
-}
-
object RpcNames {
val SERVICE_STUB_NAME: Name = Name.identifier("\$rpcServiceStub")
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt
index 2a8f38125..806540499 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcAdditionalCheckers.kt
@@ -7,7 +7,6 @@ package kotlinx.rpc.codegen
import kotlinx.rpc.codegen.checkers.FirCheckedAnnotationHelper
import kotlinx.rpc.codegen.checkers.FirRpcDeclarationCheckers
import kotlinx.rpc.codegen.checkers.FirRpcExpressionCheckers
-import kotlinx.rpc.codegen.checkers.diagnostics.FirRpcStrictModeDiagnostics
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.declaration.DeclarationCheckers
@@ -32,7 +31,6 @@ class FirRpcAdditionalCheckers(
session = session,
serializationIsPresent = serializationIsPresent,
annotationTypeSafetyEnabled = configuration.get(RpcFirConfigurationKeys.ANNOTATION_TYPE_SAFETY, true),
- modes = configuration.strictModeAggregator(),
)
override val declarationCheckers: DeclarationCheckers = FirRpcDeclarationCheckers(ctx)
@@ -43,10 +41,7 @@ class FirCheckersContext(
private val session: FirSession,
val serializationIsPresent: Boolean,
val annotationTypeSafetyEnabled: Boolean,
- modes: StrictModeAggregator,
) {
- val strictModeDiagnostics = FirRpcStrictModeDiagnostics(modes)
-
val typeParametersCache = session.firCachesFactory.createCache { typeParameter: FirTypeParameterSymbol ->
FirCheckedAnnotationHelper.checkedAnnotations(session, typeParameter)
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcExtensionRegistrar.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcExtensionRegistrar.kt
index ae29208d0..0502bf622 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcExtensionRegistrar.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcExtensionRegistrar.kt
@@ -30,7 +30,5 @@ class FirRpcExtensionRegistrar(private val configuration: CompilerConfiguration)
}
+CFactory { FirRpcAdditionalCheckers(it, serializationIsPresent, configuration) }
-
- +SFactory { FirRpcSupertypeGenerator(it, logger) }
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt
deleted file mode 100644
index 268ce13a4..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcSupertypeGeneratorAbstract.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-/*
- * 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.codegen
-
-import kotlinx.rpc.codegen.common.RpcClassId
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClass
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.declarations.utils.isInterface
-import org.jetbrains.kotlin.fir.extensions.FirDeclarationPredicateRegistrar
-import org.jetbrains.kotlin.fir.extensions.FirSupertypeGenerationExtension
-import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.name.ClassId
-
-abstract class FirRpcSupertypeGeneratorAbstract(
- session: FirSession,
- @Suppress("unused") private val logger: MessageCollector,
-) : FirSupertypeGenerationExtension(session) {
- override fun FirDeclarationPredicateRegistrar.registerPredicates() {
- register(FirRpcPredicates.rpc)
- }
-
- override fun needTransformSupertypes(declaration: FirClassLikeDeclaration): Boolean {
- return session.predicateBasedProvider.matches(
- predicate = FirRpcPredicates.rpc,
- declaration = declaration,
- ) && declaration is FirClass && declaration.isInterface
- }
-
- protected fun computeAdditionalSupertypesAbstract(
- resolvedSupertypes: List,
- ): List {
- if (resolvedSupertypes.any { it.doesMatchesClassId(session, RpcClassId.remoteServiceInterface) }) {
- return emptyList()
- }
-
- return listOf(RpcClassId.remoteServiceInterface)
- }
-}
-
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt
index 20869b4cc..54ed7fdea 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/FirRpcUtils.kt
@@ -4,7 +4,6 @@
package kotlinx.rpc.codegen
-import kotlinx.rpc.codegen.common.RpcClassId
import org.jetbrains.kotlin.KtSourceElement
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.toAnnotationClassId
@@ -15,17 +14,11 @@ import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
import org.jetbrains.kotlin.fir.resolve.fullyExpandedType
import org.jetbrains.kotlin.fir.symbols.FirBasedSymbol
import org.jetbrains.kotlin.fir.symbols.SymbolInternals
-import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.types.ConeClassLikeType
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
import org.jetbrains.kotlin.fir.types.FirTypeRef
import org.jetbrains.kotlin.fir.types.coneTypeSafe
import org.jetbrains.kotlin.name.ClassId
-fun FirClassSymbol<*>.isRemoteService(session: FirSession): Boolean = resolvedSuperTypeRefs.any {
- it.doesMatchesClassId(session, RpcClassId.remoteServiceInterface)
-}
-
fun FirBasedSymbol<*>.rpcAnnotationSource(
session: FirSession,
predicate: DeclarationPredicate,
@@ -53,14 +46,6 @@ fun List.rpcAnnotation(session: FirSession, predicate: Declaratio
}
}
-fun FirClassSymbol<*>.remoteServiceSupertypeSource(session: FirSession): KtSourceElement? {
- return remoteServiceSupertype(session)?.source
-}
-
-fun FirClassSymbol<*>.remoteServiceSupertype(session: FirSession): FirResolvedTypeRef? {
- return resolvedSuperTypeRefs.find { it.doesMatchesClassId(session, RpcClassId.remoteServiceInterface) }
-}
-
@OptIn(SymbolInternals::class)
internal fun FirTypeRef.doesMatchesClassId(session: FirSession, classId: ClassId): Boolean {
return coneTypeSafe()?.fullyExpandedType(session)?.lookupTag?.classId == classId
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/StrictMode.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/StrictMode.kt
deleted file mode 100644
index b8e4ca918..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/StrictMode.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-/*
- * 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.codegen
-
-import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption
-import org.jetbrains.kotlin.compiler.plugin.CliOption
-import org.jetbrains.kotlin.config.CompilerConfiguration
-import org.jetbrains.kotlin.config.CompilerConfigurationKey
-import kotlin.text.lowercase
-
-enum class StrictMode {
- NONE, WARNING, ERROR;
-
- companion object {
- fun fromCli(value: String): StrictMode? {
- return when (value.lowercase()) {
- "none" -> NONE
- "warning" -> WARNING
- "error" -> ERROR
- else -> null
- }
- }
- }
-}
-
-data class StrictModeAggregator(
- val stateFlow: StrictMode,
- val sharedFlow: StrictMode,
- val nestedFlow: StrictMode,
- val streamScopedFunctions: StrictMode,
- val suspendingServerStreaming: StrictMode,
- val notTopLevelServerFlow: StrictMode,
- val fields: StrictMode,
-)
-
-object StrictModeConfigurationKeys {
- val STATE_FLOW = CompilerConfigurationKey.create("state flow rpc mode")
- val SHARED_FLOW = CompilerConfigurationKey.create("shared flow rpc mode")
- val NESTED_FLOW = CompilerConfigurationKey.create("nested flow rpc mode")
- val STREAM_SCOPED_FUNCTIONS = CompilerConfigurationKey.create("stream scoped rpc mode")
- val SUSPENDING_SERVER_STREAMING = CompilerConfigurationKey.create(
- "suspending server streaming rpc mode"
- )
- val NOT_TOP_LEVEL_SERVER_FLOW = CompilerConfigurationKey.create("not top level server flow rpc mode")
- val FIELDS = CompilerConfigurationKey.create("fields rpc mode")
-}
-
-fun CompilerConfiguration.strictModeAggregator(): StrictModeAggregator {
- return StrictModeAggregator(
- stateFlow = get(StrictModeConfigurationKeys.STATE_FLOW, StrictMode.ERROR),
- sharedFlow = get(StrictModeConfigurationKeys.SHARED_FLOW, StrictMode.ERROR),
- nestedFlow = get(StrictModeConfigurationKeys.NESTED_FLOW, StrictMode.ERROR),
- streamScopedFunctions = get(StrictModeConfigurationKeys.STREAM_SCOPED_FUNCTIONS, StrictMode.ERROR),
- suspendingServerStreaming = get(StrictModeConfigurationKeys.SUSPENDING_SERVER_STREAMING, StrictMode.ERROR),
- notTopLevelServerFlow = get(StrictModeConfigurationKeys.NOT_TOP_LEVEL_SERVER_FLOW, StrictMode.ERROR),
- fields = get(StrictModeConfigurationKeys.FIELDS, StrictMode.ERROR),
- )
-}
-
-object StrictModeCliOptions {
- val STATE_FLOW = CliOption(
- optionName = "strict-stateFlow",
- valueDescription = VALUE_DESCRIPTION,
- description = description("StateFlow"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val SHARED_FLOW = CliOption(
- optionName = "strict-sharedFlow",
- valueDescription = VALUE_DESCRIPTION,
- description = description("SharedFlow"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val NESTED_FLOW = CliOption(
- optionName = "strict-nested-flow",
- valueDescription = VALUE_DESCRIPTION,
- description = description("Nested flows"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val STREAM_SCOPED_FUNCTIONS = CliOption(
- optionName = "strict-stream-scope",
- valueDescription = VALUE_DESCRIPTION,
- description = description("Stream Scopes"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val SUSPENDING_SERVER_STREAMING = CliOption(
- optionName = "strict-suspending-server-streaming",
- valueDescription = VALUE_DESCRIPTION,
- description = description("suspending server streaming methods"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val NOT_TOP_LEVEL_SERVER_FLOW = CliOption(
- optionName = "strict-not-top-level-server-flow",
- valueDescription = VALUE_DESCRIPTION,
- description = description("not top-level server streaming declarations"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- val FIELDS = CliOption(
- optionName = "strict-fields",
- valueDescription = VALUE_DESCRIPTION,
- description = description("fields"),
- required = false,
- allowMultipleOccurrences = false,
- )
-
- const val VALUE_DESCRIPTION = "none, warning or error"
-
- fun description(entity: String): String {
- return "Diagnostic level for $entity in @Rpc services."
- }
-
- val configurationMapper = mapOf(
- STATE_FLOW to StrictModeConfigurationKeys.STATE_FLOW,
- SHARED_FLOW to StrictModeConfigurationKeys.SHARED_FLOW,
- NESTED_FLOW to StrictModeConfigurationKeys.NESTED_FLOW,
- STREAM_SCOPED_FUNCTIONS to StrictModeConfigurationKeys.STREAM_SCOPED_FUNCTIONS,
- SUSPENDING_SERVER_STREAMING to StrictModeConfigurationKeys.SUSPENDING_SERVER_STREAMING,
- NOT_TOP_LEVEL_SERVER_FLOW to StrictModeConfigurationKeys.NOT_TOP_LEVEL_SERVER_FLOW,
- FIELDS to StrictModeConfigurationKeys.FIELDS,
- )
-}
-
-fun AbstractCliOption.processAsStrictModeOption(value: String, configuration: CompilerConfiguration): Boolean {
- val key = StrictModeCliOptions.configurationMapper[this] ?: return false
- val mode = StrictMode.fromCli(value) ?: return false
-
- configuration.put(key, mode)
- return true
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt
index edf1ca500..8049d18e6 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcAnnotationChecker.kt
@@ -8,8 +8,6 @@ import kotlinx.rpc.codegen.FirCheckersContext
import kotlinx.rpc.codegen.FirRpcPredicates
import kotlinx.rpc.codegen.checkers.diagnostics.FirRpcDiagnostics
import kotlinx.rpc.codegen.common.RpcClassId
-import kotlinx.rpc.codegen.isRemoteService
-import kotlinx.rpc.codegen.remoteServiceSupertypeSource
import kotlinx.rpc.codegen.rpcAnnotation
import kotlinx.rpc.codegen.rpcAnnotationSource
import org.jetbrains.kotlin.descriptors.ClassKind
@@ -51,14 +49,6 @@ object FirRpcAnnotationChecker {
)
}
- if (declaration.symbol.isRemoteService(context.session) && !rpcAnnotated) {
- reporter.reportOn(
- source = declaration.symbol.remoteServiceSupertypeSource(context.session),
- factory = FirRpcDiagnostics.MISSING_RPC_ANNOTATION,
- context = context,
- )
- }
-
if (rpcAnnotated && !ctx.serializationIsPresent && isMetaAnnotated) {
reporter.reportOn(
source = declaration.symbol.rpcAnnotationSource(
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcCheckers.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcCheckers.kt
index c29af0220..a31a73cff 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcCheckers.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcCheckers.kt
@@ -16,7 +16,7 @@ import org.jetbrains.kotlin.fir.analysis.checkers.expression.FirFunctionCallChec
class FirRpcDeclarationCheckers(ctx: FirCheckersContext) : DeclarationCheckers() {
override val regularClassCheckers: Set = setOfNotNull(
FirRpcAnnotationCheckerVS(ctx),
- if (ctx.serializationIsPresent) FirRpcStrictModeClassCheckerVS(ctx) else null,
+ if (ctx.serializationIsPresent) FirRpcStrictModeClassCheckerVS() else null,
FirRpcServiceDeclarationCheckerVS(ctx),
)
@@ -36,6 +36,5 @@ class FirRpcDeclarationCheckers(ctx: FirCheckersContext) : DeclarationCheckers()
class FirRpcExpressionCheckers(ctx: FirCheckersContext) : ExpressionCheckers() {
override val functionCallCheckers: Set = setOf(
FirCheckedAnnotationFunctionCallCheckerVS(ctx),
- FirRpcStrictModeExpressionCheckerVS(ctx),
)
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcStrictModeClassChecker.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcStrictModeClassChecker.kt
index 5f6eaf1c6..5b0ba1dfd 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcStrictModeClassChecker.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/FirRpcStrictModeClassChecker.kt
@@ -4,10 +4,8 @@
package kotlinx.rpc.codegen.checkers
-import kotlinx.rpc.codegen.FirCheckersContext
import kotlinx.rpc.codegen.FirRpcPredicates
import kotlinx.rpc.codegen.checkers.diagnostics.FirRpcStrictModeDiagnostics
-import kotlinx.rpc.codegen.common.RpcCallableId
import kotlinx.rpc.codegen.common.RpcClassId
import kotlinx.rpc.codegen.vsApi
import org.jetbrains.kotlin.KtSourceElement
@@ -19,9 +17,7 @@ import org.jetbrains.kotlin.fir.analysis.checkers.extractArgumentsTypeRefAndSour
import org.jetbrains.kotlin.fir.analysis.checkers.toClassLikeSymbol
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.declarations.utils.isSuspend
-import org.jetbrains.kotlin.fir.expressions.FirFunctionCall
import org.jetbrains.kotlin.fir.extensions.predicateBasedProvider
-import org.jetbrains.kotlin.fir.references.toResolvedCallableSymbol
import org.jetbrains.kotlin.fir.scopes.impl.toConeType
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
@@ -33,33 +29,8 @@ import org.jetbrains.kotlin.utils.memoryOptimizedPlus
import org.jetbrains.kotlinx.serialization.compiler.fir.services.FirSerializablePropertiesProvider
import org.jetbrains.kotlinx.serialization.compiler.fir.services.serializablePropertiesProvider
-object FirRpcStrictModeExpressionChecker {
- private val streamScopeFunctions = setOf(
- RpcCallableId.StreamScope,
- RpcCallableId.streamScoped,
- RpcCallableId.withStreamScope,
- RpcCallableId.invokeOnStreamScopeCompletion,
- )
-
- fun check(
- ctx: FirCheckersContext,
- expression: FirFunctionCall,
- context: CheckerContext,
- reporter: DiagnosticReporter,
- ) {
- expression.calleeReference.toResolvedCallableSymbol()?.let { symbol ->
- if (symbol.callableId in streamScopeFunctions) {
- ctx.strictModeDiagnostics.STREAM_SCOPE_FUNCTION_IN_RPC?.let {
- reporter.reportOn(expression.calleeReference.source, it, context)
- }
- }
- }
- }
-}
-
object FirRpcStrictModeClassChecker {
fun check(
- ctx: FirCheckersContext,
declaration: FirRegularClass,
context: CheckerContext,
reporter: DiagnosticReporter,
@@ -72,13 +43,11 @@ object FirRpcStrictModeClassChecker {
vsApi { declaration.declarationsVS(context.session) }.forEach { declaration ->
when (declaration) {
is FirPropertySymbol -> {
- ctx.strictModeDiagnostics.FIELD_IN_RPC_SERVICE?.let {
- reporter.reportOn(declaration.source, it, context)
- }
+ reporter.reportOn(declaration.source, FirRpcStrictModeDiagnostics.FIELD_IN_RPC_SERVICE, context)
}
is FirNamedFunctionSymbol -> {
- checkFunction(ctx, declaration, context, reporter, serializablePropertiesProvider)
+ checkFunction(declaration, context, reporter, serializablePropertiesProvider)
}
else -> {}
@@ -87,14 +56,13 @@ object FirRpcStrictModeClassChecker {
}
private fun checkFunction(
- ctx: FirCheckersContext,
function: FirNamedFunctionSymbol,
context: CheckerContext,
reporter: DiagnosticReporter,
serializablePropertiesProvider: FirSerializablePropertiesProvider,
) {
fun reportOn(element: KtSourceElement?, checker: FirRpcStrictModeDiagnostics.() -> KtDiagnosticFactory0?) {
- reporter.reportOn(element, ctx.strictModeDiagnostics.checker() ?: return, context)
+ reporter.reportOn(element, FirRpcStrictModeDiagnostics.checker() ?: return, context)
}
val returnClassSymbol = vsApi {
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt
index adada6411..48cbb9bbd 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/FirRpcDiagnostics.kt
@@ -6,7 +6,6 @@
package kotlinx.rpc.codegen.checkers.diagnostics
-import kotlinx.rpc.codegen.StrictModeAggregator
import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
import org.jetbrains.kotlin.diagnostics.error0
import org.jetbrains.kotlin.diagnostics.error1
@@ -26,7 +25,6 @@ import org.jetbrains.kotlin.psi.KtElement
// ###########################################################################
object FirRpcDiagnostics : RpcKtDiagnosticsContainer() {
- val MISSING_RPC_ANNOTATION by error0()
val MISSING_SERIALIZATION_MODULE by error0()
val WRONG_RPC_ANNOTATION_TARGET by error1()
val CHECKED_ANNOTATION_VIOLATION by error3>()
@@ -40,17 +38,15 @@ object FirRpcDiagnostics : RpcKtDiagnosticsContainer() {
}
}
-@Suppress("PropertyName", "detekt.VariableNaming")
-class FirRpcStrictModeDiagnostics(val modes: StrictModeAggregator) : RpcKtDiagnosticsContainer() {
- val STATE_FLOW_IN_RPC_SERVICE by modded0(modes.stateFlow)
- val SHARED_FLOW_IN_RPC_SERVICE by modded0(modes.sharedFlow)
- val NESTED_STREAMING_IN_RPC_SERVICE by modded0(modes.nestedFlow)
- val STREAM_SCOPE_FUNCTION_IN_RPC by modded0(modes.streamScopedFunctions)
- val SUSPENDING_SERVER_STREAMING_IN_RPC_SERVICE by modded0(modes.suspendingServerStreaming)
- val NON_TOP_LEVEL_SERVER_STREAMING_IN_RPC_SERVICE by modded0(modes.notTopLevelServerFlow)
- val FIELD_IN_RPC_SERVICE by modded0(modes.fields)
+object FirRpcStrictModeDiagnostics : RpcKtDiagnosticsContainer() {
+ val STATE_FLOW_IN_RPC_SERVICE by error0()
+ val SHARED_FLOW_IN_RPC_SERVICE by error0()
+ val NESTED_STREAMING_IN_RPC_SERVICE by error0()
+ val SUSPENDING_SERVER_STREAMING_IN_RPC_SERVICE by error0()
+ val NON_TOP_LEVEL_SERVER_STREAMING_IN_RPC_SERVICE by error0()
+ val FIELD_IN_RPC_SERVICE by error0()
override fun getRendererFactoryVs(): BaseDiagnosticRendererFactory {
- return RpcStrictModeDiagnosticRendererFactory(this)
+ return RpcStrictModeDiagnosticRendererFactory
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt
index 792fb24d5..56f83a855 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/core/kotlinx/rpc/codegen/checkers/diagnostics/RpcDiagnosticRendererFactory.kt
@@ -4,21 +4,12 @@
package kotlinx.rpc.codegen.checkers.diagnostics
-import kotlinx.rpc.codegen.StrictMode
-import kotlinx.rpc.codegen.StrictModeAggregator
import org.jetbrains.kotlin.diagnostics.rendering.BaseDiagnosticRendererFactory
import org.jetbrains.kotlin.diagnostics.rendering.Renderer
import org.jetbrains.kotlin.fir.analysis.diagnostics.FirDiagnosticRenderers
object RpcDiagnosticRendererFactory : BaseDiagnosticRendererFactory() {
override val MAP by RpcKtDiagnosticFactoryToRendererMap("Rpc") { map ->
- map.put(
- factory = FirRpcDiagnostics.MISSING_RPC_ANNOTATION,
- message = "Missing @Rpc annotation. " +
- "All services children of kotlinx.rpc.RemoteService " +
- "must be annotated with kotlinx.rpc.annotations.Rpc",
- )
-
map.put(
factory = FirRpcDiagnostics.MISSING_SERIALIZATION_MODULE,
message = "Missing kotlinx.serialization plugin in the module. " +
@@ -78,68 +69,41 @@ private fun Int.indexPositionSpelled(): String {
return "$padded$suffix"
}
-class RpcStrictModeDiagnosticRendererFactory(
- private val diagnostics: FirRpcStrictModeDiagnostics,
-) : BaseDiagnosticRendererFactory() {
+object RpcStrictModeDiagnosticRendererFactory : BaseDiagnosticRendererFactory() {
override val MAP by RpcKtDiagnosticFactoryToRendererMap("RpcStrictMode") { map ->
- diagnostics.STATE_FLOW_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("StateFlow") { stateFlow },
- )
- }
-
- diagnostics.SHARED_FLOW_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("SharedFlow") { sharedFlow },
- )
- }
-
- diagnostics.NESTED_STREAMING_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("Nested streaming") { nestedFlow },
- )
- }
-
- diagnostics.STREAM_SCOPE_FUNCTION_IN_RPC?.let {
- map.put(
- factory = it,
- message = message("Stream scope usage") { streamScopedFunctions },
- )
- }
-
- diagnostics.SUSPENDING_SERVER_STREAMING_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("Suspend function declaration with server streaming") { suspendingServerStreaming },
- )
- }
-
- diagnostics.NON_TOP_LEVEL_SERVER_STREAMING_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("Not top-level server-side streaming") { sharedFlow },
- )
- }
-
- diagnostics.FIELD_IN_RPC_SERVICE?.let {
- map.put(
- factory = it,
- message = message("Field declaration") { fields },
- )
- }
- }
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.STATE_FLOW_IN_RPC_SERVICE,
+ message = message("StateFlow")
+ )
- private fun message(entityName: String, selector: StrictModeAggregator.() -> StrictMode): String {
- val actionWord = when (diagnostics.modes.selector()) {
- StrictMode.NONE -> ""
- StrictMode.WARNING -> "deprecated"
- StrictMode.ERROR -> "prohibited"
- }
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.SHARED_FLOW_IN_RPC_SERVICE,
+ message = message("SharedFlow"),
+ )
+
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.NESTED_STREAMING_IN_RPC_SERVICE,
+ message = message("Nested streaming"),
+ )
+
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.SUSPENDING_SERVER_STREAMING_IN_RPC_SERVICE,
+ message = message("Suspend function declaration with server streaming"),
+ )
+
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.NON_TOP_LEVEL_SERVER_STREAMING_IN_RPC_SERVICE,
+ message = message("Not top-level server-side streaming"),
+ )
+
+ map.put(
+ factory = FirRpcStrictModeDiagnostics.FIELD_IN_RPC_SERVICE,
+ message = message("Field declaration"),
+ )
+ }
- return "$entityName is $actionWord in @Rpc services in strict mode. " +
+ private fun message(entityName: String): String {
+ return "$entityName is prohibited in @Rpc services in strict mode. " +
"Support will be removed completely in the 0.8.0 release."
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
deleted file mode 100644
index 90269d1d8..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.codegen
-
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.types.ConeKotlinType
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-
-class FirRpcSupertypeGenerator(
- session: FirSession,
- logger: MessageCollector,
-) : FirRpcSupertypeGeneratorAbstract(session, logger) {
- override fun computeAdditionalSupertypes(
- classLikeDeclaration: FirClassLikeDeclaration,
- resolvedSupertypes: List,
- typeResolver: TypeResolveService,
- ): List {
- return computeAdditionalSupertypesAbstract(resolvedSupertypes).map {
- it.constructClassLikeType(emptyArray(), isMarkedNullable = false)
- }
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt b/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
index ca76f64c2..f6fcdbcad 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
@@ -65,16 +65,8 @@ class FirRpcServiceDeclarationCheckerVS(
}
}
-class FirRpcStrictModeExpressionCheckerVS(
- private val ctx: FirCheckersContext,
-) : FirFunctionCallChecker(MppCheckerKind.Common) {
- override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeExpressionChecker.check(ctx, expression, context, reporter)
- }
-}
-
-class FirRpcStrictModeClassCheckerVS(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) {
+class FirRpcStrictModeClassCheckerVS : FirRegularClassChecker(MppCheckerKind.Common) {
override fun check(declaration: FirRegularClass, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeClassChecker.check(ctx, declaration, context, reporter)
+ FirRpcStrictModeClassChecker.check(declaration, context, reporter)
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt b/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
deleted file mode 100644
index 6f7b2e0e1..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/latest/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-@file:Suppress("unused")
-
-package kotlinx.rpc.codegen.checkers.diagnostics
-
-import kotlinx.rpc.codegen.StrictMode
-import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory0
-import org.jetbrains.kotlin.diagnostics.Severity
-import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
-import org.jetbrains.kotlin.utils.DummyDelegate
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KClass
-import kotlin.reflect.KProperty
-
-inline fun modded0(mode: StrictMode): DiagnosticFactory0DelegateProviderOnNull {
- return DiagnosticFactory0DelegateProviderOnNull(mode, T::class)
-}
-
-class DiagnosticFactory0DelegateProviderOnNull(
- private val mode: StrictMode,
- private val psiType: KClass<*>,
-) {
- operator fun provideDelegate(
- @Suppress("unused")
- thisRef: Any?,
- prop: KProperty<*>,
- ): ReadOnlyProperty {
- val severity = when (mode) {
- StrictMode.ERROR -> Severity.ERROR
- StrictMode.WARNING -> Severity.WARNING
- StrictMode.NONE -> null
- } ?: return DummyDelegate(null)
-
- return DummyDelegate(
- KtDiagnosticFactory0(
- name = prop.name,
- severity = severity,
- defaultPositioningStrategy = SourceElementPositioningStrategies.DEFAULT,
- psiType = psiType,
- ),
- )
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
deleted file mode 100644
index 04b2667fc..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.codegen
-
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-
-class FirRpcSupertypeGenerator(
- session: FirSession,
- logger: MessageCollector,
-) : FirRpcSupertypeGeneratorAbstract(session, logger) {
- override fun computeAdditionalSupertypes(
- classLikeDeclaration: FirClassLikeDeclaration,
- resolvedSupertypes: List,
- typeResolver: TypeResolveService,
- ): List {
- return computeAdditionalSupertypesAbstract(resolvedSupertypes).map {
- vsApi {
- it.constructClassLikeType(emptyArray(), isNullable = false).toFirResolvedTypeRefVS()
- }
- }
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
index ca76f64c2..f6fcdbcad 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
@@ -65,16 +65,8 @@ class FirRpcServiceDeclarationCheckerVS(
}
}
-class FirRpcStrictModeExpressionCheckerVS(
- private val ctx: FirCheckersContext,
-) : FirFunctionCallChecker(MppCheckerKind.Common) {
- override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeExpressionChecker.check(ctx, expression, context, reporter)
- }
-}
-
-class FirRpcStrictModeClassCheckerVS(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) {
+class FirRpcStrictModeClassCheckerVS : FirRegularClassChecker(MppCheckerKind.Common) {
override fun check(declaration: FirRegularClass, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeClassChecker.check(ctx, declaration, context, reporter)
+ FirRpcStrictModeClassChecker.check(declaration, context, reporter)
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
deleted file mode 100644
index 6f7b2e0e1..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_10/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-@file:Suppress("unused")
-
-package kotlinx.rpc.codegen.checkers.diagnostics
-
-import kotlinx.rpc.codegen.StrictMode
-import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory0
-import org.jetbrains.kotlin.diagnostics.Severity
-import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
-import org.jetbrains.kotlin.utils.DummyDelegate
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KClass
-import kotlin.reflect.KProperty
-
-inline fun modded0(mode: StrictMode): DiagnosticFactory0DelegateProviderOnNull {
- return DiagnosticFactory0DelegateProviderOnNull(mode, T::class)
-}
-
-class DiagnosticFactory0DelegateProviderOnNull(
- private val mode: StrictMode,
- private val psiType: KClass<*>,
-) {
- operator fun provideDelegate(
- @Suppress("unused")
- thisRef: Any?,
- prop: KProperty<*>,
- ): ReadOnlyProperty {
- val severity = when (mode) {
- StrictMode.ERROR -> Severity.ERROR
- StrictMode.WARNING -> Severity.WARNING
- StrictMode.NONE -> null
- } ?: return DummyDelegate(null)
-
- return DummyDelegate(
- KtDiagnosticFactory0(
- name = prop.name,
- severity = severity,
- defaultPositioningStrategy = SourceElementPositioningStrategies.DEFAULT,
- psiType = psiType,
- ),
- )
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
deleted file mode 100644
index 04b2667fc..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.codegen
-
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-
-class FirRpcSupertypeGenerator(
- session: FirSession,
- logger: MessageCollector,
-) : FirRpcSupertypeGeneratorAbstract(session, logger) {
- override fun computeAdditionalSupertypes(
- classLikeDeclaration: FirClassLikeDeclaration,
- resolvedSupertypes: List,
- typeResolver: TypeResolveService,
- ): List {
- return computeAdditionalSupertypesAbstract(resolvedSupertypes).map {
- vsApi {
- it.constructClassLikeType(emptyArray(), isNullable = false).toFirResolvedTypeRefVS()
- }
- }
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
index ca76f64c2..f6fcdbcad 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
@@ -65,16 +65,8 @@ class FirRpcServiceDeclarationCheckerVS(
}
}
-class FirRpcStrictModeExpressionCheckerVS(
- private val ctx: FirCheckersContext,
-) : FirFunctionCallChecker(MppCheckerKind.Common) {
- override fun check(expression: FirFunctionCall, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeExpressionChecker.check(ctx, expression, context, reporter)
- }
-}
-
-class FirRpcStrictModeClassCheckerVS(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) {
+class FirRpcStrictModeClassCheckerVS : FirRegularClassChecker(MppCheckerKind.Common) {
override fun check(declaration: FirRegularClass, context: CheckerContext, reporter: DiagnosticReporter) {
- FirRpcStrictModeClassChecker.check(ctx, declaration, context, reporter)
+ FirRpcStrictModeClassChecker.check(declaration, context, reporter)
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt b/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
deleted file mode 100644
index 6f7b2e0e1..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/pre_2_0_21/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-@file:Suppress("unused")
-
-package kotlinx.rpc.codegen.checkers.diagnostics
-
-import kotlinx.rpc.codegen.StrictMode
-import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory0
-import org.jetbrains.kotlin.diagnostics.Severity
-import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
-import org.jetbrains.kotlin.utils.DummyDelegate
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KClass
-import kotlin.reflect.KProperty
-
-inline fun modded0(mode: StrictMode): DiagnosticFactory0DelegateProviderOnNull {
- return DiagnosticFactory0DelegateProviderOnNull(mode, T::class)
-}
-
-class DiagnosticFactory0DelegateProviderOnNull(
- private val mode: StrictMode,
- private val psiType: KClass<*>,
-) {
- operator fun provideDelegate(
- @Suppress("unused")
- thisRef: Any?,
- prop: KProperty<*>,
- ): ReadOnlyProperty {
- val severity = when (mode) {
- StrictMode.ERROR -> Severity.ERROR
- StrictMode.WARNING -> Severity.WARNING
- StrictMode.NONE -> null
- } ?: return DummyDelegate(null)
-
- return DummyDelegate(
- KtDiagnosticFactory0(
- name = prop.name,
- severity = severity,
- defaultPositioningStrategy = SourceElementPositioningStrategies.DEFAULT,
- psiType = psiType,
- ),
- )
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
deleted file mode 100644
index 90269d1d8..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.codegen
-
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.types.ConeKotlinType
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-
-class FirRpcSupertypeGenerator(
- session: FirSession,
- logger: MessageCollector,
-) : FirRpcSupertypeGeneratorAbstract(session, logger) {
- override fun computeAdditionalSupertypes(
- classLikeDeclaration: FirClassLikeDeclaration,
- resolvedSupertypes: List,
- typeResolver: TypeResolveService,
- ): List {
- return computeAdditionalSupertypesAbstract(resolvedSupertypes).map {
- it.constructClassLikeType(emptyArray(), isMarkedNullable = false)
- }
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
index 684070a55..dad111824 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
@@ -71,18 +71,9 @@ class FirRpcServiceDeclarationCheckerVS(
}
}
-class FirRpcStrictModeExpressionCheckerVS(
- private val ctx: FirCheckersContext,
-) : FirFunctionCallChecker(MppCheckerKind.Common) {
- context(context: CheckerContext, reporter: DiagnosticReporter)
- override fun check(expression: FirFunctionCall) {
- FirRpcStrictModeExpressionChecker.check(ctx, expression, context, reporter)
- }
-}
-
-class FirRpcStrictModeClassCheckerVS(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) {
+class FirRpcStrictModeClassCheckerVS : FirRegularClassChecker(MppCheckerKind.Common) {
context(context: CheckerContext, reporter: DiagnosticReporter)
override fun check(declaration: FirRegularClass) {
- FirRpcStrictModeClassChecker.check(ctx, declaration, context, reporter)
+ FirRpcStrictModeClassChecker.check(declaration, context, reporter)
}
}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
deleted file mode 100644
index 6f7b2e0e1..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2/kotlinx/rpc/codegen/checkers/diagnostics/DiagnosticFactories.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2023-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-@file:Suppress("unused")
-
-package kotlinx.rpc.codegen.checkers.diagnostics
-
-import kotlinx.rpc.codegen.StrictMode
-import org.jetbrains.kotlin.diagnostics.KtDiagnosticFactory0
-import org.jetbrains.kotlin.diagnostics.Severity
-import org.jetbrains.kotlin.diagnostics.SourceElementPositioningStrategies
-import org.jetbrains.kotlin.utils.DummyDelegate
-import kotlin.properties.ReadOnlyProperty
-import kotlin.reflect.KClass
-import kotlin.reflect.KProperty
-
-inline fun modded0(mode: StrictMode): DiagnosticFactory0DelegateProviderOnNull {
- return DiagnosticFactory0DelegateProviderOnNull(mode, T::class)
-}
-
-class DiagnosticFactory0DelegateProviderOnNull(
- private val mode: StrictMode,
- private val psiType: KClass<*>,
-) {
- operator fun provideDelegate(
- @Suppress("unused")
- thisRef: Any?,
- prop: KProperty<*>,
- ): ReadOnlyProperty {
- val severity = when (mode) {
- StrictMode.ERROR -> Severity.ERROR
- StrictMode.WARNING -> Severity.WARNING
- StrictMode.NONE -> null
- } ?: return DummyDelegate(null)
-
- return DummyDelegate(
- KtDiagnosticFactory0(
- name = prop.name,
- severity = severity,
- defaultPositioningStrategy = SourceElementPositioningStrategies.DEFAULT,
- psiType = psiType,
- ),
- )
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
deleted file mode 100644
index 90269d1d8..000000000
--- a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/FirRpcSupertypeGenerator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.codegen
-
-import org.jetbrains.kotlin.cli.common.messages.MessageCollector
-import org.jetbrains.kotlin.fir.FirSession
-import org.jetbrains.kotlin.fir.declarations.FirClassLikeDeclaration
-import org.jetbrains.kotlin.fir.types.ConeKotlinType
-import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
-import org.jetbrains.kotlin.fir.types.constructClassLikeType
-
-class FirRpcSupertypeGenerator(
- session: FirSession,
- logger: MessageCollector,
-) : FirRpcSupertypeGeneratorAbstract(session, logger) {
- override fun computeAdditionalSupertypes(
- classLikeDeclaration: FirClassLikeDeclaration,
- resolvedSupertypes: List,
- typeResolver: TypeResolveService,
- ): List {
- return computeAdditionalSupertypesAbstract(resolvedSupertypes).map {
- it.constructClassLikeType(emptyArray(), isMarkedNullable = false)
- }
- }
-}
diff --git a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
index 684070a55..dad111824 100644
--- a/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
+++ b/compiler-plugin/compiler-plugin-k2/src/main/v_2_2_2/kotlinx/rpc/codegen/checkers/FirRpcCheckersVS.kt
@@ -71,18 +71,9 @@ class FirRpcServiceDeclarationCheckerVS(
}
}
-class FirRpcStrictModeExpressionCheckerVS(
- private val ctx: FirCheckersContext,
-) : FirFunctionCallChecker(MppCheckerKind.Common) {
- context(context: CheckerContext, reporter: DiagnosticReporter)
- override fun check(expression: FirFunctionCall) {
- FirRpcStrictModeExpressionChecker.check(ctx, expression, context, reporter)
- }
-}
-
-class FirRpcStrictModeClassCheckerVS(private val ctx: FirCheckersContext) : FirRegularClassChecker(MppCheckerKind.Common) {
+class FirRpcStrictModeClassCheckerVS : FirRegularClassChecker(MppCheckerKind.Common) {
context(context: CheckerContext, reporter: DiagnosticReporter)
override fun check(declaration: FirRegularClass) {
- FirRpcStrictModeClassChecker.check(ctx, declaration, context, reporter)
+ FirRpcStrictModeClassChecker.check(declaration, context, reporter)
}
}
diff --git a/core/api/core.api b/core/api/core.api
index 6623d8438..4830d8914 100644
--- a/core/api/core.api
+++ b/core/api/core.api
@@ -1,15 +1,4 @@
-public final class kotlinx/rpc/AwaitFieldInitializationKt {
- public static final fun awaitFieldInitialization (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun awaitFieldInitialization (Ljava/lang/Object;Lkotlin/reflect/KClass;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-}
-
-public final class kotlinx/rpc/RegisterFieldKt {
- public static final fun registerPlainFlowField (Lkotlinx/rpc/RpcClient;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/descriptor/RpcServiceDescriptor;Ljava/lang/String;J)Lkotlinx/coroutines/flow/Flow;
- public static final fun registerSharedFlowField (Lkotlinx/rpc/RpcClient;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/descriptor/RpcServiceDescriptor;Ljava/lang/String;J)Lkotlinx/coroutines/flow/SharedFlow;
- public static final fun registerStateFlowField (Lkotlinx/rpc/RpcClient;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/descriptor/RpcServiceDescriptor;Ljava/lang/String;J)Lkotlinx/coroutines/flow/StateFlow;
-}
-
-public abstract interface class kotlinx/rpc/RemoteService : kotlinx/coroutines/CoroutineScope {
+public abstract interface class kotlinx/rpc/RemoteService {
}
public final class kotlinx/rpc/RpcCall {
@@ -29,27 +18,14 @@ public final class kotlinx/rpc/RpcCall {
public fun toString ()Ljava/lang/String;
}
-public abstract interface class kotlinx/rpc/RpcClient : kotlinx/coroutines/CoroutineScope {
+public abstract interface class kotlinx/rpc/RpcClient {
public abstract fun call (Lkotlinx/rpc/RpcCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public abstract fun callAsync (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/Deferred;
public abstract fun callServerStreaming (Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/flow/Flow;
- public abstract fun provideStubContext (J)Lkotlin/coroutines/CoroutineContext;
-}
-
-public final class kotlinx/rpc/RpcClient$DefaultImpls {
- public static fun callServerStreaming (Lkotlinx/rpc/RpcClient;Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/flow/Flow;
}
-public abstract interface annotation class kotlinx/rpc/RpcEagerField : java/lang/annotation/Annotation {
-}
-
-public abstract interface class kotlinx/rpc/RpcServer : kotlinx/coroutines/CoroutineScope {
- public abstract fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
-}
-
-public final class kotlinx/rpc/UninitializedRpcFieldException : java/lang/Exception {
- public fun (Ljava/lang/String;Lkotlin/reflect/KProperty;)V
- public fun getMessage ()Ljava/lang/String;
+public abstract interface class kotlinx/rpc/RpcServer {
+ public abstract fun deregisterService (Lkotlin/reflect/KClass;)V
+ public abstract fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)V
}
public final class kotlinx/rpc/WithServiceKt {
@@ -76,10 +52,6 @@ public final class kotlinx/rpc/descriptor/RpcCallable {
public abstract interface class kotlinx/rpc/descriptor/RpcInvokator {
}
-public abstract interface class kotlinx/rpc/descriptor/RpcInvokator$Field : kotlinx/rpc/descriptor/RpcInvokator {
- public abstract fun call (Ljava/lang/Object;)Ljava/lang/Object;
-}
-
public abstract interface class kotlinx/rpc/descriptor/RpcInvokator$Method : kotlinx/rpc/descriptor/RpcInvokator {
public abstract fun call (Ljava/lang/Object;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RemoteService.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RemoteService.kt
index 71f3ceaea..ccb3512ad 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/RemoteService.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/RemoteService.kt
@@ -4,15 +4,12 @@
package kotlinx.rpc
-import kotlinx.coroutines.CoroutineScope
import kotlinx.rpc.annotations.Rpc
/**
* Marker interface for an RPC service.
- * Provides type safety and [CoroutineScope] for [Rpc] annotated services.
- *
- * Every [RemoteService] service MUST be annotated with [Rpc] annotation.
*
* @see Rpc
*/
-public interface RemoteService : CoroutineScope
+@Deprecated("Use of RemoteService is deprecated. Use only @Rpc annotation", level = DeprecationLevel.ERROR)
+public interface RemoteService
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt
index 6c123fd34..b7cc9a488 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcCall.kt
@@ -6,16 +6,13 @@ package kotlinx.rpc
import kotlinx.rpc.descriptor.RpcServiceDescriptor
-@Deprecated("Use RpcCall instead", ReplaceWith("RpcCall"), level = DeprecationLevel.ERROR)
-public typealias RPCCall = RpcCall
-
/**
- * Represents a method or field call of an RPC service.
+ * Represents a method call from an RPC service.
*
* @property descriptor [RpcServiceDescriptor] of a service that made the call.
- * @property callableName The name of the callable. Can be the name of the method or field.
+ * @property callableName The name of the method being called.
* @property data The data for the call.
- * @property serviceId id of the service, that made the call.
+ * @property serviceId The id of the service that made the call.
*/
public data class RpcCall(
val descriptor: RpcServiceDescriptor<*>,
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt
index e1630a573..034101f73 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcClient.kt
@@ -4,45 +4,23 @@
package kotlinx.rpc
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
import kotlinx.coroutines.flow.Flow
-import kotlin.coroutines.CoroutineContext
-
-@Deprecated("Use RpcClient instead", ReplaceWith("RpcClient"), level = DeprecationLevel.ERROR)
-public typealias RPCClient = RpcClient
/**
* [RpcClient] represents an abstraction of an RPC client, that can handle requests from several RPC services,
* transform them, send to the server and handle responses and errors.
- * [CoroutineScope] defines the lifetime of the client.
*/
-public interface RpcClient : CoroutineScope {
+public interface RpcClient {
/**
* This method is used by generated clients to perform a call to the server.
*
* @param T type of the result
* @param call an object that contains all required information about the called method,
* that is needed to route it properly to the server.
- * @return actual result of the call, for example, data from the server.
+ * @return result of the call, for example, data from the server.
*/
public suspend fun call(call: RpcCall): T
- /**
- * This method is used by generated clients to perform a call to the server.
- *
- * @param T type of the result
- * @param serviceScope service's coroutine scope
- * @param call an object that contains all required information about the called method,
- * that is needed to route it properly to the server.
- * @return actual result of the call, for example, data from the server
- */
- @Deprecated(
- "This method was primarily used for fields in RPC services, which are now deprecated. " +
- "See https://kotlin.github.io/kotlinx-rpc/strict-mode.html fields guide for more information"
- )
- public fun callAsync(serviceScope: CoroutineScope, call: RpcCall): Deferred
-
/**
* This method is used by generated clients to perform a call to the server
* that returns a streaming flow.
@@ -50,18 +28,7 @@ public interface RpcClient : CoroutineScope {
* @param T type of the result
* @param call an object that contains all required information about the called method,
* that is needed to route it properly to the server.
- * @return the actual result of the call, for example, data from the server
- */
- public fun callServerStreaming(call: RpcCall): Flow {
- error("Non-suspending server streaming is not supported by this client")
- }
-
- /**
- * Provides child [CoroutineContext] for a new [RemoteService] service stub.
- *
- * This function shouldn't be called directly.
- *
- * @param serviceId id of the new service. Used for service cancellation messages.
+ * @return result of the call, for example, data from the server
*/
- public fun provideStubContext(serviceId: Long): CoroutineContext
+ public fun callServerStreaming(call: RpcCall): Flow
}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt
deleted file mode 100644
index 9f68337c7..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcEagerField.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * 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
-
-/**
- * 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)
-public typealias RPCEagerField = RpcEagerField
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/RpcServer.kt b/core/src/commonMain/kotlin/kotlinx/rpc/RpcServer.kt
index 887a5b29e..06b59f467 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/RpcServer.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/RpcServer.kt
@@ -4,33 +4,43 @@
package kotlinx.rpc
-import kotlinx.coroutines.CoroutineScope
import kotlinx.rpc.annotations.Rpc
-import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass
-@Deprecated("Use RpcServer instead", ReplaceWith("RpcServer"), level = DeprecationLevel.ERROR)
-public typealias RPCServer = RpcServer
-
/**
* RpcServer is used to accept RPC messages, route them to a specific service, and process given responses.
* Server may contain multiple services.
- * [CoroutineScope] defines server lifetime.
*/
-public interface RpcServer : CoroutineScope {
+public interface RpcServer {
/**
- * Registers new service to the server. Server will route all designated messages to it.
+ * Registers new service. Server will route all designated messages to it.
* Service of any type should be unique on the server, but RpcServer doesn't specify the actual retention policy.
*
* @param Service the exact type of the server to be registered.
- * For example, for service with `MyService` interface and `MyServiceImpl` implementation,
- * type `MyService` should be specified explicitly.
+ * For example, for a service with `MyService` interface and `MyServiceImpl` implementation
+ * the type `MyService` should be specified explicitly.
* @param serviceKClass [KClass] of the [Service].
* @param serviceFactory function that produces the actual implementation of the service that will handle the calls.
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
*/
public fun <@Rpc Service : Any> registerService(
serviceKClass: KClass,
- serviceFactory: (CoroutineContext) -> Service,
+ serviceFactory: () -> Service,
+ )
+
+ /**
+ * Deregisters a service. Server will stop routing messages to it.
+ *
+ * @param Service the exact type of the server to be deregistered, the same one that was used for registration.
+ * For example, for a service with `MyService` interface and `MyServiceImpl` implementation
+ * the type `MyService` should be specified explicitly.
+ * @param serviceKClass [KClass] of the [Service].
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
+ */
+ public fun <@Rpc Service : Any> deregisterService(
+ serviceKClass: KClass,
)
}
@@ -39,12 +49,14 @@ public interface RpcServer : CoroutineScope {
* Service of any type should be unique on the server, but RpcServer doesn't specify the actual retention policy.
*
* @param Service the exact type of the server to be registered.
- * For example, for service with `MyService` interface and `MyServiceImpl` implementation,
- * type `MyService` should be specified explicitly.
+ * For example, for a service with `MyService` interface and `MyServiceImpl` implementation
+ * the type `MyService` should be specified explicitly.
* @param serviceFactory function that produces the actual implementation of the service that will handle the calls.
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
*/
public inline fun <@Rpc reified Service : Any> RpcServer.registerService(
- noinline serviceFactory: (CoroutineContext) -> Service,
+ noinline serviceFactory: () -> Service,
) {
registerService(Service::class, serviceFactory)
}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt b/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt
deleted file mode 100644
index 927c1cc48..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/UninitializedRpcFieldException.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * 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
-
-import kotlin.reflect.KProperty
-
-@Deprecated(
- "Use UninitializedRpcFieldException instead",
- ReplaceWith("UninitializedRpcFieldException"),
- level = DeprecationLevel.ERROR,
-)
-public typealias UninitializedRPCFieldException = UninitializedRpcFieldException
-
-/**
- * Thrown when an uninitialized field of an RPC service is accessed.
- *
- * 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/annotations/CheckedTypeAnnotation.kt b/core/src/commonMain/kotlin/kotlinx/rpc/annotations/CheckedTypeAnnotation.kt
index 3f461a6fd..0f29fa644 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/annotations/CheckedTypeAnnotation.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/annotations/CheckedTypeAnnotation.kt
@@ -5,8 +5,11 @@
package kotlinx.rpc.annotations
/**
- * Marks an annotation as a one that marks
- * a type argument as a one that requires its resolved type to be annotated this annotation.
+ * Meta annotation.
+ * Used to perform [annotation type-safety](https://kotlin.github.io/kotlinx-rpc/annotation-type-safety.html) checks.
+ *
+ * When an annotation class (for example, `@X`) is marked with `@CheckedTypeAnnotation` -
+ * Any other type can be marked with `@X` to perform safety checks on type parameters that are also marked with `@X`.
*
* Example:
* ```kotlin
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/annotations/Rpc.kt b/core/src/commonMain/kotlin/kotlinx/rpc/annotations/Rpc.kt
index d122b1852..3eda61e3e 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/annotations/Rpc.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/annotations/Rpc.kt
@@ -4,39 +4,41 @@
package kotlinx.rpc.annotations
-import kotlinx.rpc.RemoteService
-
/**
* Every [Rpc] annotated interface will have a code generation process run on it,
* making the interface effectively usable for RPC calls.
*
- * Every [Rpc] annotated interface MAY inherit from the [RemoteService] interface.
- * If it is not done explicitly, the supertype will be added during the compilation process.
- * In that case an IDE will highlight false-positive type mismatch errors,
- * so it is recommended to add the [RemoteService] parent explicitly, until proper IDE support is provided.
+ * Example usage.
*
- * Example usage:
+ * Define an interface and mark it with `@Rpc`.
+ * Compiler plugin will ensure that all declarations inside the interface are valid.
* ```kotlin
- * // common code
* @Rpc
- * interface MyService : RemoteService {
+ * interface MyService {
* suspend fun sayHello(firstName: String, lastName: String, age: Int): String
* }
- * // client code
+ * ```
+ * On the client side use [kotlinx.rpc.RpcClient] to get a generated instance of the service, and use it to make calls:
+ * ```kotlin
* val rpcClient: RpcClient
* val myService = rpcClient.withService()
* val greetingFromServer = myService.sayHello("Alex", "Smith", 35)
- * // server code
- * class MyServiceImpl(override val coroutineContext: CoroutineContext) : MyService {
+ * ```
+ * On the server side, define an implementation of this interface and register it on an [kotlinx.rpc.RpcServer]:
+ * ```kotlin
+ * class MyServiceImpl : MyService {
* override suspend fun sayHello(firstName: String, lastName: String, age: Int): String {
* return "Hello, $firstName $lastName, of age $age. I am your server!"
* }
* }
* val server: RpcServer
- * server.registerService { ctx -> MyServiceImpl(ctx) }
+ * server.registerService { MyServiceImpl() }
* ```
*
- * @see [RemoteService]
+ * @see kotlinx.rpc.RpcClient
+ * @see kotlinx.rpc.RpcServer
+ * @see CheckedTypeAnnotation
+ * @see kotlinx.rpc.withService
*/
@CheckedTypeAnnotation
@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS, AnnotationTarget.TYPE_PARAMETER)
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt b/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt
deleted file mode 100644
index 3a93aef0d..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/awaitFieldInitialization.kt
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * 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
-
-import kotlinx.rpc.annotations.Rpc
-import kotlinx.rpc.descriptor.serviceDescriptorOf
-import kotlinx.rpc.internal.RpcDeferredField
-import kotlin.reflect.KClass
-
-/**
- * Waits for the initialization of an RPC field in the generated client:
- *
- * ```kotlin
- * @Rpc
- * interface MyService : RemoteService {
- * val stateFlow: StateFlow
- * }
- *
- * val service = rpcClient.withService()
- * val currentValue = service.awaitFieldInitialization { stateFlow }.value
- * ```
- *
- * @param T service type
- * @param R field type
- * @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()
-
- if (field is RpcDeferredField<*>) {
- @Suppress("UNCHECKED_CAST")
- return (field as RpcDeferredField).await()
- }
-
- error("Please choose required field for a valid RPC client generated by RpcClient.withService method")
-}
-
-/**
- * Waits for the initialization of all RPC fields in the generated client:
- *
- * ```kotlin
- * @Rpc
- * interface MyService : RemoteService {
- * val stateFlow1: StateFlow
- * val stateFlow2: StateFlow
- * }
- *
- * val service = rpcClient.withService()
- * val currentValue = service.awaitFieldInitialization()
- * // fields `stateFlow1` and `stateFlow2` are initialized
- * ```
- *
- * @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)
-}
-
-/**
- * Waits for the initialization of all RPC fields in the generated client:
- *
- * ```kotlin
- * @Rpc
- * interface MyService : RemoteService {
- * val stateFlow1: StateFlow
- * val stateFlow2: StateFlow
- * }
- *
- * val service = rpcClient.withService()
- * val currentValue = service.awaitFieldInitialization(MyService::class)
- * // fields `stateFlow1` and `stateFlow2` are initialized
- * ```
- *
- * @param T service type
- * @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)
- .forEach { field ->
- field.await()
- }
-
- return this
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt
index 3ace7facc..4873b9193 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/descriptor/RpcServiceDescriptor.kt
@@ -8,7 +8,6 @@ import kotlinx.rpc.RpcClient
import kotlinx.rpc.annotations.Rpc
import kotlinx.rpc.internal.*
import kotlinx.rpc.internal.utils.ExperimentalRpcApi
-import kotlinx.rpc.internal.utils.InternalRpcApi
import kotlin.reflect.KClass
import kotlin.reflect.KType
@@ -43,13 +42,6 @@ public fun <@Rpc T : Any> serviceDescriptorOf(kClass: KClass): RpcServiceDesc
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?
public fun createInstance(serviceId: Long, client: RpcClient): T
@@ -71,15 +63,6 @@ public sealed interface RpcInvokator<@Rpc T : Any> {
public fun interface Method<@Rpc T : Any> : RpcInvokator {
public suspend fun call(service: T, data: Any?): 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?
- }
}
@ExperimentalRpcApi
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/FieldDataObject.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/FieldDataObject.kt
deleted file mode 100644
index 64007f9df..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/FieldDataObject.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlinx.serialization.Serializable
-
-/**
- * Used for field initialization call
- */
-@Serializable
-@InternalRpcApi
-public object FieldDataObject
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcDeferredField.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcDeferredField.kt
deleted file mode 100644
index ff1651336..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcDeferredField.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.rpc.internal.utils.InternalRpcApi
-
-@InternalRpcApi
-public interface RpcDeferredField {
- public suspend fun await(): Self
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFieldProvider.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFieldProvider.kt
deleted file mode 100644
index 2b58f865b..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFieldProvider.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.rpc.UninitializedRpcFieldException
-import kotlin.reflect.KProperty
-
-internal class RpcFieldProvider(
- private val serviceName: String,
- private val deferred: Deferred = CompletableDeferred(),
- val getter: T.() -> R,
-) {
- @OptIn(ExperimentalCoroutinesApi::class)
- operator fun getValue(ref: Any?, property: KProperty<*>): R {
- if (deferred.isCompleted) {
- return deferred.getCompleted().getter()
- }
-
- throw UninitializedRpcFieldException(serviceName, property)
- }
-}
-
-@Suppress("unused")
-internal fun RpcFieldProvider(
- serviceName: String,
- deferred: CompletableDeferred = CompletableDeferred()
-): RpcFieldProvider {
- return RpcFieldProvider(serviceName, deferred) { this }
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFlow.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFlow.kt
deleted file mode 100644
index a074d0226..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/RpcFlow.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-
-internal sealed class RpcFlow>(
- private val serviceName: String,
- protected val deferred: Deferred,
-) : RpcDeferredField {
- override suspend fun await(): FlowT {
- return deferred.await()
- }
-
- internal class Plain(
- serviceName: String,
- deferred: Deferred>,
- ) : RpcFlow>(serviceName, deferred), Flow {
- override suspend fun collect(collector: FlowCollector) {
- deferred.await().collect(collector)
- }
- }
-
- internal class Shared(
- serviceName: String,
- deferred: Deferred>,
- ) : RpcFlow>(serviceName, deferred), SharedFlow {
- override val replayCache: List by rpcProperty { replayCache }
-
- override suspend fun collect(collector: FlowCollector): Nothing {
- deferred.await().collect(collector)
- }
- }
-
- internal class State(
- serviceName: String,
- deferred: Deferred>,
- ) : RpcFlow>(serviceName, deferred), StateFlow {
- override val value: T by rpcProperty { value }
-
- override val replayCache: List by rpcProperty { replayCache }
-
- override suspend fun collect(collector: FlowCollector): Nothing {
- deferred.await().collect(collector)
- }
- }
-
- protected fun rpcProperty(getter: FlowT.() -> R): RpcFieldProvider {
- return RpcFieldProvider(serviceName, deferred, getter)
- }
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/ServiceScope.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/ServiceScope.kt
deleted file mode 100644
index 3c496a663..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/ServiceScope.kt
+++ /dev/null
@@ -1,50 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.currentCoroutineContext
-import kotlinx.coroutines.withContext
-import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-import kotlin.coroutines.CoroutineContext
-
-@InternalRpcApi
-public class ServiceScope(public val serviceCoroutineScope: CoroutineScope) : CoroutineContext.Element {
- internal companion object Key : CoroutineContext.Key
-
- override val key: CoroutineContext.Key<*> = Key
-}
-
-@InternalRpcApi
-public suspend fun createServiceScope(serviceCoroutineScope: CoroutineScope): ServiceScope {
- val context = currentCoroutineContext()
-
- if (context[ServiceScope.Key] != null) {
- error("serviceScoped nesting is not allowed")
- }
-
- return ServiceScope(serviceCoroutineScope)
-}
-
-@InternalRpcApi
-public suspend fun serviceScopeOrNull(): ServiceScope? {
- return currentCoroutineContext()[ServiceScope.Key]
-}
-
-@InternalRpcApi
-@OptIn(ExperimentalContracts::class)
-public suspend inline fun serviceScoped(
- serviceCoroutineScope: CoroutineScope,
- noinline block: suspend CoroutineScope.() -> T,
-): T {
- contract {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
-
- return withContext(createServiceScope(serviceCoroutineScope), block)
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/internal/scopedClientCall.kt b/core/src/commonMain/kotlin/kotlinx/rpc/internal/scopedClientCall.kt
deleted file mode 100644
index 8a47691e1..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/internal/scopedClientCall.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.internal
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.rpc.internal.utils.InternalRpcApi
-
-/**
- * Scopes client RPC call from a service with [serviceScope].
- *
- * Used by code generators.
- */
-@InternalRpcApi
-@Suppress("unused")
-public suspend inline fun scopedClientCall(serviceScope: CoroutineScope, crossinline body: suspend () -> T): T {
- return serviceScoped(serviceScope) {
- body()
- }
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt b/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt
deleted file mode 100644
index 74f5b3987..000000000
--- a/core/src/commonMain/kotlin/kotlinx/rpc/registerField.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-/*
- * 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
-
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Deferred
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.rpc.descriptor.RpcServiceDescriptor
-import kotlinx.rpc.internal.FieldDataObject
-import kotlinx.rpc.internal.RpcFlow
-
-/**
- * Registers Flow field of the interface. Sends initialization request, subscribes to emitted values
- * and returns the instance of the flow to be consumed
- *
- * @param T type parameter for Flow
- * @param serviceScope Service's coroutine scope
- * @param descriptor descriptor of the service, that made the call
- * that is used to be mapped to the corresponding field on a server.
- * @param fieldName the name of the field.
- * @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<*>,
- fieldName: String,
- serviceId: Long,
-): Flow {
- return RpcFlow.Plain(descriptor.fqName, initializeFlowField(serviceScope, descriptor, fieldName, serviceId))
-}
-
-/**
- * Registers SharedFlow field of the interface. Sends initialization request, subscribes to emitted values
- * and returns the instance of the flow to be consumed
- *
- * @param T type parameter for SharedFlow
- * @param serviceScope Service's coroutine scope
- * @param descriptor descriptor of the service, that made the call
- * that is used to be mapped to the corresponding field on a server.
- * @param fieldName the name of the field.
- * @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<*>,
- fieldName: String,
- serviceId: Long,
-): SharedFlow {
- return RpcFlow.Shared(descriptor.fqName, initializeFlowField(serviceScope, descriptor, fieldName, serviceId))
-}
-
-/**
- * Registers StateFlow field of the interface. Sends initialization request, subscribes to emitted values
- * and returns the instance of the flow to be consumed
- *
- * @param T type parameter for StateFlow
- * @param serviceScope Service's coroutine scope
- * @param descriptor descriptor of the service, that made the call
- * that is used to be mapped to the corresponding field on a server.
- * @param fieldName the name of the field.
- * @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<*>,
- fieldName: String,
- serviceId: Long,
-): StateFlow {
- return RpcFlow.State(descriptor.fqName, initializeFlowField(serviceScope, descriptor, fieldName, serviceId))
-}
-
-private fun > RpcClient.initializeFlowField(
- serviceScope: CoroutineScope,
- descriptor: RpcServiceDescriptor<*>,
- fieldName: String,
- serviceId: Long,
-): Deferred {
- return callAsync(serviceScope, RpcCall(descriptor, fieldName, FieldDataObject, serviceId))
-}
diff --git a/core/src/commonMain/kotlin/kotlinx/rpc/withService.kt b/core/src/commonMain/kotlin/kotlinx/rpc/withService.kt
index 73df9f149..3aab53a8b 100644
--- a/core/src/commonMain/kotlin/kotlinx/rpc/withService.kt
+++ b/core/src/commonMain/kotlin/kotlinx/rpc/withService.kt
@@ -12,25 +12,25 @@ import kotlin.reflect.KClass
import kotlin.reflect.KType
/**
- * Creates instance of the generated service [T], that is able to communicate with server using [RpcClient].
- *
- * [awaitFieldInitialization] method can be used on that instance.
+ * Creates an instance of the generated service [T], that is able to communicate with a server using this [RpcClient].
*
* @param T the exact type of the service to be created.
* @return instance of the generated service.
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
*/
public inline fun <@Rpc reified T : Any> RpcClient.withService(): T {
return withService(T::class)
}
/**
- * Creates instance of the generated service [T], that is able to communicate with server using [RpcClient].
- *
- * [awaitFieldInitialization] method can be used on that instance.
+ * Creates an instance of the generated service [T], that is able to communicate with a server using this [RpcClient].
*
* @param T the exact type of the service to be created.
* @param serviceKType [KType] of the service to be created.
* @return instance of the generated service.
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
*/
public fun <@Rpc T : Any> RpcClient.withService(serviceKType: KType): T {
return withService(serviceKType.rpcInternalKClass())
@@ -43,13 +43,13 @@ public fun <@Rpc T : Any> RpcClient.withService(serviceKType: KType): T {
private val SERVICE_ID = atomic(0L)
/**
- * Creates instance of the generated service [T], that is able to communicate with server using [RpcClient].
- *
- * [awaitFieldInitialization] method can be used on that instance.
+ * Creates an instance of the generated service [T], that is able to communicate with a server using this [RpcClient].
*
* @param T the exact type of the service to be created.
* @param serviceKClass [KClass] of the service to be created.
* @return instance of the generated service.
+ *
+ * @see kotlinx.rpc.annotations.CheckedTypeAnnotation
*/
public fun <@Rpc T : Any> RpcClient.withService(serviceKClass: KClass): T {
val descriptor = serviceDescriptorOf(serviceKClass)
diff --git a/docs/pages/kotlinx-rpc/rpc.tree b/docs/pages/kotlinx-rpc/rpc.tree
index aaf3fa079..979360ba4 100644
--- a/docs/pages/kotlinx-rpc/rpc.tree
+++ b/docs/pages/kotlinx-rpc/rpc.tree
@@ -24,7 +24,11 @@
-
+
+
+
+
+
@@ -36,6 +40,7 @@
+
diff --git a/docs/pages/kotlinx-rpc/topics/0-8-0.topic b/docs/pages/kotlinx-rpc/topics/0-8-0.topic
new file mode 100644
index 000000000..b61dfafd9
--- /dev/null
+++ b/docs/pages/kotlinx-rpc/topics/0-8-0.topic
@@ -0,0 +1,363 @@
+
+
+
+
+
+
+
+ Version 0.8.0
brings a lot of changes,
+ mainly targeted to remove inherently broken functionality and simplify kRPC protocol where possible.
+ This is reflected in the number of breaking changes and deprecations.
+
+
+ This page aims to cover all such changes and associated migration instructions.
+
+
+
+
+ Strict mode is now enforced and can't be disabled.
+ See for detailed migrations.
+
+
+
+
+
+ The following changes are reflected in the kRPC protocol on the wire:
+
+
+
+ KrpcServer
doesn't send CANCELLATION_ACK messages anymore.
+
+
+ KrpcClient
sends REQUEST cancellation messages for every individually finished call,
+ canceled or finished successfully
+
+
+
+ Though changes should not affect most users,
+ for those who like to look at the wire it might be useful to know.
+
+
+
+
+
+ Some changes in the behavior of kRPC clients and servers:
+
+
+
+
+ Lifetime for client-side streams is changed.
+
+
+ Previously, the stream scopes bounded client-side streams.
+ However, stream scopes are completely removed now,
+ so the client-side streams lifetime is now bound to the request's lifetime.
+ This means that when the function returns, every client stream is closed.
+
+
+
+
+ Serial format is now only constructed once per client.
+
+
+ Previously, the serial format was constructed once per RPC call.
+ The serial format can be passed using the KrpcConfig
.
+ And the builder code was executed once per every call.
+
+
+ Now this behavior is removed.
+ The serial format is constructed once per client.
+
+
+
+
+ Services are now instantiated once per service type (Rpc FQ name)
+ and not once per client-side instance.
+
+
+ Services lost their CoroutineScopes (see ).
+ That means that there are no individual lifetimes for each service instance now.
+ Instead, now each service stub on a client is merely a proxy for function calls.
+ And on the server side, the service implementation is instantiated once per service type.
+ To control this behavior more granularly on the server, new deregisterService
+ function is introduced.
+
+
+ For kRPC servers, the factory function for service instances is now executed once per service type:
+
+ rpcServer.registerService<MyService> { MyServiceImpl() }
+
+
+
+
+
+ Handshake is now cold.
+
+
+ Previously, the handshake was executed on client creation.
+ Now, the handshake is executed on the first RPC request.
+
+
+
+
+
+
+
+
+
+ RpcClient.callServerStreaming
lost its default implementation:
+
+
+
+
+ interface RpcClient {
+ fun <T> callServerStreaming(call: RpcCall): Flow<T> {
+ error("Not implemented")
+ }
+ }
+
+
+ interface RpcClient {
+ fun <T> callServerStreaming(call: RpcCall): Flow<T>
+ }
+
+
+
+
+
+ @Rpc
services lost their CoroutineScope:
+
+
+
+ val service = client.withService<MyService>()
+ assert(service is CoroutineScope) // OK
+
+
+ val service = client.withService<MyService>()
+ assert(service is CoroutineScope) // fail
+
+
+
+
+
+ RpcClient
lost its CoroutineScope
:
+
+
+
+ interface RpcClient : CoroutineScope
+
+
+ interface RpcClient
+
+
+
+
+
+ RpcServer
lost its CoroutineScope
:
+
+
+
+ interface RpcServer : CoroutineScope
+
+
+ interface RpcServer
+
+
+
+
+
+ RpcServer.registerService
factory
parameter changes type
+ from (CoroutineContext) -> Service
to () -> Service
:
+
+
+
+ interface RpcServer {
+ fun <@Rpc Service : Any> registerService(
+ serviceKClass: KClass<Service>,
+ serviceFactory: (CoroutineContext) -> Service,
+ )
+ }
+
+ inline fun <@Rpc reified Service : Any> RpcServer.registerService(
+ noinline serviceFactory: (CoroutineContext) -> Service,
+ ) {
+ registerService(Service::class, serviceFactory)
+ }
+
+
+ interface RpcServer {
+ fun <@Rpc Service : Any> registerService(
+ serviceKClass: KClass<Service>,
+ serviceFactory: () -> Service,
+ )
+ }
+
+ inline fun <@Rpc reified Service : Any> RpcServer.registerService(
+ noinline serviceFactory: () -> Service,
+ ) {
+ registerService(Service::class, serviceFactory)
+ }
+
+
+
+
+
+ RpcServer.registerService
lost its CoroutineContext
parameter:
+
+
+
+ interface RpcServer
+
+
+ interface RpcServer {
+ fun <@Rpc Service : Any> deregisterService(
+ serviceKClass: KClass<Service>,
+ )
+ }
+
+
+
+
+
+ For Ktor, HttpClient.rpc
extension function is now non-suspendable.
+
+
+
+ KtorRpcClient.webSocketSession
is now wrapped in Deferred:
+
+
+ interface KtorRpcClient : RpcClient {
+ val webSocketSession: WebSocketSession
+ }
+
+
+ interface KtorRpcClient : RpcClient {
+ val webSocketSession: Deferred<WebSocketSession>
+ }
+
+
+
+
+
+ KrpcClient
abstract class has two new abstract methods:
+ initializeConfig
and initializeTransport
.
+ They can be used to delay transport initialization until the first RPC call.
+
+
+ To mimic old behavior, InitializedKrpcClient
can be used:
+
+
+
+ class MyClient(
+ config: KrpcConfig,
+ transport: KrpcTransport,
+ ) : KrpcClient(config, transport)
+
+
+ class MyClient(
+ config: KrpcConfig,
+ transport: KrpcTransport,
+ ) : InitializedKrpcClient(config, transport)
+
+
+
+
+
+
+
+
+ The following APIs are removed:
+
+
+ kotlinx.rpc.RpcClient.callAsync
- previously deprecated
+ kotlinx.rpc.RpcClient.provideStubContext
+
+ kotlinx.rpc.registerPlainFlowField
- previously deprecated
+ kotlinx.rpc.registerSharedFlowField
- previously deprecated
+ kotlinx.rpc.registerStateFlowField
- previously deprecated
+ kotlinx.rpc.awaitFieldInitialization
- previously deprecated
+ kotlinx.rpc.UninitializedRpcFieldException
- previously deprecated
+ kotlinx.rpc.UninitializedRPCFieldException
- previously deprecated
+ kotlinx.rpc.RpcEagerField
- previously deprecated
+ kotlinx.rpc.RPCCall
- previously deprecated alias
+ kotlinx.rpc.RPCClient
- previously deprecated alias
+
+ kotlinx.rpc.descriptor.RpcInvokator.Field
- previously deprecated
+ kotlinx.rpc.descriptor.RpcServiceDescriptor.getFields
- previously deprecated
+
+ kotlinx.rpc.krpc.StreamScope
- previously deprecated
+ kotlinx.rpc.krpc.streamScoped
- previously deprecated
+ kotlinx.rpc.krpc.withStreamScope
- previously deprecated
+ kotlinx.rpc.krpc.invokeOnStreamScopeCompletion
- previously deprecated
+ kotlinx.rpc.krpc.KrpcConfigBuilder.SharedFlowParametersBuilder
- previously deprecated
+ kotlinx.rpc.krpc.KrpcConfigBuilder.sharedFlowParameters
- previously deprecated
+ kotlinx.rpc.krpc.KrpcConfig.sharedFlowBuilder
- previously deprecated
+ kotlinx.rpc.krpc.RPCTransport
- previously deprecated alias
+ kotlinx.rpc.krpc.RPCTransportMessage
- previously deprecated alias
+ kotlinx.rpc.krpc.RPCConfigBuilder
- previously deprecated alias
+ kotlinx.rpc.krpc.client.KRPCClient
- previously deprecated alias
+ kotlinx.rpc.krpc.server.RPCServer
- previously deprecated alias
+ kotlinx.rpc.krpc.server.KRPCServer
- previously deprecated alias
+ kotlinx.rpc.krpc.serialization.RPCSerialFormat
- previously deprecated alias
+ kotlinx.rpc.krpc.serialization.RPCSerialFormatBuilder
- previously deprecated alias
+ kotlinx.rpc.krpc.serialization.RPCSerialFormatConfiguration
- previously deprecated alias
+ kotlinx.rpc.krpc.ktor.client.RPC
- previously deprecated alias
+ kotlinx.rpc.krpc.ktor.client.installRPC
- previously deprecated alias
+ kotlinx.rpc.krpc.ktor.server.RPC
- previously deprecated alias
+ kotlinx.rpc.krpc.ktor.server.RPCRoute
- previously deprecated alias
+
+
+
+
+
+
+
+ Gradle's rpc.strict
extension is deprecated with an error.
+ Strict mode is now enforced and can't be disabled.
+ See for detailed migrations.
+
+
+
+
+ RemoteService
is deprecated with error;
+ services are no longer having this interface added during the compilation.
+ See for services' lifetime information.
+
+
+
+
+
+
+
+
+
+ MISSING_RPC_ANNOTATION
compiler inspection is removed.
+
+
+
+
+ ABI incompatible change: KrpcTransport.receiveCatching
is now an extension function.
+
+
+
+
+ The following compiler plugin options are removed:
+
+
+ strict-stateFlow
+ strict-sharedFlow
+ strict-nested-flow
+ strict-stream-scope
+ strict-suspending-server-streaming
+ strict-not-top-level-server-flow
+ strict-fields
+
+
+
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic
index 135146a7b..be8df1ed2 100644
--- a/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic
+++ b/docs/pages/kotlinx-rpc/topics/annotation-type-safety.topic
@@ -29,7 +29,7 @@
@Rpc
- interface MyService : RemoteService
+ interface MyService
class MyServiceImpl : MyService
@@ -50,12 +50,31 @@
// T is resolved to MyService,
// but 'body' returns MyServiceImpl
- registerService<MyService> { ctx -> MyServiceImpl(ctx) }
+ registerService<MyService> { MyServiceImpl() }
// Error: T is resolved to MyServiceImpl
- registerService { ctx -> MyServiceImpl(ctx) }
+ registerService { MyServiceImpl() }
+
+ Annotation type-safety can be enforced recursively:
+
+
+ @Rpc
+ annotation class Grpc
+
+ @Grpc
+ interface MyGrpcService
+
+ fun <@Rpc T> acceptAnyRpcType()
+ fun <@Grpc T> acceptOnlyGrpcType()
+
+ acceptAnyRpcType<MyService>() // OK
+ acceptAnyRpcType<MyGrpcService>() // OK
+
+ acceptOnlyGrpcType<MyService>() // Compiler time error
+ acceptOnlyGrpcType<MyGrpcService>() // OK
+
This feature is highly experimental and may lead to unexpected behaviour. If you encounter any issues,
diff --git a/docs/pages/kotlinx-rpc/topics/configuration.topic b/docs/pages/kotlinx-rpc/topics/configuration.topic
index 0b382bd18..18f195cb9 100644
--- a/docs/pages/kotlinx-rpc/topics/configuration.topic
+++ b/docs/pages/kotlinx-rpc/topics/configuration.topic
@@ -8,97 +8,78 @@
- KrpcConfig
is a class used to configure KrpcClient
and KrpcServer
- (not to be confused with KrpcClient
and KrpcServer
).
- It has two children: KrpcConfig.Client
and KrpcConfig.Server
.
- Client
and Server
may have shared properties as well as distinct ones.
- To create instances of these configurations, DSL builders are provided
- (KrpcConfigBuilder.Client
class with rpcClientConfig
function
- and KrpcConfigBuilder.Server
class with rpcServerConfig
function respectively):
+
+ KrpcConfig
is a class used to configure KrpcClient
and KrpcServer
+ (not to be confused with RpcClient
and RpcServer
).
+
+
+ It has two children: KrpcConfig.Client
and KrpcConfig.Server
.
+ Client
and Server
may have shared properties as well as distinct ones.
+ To create instances of these configurations, DSL builders are provided:
+
+
+
+ rpcClientConfig
+
+
+ rpcServerConfig
+
+
+
+
+ val config: KrpcConfig.Client = rpcClientConfig { // same for KrpcConfig.Server with rpcServerConfig
+ waitForServices = true // default parameter
+ }
+
+
+ The following configuration options are available:
+
+
+
+ serialization
DSL
+
+
+ This parameter defines how serialization should work in RPC services
+ and is present in both client and server configurations.
+
+
+ The serialization process is used to encode and decode data in RPC requests,
+ so that the data can be transferred through the network.
+
+
+ Currently only StringFormat
and BinaryFormat
from
+ kotlinx.serialization are supported,
+ and by default you can choose from Json, Protobuf or Cbor formats:
- val config: KrpcConfig.Client = rpcClientConfig { // same for KrpcConfig.Server with rpcServerConfig
- waitForServices = true // default parameter
+ rpcClientConfig {
+ serialization {
+ json { /* this: JsonBuilder from kotlinx.serialization */ }
+ cbor { /* this: CborBuilder from kotlinx.serialization */ }
+ protobuf { /* this: ProtobufBuilder from kotlinx.serialization */ }
+ }
}
- The following configuration options are available:
-
-
- serialization
DSL
-
- This parameter defines how serialization should work in RPC services
- and is present in both client and server configurations.
- The serialization process is used to encode and decode data in RPC requests,
- so that the data can be transferred through the network.
- Currently only StringFormat
and BinaryFormat
from
- kotlinx.serialization are supported,
- and by default you can choose from Json, Protobuf or Cbor formats:
-
-
- rpcClientConfig {
- serialization {
- json { /* this: JsonBuilder from kotlinx.serialization */ }
- cbor { /* this: CborBuilder from kotlinx.serialization */ }
- protobuf { /* this: ProtobufBuilder from kotlinx.serialization */ }
- }
- }
-
- 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.
-
-
-
- sharedFlowParameters
DSL
-
-
-
- These parameters are deprecated since 0.5.0
. For more information,
- see the migration guide.
-
-
-
- rpcClientConfig {
- sharedFlowParameters {
- replay = 1 // default parameter
- extraBufferCapacity = 10 // default parameter
- onBufferOverflow = BufferOverflow.SUSPEND // default parameter
- }
- }
-
- This parameter is needed to decode SharedFlow
parameters received from a peer.
- MutableSharedFlow
, the default function to create a SharedFlow
instance,
- has the following signature:
-
-
- fun <T> MutableSharedFlow(
- replay: Int = 0,
- extraBufferCapacity: Int = 0,
- onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
- ): MutableSharedFlow<T> { /* ... */
- }
-
- It creates a SharedFlowImpl
class that contains these parameters as properties,
- but this class in internal in kotlinx.coroutines
and neither SharedFlow
,
- nor MutableShatedFlow
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 KrpcConfig
.
-
-
-
- waitForServices
DSL
-
- waitForServices
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 KrpcClient
or KrpcServer
.
- If set to true
, 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 true
.
-
+
+ Only last defined format will be used to serialize requests.
+ If no format is specified, a runtime error will be thrown.
+ You can also define a custom format.
+
+
+
+
+ waitForServices
DSL
+
+
+ waitForServices
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 KrpcClient
or KrpcServer
.
+ If set to true
, 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 true
.
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/features.topic b/docs/pages/kotlinx-rpc/topics/features.topic
index 8a11a73f9..c0b0c29d3 100644
--- a/docs/pages/kotlinx-rpc/topics/features.topic
+++ b/docs/pages/kotlinx-rpc/topics/features.topic
@@ -24,7 +24,7 @@
)
@Rpc
- interface MyService : RemoteService {
+ interface MyService {
fun sendStream(stream: Flow<Int>): Flow<String>
suspend fun streamRequest(request: StreamRequest)
@@ -44,7 +44,7 @@
)
@Rpc
- interface MyService : RemoteService {
+ interface MyService {
fun serverStream(): Flow<String> // ok
suspend fun serverStream(): Flow<String> // not ok
suspend fun serverStream(): StreamResult // not ok
@@ -52,7 +52,7 @@
- Note that flows that are declared in classes (like in StreamResult
) require a
+ Note that flows that are declared in classes (like in StreamRequest
) require a
Contextual
annotation.
diff --git a/docs/pages/kotlinx-rpc/topics/krpc-client.topic b/docs/pages/kotlinx-rpc/topics/krpc-client.topic
new file mode 100644
index 000000000..ee70d0493
--- /dev/null
+++ b/docs/pages/kotlinx-rpc/topics/krpc-client.topic
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+ KrpcClient
is an abstract class that implements RpcClient
and kRPC protocol
+ logic.
+
+
+
+ The client comes in two forms:
+
+
+
+ KrpcClient
+
+
+ InitializedKrpcClient
+
+
+
+ The only difference between them is that KrpcClient
allows to delay the initialization
+ of the transport until the first RPC request is sent.
+ InitializedKrpcClient
is initialized right away with a ready KrpcTransport
+ instance.
+
+
+
+
+ The only thing required to be implemented is the transporting of the raw data.
+ Abstract transport is represented by KrpcTransport
interface.
+
+
+ To implement your own KrpcTransport
+ you need to be able to transfer strings and/or raw bytes (Kotlin's ByteArray
).
+ Additionally, the library will provide you with integrations with different
+ libraries that are used to work with the network.
+
+
+
+ See below an example usage of kRPC with a custom transport:
+
+
+
+ class MySimpleRpcTransport : KrpcTransport {
+ val outChannel = Channel<KrpcTransportMessage>()
+ val inChannel = Channel<KrpcTransportMessage>()
+
+ override val coroutineContext: CoroutineContext = Job()
+
+ override suspend fun send(message: KrpcTransportMessage) {
+ outChannel.send(message)
+ }
+
+ override suspend fun receive(): KrpcTransportMessage {
+ return inChannel.receive()
+ }
+ }
+
+ class MySimpleRpcClient : KrpcClient(rpcClientConfig(), MySimpleRpcTransport())
+
+ val client = MySimpleRpcClient()
+ val service: MyService = client.withService<MyService>()
+
+
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic b/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic
new file mode 100644
index 000000000..d747c4056
--- /dev/null
+++ b/docs/pages/kotlinx-rpc/topics/krpc-ktor.topic
@@ -0,0 +1,151 @@
+
+
+
+
+
+
+
+ The kotlinx.rpc
library provides integration with the Ktor
+ framework.
+ This includes both server and client APIs.
+ Under the hood, the library uses WebSocket plugin
+ to create a KrpcTransport
and send and receive messages through it.
+
+
+
+ kotlinx.rpc
provides a way to plug-in into existing Ktor clients with your RPC services.
+ To do that, the following DSL can be used:
+
+
+
+ val ktorClient = HttpClient {
+ installKrpc {
+ waitForServices = true
+ }
+ }
+
+ val rpcClient: KtorKrpcClient =
+ ktorClient.rpc("ws://localhost:4242/services") {
+ rpcConfig {
+ waitForServices = false
+ }
+ }
+
+ // get an RPC service
+ val myService: MyService = rpcClient.withService<MyService>()
+
+
+ Note that in this example, only the latter defined KrpcConfig
will be used.
+
+
+
+
+ kotlinx.rpc
provides a way to plug-in into existing server routing with your RPC
+ services.
+ To do that, the following DSL can be used:
+
+
+
+ fun Application.module() {
+ install(Krpc) {
+ waitForServices = true
+ }
+
+ routing {
+ rpc("/services") {
+ rpcConfig {
+ waitForServices = false
+ }
+
+ registerService<MyService> { MyServiceImpl() }
+ registerService<MyOtherService> { MyOtherServiceImpl() }
+ // more services if needed
+ }
+ }
+ }
+
+
+
+
+ An example code for a Ktor web application may look like this:
+
+
+
+ In common code, shared classes and services are defined:
+
+
+ @Serializable
+ data class ProcessedImage(
+ val url: String,
+ val numberOfCats: Int,
+ val numberOfDogs: Int
+ )
+
+ @Rpc
+ interface ImageService : RemoteService {
+ suspend fun processImage(url: String): ProcessedImage
+ }
+
+
+ In client code, create an HTTP client and access the service on a the desired route:
+
+
+ val client = HttpClient {
+ installKrpc {
+ serialization {
+ json()
+ }
+ }
+ }
+
+ val service = client
+ .rpc("/image-recognizer")
+ .withService<ImageService>()
+
+ service.processImage(url = "https://catsanddogs.com/cats/1")
+
+
+ In server code, provide an implementation of the ImageService
and register it on a route:
+
+
+ class ImageServiceImpl : ImageService {
+ // some user defined classes
+ private val downloader = Downloader()
+ private val recognizer = AnimalRecognizer()
+
+ override suspend fun processImage(url: Srting): ProcessedImage {
+ val image = downloader.loadImage(url)
+ return ProcessedImage(
+ url,
+ recognizer.getNumberOfCatsOnImage(image),
+ recognizer.getNumberOfDogsOnImage(image)
+ )
+ }
+ }
+
+ fun main() {
+ embeddedServer(Netty, port = 8080) {
+ install(Krpc) {
+ serialization {
+ json()
+ }
+ }
+
+ routing {
+ rpc("/image-recognizer") {
+ registerService<ImageService> { ImageServiceImpl() }
+ }
+ }
+ }.start(wait = true)
+ }
+
+
+ For more details and complete examples, see the code samples.
+
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/krpc-server.topic b/docs/pages/kotlinx-rpc/topics/krpc-server.topic
new file mode 100644
index 000000000..d7da1d6f8
--- /dev/null
+++ b/docs/pages/kotlinx-rpc/topics/krpc-server.topic
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+ KrpcServer
abstract class implements RpcServer
+ and all the logic for processing RPC messages
+ and again leaves KrpcTransport
methods for the specific implementations
+ (see transports).
+
+
+
+ Example usage with custom transport:
+
+
+
+ // same MySimpleRpcTransport as in the client example above
+ class MySimpleRpcServer : KrpcServer(rpcServerConfig(), MySimpleRpcTransport())
+
+ val server = MySimpleRpcServer()
+ server.registerService<MyService> { MyServiceImpl() }
+
+
+
+ Note that here we pass explicit MyService
type parameter to the registerService
+ method.
+ You must explicitly specify the type of the service interface here,
+ otherwise the server service will not be found.
+
+
+ See for more details.
+
+
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/plugins.topic b/docs/pages/kotlinx-rpc/topics/plugins.topic
index 67f4fe05d..c85326af0 100644
--- a/docs/pages/kotlinx-rpc/topics/plugins.topic
+++ b/docs/pages/kotlinx-rpc/topics/plugins.topic
@@ -9,22 +9,32 @@
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"
title="Gradle plugin" id="plugins">
- The kotlinx.rpc
library offers a Gradle plugin
- that simplifies project configuration by automating repetitive tasks: `org.jetbrains.kotlinx.rpc.plugin`
+ The kotlinx.rpc
library offers a Gradle
+ plugin
+ that simplifies project configuration by automating repetitive tasks and configuring code generation.
-
-
- The org.jetbrains.kotlinx.rpc.plugin
plugin sets up code generation configurations.
-
-
- plugins {
- kotlin("jvm") version "%kotlin-version%"
- id("org.jetbrains.kotlinx.rpc.plugin") version "%kotlinx-rpc-version%"
- }
-
-
- For multi-project setups you must add the plugin to all modules where services are declared or used.
-
-
+
+ plugins {
+ kotlin("jvm") version "%kotlin-version%"
+ id("org.jetbrains.kotlinx.rpc.plugin") version "%kotlinx-rpc-version%"
+ }
+
+
+ For multi-project setups you must add the plugin to all modules where services are declared or used.
+
+
+ Plugin provides an rpc
extension:
+
+ rpc {
+ annotationTypeSafetyEnabled = true
+ }
+
+ Options:
+
+
+ annotationTypeSafetyEnabled
- whether to enable type safety checks for annotations.
+ See .
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/rpc-clients.topic b/docs/pages/kotlinx-rpc/topics/rpc-clients.topic
index 7aa8c2024..30deafd23 100644
--- a/docs/pages/kotlinx-rpc/topics/rpc-clients.topic
+++ b/docs/pages/kotlinx-rpc/topics/rpc-clients.topic
@@ -8,22 +8,22 @@
-
- For each declared service, kotlinx.rpc
will generate an actual client implementation
- that can be used to send requests to a server. You must not use generated code directly. Instead,
- you should use the APIs that will provide you with the instance of your interface. This generated instance is
- commonly called a stub in RPC.
-
+
+ For each declared service, kotlinx.rpc
will generate an actual client implementation
+ that can be used to send requests to a server. This generated instance is
+ commonly called a stub in RPC.
+
- Note that we talk about generated stubs (service implementations on the client side)
- that must not be called directly.
- There might be a case when the generated code is not a stub, but a service declaration itself
- (for example, the services generated from
- .proto files).
- In this case, you can use the generated code.
+ Note that we talk about generated stubs (service interface implementations on the client side)
+ that must not be called directly.
+ There might be a case when the generated code is not a stub, but a service declaration itself
+ (for example, the services generated from
+ .proto files).
+ In this case, you can use the generated code.
+ See .
- To be able to obtain an instance of your service, you need to have an RpcClient
.
+ To be able to get an instance of your service, you need to have an RpcClient
.
You can call the withService()
method on your client:
@@ -31,6 +31,9 @@
val myService: MyService = rpcClient.withService<MyService>()
+
+ Note that the type parameter of withService
must be annotated with @Rpc
annotation.
+
Now you have your client instance that is able to send RPC requests to your server.
RpcClient
can have multiple services that communicate through it.
@@ -42,42 +45,7 @@
You can provide your own implementations of the RpcClient
.
But kotlinx.rpc
already provides one out-of-the-box solution that uses
- in-house RPC protocol (called kRPC), and we are working on supporting more protocols
- (with priority on gRPC).
+ in-house RPC protocol (called kRPC), and we are working on supporting more
+ protocols (see ).
-
-
- KrpcClient
is an abstract class that implements RpcClient
and kRPC protocol
- logic.
- The only thing required to be implemented is the transporting of the raw data.
- Abstract transport is represented by KrpcTransport
interface.
- To implement your own KrpcTransport
- you need to be able to transfer strings and/or raw bytes (Kotlin's ByteArray
).
- Additionally, the library will provide you with integrations with different
- libraries that are used to work with the network.
-
- See below an example usage of kRPC with a custom transport:
-
-
- class MySimpleRpcTransport : KrpcTransport {
- val outChannel = Channel<KrpcTransportMessage>()
- val inChannel = Channel<KrpcTransportMessage>()
-
- override val coroutineContext: CoroutineContext = Job()
-
- override suspend fun send(message: KrpcTransportMessage) {
- outChannel.send(message)
- }
-
- override suspend fun receive(): KrpcTransportMessage {
- return inChannel.receive()
- }
- }
-
- class MySimpleRpcClient : KrpcClient(rpcClientConfig(), MySimpleRpcTransport())
-
- val client = MySimpleRpcClient()
- val service: MyService = client.withService<MyService>()
-
-
diff --git a/docs/pages/kotlinx-rpc/topics/rpc-servers.topic b/docs/pages/kotlinx-rpc/topics/rpc-servers.topic
index 7f2e3b97c..3d9d9e104 100644
--- a/docs/pages/kotlinx-rpc/topics/rpc-servers.topic
+++ b/docs/pages/kotlinx-rpc/topics/rpc-servers.topic
@@ -8,51 +8,39 @@
- RpcServer
interface represents an RPC server,
- that accepts RPC messages and may contain multiple services to route to.
- RpcServer
uses data from incoming RPC messages
- and routes it to the designated service and sends service's response back to the corresponding client.
-
- You can provide your own RpcServer
implementation
- or use the one provided out of the box.
- Note that client and server must use the same RPC protocol to communicate.
- Use registerService
function to add your own factory for implemented RPC services.
- This factory function should accept CoroutineContext
argument and pass it to the service,
- which should use it to override coroutineContext
property of parent interface.
- This ensures proper application lifetime for your services.
- Example usage:
+
+ RpcServer
interface represents an RPC server,
+ that accepts RPC messages and may contain multiple services to route to.
+ RpcServer
uses data from incoming RPC messages
+ and routes it to the designated service and sends service's response back to the corresponding client.
+
+
+ You can provide your own RpcServer
implementation
+ or use the one provided out of the box.
+ Note that client and server must use the same RPC protocol to communicate.
+
+
+ Use registerService
function to add your own factory for implemented RPC services:
+
-
- val server: RpcServer
+
+ val server: RpcServer
- server.registerService<MyService> { ctx: CoroutineContext -> MyServiceImpl(ctx) }
-
- The registerService
function requires the explicit type of the declared RPC service.
+ server.registerService<MyService> { MyServiceImpl() }
+
+
+
+ The registerService
function requires the explicit type of the declared RPC service.
That means that the code will not work if you provide it with the type of the service implementation:
// Wrong! Should be `MyService` as type argument
- server.registerService<MyServiceImpl> { ctx: CoroutineContext -> MyServiceImpl(ctx) }
+ server.registerService<MyServiceImpl> { MyServiceImpl() }
-
- KrpcServer
abstract class implements RpcServer
- and all the logic for processing RPC messages
- and again leaves RpcTransport
methods for the specific implementations
- (see transports).
- Example usage with custom transport:
-
-
- // same MySimpleRpcTransport as in the client example above
- class MySimpleRpcServer : KrpcServer(rpcServerConfig(), MySimpleRpcTransport())
-
- val server = MySimpleRpcServer()
- server.registerService<MyService> { ctx -> MyServiceImpl(ctx) }
-
- Note that here we pass explicit MyService
type parameter to the registerService
- method.
- You must explicitly specify the type of the service interface here,
- otherwise the server service will not be found.
-
+
+ See for more details.
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/services.topic b/docs/pages/kotlinx-rpc/topics/services.topic
index 0cfa5ecb2..50bf9eb09 100644
--- a/docs/pages/kotlinx-rpc/topics/services.topic
+++ b/docs/pages/kotlinx-rpc/topics/services.topic
@@ -9,45 +9,87 @@
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"
title="Services" id="services">
Services are the centerpieces of the library.
- A service is an interface annotated with the @Rpc
annotation,
- and contains a set of methods and fields
- that can be executed or accessed remotely.
- Additionally, a service always has a type of RemoteService
,
- which can be specified explicitly, or assumed implicitly by the compiler.
+ A service is an interface annotated with the @Rpc
annotation,
+ and contains a set of methods that can be executed remotely.
-
- Note that implicit typing is currently not supported in IDEs, but is a work in progress.
-
A simple service can be declared as follows:
-
- @Rpc
- interface MyService : RemoteService {
- suspend fun hello(name: String): String
- }
-
- Here we declare the method hello
, that accepts one argument and returns a string.
- The method is marked with a suspend
modifier, which means that we use
- coroutines
- to send RPC requests.
- Note that for now, only suspend
methods are supported in service interfaces.
-
- Depending on the type of the protocol you use, services may support different features and
- declarations.
-
- To be able to use a service both in client and server code,
- the interface should be placed in the common module
- — kudos to Kotlin Multiplatform.
- Now we can implement the service, so server knows how to process incoming requests.
-
-
- class MyServiceImpl(
- override val coroutineContext: CoroutineContext,
- ) : MyService {
- override suspend fun hello(name: String): String {
- return "Hello, $name! I'm server!"
- }
- }
-
- The server will use that implementation to answer the client requests.
+
+ @Rpc
+ interface MyService {
+ suspend fun hello(name: String): String
+ }
+
+
+ Here we declare the method hello
, that accepts one argument and returns a string.
+ The method is marked with a suspend
modifier, which means that we use
+ coroutines
+ to send RPC requests.
+
+
+
+ Depending on the type of the protocol you use, services may support different features and
+ declarations.
+
+
+
+ To be able to use a service both in client and server code,
+ the interface should be placed in the common module
+ — kudos to Kotlin Multiplatform.
+
+
+ Now we can implement the service, so the server knows how to process incoming requests.
+
+
+ class MyServiceImpl : MyService {
+ override suspend fun hello(name: String): String {
+ return "Hello, $name! I'm server!"
+ }
+ }
+
+
+ The server will use that implementation to answer the client requests.
+
+
+
+ The library supports streaming of data.
+ To declare a streaming method, you need to use the Flow
type.
+ For example, to declare a method that returns a stream of integers:
+
+
+ @Rpc
+ interface MyService {
+ fun stream(): Flow<Int>
+ }
+
+
+ Note that the method is not marked with the suspend
modifier.
+
+
+ You can also pass a Flow
as a parameter to the method.
+ For example, to declare a method that accepts a stream of integers:
+
+
+ @Rpc
+ interface MyService {
+ suspend fun sendStream(stream: Flow<Int>)
+ }
+
+
+ Or make streams bidirectional:
+
+
+ @Rpc
+ interface MyService {
+ fun bidi(stream: Flow<Int>): Flow<Int>
+ }
+
+
+
+ Note that the function is not suspend
+ only when the return type is a Flow
.
+ Flow
in parameter list does not affect the function's signature.
+
+
+
diff --git a/docs/pages/kotlinx-rpc/topics/strict-mode.topic b/docs/pages/kotlinx-rpc/topics/strict-mode.topic
index 0d512e3ba..33e660682 100644
--- a/docs/pages/kotlinx-rpc/topics/strict-mode.topic
+++ b/docs/pages/kotlinx-rpc/topics/strict-mode.topic
@@ -10,11 +10,11 @@
title="Strict mode" id="strict-mode">
- Starting with version 0.5.0
, the library introduces major changes to the service APIs.
- The following declarations will be gradually restricted:
+ Starting with version 0.5.0
, the library introduced major changes to the service APIs.
+ The following declarations are now restricted:
- String mode will be enforced in the 0.8.0
release.
+ Strict mode is enforced irreversibly since 0.8.0
.
Deprecation level: ERROR
@@ -148,37 +148,4 @@
}
-
-
-
- Deprecation levels are controlled by the Gradle rpc
extension:
-
-
- // build.gradle.kts
- plugins {
- id("org.jetbrains.kotlinx.rpc.plugin")
- }
-
- rpc {
- strict {
- stateFlow = RpcStrictMode.ERROR
- sharedFlow = RpcStrictMode.ERROR
- nestedFlow = RpcStrictMode.ERROR
- notTopLevelServerFlow = RpcStrictMode.ERROR
- fields = RpcStrictMode.ERROR
- suspendingServerStreaming = RpcStrictMode.ERROR
- streamScopedFunctions = RpcStrictMode.ERROR
- }
- }
-
-
- Modes RpcStrictMode.NONE
and RpcStrictMode.WARNING
are available.
-
-
-
- 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/topics/transport.topic b/docs/pages/kotlinx-rpc/topics/transport.topic
index 8eb26bd7b..2beca318d 100644
--- a/docs/pages/kotlinx-rpc/topics/transport.topic
+++ b/docs/pages/kotlinx-rpc/topics/transport.topic
@@ -9,143 +9,21 @@
xsi:noNamespaceSchemaLocation="https://resources.jetbrains.com/writerside/1.0/topic.v2.xsd"
title="Transport" id="transport">
- Transport layer exists to abstract from the RPC requests logic and focus on delivering and receiving
- encoded RPC messages in kRPC Protocol.
- This layer is represented by KrpcTransport
interface.
- It supports two message formats — string and binary,
- and depending on which serialization format you choose,
- one or the other will be used.
-
-
- The kotlinx.rpc
library provides integration with the Ktor
- framework with the in-house RPC
- protocol.
- This includes both server and client APIs.
- Under the hood, the library uses WebSocket plugin
- to create a KrpcTransport
and send and receive messages through it.
-
- kotlinx.rpc
provides a way to plug-in into existing Ktor clients with your RPC services.
- To do that, the following DSL can be used:
-
-
- val ktorClient = HttpClient {
- installKrpc { // this: KrpcConfigBuilder.Client
- waitForServices = true
- }
- }
-
- val rpcClient: KtorKrpcClient =
- ktorClient.rpc("ws://localhost:4242/services") { // this: HttpRequestBuilder
- rpcConfig { // this: KrpcConfigBuilder.Client
- waitForServices = false
- }
- }
-
- // access WebSocketSession that created the connection
- rpcClient.webSocketSession
-
- // create RPC service
- val myService: MyService = rpcClient.withService<MyService>()
-
- Note that in this example, only the latter defined KrpcConfig
will be used.
-
-
- kotlinx.rpc
provides a way to plug-in into existing server routing with your RPC
- services.
- To do that, the following DSL can be used:
-
-
- fun Application.module() {
- install(Krpc) { // this: KrpcConfigBuilder.Server
- waitForServices = true
- }
-
- routing {
- rpc("/services") { // this KrpcRoute, inherits WebSocketSession
- rpcConfig { // this: KrpcConfigBuilder.Server
- waitForServices = false
- }
-
- registerService<MyService> { ctx -> MyServiceImpl(ctx) }
- registerService<MyOtherService> { ctx - MyOtherServiceImpl(ctx) }
- // etc
- }
- }
- }
-
-
-
- An example code for a Ktor web application may look like this:
-
-
- // ### COMMON CODE ###
- @Serializable
- data class ProcessedImage(
- val url: String,
- val numberOfCats: Int,
- val numberOfDogs: Int
- )
-
- @Rpc
- interface ImageService : RemoteService {
- suspend fun processImage(url: String): ProcessedImage
- }
-
- // ### CLIENT CODE ###
-
- val client = HttpClient {
- installKrpc {
- serialization {
- json()
- }
- }
- }
-
- val service = client.rpc("/image-recognizer").withService<ImageService>()
-
- service.processImage(url = "https://catsanddogs.com/cats/1")
-
- // ### SERVER CODE ###
-
- class ImageServiceImpl(override val coroutineContext: CoroutineContext) : ImageService {
- // user defined classes
- private val downloader = Downloader()
- private val recognizer = AnimalRecognizer()
-
- override suspend fun processImage(url: Srting): ProcessedImage {
- val image = downloader.loadImage(url)
- return ProcessedImage(
- url,
- recognizer.getNumberOfCatsOnImage(image),
- recognizer.getNumberOfDogsOnImage(image)
- )
- }
- }
-
- fun main() {
- embeddedServer(Netty, port = 8080) {
- install(Krpc) {
- serialization {
- json()
- }
- }
-
- routing {
- rpc("/image-recognizer") {
- registerService<ImageService> { ctx -> ImageServiceImpl(ctx) }
- }
- }
- }.start(wait = true)
- }
-
- For more details and complete examples, see the code samples.
-
-
-
-
- Generally, there are no specific guidelines on how RPC should be set up for different transports,
+
+ Transport layer exists to abstract from the RPC requests logic and focus on delivering and receiving
+ encoded RPC messages in kRPC Protocol.
+ This layer is represented by the KrpcTransport
interface.
+ It supports two message formats — string and binary,
+ and depends on which serialization format you
+ choose.
+
+
+
+
+ Generally, there are no specific guidelines on how RPC should be set up for different transports,
but structures and APIs used to develop integration with Ktor should outline the common approach.
You can provide your own transport and even your own fully implemented protocols,
- while the library will take care of code generation.
+ while the library will take care of code generation.
+
diff --git a/docs/pages/kotlinx-rpc/topics/versions.topic b/docs/pages/kotlinx-rpc/topics/versions.topic
index 5532ec839..877238bb4 100644
--- a/docs/pages/kotlinx-rpc/topics/versions.topic
+++ b/docs/pages/kotlinx-rpc/topics/versions.topic
@@ -8,42 +8,47 @@
- As kotlinx.rpc
uses Kotlin compiler plugin,
- we rely on internal functionality that may change over time with any new Kotlin version.
- To prevent the library from breaking with an incompatible Kotlin version,
- we use version prefix for artifacts with code generating functionality.
-
-
- We provide core version updates for all stable versions of
- the last three
- major Kotlin releases.
- So if the last stable Kotlin version is %kotlin-version%
, as at the time of writing this guide,
- the following versions of Kotlin are supported:
-
-
- 2.0.0, 2.0.10, 2.0.20, 2.0.21
- 2.1.0, 2.1.10, 2.1.20, 2.1.21
-
+
+ As kotlinx.rpc
uses Kotlin compiler plugin,
+ we rely on internal functionality that may change over time with any new Kotlin version.
+ To prevent the library from breaking with an incompatible Kotlin version,
+ we use version prefix for artifacts with code generating functionality.
+
+
+ We provide core version updates for all stable versions of
+ the last three
+ major Kotlin releases.
+ So if the last stable Kotlin version is %kotlin-version%
, as at the time of writing this guide,
+ the following versions of Kotlin are supported:
+
+
+ 2.0.0, 2.0.10, 2.0.20, 2.0.21
+ 2.1.0, 2.1.10, 2.1.20, 2.1.21
+
+
+ Our code generation will support these versions (See more on code
+ generation artifacts).
+ Runtime artifacts are configured with
+
+ language-version
and api-version
parameters
+
+ for the oldest supported minor version of Kotlin.
+
+
+ The kotlinx-rpc
library is currently not stable.
+ As a result, we cannot guarantee compatibility with all Kotlin versions for our runtime dependencies at this
+ time.
+ However, we are committed to maintaining compatibility as much as possible.
+ It is in our plans to ensure that all runtime dependencies are compatible with all supported Kotlin versions by
+ the time of the stable release.
+
+
- Our code generation will support these versions (See more on code generation artifacts).
- Runtime artifacts are configured with
-
- language-version
and api-version
parameters
-
- for the oldest supported minor version of Kotlin.
+ To simplify project configuration, our
+ is able to set proper runtime dependencies versions automatically based
+ on the project's Kotlin version and the Gradle plugin version
+ which is used as a core library version.
-
- The kotlinx-rpc
library is currently not stable.
- As a result, we cannot guarantee compatibility with all Kotlin versions for our runtime dependencies at this time.
- However, we are committed to maintaining compatibility as much as possible.
- It is in our plans to ensure that all runtime dependencies are compatible with all supported Kotlin versions by the time of the stable release.
-
-
-
- To simplify project configuration, both our Gradle plugins
- are able to set proper runtime dependencies versions automatically based
- on the project's Kotlin version and the Gradle plugin version
- which is used as a core library version.
plugins {
// project's Kotlin plugin
@@ -77,6 +82,6 @@
org.jetbrains.kotlinx:kotlinx-rpc-compiler-plugin-k2:%kotlin-version%-%kotlinx-rpc-version%
org.jetbrains.kotlinx:kotlinx-rpc-compiler-plugin-backend:%kotlin-version%-%kotlinx-rpc-version%
- Such dependencies are managed automatically, and should not be used explicitly.
+ These dependencies are managed automatically, and should not be used explicitly.
diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt
index c64ed04a0..6f71a2318 100644
--- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt
+++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/Extensions.kt
@@ -31,8 +31,8 @@ open class RpcExtension @Inject constructor(objects: ObjectFactory) {
/**
* Strict mode settings.
- * Allows configuring the reporting state of deprecated features.
*/
+ @Deprecated("Strict mode enabled irreversibly. This option can't change it.", level = DeprecationLevel.ERROR)
fun strict(configure: Action) {
configure.execute(strict)
}
@@ -88,6 +88,7 @@ open class RpcStrictModeExtension @Inject constructor(objects: ObjectFactory) {
*
* Consider using regular streaming.
*/
+ @Deprecated("Field are deprecated with level ERROR. This option can't change it.")
val fields: Property = objects.strictModeProperty()
private fun ObjectFactory.strictModeProperty(
diff --git a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt
index d75dc0a5d..c70a82e61 100644
--- a/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt
+++ b/gradle-plugin/src/main/kotlin/kotlinx/rpc/compilerPlugins.kt
@@ -29,20 +29,8 @@ class CompilerPluginCli : KotlinCompilerPluginSupportPlugin by compilerPlugin({
val extension = it.target.project.extensions.findByType()
?: RpcExtension(it.target.project.objects)
- val strict = extension.strict
-
it.target.project.provider {
listOf(
- SubpluginOption("strict-stateFlow", strict.stateFlow.get().toCompilerArg()),
- SubpluginOption("strict-sharedFlow", strict.sharedFlow.get().toCompilerArg()),
- SubpluginOption("strict-nested-flow", strict.nestedFlow.get().toCompilerArg()),
- SubpluginOption("strict-stream-scope", strict.streamScopedFunctions.get().toCompilerArg()),
- SubpluginOption(
- "strict-suspending-server-streaming",
- strict.suspendingServerStreaming.get().toCompilerArg()
- ),
- SubpluginOption("strict-not-top-level-server-flow", strict.notTopLevelServerFlow.get().toCompilerArg()),
- SubpluginOption("strict-fields", strict.fields.get().toCompilerArg()),
@OptIn(RpcDangerousApi::class)
SubpluginOption("annotation-type-safety", extension.annotationTypeSafetyEnabled.get().toString()),
)
diff --git a/krpc/krpc-client/api/krpc-client.api b/krpc/krpc-client/api/krpc-client.api
index 42ce07884..4579e9b42 100644
--- a/krpc/krpc-client/api/krpc-client.api
+++ b/krpc/krpc-client/api/krpc-client.api
@@ -1,11 +1,17 @@
-public abstract class kotlinx/rpc/krpc/client/KrpcClient : kotlinx/rpc/krpc/internal/KrpcServiceHandler, kotlinx/rpc/RpcClient, kotlinx/rpc/krpc/internal/KrpcEndpoint {
+public abstract class kotlinx/rpc/krpc/client/InitializedKrpcClient : kotlinx/rpc/krpc/client/KrpcClient {
public fun (Lkotlinx/rpc/krpc/KrpcConfig$Client;Lkotlinx/rpc/krpc/KrpcTransport;)V
+ protected final fun initializeConfig ()Lkotlinx/rpc/krpc/KrpcConfig$Client;
+ protected final fun initializeTransport (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+}
+
+public abstract class kotlinx/rpc/krpc/client/KrpcClient : kotlinx/rpc/RpcClient, kotlinx/rpc/krpc/internal/KrpcEndpoint {
+ public fun ()V
+ public final fun awaitCompletion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun call (Lkotlinx/rpc/RpcCall;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public fun callAsync (Lkotlinx/coroutines/CoroutineScope;Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/Deferred;
- public fun callServerStreaming (Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/flow/Flow;
- protected final fun getConfig ()Lkotlinx/rpc/krpc/KrpcConfig$Client;
- public synthetic fun getConfig ()Lkotlinx/rpc/krpc/KrpcConfig;
- public final fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
- protected final fun getLogger ()Lkotlinx/rpc/krpc/internal/logging/RpcInternalCommonLogger;
+ public final fun callServerStreaming (Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/flow/Flow;
+ public final fun close (Ljava/lang/String;)V
+ public static synthetic fun close$default (Lkotlinx/rpc/krpc/client/KrpcClient;Ljava/lang/String;ILjava/lang/Object;)V
+ protected abstract fun initializeConfig ()Lkotlinx/rpc/krpc/KrpcConfig$Client;
+ protected abstract fun initializeTransport (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
diff --git a/krpc/krpc-client/build.gradle.kts b/krpc/krpc-client/build.gradle.kts
index 8b91ed2e6..195b9dabd 100644
--- a/krpc/krpc-client/build.gradle.kts
+++ b/krpc/krpc-client/build.gradle.kts
@@ -28,14 +28,3 @@ kotlin {
}
}
-rpc {
- strict {
- stateFlow = RpcStrictMode.NONE
- sharedFlow = RpcStrictMode.NONE
- nestedFlow = RpcStrictMode.NONE
- streamScopedFunctions = RpcStrictMode.NONE
- suspendingServerStreaming = RpcStrictMode.NONE
- notTopLevelServerFlow = RpcStrictMode.NONE
- fields = RpcStrictMode.NONE
- }
-}
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 94f1c630b..65dd4fcd9 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
@@ -8,118 +8,190 @@ import kotlinx.atomicfu.atomic
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.rpc.RpcCall
import kotlinx.rpc.RpcClient
import kotlinx.rpc.annotations.Rpc
import kotlinx.rpc.descriptor.RpcCallable
-import kotlinx.rpc.internal.serviceScopeOrNull
+import kotlinx.rpc.descriptor.RpcInvokator
import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlinx.rpc.internal.utils.RpcInternalSupervisedCompletableDeferred
import kotlinx.rpc.internal.utils.getOrNull
import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
import kotlinx.rpc.krpc.*
+import kotlinx.rpc.krpc.client.internal.ClientStreamContext
+import kotlinx.rpc.krpc.client.internal.ClientStreamSerializer
import kotlinx.rpc.krpc.client.internal.KrpcClientConnector
+import kotlinx.rpc.krpc.client.internal.StreamCall
import kotlinx.rpc.krpc.internal.*
import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
import kotlinx.serialization.BinaryFormat
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialFormat
import kotlinx.serialization.StringFormat
-import kotlin.coroutines.CoroutineContext
+import kotlinx.serialization.modules.SerializersModule
+import kotlin.collections.first
+import kotlin.concurrent.Volatile
import kotlin.coroutines.cancellation.CancellationException
-
-@Deprecated("Use KrpcClient instead", ReplaceWith("KrpcClient"), level = DeprecationLevel.ERROR)
-public typealias KRPCClient = KrpcClient
+import kotlin.properties.Delegates
/**
- * Default implementation of [RpcClient].
- * Takes care of tracking requests and responses,
- * serializing data, tracking streams, processing exceptions, and other protocol responsibilities.
- * Leaves out the delivery of encoded messages to the specific implementations.
- *
- * A simple example of how this client may be implemented:
- * ```kotlin
- * class MyTransport : RpcTransport { /*...*/ }
+ * Represents an initialized [KrpcClient] that wraps a predefined [config] and [transport] parameters.
*
- * class MyClient(config: RpcConfig.Client): KrpcClient(config, MyTransport())
- * ```
- *
- * @property config configuration provided for that specific client. Applied to all services that use this client.
+ * @param config configuration provided for that specific client. Applied to all services that use this client.
+ * See [KrpcClient.initializeConfig].
* @param transport [KrpcTransport] instance that will be used to send and receive RPC messages.
- * IMPORTANT: Must be exclusive to this client, otherwise unexpected behavior may occur.
+ * See [KrpcClient.initializeTransport].
*/
-@OptIn(InternalCoroutinesApi::class)
-public abstract class KrpcClient(
- final override val config: KrpcConfig.Client,
- transport: KrpcTransport,
-) : KrpcServiceHandler(), RpcClient, KrpcEndpoint {
- // we make a child here, so we can send cancellation messages before closing the connection
- final override val coroutineContext: CoroutineContext = SupervisorJob(transport.coroutineContext.job)
+public abstract class InitializedKrpcClient(
+ private val config: KrpcConfig.Client,
+ private val transport: KrpcTransport,
+): KrpcClient() {
+ final override suspend fun initializeTransport(): KrpcTransport {
+ return transport
+ }
- private val connector by lazy {
- KrpcClientConnector(config.serialFormatInitializer.build(), transport, config.waitForServices)
+ final override fun initializeConfig(): KrpcConfig.Client {
+ return config
}
+}
- private var connectionId: Long? = null
+/**
+ * kRPC implementation of the [RpcClient].
+ * Takes care of tracking requests and responses,
+ * serializing data, tracking streams, processing exceptions, and other protocol responsibilities.
+ * Leaves out the delivery of encoded messages to the specific implementations with [KrpcTransport].
+ */
+@OptIn(InternalCoroutinesApi::class)
+public abstract class KrpcClient : RpcClient, KrpcEndpoint {
+ /**
+ * Called once to provide [KrpcTransport] for this client.
+ *
+ * IMPORTANT: The provided instance must be exclusive to this client, otherwise unexpected behavior may occur.
+ */
+ protected abstract suspend fun initializeTransport(): KrpcTransport
- @InternalRpcApi
- final override val sender: KrpcMessageSender
- get() = connector
+ /**
+ * Called once to provide [KrpcConfig.Client] for this client.
+ * Called only after [initializeTransport].
+ *
+ * Configuration is applied to all services that use this client.
+ */
+ protected abstract fun initializeConfig(): KrpcConfig.Client
- private val callCounter = atomic(0L)
+ /**
+ * Close this client, removing all the services and stopping accepting messages.
+ */
+ public fun close(message: String? = null) {
+ if (!isTransportReady) {
+ return
+ }
- final override val logger: RpcInternalCommonLogger = RpcInternalCommonLogger.logger(rpcInternalObjectId())
+ internalScope.cancel(message ?: "Client closed")
+ }
- private val serverSupportedPlugins: CompletableDeferred> = CompletableDeferred()
+ /**
+ * Waits until the client is closed.
+ */
+ public suspend fun awaitCompletion() {
+ if (!isTransportReady) {
+ return
+ }
- @InternalRpcApi
- final override val supportedPlugins: Set
- get() = serverSupportedPlugins.getOrNull() ?: emptySet()
+ internalScope.coroutineContext.job.join()
+ }
- private var clientCancelled = false
+ /*
+ * #####################################################################
+ * # #
+ * # INTERNALS AHEAD #
+ * # #
+ * #####################################################################
+ */
- // callId to serviceTypeString
- private val cancellingRequests = RpcInternalConcurrentHashMap()
+ private var isTransportReady: Boolean = false
+ private var transport: KrpcTransport by Delegates.notNull()
- init {
- coroutineContext.job.invokeOnCompletion(onCancelling = true) {
- clientCancelled = true
+ private val config: KrpcConfig.Client by lazy {
+ initializeConfig()
+ }
- sendCancellation(CancellationType.ENDPOINT, null, null, closeTransportAfterSending = true)
- }
+ @Volatile
+ private var clientCancelled = false
- launch {
- connector.subscribeToGenericMessages(::handleGenericMessage)
+ private fun checkTransportReadiness() {
+ if (!isTransportReady) {
+ error(
+ "Internal error, please contact developers for the support. " +
+ "Transport is not initialized, first scope access must come from an RPC request."
+ )
}
}
- @OptIn(InternalCoroutinesApi::class)
@InternalRpcApi
- final override fun provideStubContext(serviceId: Long): CoroutineContext {
- val childContext = SupervisorJob(coroutineContext.job).withClientStreamScope()
+ public val internalScope: CoroutineScope by lazy {
+ checkTransportReadiness()
- childContext.job.invokeOnCompletion(onCancelling = true) {
- if (!clientCancelled) {
- // cancellation only by serviceId
- sendCancellation(CancellationType.SERVICE, serviceId.toString(), null)
+ val context = SupervisorJob(transport.coroutineContext.job)
+
+ context.job.invokeOnCompletion(onCancelling = true) {
+ clientCancelled = true
+
+ sendCancellation(CancellationType.ENDPOINT, null, null, closeTransportAfterSending = true)
+
+ @OptIn(DelicateCoroutinesApi::class)
+ @Suppress("detekt.GlobalCoroutineUsage")
+ GlobalScope.launch(CoroutineName("client-request-channels-closing")) {
+ requestChannels.values.forEach { it.close(CancellationException("Client cancelled")) }
+ requestChannels.clear()
}
}
- return childContext
+ CoroutineScope(context)
}
- private val initHandshake: Job = launch {
- connector.sendMessage(KrpcProtocolMessage.Handshake(KrpcPlugin.ALL))
+ // we make a child here, so we can send cancellation messages before closing the connection
+ private val connector by lazy {
+ checkTransportReadiness()
- connector.subscribeToProtocolMessages(::handleProtocolMessage)
+ KrpcClientConnector(config.serialFormatInitializer.build(), transport, config.waitForServices)
}
+ private var connectionId: Long? = null
+
+ @InternalRpcApi
+ final override val sender: KrpcMessageSender
+ get() = connector
+
+ private val callCounter = atomic(0L)
+
+ private val logger: RpcInternalCommonLogger = RpcInternalCommonLogger.logger(rpcInternalObjectId())
+
+ private val serverSupportedPlugins: CompletableDeferred> = CompletableDeferred()
+
+ private val requestChannels = RpcInternalConcurrentHashMap>()
+
+ @InternalRpcApi
+ final override val supportedPlugins: Set
+ get() = serverSupportedPlugins.getOrNull() ?: emptySet()
+
+ // callId to serviceTypeString
+ private val cancellingRequests = RpcInternalConcurrentHashMap()
+
/**
* Starts the handshake process and awaits for completion.
* If the handshake was completed before, nothing happens.
*/
- private suspend fun awaitHandshakeCompletion() {
- initHandshake.join()
+ private suspend fun initializeAndAwaitHandshakeCompletion() {
+ transport = initializeTransport()
+ isTransportReady = true
+
+ connector.subscribeToGenericMessages(::handleGenericMessage)
+ connector.subscribeToProtocolMessages(::handleProtocolMessage)
+
+ connector.sendMessage(KrpcProtocolMessage.Handshake(KrpcPlugin.ALL))
+
serverSupportedPlugins.await()
}
@@ -144,179 +216,18 @@ public abstract class KrpcClient(
}
}
- @Deprecated(
- "This method was primarily used for fields in RPC services, which are now deprecated. " +
- "See https://kotlin.github.io/kotlinx-rpc/strict-mode.html fields guide for more information"
- )
- override fun callAsync(
- serviceScope: CoroutineScope,
- call: RpcCall,
- ): Deferred {
- val callable = call.descriptor.getCallable(call.callableName)
- ?: error("Unexpected callable '${call.callableName}' for ${call.descriptor.fqName} service")
-
- val deferred = RpcInternalSupervisedCompletableDeferred(serviceScope.coroutineContext.job)
-
- /**
- * Launched on the service scope (receiver)
- * Moreover, this scope has [StreamScope] that is used to handle field streams.
- * [StreamScope] is provided to a service via [provideStubContext].
- */
- serviceScope.launch {
- val rpcCall = call(call, callable, deferred)
-
- deferred.invokeOnCompletion { cause ->
- if (cause == null) {
- rpcCall.streamContext.valueOrNull?.launchIf({ incomingHotFlowsAvailable }) {
- handleIncomingHotFlows(it)
- }
- }
- }
- }
-
- return deferred
- }
-
final override suspend fun call(call: RpcCall): T {
- val callable = call.descriptor.getCallable(call.callableName)
- ?: error("Unexpected callable '${call.callableName}' for ${call.descriptor.fqName} service")
-
- val callCompletableResult = RpcInternalSupervisedCompletableDeferred()
- val rpcCall = call(call, callable, callCompletableResult)
- val result = callCompletableResult.await()
-
- // incomingHotFlowsAvailable value is known after await
- rpcCall.streamContext.valueOrNull?.launchIf({ incomingHotFlowsAvailable }) {
- handleIncomingHotFlows(it)
- }
-
- return result
+ return callServerStreaming(call).first()
}
- private suspend fun call(
- call: RpcCall,
- callable: RpcCallable<*>,
- callResult: CompletableDeferred,
- ): RpcCallStreamContextFormatAndId {
- val wrappedCallResult = RequestCompletableDeferred(callResult)
- val rpcCall = prepareAndExecuteCall(call, callable, wrappedCallResult)
-
- rpcCall.streamContext.valueOrNull?.launchIf({ outgoingStreamsAvailable }) {
- handleOutgoingStreams(it, rpcCall.serialFormat, call.descriptor.fqName)
- }
-
- val handle = serviceScopeOrNull()?.run {
- serviceCoroutineScope.coroutineContext.job.invokeOnCompletion(onCancelling = true) { cause ->
- // service can only be canceled, it can't complete successfully
- callResult.completeExceptionally(CancellationException(cause))
-
- rpcCall.streamContext.valueOrNull?.cancel("Service cancelled", cause)
- }
- }
-
- callResult.invokeOnCompletion { cause ->
- if (cause != null) {
- cancellingRequests[rpcCall.callId] = call.descriptor.fqName
-
- rpcCall.streamContext.valueOrNull?.cancel("Request failed", cause)
-
- if (!wrappedCallResult.callExceptionOccurred) {
- sendCancellation(CancellationType.REQUEST, call.serviceId.toString(), rpcCall.callId)
- }
-
- handle?.dispose()
- } else {
- val streamScope = rpcCall.streamContext.valueOrNull?.streamScope
-
- if (streamScope == null) {
- handle?.dispose()
-
- connector.unsubscribeFromMessages(call.descriptor.fqName, rpcCall.callId)
- }
-
- streamScope?.onScopeCompletion(rpcCall.callId) {
- handle?.dispose()
-
- cancellingRequests[rpcCall.callId] = call.descriptor.fqName
-
- sendCancellation(CancellationType.REQUEST, call.serviceId.toString(), rpcCall.callId)
- }
- }
- }
-
- return rpcCall
- }
-
- private suspend fun prepareAndExecuteCall(
- call: RpcCall,
- callable: RpcCallable<*>,
- callResult: RequestCompletableDeferred<*>,
- ): RpcCallStreamContextFormatAndId {
- // we should wait for the handshake to finish
- awaitHandshakeCompletion()
-
- val id = callCounter.incrementAndGet()
-
- val callId = "$connectionId:${callable.name}:$id"
-
- logger.trace { "start a call[$callId] ${callable.name}" }
-
- val fallbackScope = serviceScopeOrNull()
- ?.serviceCoroutineScope
- ?.let { streamScopeOrNull(it) }
-
- val streamContext = LazyKrpcStreamContext(streamScopeOrNull(), fallbackScope) {
- KrpcStreamContext(callId, config, connectionId, call.serviceId, it)
- }
- val serialFormat = prepareSerialFormat(streamContext)
- val firstMessage = serializeRequest(callId, call, callable, serialFormat)
-
- @Suppress("UNCHECKED_CAST")
- executeCall(
- callId = callId,
- streamContext = streamContext,
- call = call,
- callable = callable,
- firstMessage = firstMessage,
- serialFormat = serialFormat,
- callResult = callResult as RequestCompletableDeferred
- )
-
- return RpcCallStreamContextFormatAndId(streamContext, serialFormat, callId)
- }
-
- private data class RpcCallStreamContextFormatAndId(
- val streamContext: LazyKrpcStreamContext,
- val serialFormat: SerialFormat,
- val callId: String,
- )
-
- private suspend fun executeCall(
- callId: String,
- streamContext: LazyKrpcStreamContext,
- call: RpcCall,
- callable: RpcCallable<*>,
- firstMessage: KrpcCallMessage,
- serialFormat: SerialFormat,
- callResult: RequestCompletableDeferred,
- ) {
- connector.subscribeToCallResponse(call.descriptor.fqName, callId) { message ->
- if (cancellingRequests.containsKey(callId)) {
- return@subscribeToCallResponse
- }
-
- handleMessage(message, streamContext, callable, serialFormat, callResult)
- }
-
- connector.sendMessage(firstMessage)
- }
-
- private val noFlowSerialFormat = config.serialFormatInitializer.build()
-
@Suppress("detekt.CyclomaticComplexMethod")
- override fun callServerStreaming(call: RpcCall): Flow {
+ final override fun callServerStreaming(call: RpcCall): Flow {
return flow {
- awaitHandshakeCompletion()
+ if (clientCancelled) {
+ error("Client cancelled")
+ }
+
+ initializeAndAwaitHandshakeCompletion()
val id = callCounter.incrementAndGet()
val callable = call.descriptor.getCallable(call.callableName)
@@ -326,14 +237,9 @@ public abstract class KrpcClient(
val channel = Channel()
- val streamScope = StreamScope(currentCoroutineContext())
-
try {
- val streamContext = LazyKrpcStreamContext(streamScope, null) {
- KrpcStreamContext(callId, config, connectionId, call.serviceId, it)
- }
-
- val serialFormat = prepareSerialFormat(streamContext)
+ @Suppress("UNCHECKED_CAST")
+ requestChannels[callId] = channel as Channel
val request = serializeRequest(
callId = callId,
@@ -343,36 +249,63 @@ public abstract class KrpcClient(
pluginParams = mapOf(KrpcPluginKey.NON_SUSPENDING_SERVER_FLOW_MARKER to ""),
)
- connector.sendMessage(request)
-
connector.subscribeToCallResponse(call.descriptor.fqName, callId) { message ->
- handleServerStreamingMessage(message, channel, callable, call, callId)
- }
+ if (cancellingRequests.containsKey(callId)) {
+ return@subscribeToCallResponse
+ }
- streamContext.valueOrNull?.launchIf({ outgoingStreamsAvailable }) {
- handleOutgoingStreams(it, serialFormat, call.descriptor.fqName)
+ handleServerStreamingMessage(message, channel, callable)
}
- while (true) {
- val element = channel.receiveCatching()
- if (element.isClosed) {
- val ex = element.exceptionOrNull() ?: break
- throw ex
+ connector.sendMessage(request)
+
+ coroutineScope {
+ val clientStreamsJob = launch(CoroutineName("client-stream-root-${call.serviceId}-$callId")) {
+ supervisorScope {
+ clientStreamContext.streams[callId].orEmpty().forEach {
+ launch(CoroutineName("client-stream-${call.serviceId}-$callId-${it.streamId}")) {
+ handleOutgoingStream(it, serialFormat, call.descriptor.fqName)
+ }
+ }
+ }
}
- if (!element.isFailure) {
- emit(element.getOrThrow())
+ try {
+ consumeAndEmitServerMessages(channel)
+ } finally {
+ clientStreamsJob.cancelAndJoin()
+ clientStreamContext.streams.remove(callId)
}
}
} 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)
+ if (!clientCancelled) {
+ cancellingRequests[callId] = call.descriptor.fqName
+
+ sendCancellation(CancellationType.REQUEST, call.descriptor.fqName, callId)
+
+ connector.unsubscribeFromMessages(call.descriptor.fqName, callId) {
+ cancellingRequests.remove(callId)
+ }
+ }
throw e
} finally {
- streamScope.close()
channel.close()
+ requestChannels.remove(callId)
+ }
+ }
+ }
+
+ private suspend fun FlowCollector.consumeAndEmitServerMessages(channel: Channel) {
+ while (true) {
+ val element = channel.receiveCatching()
+ if (element.isClosed) {
+ val ex = element.exceptionOrNull() ?: break
+ throw ex
+ }
+
+ if (!element.isFailure) {
+ emit(element.getOrThrow())
}
}
}
@@ -381,8 +314,6 @@ public abstract class KrpcClient(
message: KrpcCallMessage,
channel: Channel,
callable: RpcCallable,
- call: RpcCall,
- callId: String,
) {
when (message) {
is KrpcCallMessage.CallData -> {
@@ -405,10 +336,10 @@ public abstract class KrpcClient(
is KrpcCallMessage.CallSuccess, is KrpcCallMessage.StreamMessage -> {
val value = runCatching {
- val serializerResult = noFlowSerialFormat.serializersModule
+ val serializerResult = serialFormat.serializersModule
.rpcSerializerForType(callable.returnType)
- decodeMessageData(noFlowSerialFormat, serializerResult, message)
+ decodeMessageData(serialFormat, serializerResult, message)
}
@Suppress("UNCHECKED_CAST")
@@ -416,82 +347,21 @@ public abstract class KrpcClient(
}
is KrpcCallMessage.StreamFinished -> {
- connector.unsubscribeFromMessages(call.descriptor.fqName, callId)
channel.close()
}
is KrpcCallMessage.StreamCancel -> {
- connector.unsubscribeFromMessages(call.descriptor.fqName, callId)
val cause = message.cause.deserialize()
channel.close(cause)
}
}
}
- private suspend fun handleMessage(
- message: KrpcCallMessage,
- streamContext: LazyKrpcStreamContext,
- callable: RpcCallable<*>,
- serialFormat: SerialFormat,
- callResult: RequestCompletableDeferred,
- ) {
- when (message) {
- is KrpcCallMessage.CallData -> {
- error("Unexpected message")
- }
-
- is KrpcCallMessage.CallException -> {
- val cause = runCatching {
- message.cause.deserialize()
- }
-
- val result = if (cause.isFailure) {
- cause.exceptionOrNull()!!
- } else {
- cause.getOrNull()!!
- }
-
- callResult.callExceptionOccurred = true
- callResult.completeExceptionally(result)
- }
-
- is KrpcCallMessage.CallSuccess -> {
- val value = runCatching {
- val serializerResult = serialFormat.serializersModule.rpcSerializerForType(callable.returnType)
-
- decodeMessageData(serialFormat, serializerResult, message)
- }
-
- callResult.completeWith(value)
- }
-
- is KrpcCallMessage.StreamCancel -> {
- streamContext.awaitInitialized().cancelStream(message)
- }
-
- is KrpcCallMessage.StreamFinished -> {
- streamContext.awaitInitialized().closeStream(message)
- }
-
- is KrpcCallMessage.StreamMessage -> {
- streamContext.awaitInitialized().send(message, serialFormat)
- }
- }
- }
-
@InternalRpcApi
final override suspend fun handleCancellation(message: KrpcGenericMessage) {
when (val type = message.cancellationType()) {
CancellationType.ENDPOINT -> {
- cancel("Closing client after server cancellation") // we cancel this client
- }
-
- CancellationType.CANCELLATION_ACK -> {
- val callId = message[KrpcPluginKey.CANCELLATION_ID]
- ?: error("Expected CANCELLATION_ID for cancellation of type 'request'")
-
- val serviceTypeString = cancellingRequests.remove(callId) ?: return
- connector.unsubscribeFromMessages(serviceTypeString, callId)
+ internalScope.cancel("Closing client after server cancellation") // we cancel this client
}
else -> {
@@ -513,7 +383,10 @@ public abstract class KrpcClient(
val serializerData = serialFormat.serializersModule.rpcSerializerForType(callable.dataType)
return when (serialFormat) {
is StringFormat -> {
- val stringValue = serialFormat.encodeToString(serializerData, call.data)
+ val stringValue = clientStreamContext.scoped(callId, call.serviceId) {
+ serialFormat.encodeToString(serializerData, call.data)
+ }
+
KrpcCallMessage.CallDataString(
callId = callId,
serviceType = call.descriptor.fqName,
@@ -527,7 +400,10 @@ public abstract class KrpcClient(
}
is BinaryFormat -> {
- val binaryValue = serialFormat.encodeToByteArray(serializerData, call.data)
+ val binaryValue = clientStreamContext.scoped(callId, call.serviceId) {
+ serialFormat.encodeToByteArray(serializerData, call.data)
+ }
+
KrpcCallMessage.CallDataBinary(
callId = callId,
serviceType = call.descriptor.fqName,
@@ -545,8 +421,104 @@ public abstract class KrpcClient(
}
}
}
-}
-private class RequestCompletableDeferred(delegate: CompletableDeferred) : CompletableDeferred by delegate {
- var callExceptionOccurred: Boolean = false
+ private suspend fun handleOutgoingStream(
+ outgoingStream: StreamCall,
+ serialFormat: SerialFormat,
+ serviceTypeString: String,
+ ) {
+ try {
+ collectAndSendOutgoingStream(
+ serialFormat = serialFormat,
+ flow = outgoingStream.stream,
+ outgoingStream = outgoingStream,
+ serviceTypeString = serviceTypeString,
+ )
+ } catch (e: CancellationException) {
+ throw e
+ } catch (@Suppress("detekt.TooGenericExceptionCaught") cause: Throwable) {
+ val serializedReason = serializeException(cause)
+ val message = KrpcCallMessage.StreamCancel(
+ callId = outgoingStream.callId,
+ serviceType = serviceTypeString,
+ streamId = outgoingStream.streamId,
+ cause = serializedReason,
+ connectionId = outgoingStream.connectionId,
+ serviceId = outgoingStream.serviceId,
+ )
+ sender.sendMessage(message)
+
+ throw cause
+ }
+
+ val message = KrpcCallMessage.StreamFinished(
+ callId = outgoingStream.callId,
+ serviceType = serviceTypeString,
+ streamId = outgoingStream.streamId,
+ connectionId = outgoingStream.connectionId,
+ serviceId = outgoingStream.serviceId,
+ )
+
+ sender.sendMessage(message)
+ }
+
+ private suspend fun collectAndSendOutgoingStream(
+ serialFormat: SerialFormat,
+ flow: Flow<*>,
+ serviceTypeString: String,
+ outgoingStream: StreamCall,
+ ) {
+ flow.collect {
+ val message = when (serialFormat) {
+ is StringFormat -> {
+ val stringData = serialFormat.encodeToString(outgoingStream.elementSerializer, it)
+ KrpcCallMessage.StreamMessageString(
+ callId = outgoingStream.callId,
+ serviceType = serviceTypeString,
+ streamId = outgoingStream.streamId,
+ data = stringData,
+ connectionId = outgoingStream.connectionId,
+ serviceId = outgoingStream.serviceId,
+ )
+ }
+
+ is BinaryFormat -> {
+ val binaryData = serialFormat.encodeToByteArray(outgoingStream.elementSerializer, it)
+ KrpcCallMessage.StreamMessageBinary(
+ callId = outgoingStream.callId,
+ serviceType = serviceTypeString,
+ streamId = outgoingStream.streamId,
+ data = binaryData,
+ connectionId = outgoingStream.connectionId,
+ serviceId = outgoingStream.serviceId,
+ )
+ }
+
+ else -> {
+ unsupportedSerialFormatError(serialFormat)
+ }
+ }
+
+ sender.sendMessage(message)
+ }
+ }
+
+ private val clientStreamContext: ClientStreamContext = ClientStreamContext(connectionId = connectionId)
+
+ private val serialFormat: SerialFormat by lazy {
+ val module = SerializersModule {
+ contextual(Flow::class) {
+ @Suppress("UNCHECKED_CAST")
+ ClientStreamSerializer(clientStreamContext, it.first() as KSerializer)
+ }
+ }
+
+ config.serialFormatInitializer.applySerializersModuleAndBuild(module)
+ }
+
+ private fun RpcCallable<*>.toMessageCallType(): KrpcCallMessage.CallType {
+ return when (invokator) {
+ is RpcInvokator.Method -> KrpcCallMessage.CallType.Method
+ }
+ }
}
diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamContext.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamContext.kt
new file mode 100644
index 000000000..772588d87
--- /dev/null
+++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamContext.kt
@@ -0,0 +1,56 @@
+/*
+ * 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.client.internal
+
+import kotlinx.atomicfu.atomic
+import kotlinx.coroutines.flow.Flow
+import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
+import kotlinx.rpc.internal.utils.thread.RpcInternalThreadLocal
+import kotlinx.serialization.KSerializer
+
+internal class ClientStreamContext(private val connectionId: Long?) {
+ val streams = RpcInternalConcurrentHashMap>()
+
+ private val currentCallId = RpcInternalThreadLocal()
+ private val currentServiceId = RpcInternalThreadLocal()
+
+ fun scoped(callId: String, serviceId: Long, body: () -> T): T {
+ try {
+ currentCallId.set(callId)
+ currentServiceId.set(serviceId)
+ return body()
+ } finally {
+ currentCallId.remove()
+ currentServiceId.remove()
+ }
+ }
+
+ private val streamIdCounter = atomic(0L)
+
+ fun registerClientStream(value: Flow<*>, elementKind: KSerializer<*>): String {
+ val callId = currentCallId.get() ?: error("No call id")
+ val serviceId = currentServiceId.get() ?: error("No service id")
+ val streamId = "$STREAM_ID_PREFIX${streamIdCounter.getAndIncrement()}"
+
+ @Suppress("UNCHECKED_CAST")
+ val stream = StreamCall(
+ callId = callId,
+ streamId = streamId,
+ stream = value,
+ elementSerializer = elementKind as KSerializer,
+ connectionId = connectionId,
+ serviceId = serviceId
+ )
+
+ @Suppress("UNCHECKED_CAST")
+ streams.merge(callId, listOf(stream)) { old, new -> old + new }
+
+ return streamId
+ }
+
+ private companion object {
+ private const val STREAM_ID_PREFIX = "stream:"
+ }
+}
diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamSerializer.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamSerializer.kt
new file mode 100644
index 000000000..d677dbf7f
--- /dev/null
+++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/ClientStreamSerializer.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.rpc.krpc.client.internal
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.rpc.krpc.internal.StreamSerializer
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+internal class ClientStreamSerializer(
+ val context: ClientStreamContext,
+ val elementType: KSerializer,
+) : KSerializer>, StreamSerializer() {
+ override val descriptor: SerialDescriptor by lazy {
+ buildClassSerialDescriptor("ClientStreamSerializer") {
+ element(STREAM_ID_SERIAL_NAME, streamIdDescriptor)
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): Flow<*> {
+ error("This method must not be called. Please report to the developer.")
+ }
+
+ override fun serialize(encoder: Encoder, value: Flow<*>) {
+ val id = context.registerClientStream(value, elementType)
+
+ encoder.encodeString(id)
+ }
+}
diff --git a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt
index 105ead2e1..ebee09b5c 100644
--- a/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt
+++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/KrpcClientConnector.kt
@@ -36,9 +36,8 @@ internal class KrpcClientConnector private constructor(
}
)
- @Suppress("unused")
- fun unsubscribeFromMessages(serviceTypeString: String, callId: String) {
- connector.unsubscribeFromMessages(CallSubscriptionId.Service(serviceTypeString, callId))
+ fun unsubscribeFromMessages(serviceTypeString: String, callId: String, callback: () -> Unit = {}) {
+ connector.unsubscribeFromMessages(CallSubscriptionId.Service(serviceTypeString, callId), callback)
}
suspend fun subscribeToCallResponse(
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamCall.kt b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/StreamCall.kt
similarity index 56%
rename from krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamCall.kt
rename to krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/StreamCall.kt
index 6d22b4f25..4c4a556c5 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamCall.kt
+++ b/krpc/krpc-client/src/commonMain/kotlin/kotlinx/rpc/krpc/client/internal/StreamCall.kt
@@ -1,16 +1,16 @@
/*
- * 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.internal
+package kotlinx.rpc.krpc.client.internal
+import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.KSerializer
-internal data class KrpcStreamCall(
+internal data class StreamCall(
val callId: String,
val streamId: String,
- val stream: Any,
- val kind: StreamKind,
+ val stream: Flow<*>,
val elementSerializer: KSerializer,
val connectionId: Long?,
val serviceId: Long?,
diff --git a/krpc/krpc-core/api/krpc-core.api b/krpc/krpc-core/api/krpc-core.api
index 517af2dde..7b7696b17 100644
--- a/krpc/krpc-core/api/krpc-core.api
+++ b/krpc/krpc-core/api/krpc-core.api
@@ -1,29 +1,23 @@
public abstract interface class kotlinx/rpc/krpc/KrpcConfig {
public abstract fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder;
- public abstract fun getSharedFlowBuilder ()Lkotlin/jvm/functions/Function0;
public abstract fun getWaitForServices ()Z
}
public final class kotlinx/rpc/krpc/KrpcConfig$Client : kotlinx/rpc/krpc/KrpcConfig {
public fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder;
- public fun getSharedFlowBuilder ()Lkotlin/jvm/functions/Function0;
public fun getWaitForServices ()Z
}
public final class kotlinx/rpc/krpc/KrpcConfig$Server : kotlinx/rpc/krpc/KrpcConfig {
public fun getSerialFormatInitializer ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder;
- public fun getSharedFlowBuilder ()Lkotlin/jvm/functions/Function0;
public fun getWaitForServices ()Z
}
public abstract class kotlinx/rpc/krpc/KrpcConfigBuilder {
- protected final fun getSharedFlowBuilder ()Lkotlin/jvm/functions/Function0;
public final fun getWaitForServices ()Z
protected final fun rpcSerialFormat ()Lkotlinx/rpc/krpc/serialization/KrpcSerialFormatBuilder;
public final fun serialization (Lkotlin/jvm/functions/Function1;)V
- protected final fun setSharedFlowBuilder (Lkotlin/jvm/functions/Function0;)V
public final fun setWaitForServices (Z)V
- public final fun sharedFlowParameters (Lkotlin/jvm/functions/Function1;)V
}
public final class kotlinx/rpc/krpc/KrpcConfigBuilder$Client : kotlinx/rpc/krpc/KrpcConfigBuilder {
@@ -36,17 +30,6 @@ public final class kotlinx/rpc/krpc/KrpcConfigBuilder$Server : kotlinx/rpc/krpc/
public final fun build ()Lkotlinx/rpc/krpc/KrpcConfig$Server;
}
-public final class kotlinx/rpc/krpc/KrpcConfigBuilder$SharedFlowParametersBuilder {
- public static final field DEFAULT_EXTRA_BUFFER_CAPACITY I
- public static final field DEFAULT_REPLAY I
- public final fun getExtraBufferCapacity ()I
- public final fun getOnBufferOverflow ()Lkotlinx/coroutines/channels/BufferOverflow;
- public final fun getReplay ()I
- public final fun setExtraBufferCapacity (I)V
- public final fun setOnBufferOverflow (Lkotlinx/coroutines/channels/BufferOverflow;)V
- public final fun setReplay (I)V
-}
-
public final class kotlinx/rpc/krpc/KrpcConfigKt {
public static final fun rpcClientConfig (Lkotlin/jvm/functions/Function1;)Lkotlinx/rpc/krpc/KrpcConfig$Client;
public static synthetic fun rpcClientConfig$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/krpc/KrpcConfig$Client;
@@ -56,12 +39,11 @@ public final class kotlinx/rpc/krpc/KrpcConfigKt {
public abstract interface class kotlinx/rpc/krpc/KrpcTransport : kotlinx/coroutines/CoroutineScope {
public abstract fun receive (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public abstract fun receiveCatching-IoAF18A (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public abstract fun send (Lkotlinx/rpc/krpc/KrpcTransportMessage;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
-public final class kotlinx/rpc/krpc/KrpcTransport$DefaultImpls {
- public static fun receiveCatching-IoAF18A (Lkotlinx/rpc/krpc/KrpcTransport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+public final class kotlinx/rpc/krpc/KrpcTransportKt {
+ public static final fun receiveCatching (Lkotlinx/rpc/krpc/KrpcTransport;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
}
public abstract interface class kotlinx/rpc/krpc/KrpcTransportMessage {
@@ -77,15 +59,3 @@ public final class kotlinx/rpc/krpc/KrpcTransportMessage$StringMessage : kotlinx
public final fun getValue ()Ljava/lang/String;
}
-public final class kotlinx/rpc/krpc/StreamScope : java/lang/AutoCloseable {
- public fun close ()V
-}
-
-public final class kotlinx/rpc/krpc/StreamScopeKt {
- public static final fun StreamScope (Lkotlin/coroutines/CoroutineContext;)Lkotlinx/rpc/krpc/StreamScope;
- public static final fun invokeOnStreamScopeCompletion (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static synthetic fun invokeOnStreamScopeCompletion$default (ZLkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
- public static final fun streamScoped (Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun withStreamScope (Lkotlinx/rpc/krpc/StreamScope;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
-}
-
diff --git a/krpc/krpc-core/build.gradle.kts b/krpc/krpc-core/build.gradle.kts
index 29040a7dc..fe19a6840 100644
--- a/krpc/krpc-core/build.gradle.kts
+++ b/krpc/krpc-core/build.gradle.kts
@@ -14,6 +14,8 @@ plugins {
applyAtomicfuPlugin()
kotlin {
+ compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes")
+
sourceSets {
commonMain {
dependencies {
@@ -28,15 +30,3 @@ kotlin {
}
}
}
-
-rpc {
- strict {
- stateFlow = RpcStrictMode.NONE
- sharedFlow = RpcStrictMode.NONE
- nestedFlow = RpcStrictMode.NONE
- streamScopedFunctions = RpcStrictMode.NONE
- suspendingServerStreaming = RpcStrictMode.NONE
- notTopLevelServerFlow = RpcStrictMode.NONE
- fields = RpcStrictMode.NONE
- }
-}
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 962e965e1..566afada2 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
@@ -4,115 +4,14 @@
package kotlinx.rpc.krpc
-import kotlinx.coroutines.channels.BufferOverflow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.rpc.internal.utils.InternalRpcApi
import kotlinx.rpc.krpc.serialization.KrpcSerialFormat
import kotlinx.rpc.krpc.serialization.KrpcSerialFormatBuilder
import kotlinx.rpc.krpc.serialization.KrpcSerialFormatConfiguration
-@Deprecated("Use KrpcConfigBuilder instead", ReplaceWith("KrpcConfigBuilder"), level = DeprecationLevel.ERROR)
-public typealias RPCConfigBuilder = KrpcConfigBuilder
-
/**
* Builder for [KrpcConfig]. Provides DSL to configure parameters for KrpcClient and/or KrpcServer.
*/
-public sealed class KrpcConfigBuilder private constructor() {
- /**
- * DSL for parameters of [MutableSharedFlow] and [SharedFlow].
- *
- * This is a temporary solution that hides the problem of transferring these parameters.
- * [SharedFlow] and [MutableSharedFlow] do not define theirs 'replay', 'extraBufferCapacity' and 'onBufferOverflow'
- * 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
-
- /**
- * The number of values buffered in addition to replay.
- * 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
-
- /**
- * Configures an emit action on buffer overflow.
- * Optional, defaults to suspending attempts to emit a value.
- * Values other than [BufferOverflow.SUSPEND] are supported only when replay > 0 or extraBufferCapacity > 0.
- * Buffer overflow can happen only when there is at least one subscriber
- * that is not ready to accept the new value.
- * 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)
- }
-
- private companion object {
- /**
- * Default value of [replay]
- */
- const val DEFAULT_REPLAY = 1
- /**
- * Default value of [extraBufferCapacity]
- */
- const val DEFAULT_EXTRA_BUFFER_CAPACITY = 10
- }
- }
-
- @Suppress("DEPRECATION")
- protected var sharedFlowBuilder: () -> MutableSharedFlow = SharedFlowParametersBuilder().builder()
-
- /**
- * @see SharedFlowParametersBuilder
- */
- @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()
- }
-
- private var serialFormatInitializer: KrpcSerialFormatBuilder<*, *>? = null
-
- private val configuration = object : KrpcSerialFormatConfiguration {
- override fun register(rpcSerialFormatInitializer: KrpcSerialFormatBuilder.Binary<*, *>) {
- serialFormatInitializer = rpcSerialFormatInitializer
- }
-
- override fun register(rpcSerialFormatInitializer: KrpcSerialFormatBuilder.String<*, *>) {
- serialFormatInitializer = rpcSerialFormatInitializer
- }
- }
-
+public sealed class KrpcConfigBuilder protected constructor() {
/**
* DSL for serialization configuration.
*
@@ -132,18 +31,11 @@ public sealed class KrpcConfigBuilder private constructor() {
configuration.builder()
}
- protected fun rpcSerialFormat(): KrpcSerialFormatBuilder<*, *> {
- return when (val format = serialFormatInitializer) {
- null -> error("Please, choose serialization format")
- else -> format
- }
- }
-
/**
- * A flag indicating whether the client should wait for subscribers
+ * A flag indicating whether a client or a server should wait for subscribers
* if no service is available to process a message immediately.
- * If false, the endpoint that sent the message will receive call exception
- * that says that there were no services to process its message.
+ * If `false`, the endpoint that sent the unprocessed message will receive a call exception
+ * saying there were no services to process the message.
*/
public var waitForServices: Boolean = true
@@ -153,7 +45,6 @@ public sealed class KrpcConfigBuilder private constructor() {
public class Client : KrpcConfigBuilder() {
public fun build(): KrpcConfig.Client {
return KrpcConfig.Client(
- sharedFlowBuilder = sharedFlowBuilder,
serialFormatInitializer = rpcSerialFormat(),
waitForServices = waitForServices,
)
@@ -166,34 +57,65 @@ public sealed class KrpcConfigBuilder private constructor() {
public class Server : KrpcConfigBuilder() {
public fun build(): KrpcConfig.Server {
return KrpcConfig.Server(
- sharedFlowBuilder = sharedFlowBuilder,
serialFormatInitializer = rpcSerialFormat(),
waitForServices = waitForServices,
)
}
}
-}
-@Deprecated("Use KrpcConfig instead", ReplaceWith("KrpcConfig"), level = DeprecationLevel.ERROR)
-public typealias RPCConfig = KrpcConfig
+ /*
+ * #####################################################################
+ * # #
+ * # CLASS INTERNALS AHEAD #
+ * # #
+ * #####################################################################
+ */
+
+ private var serialFormatInitializer: KrpcSerialFormatBuilder<*, *>? = null
+
+ private val configuration = object : KrpcSerialFormatConfiguration {
+ override fun register(rpcSerialFormatInitializer: KrpcSerialFormatBuilder.Binary<*, *>) {
+ serialFormatInitializer = rpcSerialFormatInitializer
+ }
+
+ override fun register(rpcSerialFormatInitializer: KrpcSerialFormatBuilder.String<*, *>) {
+ serialFormatInitializer = rpcSerialFormatInitializer
+ }
+ }
+
+ protected fun rpcSerialFormat(): KrpcSerialFormatBuilder<*, *> {
+ return when (val format = serialFormatInitializer) {
+ null -> error("Please, choose serialization format")
+ else -> format
+ }
+ }
+}
/**
* Configuration class that is used by kRPC protocol's client and server (KrpcClient and KrpcServer).
*/
public sealed interface KrpcConfig {
- @InternalRpcApi
- public val sharedFlowBuilder: () -> MutableSharedFlow
- @InternalRpcApi
+ /**
+ * @see KrpcConfigBuilder.serialization
+ */
public val serialFormatInitializer: KrpcSerialFormatBuilder<*, *>
- @InternalRpcApi
+
+ /**
+ * @see KrpcConfigBuilder.waitForServices
+ */
public val waitForServices: Boolean
/**
* @see [KrpcConfig]
*/
public class Client internal constructor(
- override val sharedFlowBuilder: () -> MutableSharedFlow,
+ /**
+ * @see KrpcConfigBuilder.serialization
+ */
override val serialFormatInitializer: KrpcSerialFormatBuilder<*, *>,
+ /**
+ * @see KrpcConfigBuilder.waitForServices
+ */
override val waitForServices: Boolean,
) : KrpcConfig
@@ -201,8 +123,13 @@ public sealed interface KrpcConfig {
* @see [KrpcConfig]
*/
public class Server internal constructor(
- override val sharedFlowBuilder: () -> MutableSharedFlow,
+ /**
+ * @see KrpcConfigBuilder.serialization
+ */
override val serialFormatInitializer: KrpcSerialFormatBuilder<*, *>,
+ /**
+ * @see KrpcConfigBuilder.waitForServices
+ */
override val waitForServices: Boolean,
) : KrpcConfig
}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt
index 0a5b15673..563913f19 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/KrpcTransport.kt
@@ -6,56 +6,59 @@ package kotlinx.rpc.krpc
import kotlinx.coroutines.CoroutineScope
-@Deprecated("Use KrpcTransportMessage instead", ReplaceWith("KrpcTransportMessage"), level = DeprecationLevel.ERROR)
-public typealias RPCTransportMessage = KrpcTransportMessage
-
/**
* A single message that can be transferred from one RPC endpoint to another.
* Can be either of string or binary type.
*/
public sealed interface KrpcTransportMessage {
+ /**
+ * A single message of the string type that can be transferred from one RPC endpoint to another.
+ */
public class StringMessage(public val value: String) : KrpcTransportMessage
+ /**
+ * A single message of the binary type that can be transferred from one RPC endpoint to another.
+ */
public class BinaryMessage(public val value: ByteArray) : KrpcTransportMessage
}
-@Deprecated("Use KrpcTransport instead", ReplaceWith("KrpcTransport"), level = DeprecationLevel.ERROR)
-public typealias RPCTransport = KrpcTransport
-
/**
* An abstraction of transport capabilities for KrpcClient and KrpcServer.
*
* For developers of custom transports:
* - The implementation should be able to handle both binary and string formats,
* though not necessary if you absolutely sure that only one will be supplied and received.
- * - The KrpcClient and KrpcServer suppose that they have exclusive instance of transport.
+ * - The KrpcClient and KrpcServer suppose that they have an exclusive instance of transport.
* That means that each client or/and server should have only one transport instance,
* otherwise some messages may be lost or processed incorrectly.
- * - The implementation should manage lifetime of the connection using its [CoroutineScope].
*
- * Good example of the implementation is KtorTransport, that uses websocket protocol to deliver messages.
+ * [CoroutineScope] is used to define connection's lifetime.
+ * If canceled, no messages will be able to go to the other side,
+ * so ideally, it should be canceled only after its client or server is.
+ *
+ * A good example of the implementation is KtorTransport, that uses websocket protocol to deliver messages.
*/
public interface KrpcTransport : CoroutineScope {
/**
- * Sends a single encoded RPC message over network (or any other medium) to a peer endpoint.
+ * Sends a single encoded RPC message over a network (or any other medium) to a peer endpoint.
*
* @param message a message to send. Either of string or binary type.
*/
public suspend fun send(message: KrpcTransportMessage)
/**
- * Suspends until next RPC message from a peer endpoint is received and then returns it.
+ * Suspends until the next RPC message from a peer endpoint is received and then returns it.
*
- * @return received RPC message.
+ * @return the received RPC message.
*/
public suspend fun receive(): KrpcTransportMessage
+}
- /**
- * Suspends until next RPC message from a peer endpoint is received and then returns it.
- *
- * @return received RPC message as a [Result].
- */
- public suspend fun receiveCatching(): Result {
- return runCatching { receive() }
- }
+/**
+ * Suspends until the next RPC message from a peer endpoint is received and then returns it.
+ *
+ * @return the received RPC message as a [Result].
+ */
+public suspend fun KrpcTransport.receiveCatching(): Result {
+ return runCatching { receive() }
}
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
deleted file mode 100644
index 90b75b662..000000000
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/StreamScope.kt
+++ /dev/null
@@ -1,292 +0,0 @@
-/*
- * 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
-
-import kotlinx.coroutines.*
-import kotlinx.rpc.internal.utils.ExperimentalRpcApi
-import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
-import kotlin.contracts.ExperimentalContracts
-import kotlin.contracts.InvocationKind
-import kotlin.contracts.contract
-import kotlin.coroutines.CoroutineContext
-import kotlin.coroutines.coroutineContext
-import kotlin.js.JsName
-
-/**
- * Stream scope handles all RPC streams that are launched inside it.
- * Streams are alive until stream scope is. Streams can outlive their initial request scope.
- *
- * Streams are grouped by the request that initiated them.
- * Each group can have a completion callback associated with it.
- *
- * Stream scope is a child of the [CoroutineContext] it was created in.
- * 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,
-) : AutoCloseable {
- internal class Element(internal val scope: StreamScope) : CoroutineContext.Element {
- override val key: CoroutineContext.Key = Key
-
- internal companion object Key : CoroutineContext.Key
- }
-
- internal val contextElement = Element(this)
-
- private val scopeJob = SupervisorJob(parentContext.job)
-
- private val requests = RpcInternalConcurrentHashMap()
-
- init {
- scopeJob.invokeOnCompletion {
- close()
- }
- }
-
- @InternalRpcApi
- public fun onScopeCompletion(handler: (Throwable?) -> Unit) {
- scopeJob.invokeOnCompletion(handler)
- }
-
- @InternalRpcApi
- public fun onScopeCompletion(callId: String, handler: (Throwable?) -> Unit) {
- getRequestScope(callId).coroutineContext.job.invokeOnCompletion(onCancelling = true, handler = handler)
- }
-
- @InternalRpcApi
- public fun cancelRequestScopeById(callId: String, message: String, cause: Throwable?): Job? {
- return requests.remove(callId)?.apply { cancel(message, cause) }?.coroutineContext?.job
- }
-
- // Group stream launches by callId. In case one fails, so do others
- @InternalRpcApi
- public fun launch(callId: String, block: suspend CoroutineScope.() -> Unit): Job {
- return getRequestScope(callId).launch(block = block)
- }
-
- override fun close() {
- scopeJob.cancel("Stream scope closed")
- requests.clear()
- }
-
- private fun getRequestScope(callId: String): CoroutineScope {
- return requests.computeIfAbsent(callId) { CoroutineScope(Job(scopeJob.job)) }
- }
-
- internal class CallScope(val callId: String) : CoroutineContext.Element {
- object Key : CoroutineContext.Key
-
- override val key: CoroutineContext.Key<*> = Key
- }
-
- @InternalRpcApi
- public enum class Role {
- Client, Server;
- }
-}
-
-@InternalRpcApi
-public fun CoroutineContext.withClientStreamScope(): CoroutineContext = withStreamScope(StreamScope.Role.Client)
-
-@InternalRpcApi
-public fun CoroutineContext.withServerStreamScope(): CoroutineContext = withStreamScope(StreamScope.Role.Server)
-
-@OptIn(InternalCoroutinesApi::class)
-internal fun CoroutineContext.withStreamScope(role: StreamScope.Role): CoroutineContext {
- return this + StreamScope(this, role).contextElement.apply {
- this@withStreamScope.job.invokeOnCompletion(onCancelling = true) { scope.close() }
- }
-}
-
-@InternalRpcApi
-public suspend fun streamScopeOrNull(): StreamScope? {
- return currentCoroutineContext()[StreamScope.Element.Key]?.scope
-}
-
-@InternalRpcApi
-public fun streamScopeOrNull(scope: CoroutineScope): StreamScope? {
- return scope.coroutineContext[StreamScope.Element.Key]?.scope
-}
-
-internal fun noStreamScopeError(): Nothing {
- error(
- "Stream scopes can only be used inside the 'streamScoped' block. \n" +
- "To use stream scope API on a client - wrap your call with 'streamScoped' block.\n" +
- "To use stream scope API on a server - use must use 'streamScoped' block for this call on a client."
- )
-}
-
-@InternalRpcApi
-public suspend fun callScoped(callId: String, block: suspend CoroutineScope.() -> T): T {
- val context = currentCoroutineContext()
-
- if (context[StreamScope.CallScope.Key] != null) {
- error("Nested callScoped calls are not allowed")
- }
-
- val callScope = StreamScope.CallScope(callId)
-
- return withContext(callScope, block)
-}
-
-/**
- * Defines lifetime for all RPC streams that are used inside it.
- * When the [block] ends - all streams that were created inside it are canceled.
- * The same happens when an exception is thrown.
- *
- * All RPC calls that use streams, either sending or receiving them,
- * MUST use this scope to define their lifetime.
- *
- * Lifetimes inside [streamScoped] are hierarchical,
- * meaning that there is parent lifetime for all calls inside this block,
- * and each call has its own lifetime independent of others.
- * This also means that all streams from one call share the same lifetime.
- *
- * Examples:
- * ```kotlin
- * streamScoped {
- * val flow = flow { /* ... */ }
- * service.sendStream(flow) // will stop sending updates when 'streamScoped' block is finished
- * }
- * ```
- *
- * ```kotlin
- * streamScoped {
- * launch {
- * val flow1 = flow { /* ... */ }
- * service.sendStream(flow)
- * }
- *
- * // if call with 'flow1' is canceled or failed - this flow will continue working
- * launch {
- * val flow2 = flow { /* ... */ }
- * service.sendStream(flow)
- * }
- * }
- * ```
- */
-@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 {
- callsInPlace(block, InvocationKind.EXACTLY_ONCE)
- }
-
- val context = currentCoroutineContext()
- .apply {
- checkContextForStreamScope()
- }
-
- val streamScope = StreamScope(context, StreamScope.Role.Client)
-
- return withContext(streamScope.contextElement) {
- streamScope.use {
- block()
- }
- }
-}
-
-private fun CoroutineContext.checkContextForStreamScope() {
- if (this[StreamScope.Element] != null) {
- error(
- "One of the following caused a failure: \n" +
- "- nested 'streamScoped' or `withStreamScope` calls are not allowed.\n" +
- "- 'streamScoped' or `withStreamScope` calls are not allowed in server RPC services."
- )
- }
-}
-
-/**
- * Creates a [StreamScope] entity for manual stream management.
- */
-@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()
-
- return StreamScope(parent, StreamScope.Role.Client)
-}
-
-/**
- * Adds manually managed [StreamScope] to the current context.
- */
-@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)
- }
-
- currentCoroutineContext().checkContextForStreamScope()
-
- return withContext(scope.contextElement, block)
-}
-
-/**
- * This is a callback that will run when stream scope (created by [streamScoped] function) ends.
- * Typically, this is used to release stream resources that may be occupied by a call:
- * ```kotlin
- * // service on server
- * override suspend fun returnStateFlow(): StateFlow {
- * val state = MutableStateFlow(-1)
- *
- * incomingHotFlowJob = launch {
- * repeat(Int.MAX_VALUE) { value ->
- * state.value = value
- *
- * delay(1000) // intense work
- * }
- * }
- *
- * // release resources allocated for state flow, when it is closed on the client
- * invokeOnStreamScopeCompletion {
- * incomingHotFlowJob.cancel()
- * }
- *
- * return state
- * }
- * ```
- */
-@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()
-
- if (streamScope.role == StreamScope.Role.Client) {
- streamScope.onScopeCompletion(block)
- return
- }
-
- val callScope = coroutineContext[StreamScope.CallScope.Key]
-
- when {
- callScope != null -> streamScope.onScopeCompletion(callScope.callId, block)
-
- throwIfNoScope -> error(
- "'invokeOnStreamScopeCompletion' can only be called with corresponding 'streamScoped' block on a client"
- )
- }
-}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/CancellationType.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/CancellationType.kt
index 9855e3c6f..b268df712 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/CancellationType.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/CancellationType.kt
@@ -11,8 +11,10 @@ import kotlinx.rpc.internal.utils.InternalRpcApi
@Suppress("detekt.MagicNumber")
public enum class CancellationType(override val uniqueIndex: Int) : RpcInternalIndexedEnum {
ENDPOINT(0),
+ @Deprecated("Service cancellation is deprecated.")
SERVICE(1),
REQUEST(2),
+ @Deprecated("Cancellation acknowledgement is deprecated.")
CANCELLATION_ACK(3),
;
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt
index ed1b04829..bf4808223 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcConnector.kt
@@ -5,16 +5,21 @@
package kotlinx.rpc.krpc.internal
import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.rpc.internal.utils.InternalRpcApi
+import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
import kotlinx.rpc.krpc.KrpcTransport
import kotlinx.rpc.krpc.KrpcTransportMessage
import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer
+import kotlinx.rpc.krpc.receiveCatching
import kotlinx.serialization.*
+import kotlin.time.Duration.Companion.seconds
@InternalRpcApi
public interface KrpcMessageSender : CoroutineScope {
@@ -39,7 +44,7 @@ private typealias KrpcMessageHandler = suspend (KrpcMessage) -> Unit
* DO NOT use actual dumper in production!
*/
@InternalRpcApi
-public class KrpcConnector(
+public class KrpcConnector(
private val serialFormat: SerialFormat,
private val transport: KrpcTransport,
private val waitForSubscribers: Boolean = true,
@@ -49,10 +54,9 @@ public class KrpcConnector(
private val role = if (isServer) SERVER_ROLE else CLIENT_ROLE
private val logger = RpcInternalCommonLogger.logger(rpcInternalObjectId(role))
- private val mutex = Mutex()
-
- private val waiting = mutableMapOf>()
- private val subscriptions = mutableMapOf()
+ private val waiting = RpcInternalConcurrentHashMap>()
+ private val subscriptions = RpcInternalConcurrentHashMap()
+ private val processWaitersLocks = RpcInternalConcurrentHashMap()
private val dumpLogger by lazy { RpcInternalDumpLoggerContainer.provide() }
@@ -78,19 +82,23 @@ public class KrpcConnector(
transport.send(transportMessage)
}
- public fun unsubscribeFromMessages(key: SubscriptionKey) {
- launch { mutex.withLock { subscriptions.remove(key) } }
+ public fun unsubscribeFromMessages(key: SubscriptionKey, callback: () -> Unit = {}) {
+ launch(CoroutineName("krpc-connector-unsubscribe-$key")) {
+ delay(15.seconds)
+ subscriptions.remove(key)
+ processWaitersLocks.remove(key)
+ }.invokeOnCompletion {
+ callback()
+ }
}
public suspend fun subscribeToMessages(key: SubscriptionKey, handler: KrpcMessageHandler) {
- mutex.withLock {
- subscriptions[key] = handler
- processWaiters(key, handler)
- }
+ subscriptions[key] = handler
+ processWaiters(key, handler)
}
init {
- launch {
+ launch(CoroutineName("krpc-connector-receive-loop")) {
while (true) {
processMessage(transport.receiveCatching().getOrNull() ?: break)
}
@@ -119,7 +127,7 @@ public class KrpcConnector(
processMessage(message)
}
- private suspend fun processMessage(message: KrpcMessage) = mutex.withLock {
+ private suspend fun processMessage(message: KrpcMessage) = withLockForKey(message.getKey()) {
when (message) {
is KrpcCallMessage -> processServiceMessage(message)
is KrpcProtocolMessage, is KrpcGenericMessage -> processNonServiceMessage(message)
@@ -139,7 +147,7 @@ public class KrpcConnector(
}
HandlerResult.NoSubscription -> {
- waiting.getOrPut(message.getKey()) { mutableListOf() }.add(message)
+ waiting.computeIfAbsent(message.getKey()) { mutableListOf() }.add(message)
}
HandlerResult.Success -> {} // ok
@@ -152,7 +160,7 @@ public class KrpcConnector(
// todo better exception processing probably
if (result != HandlerResult.Success) {
if (waitForSubscribers) {
- waiting.getOrPut(message.getKey()) { mutableListOf() }.add(message)
+ waiting.computeIfAbsent(message.getKey()) { mutableListOf() }.add(message)
val reason = when (result) {
is HandlerResult.Failure -> {
@@ -160,7 +168,7 @@ public class KrpcConnector(
}
is HandlerResult.NoSubscription -> {
- "No service with key '${message.getKey()}' and '${message.serviceType}' type was registered." +
+ "No service with key '${message.getKey()}' and '${message.serviceType}' type was registered. " +
"Available: keys: [${subscriptions.keys.joinToString()}]"
}
@@ -171,7 +179,7 @@ public class KrpcConnector(
logger.warn((result as? HandlerResult.Failure)?.cause) {
"No registered service of ${message.serviceType} service type " +
- "was able to process message at the moment. Waiting for new services." +
+ "was able to process message ($message) at the moment. Waiting for new services. " +
"Reason: $reason"
}
@@ -182,7 +190,7 @@ public class KrpcConnector(
val cause = IllegalStateException(
"Failed to process call ${message.callId} for service ${message.serviceType}, " +
- "${subscriptions.size} attempts failed",
+ "${subscriptions.values.size} attempts failed",
initialCause,
)
@@ -225,18 +233,24 @@ public class KrpcConnector(
}
private suspend fun processWaiters(key: SubscriptionKey, handler: KrpcMessageHandler) {
- if (waiting.isEmpty()) return
+ withLockForKey(key) {
+ if (waiting.values.isEmpty()) return
- val iterator = waiting[key]?.iterator() ?: return
- while (iterator.hasNext()) {
- val message = iterator.next()
+ val iterator = waiting[key]?.iterator() ?: return
+ while (iterator.hasNext()) {
+ val message = iterator.next()
- if (tryHandle(message, handler) == HandlerResult.Success) {
- iterator.remove()
+ val tryHandle = tryHandle(message, handler)
+ if (tryHandle == HandlerResult.Success) {
+ iterator.remove()
+ }
}
}
}
+ private suspend inline fun withLockForKey(key: SubscriptionKey, action: () -> T): T =
+ processWaitersLocks.computeIfAbsent(key) { Mutex() }.withLock(action = action)
+
internal companion object {
const val SEND_PHASE = "Send"
const val RECEIVE_PHASE = "Receive"
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt
index 88c8f1070..3289f0a24 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcEndpoint.kt
@@ -4,6 +4,7 @@
package kotlinx.rpc.krpc.internal
+import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.rpc.internal.utils.InternalRpcApi
@@ -30,7 +31,7 @@ public interface KrpcEndpoint {
return
}
- val sendJob = sender.launch {
+ val sendJob = sender.launch(CoroutineName("krpc-endpoint-cancellation-$serviceId-$cancellationId")) {
val message = KrpcGenericMessage(
connectionId = null,
pluginParams = listOfNotNull(
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt
index 63ba30f37..64b678038 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcMessage.kt
@@ -115,7 +115,9 @@ public sealed interface KrpcCallMessage : KrpcMessage {
@Serializable
@SerialName("org.jetbrains.krpc.internal.transport.RPCMessage.CallType")
public enum class CallType {
- Method, Field,
+ Method,
+ @Deprecated("Fields are not supported anymore. Use Method instead.")
+ Field,
}
@InternalRpcApi
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt
index 6804eeb14..55243646a 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcPlugin.kt
@@ -55,6 +55,11 @@ public enum class KrpcPlugin(
* This feature adds support for proper service/request cancellation over the network.
*/
NON_SUSPENDING_SERVER_FLOWS(3, KrpcVersion.V_0_6_0),
+
+ /**
+ * Clients don't require cancellation acknowledgement from the peer server.
+ */
+ NO_ACK_CANCELLATION(4, KrpcVersion.V_0_8_0),
;
@InternalRpcApi
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcServiceHandler.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcServiceHandler.kt
deleted file mode 100644
index 487327fd6..000000000
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcServiceHandler.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-/*
- * 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.internal
-
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.rpc.descriptor.RpcCallable
-import kotlinx.rpc.descriptor.RpcInvokator
-import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlinx.rpc.krpc.KrpcConfig
-import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
-import kotlinx.serialization.BinaryFormat
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialFormat
-import kotlinx.serialization.StringFormat
-import kotlinx.serialization.modules.SerializersModule
-
-@InternalRpcApi
-public abstract class KrpcServiceHandler {
- protected abstract val sender: KrpcMessageSender
- protected abstract val config: KrpcConfig
- protected abstract val logger: RpcInternalCommonLogger
-
- protected suspend fun handleIncomingHotFlows(streamContext: KrpcStreamContext) {
- for (hotFlow in streamContext.incomingHotFlows) {
- streamContext.launch {
- /** Start consuming incoming requests, see [KrpcIncomingHotFlow.emit] */
- hotFlow.emit(null)
- }
- }
- }
-
- protected suspend fun handleOutgoingStreams(
- streamContext: KrpcStreamContext,
- serialFormat: SerialFormat,
- serviceTypeString: String,
- ) {
- val mutex = Mutex()
- for (outgoingStream in streamContext.outgoingStreams) {
- streamContext.launch {
- try {
- when (outgoingStream.kind) {
- StreamKind.Flow, StreamKind.SharedFlow, StreamKind.StateFlow -> {
- val stream = outgoingStream.stream as Flow<*>
-
- collectAndSendOutgoingStream(
- mutex = mutex,
- serialFormat = serialFormat,
- flow = stream,
- outgoingStream = outgoingStream,
- serviceTypeString = serviceTypeString,
- )
- }
- }
- } catch (e : CancellationException) {
- // canceled by a streamScope
- throw e
- } catch (@Suppress("detekt.TooGenericExceptionCaught") cause: Throwable) {
- mutex.withLock {
- val serializedReason = serializeException(cause)
- val message = KrpcCallMessage.StreamCancel(
- callId = outgoingStream.callId,
- serviceType = serviceTypeString,
- streamId = outgoingStream.streamId,
- cause = serializedReason,
- connectionId = outgoingStream.connectionId,
- serviceId = outgoingStream.serviceId,
- )
- sender.sendMessage(message)
- }
- throw cause
- }
-
- mutex.withLock {
- val message = KrpcCallMessage.StreamFinished(
- callId = outgoingStream.callId,
- serviceType = serviceTypeString,
- streamId = outgoingStream.streamId,
- connectionId = outgoingStream.connectionId,
- serviceId = outgoingStream.serviceId,
- )
-
- sender.sendMessage(message)
- }
- }
- }
- }
-
- @Suppress("detekt.LongParameterList")
- private suspend fun collectAndSendOutgoingStream(
- mutex: Mutex,
- serialFormat: SerialFormat,
- flow: Flow<*>,
- serviceTypeString: String,
- outgoingStream: KrpcStreamCall,
- ) {
- flow.collect {
- // because we can send new message for the new flow,
- // which is not published with `transport.send(message)`
- mutex.withLock {
- val message = when (serialFormat) {
- is StringFormat -> {
- val stringData = serialFormat.encodeToString(outgoingStream.elementSerializer, it)
- KrpcCallMessage.StreamMessageString(
- callId = outgoingStream.callId,
- serviceType = serviceTypeString,
- streamId = outgoingStream.streamId,
- data = stringData,
- connectionId = outgoingStream.connectionId,
- serviceId = outgoingStream.serviceId,
- )
- }
-
- is BinaryFormat -> {
- val binaryData = serialFormat.encodeToByteArray(outgoingStream.elementSerializer, it)
- KrpcCallMessage.StreamMessageBinary(
- callId = outgoingStream.callId,
- serviceType = serviceTypeString,
- streamId = outgoingStream.streamId,
- data = binaryData,
- connectionId = outgoingStream.connectionId,
- serviceId = outgoingStream.serviceId,
- )
- }
-
- else -> {
- unsupportedSerialFormatError(serialFormat)
- }
- }
-
- sender.sendMessage(message)
- }
- }
- }
-
- protected fun prepareSerialFormat(rpcFlowContext: LazyKrpcStreamContext): SerialFormat {
- val module = SerializersModule {
- contextual(Flow::class) {
- @Suppress("UNCHECKED_CAST")
- StreamSerializer.Flow(rpcFlowContext.initialize(), it.first() as KSerializer)
- }
-
- contextual(SharedFlow::class) {
- @Suppress("UNCHECKED_CAST")
- StreamSerializer.SharedFlow(rpcFlowContext.initialize(), it.first() as KSerializer)
- }
-
- contextual(StateFlow::class) {
- @Suppress("UNCHECKED_CAST")
- StreamSerializer.StateFlow(rpcFlowContext.initialize(), it.first() as KSerializer)
- }
- }
-
- return config.serialFormatInitializer.applySerializersModuleAndBuild(module)
- }
-
- protected fun RpcCallable<*>.toMessageCallType(): KrpcCallMessage.CallType {
- return when (invokator) {
- is RpcInvokator.Method -> KrpcCallMessage.CallType.Method
- is RpcInvokator.Field -> KrpcCallMessage.CallType.Field
- }
- }
-}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamContext.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamContext.kt
deleted file mode 100644
index 73fbd3a24..000000000
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcStreamContext.kt
+++ /dev/null
@@ -1,355 +0,0 @@
-/*
- * 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.internal
-
-import kotlinx.atomicfu.atomic
-import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.FlowCollector
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.flow
-import kotlinx.coroutines.selects.select
-import kotlinx.rpc.internal.utils.InternalRpcApi
-import kotlinx.rpc.internal.utils.getDeferred
-import kotlinx.rpc.internal.utils.getOrNull
-import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
-import kotlinx.rpc.internal.utils.set
-import kotlinx.rpc.krpc.KrpcConfig
-import kotlinx.rpc.krpc.StreamScope
-import kotlinx.rpc.krpc.noStreamScopeError
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.SerialFormat
-import kotlin.coroutines.CoroutineContext
-
-@InternalRpcApi
-public class LazyKrpcStreamContext(
- public val streamScopeOrNull: StreamScope?,
- private val fallbackScope: StreamScope? = null,
- private val initializer: (StreamScope) -> KrpcStreamContext,
-) {
- private val deferred = CompletableDeferred()
- private val lazyValue by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
- if (streamScopeOrNull == null && (STREAM_SCOPES_ENABLED || fallbackScope == null)) {
- noStreamScopeError()
- }
-
- // null pointer is impossible
- val streamScope = streamScopeOrNull ?: fallbackScope!!
- initializer(streamScope).also { deferred.complete(it) }
- }
-
- public suspend fun awaitInitialized(): KrpcStreamContext = deferred.await()
-
- public val valueOrNull: KrpcStreamContext? get() = if (deferred.isCompleted) lazyValue else null
-
- public fun initialize(): KrpcStreamContext = lazyValue
-}
-
-@InternalRpcApi
-public class KrpcStreamContext(
- private val callId: String,
- private val config: KrpcConfig,
- private val connectionId: Long?,
- private val serviceId: Long?,
- public val streamScope: StreamScope,
-) {
- private companion object {
- private const val STREAM_ID_PREFIX = "stream:"
- }
- private val closed = CompletableDeferred()
-
- // thread-safe set
- private val closedStreams = RpcInternalConcurrentHashMap>()
-
- @InternalRpcApi
- public inline fun launchIf(
- condition: KrpcStreamContext.() -> Boolean,
- noinline block: suspend CoroutineScope.(KrpcStreamContext) -> Unit,
- ) {
- if (condition(this)) {
- launch(block)
- }
- }
-
- public fun launch(block: suspend CoroutineScope.(KrpcStreamContext) -> Unit) {
- streamScope.launch(callId) {
- block(this@KrpcStreamContext)
- }
- }
-
- public fun cancel(message: String, cause: Throwable?): Job? {
- return streamScope.cancelRequestScopeById(callId, message, cause)
- }
-
- init {
- streamScope.onScopeCompletion(callId) { cause ->
- close(cause)
- }
- }
-
- private val streamIdCounter = atomic(0L)
-
- public val incomingHotFlowsAvailable: Boolean get() = incomingHotFlowsInitialized
-
- public val outgoingStreamsAvailable: Boolean get() = outgoingStreamsInitialized
-
- private var incomingStreamsInitialized: Boolean = false
- private val incomingStreams by lazy {
- incomingStreamsInitialized = true
- RpcInternalConcurrentHashMap>()
- }
-
- private var incomingChannelsInitialized: Boolean = false
- private val incomingChannels by lazy {
- incomingChannelsInitialized = true
- RpcInternalConcurrentHashMap?>>()
- }
-
- private var outgoingStreamsInitialized: Boolean = false
- internal val outgoingStreams: Channel by lazy {
- outgoingStreamsInitialized = true
- Channel(capacity = Channel.UNLIMITED)
- }
-
- private var incomingHotFlowsInitialized: Boolean = false
- internal val incomingHotFlows: Channel> by lazy {
- incomingHotFlowsInitialized = true
- Channel(Channel.UNLIMITED)
- }
-
- internal fun registerOutgoingStream(
- stream: StreamT,
- streamKind: StreamKind,
- elementSerializer: KSerializer,
- ): String {
- val id = "$STREAM_ID_PREFIX${streamIdCounter.getAndIncrement()}"
- outgoingStreams.trySend(
- KrpcStreamCall(
- callId = callId,
- streamId = id,
- stream = stream,
- kind = streamKind,
- elementSerializer = elementSerializer,
- connectionId = connectionId,
- serviceId = serviceId,
- )
- )
- return id
- }
-
- internal fun prepareIncomingStream(
- streamId: String,
- streamKind: StreamKind,
- stateFlowInitialValue: Any?,
- elementSerializer: KSerializer,
- ): StreamT {
- val incoming: Channel = Channel(Channel.UNLIMITED)
- incomingChannels[streamId] = incoming
-
- val stream = streamOf(streamId, streamKind, stateFlowInitialValue, incoming)
- incomingStreams[streamId] = KrpcStreamCall(
- callId = callId,
- streamId = streamId,
- stream = stream,
- kind = streamKind,
- elementSerializer = elementSerializer,
- connectionId = connectionId,
- serviceId = serviceId,
- )
- return stream
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun streamOf(
- streamId: String,
- streamKind: StreamKind,
- stateFlowInitialValue: Any?,
- incoming: Channel,
- ): StreamT {
- suspend fun consumeFlow(collector: FlowCollector, onError: (Throwable) -> Unit) {
- fun onClose() {
- incoming.cancel()
-
- closedStreams[streamId] = Unit
- incomingChannels.remove(streamId)?.complete(null)
- incomingStreams.remove(streamId)
- }
-
- for (message in incoming) {
- when (message) {
- is StreamCancel -> {
- onClose()
- onError(message.cause ?: streamCanceled())
- }
-
- is StreamEnd -> {
- onClose()
- if (streamKind != StreamKind.Flow) {
- onError(streamCanceled())
- }
-
- return
- }
-
- else -> {
- collector.emit(message)
- }
- }
- }
- }
-
- return when (streamKind) {
- StreamKind.Flow -> {
- flow {
- consumeFlow(this) { e -> throw e }
- }
- }
-
- StreamKind.SharedFlow -> {
- val sharedFlow: MutableSharedFlow = config.sharedFlowBuilder()
-
- object : RpcIncomingHotFlow(sharedFlow, ::consumeFlow), MutableSharedFlow by sharedFlow {
- override suspend fun collect(collector: FlowCollector): Nothing {
- super.collect(collector)
- }
-
- override suspend fun emit(value: Any?) {
- super.emit(value)
- }
- }.also { incomingHotFlows.trySend(it) }
- }
-
- StreamKind.StateFlow -> {
- val stateFlow = MutableStateFlow(stateFlowInitialValue)
-
- object : RpcIncomingHotFlow(stateFlow, ::consumeFlow), MutableStateFlow by stateFlow {
- override suspend fun collect(collector: FlowCollector): Nothing {
- super.collect(collector)
- }
-
- override suspend fun emit(value: Any?) {
- super.emit(value)
- }
- }.also { incomingHotFlows.trySend(it) }
- }
- } as StreamT
- }
-
- public suspend fun closeStream(message: KrpcCallMessage.StreamFinished) {
- incomingChannelOf(message.streamId)?.send(StreamEnd)
- }
-
- public suspend fun cancelStream(message: KrpcCallMessage.StreamCancel) {
- incomingChannelOf(message.streamId)?.send(StreamCancel(message.cause.deserialize()))
- }
-
- public suspend fun send(message: KrpcCallMessage.StreamMessage, serialFormat: SerialFormat) {
- val info: KrpcStreamCall? = select {
- incomingStreams.getDeferred(message.streamId).onAwait { it }
- closedStreams.getDeferred(message.streamId).onAwait { null }
- closed.onAwait { null }
- }
- if (info == null) return
- val result = decodeMessageData(serialFormat, info.elementSerializer, message)
- val channel = incomingChannelOf(message.streamId)
- channel?.send(result)
- }
-
- private suspend fun incomingChannelOf(streamId: String): Channel? {
- return select {
- incomingChannels.getDeferred(streamId).onAwait { it }
- closedStreams.getDeferred(streamId).onAwait { null }
- closed.onAwait { null }
- }
- }
-
- private fun close(cause: Throwable?) {
- if (closed.isCompleted) {
- return
- }
-
- closed.complete(Unit)
-
- if (incomingChannelsInitialized) {
- for (channel in incomingChannels.values) {
- if (!channel.isCompleted) {
- continue
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- channel.getCompleted()?.apply {
- trySend(StreamEnd)
-
- // close for sending, but not for receiving our cancel message, if possible.
- close(cause)
- }
- }
-
- incomingChannels.clear()
- }
-
- if (incomingStreamsInitialized) {
- incomingStreams.values
- .mapNotNull { it.getOrNull()?.stream }
- .filterIsInstance()
- .forEach { stream ->
- stream.subscriptionContexts.forEach {
- it.cancel(CancellationException("Stream closed", cause))
- }
- }
-
- incomingStreams.clear()
- }
-
- if (outgoingStreamsInitialized) {
- outgoingStreams.close()
- }
-
- if (incomingHotFlowsInitialized) {
- incomingHotFlows.close()
- }
- }
-}
-
-private fun streamCanceled() = NoSuchElementException("Stream canceled")
-
-private object StreamEnd
-
-private class StreamCancel(val cause: Throwable? = null)
-
-private abstract class RpcIncomingHotFlow(
- private val rawFlow: MutableSharedFlow,
- private val consume: suspend (FlowCollector, onError: (Throwable) -> Unit) -> Unit,
-) : MutableSharedFlow {
- val subscriptionContexts by lazy { mutableSetOf() }
-
- override suspend fun collect(collector: FlowCollector): Nothing {
- val context = currentCoroutineContext()
-
- if (context.isActive) {
- subscriptionContexts.add(context)
-
- context.job.invokeOnCompletion {
- subscriptionContexts.remove(context)
- }
- }
-
- try {
- rawFlow.collect(collector)
- } finally {
- subscriptionContexts.remove(context)
- }
- }
-
- // value can be ignored, as actual values are coming from the rawFlow
- override suspend fun emit(value: Any?) {
- consume(rawFlow) { e ->
- subscriptionContexts.forEach { it.cancel(CancellationException(e.message, e)) }
-
- subscriptionContexts.clear()
- }
- }
-}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt
index 7b8a3009a..fb310ec5e 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/KrpcVersion.kt
@@ -8,13 +8,7 @@ package kotlinx.rpc.krpc.internal
* Release versions of the library
*/
internal enum class KrpcVersion {
- /**
- * Version 0.1.0
- */
V_0_1_0_BETA,
-
- /**
- * Version 0.6.0
- */
V_0_6_0,
+ V_0_8_0,
}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/StreamSerializer.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/StreamSerializer.kt
index f34e22c77..0bdef9be7 100644
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/StreamSerializer.kt
+++ b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/StreamSerializer.kt
@@ -1,87 +1,20 @@
/*
- * 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.internal
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.*
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
+import kotlinx.rpc.internal.utils.InternalRpcApi
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
-internal sealed class StreamSerializer(private val streamKind: StreamKind) : KSerializer {
- companion object {
- private const val STREAM_SERIALIZER_NAME_PREFIX = "StreamSerializer"
+@Suppress("PropertyName", "detekt.VariableNaming")
+@InternalRpcApi
+public abstract class StreamSerializer {
+ protected val STREAM_ID_SERIAL_NAME: String = "streamId"
+ protected val STREAM_ID_SERIALIZER_NAME: String = "StreamIdSerializer"
- private const val STREAM_ID_SERIAL_NAME = "streamId"
- private const val STREAM_ID_SERIALIZER_NAME = "StreamIdSerializer"
-
- private const val STATE_FLOW_INITIAL_VALUE_SERIAL_NAME = "stateFlowInitialValue"
- }
-
- protected abstract val context: KrpcStreamContext
- protected abstract val elementType: KSerializer
-
- protected open fun ClassSerialDescriptorBuilder.descriptorExtension() { }
-
- protected open fun decodeStateFlowInitialValue(decoder: Decoder): Any? { return null }
-
- protected open fun encodeStateFlowInitialValue(encoder: Encoder, flow: StreamT) { }
-
- override val descriptor: SerialDescriptor by lazy {
- buildClassSerialDescriptor("$STREAM_SERIALIZER_NAME_PREFIX.${streamKind.name}") {
- element(STREAM_ID_SERIAL_NAME, PrimitiveSerialDescriptor(STREAM_ID_SERIALIZER_NAME, PrimitiveKind.STRING))
- descriptorExtension()
- }
- }
-
- override fun deserialize(decoder: Decoder): StreamT {
- val streamId = decoder.decodeString()
-
- val stateFlowValue = decodeStateFlowInitialValue(decoder)
-
- return context.prepareIncomingStream(streamId, streamKind, stateFlowValue, elementType) as StreamT
- }
-
- override fun serialize(encoder: Encoder, value: StreamT) {
- val id = context.registerOutgoingStream(value, streamKind, elementType)
-
- encoder.encodeString(id)
-
- encodeStateFlowInitialValue(encoder, value)
- }
-
- class Flow(
- override val context: KrpcStreamContext,
- override val elementType: KSerializer,
- ) : StreamSerializer>(StreamKind.Flow)
-
- class SharedFlow(
- override val context: KrpcStreamContext,
- override val elementType: KSerializer,
- ) : StreamSerializer>(StreamKind.SharedFlow)
-
- class StateFlow(
- override val context: KrpcStreamContext,
- override val elementType: KSerializer,
- ) : StreamSerializer>(StreamKind.StateFlow) {
- override fun ClassSerialDescriptorBuilder.descriptorExtension() {
- element(STATE_FLOW_INITIAL_VALUE_SERIAL_NAME, elementType.descriptor, isOptional = true)
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- override fun decodeStateFlowInitialValue(decoder: Decoder): Any? {
- return decoder.decodeNullableSerializableValue(elementType)
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- override fun encodeStateFlowInitialValue(encoder: Encoder, flow: kotlinx.coroutines.flow.StateFlow) {
- encoder.encodeNullableSerializableValue(elementType, flow.value)
- }
- }
-}
-
-internal enum class StreamKind {
- Flow, SharedFlow, StateFlow;
+ protected val streamIdDescriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor(STREAM_ID_SERIALIZER_NAME, PrimitiveKind.STRING)
}
diff --git a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/devStreamScope.kt b/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/devStreamScope.kt
deleted file mode 100644
index ca89f135a..000000000
--- a/krpc/krpc-core/src/commonMain/kotlin/kotlinx/rpc/krpc/internal/devStreamScope.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-/*
- * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
- */
-
-package kotlinx.rpc.krpc.internal
-
-import kotlinx.rpc.internal.utils.InternalRpcApi
-
-/**
- * For legacy internal users ONLY.
- * Special dev builds may set this value to `false`.
- *
- * If the value is `false`, absence of [kotlinx.rpc.krpc.streamScoped] for a call
- * is replaced with service's [kotlinx.rpc.krpc.StreamScope]
- * obtained via [kotlinx.rpc.krpc.withClientStreamScope].
- */
-@InternalRpcApi
-public const val STREAM_SCOPES_ENABLED: Boolean = true
diff --git a/krpc/krpc-ktor/krpc-ktor-client/api/krpc-ktor-client.api b/krpc/krpc-ktor/krpc-ktor-client/api/krpc-ktor-client.api
index 64a0821c4..199d0fbbe 100644
--- a/krpc/krpc-ktor/krpc-ktor-client/api/krpc-ktor-client.api
+++ b/krpc/krpc-ktor/krpc-ktor-client/api/krpc-ktor-client.api
@@ -1,26 +1,19 @@
public final class kotlinx/rpc/krpc/ktor/client/KrpcKt {
public static final fun getKrpc ()Lio/ktor/client/plugins/api/ClientPlugin;
- public static final fun getRPC ()Lio/ktor/client/plugins/api/ClientPlugin;
public static final fun installKrpc (Lio/ktor/client/HttpClientConfig;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun installKrpc$default (Lio/ktor/client/HttpClientConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
- public static final fun installRPC (Lio/ktor/client/HttpClientConfig;Lkotlin/jvm/functions/Function1;)V
- public static synthetic fun installRPC$default (Lio/ktor/client/HttpClientConfig;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}
public final class kotlinx/rpc/krpc/ktor/client/KtorClientDslKt {
- public static final fun rpc (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static final fun rpc (Lio/ktor/client/HttpClient;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
- public static synthetic fun rpc$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
- public static synthetic fun rpc$default (Lio/ktor/client/HttpClient;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
+ public static final fun rpc (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lkotlinx/rpc/krpc/ktor/client/KtorRpcClient;
+ public static final fun rpc (Lio/ktor/client/HttpClient;Lkotlin/jvm/functions/Function1;)Lkotlinx/rpc/krpc/ktor/client/KtorRpcClient;
+ public static synthetic fun rpc$default (Lio/ktor/client/HttpClient;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/krpc/ktor/client/KtorRpcClient;
+ public static synthetic fun rpc$default (Lio/ktor/client/HttpClient;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lkotlinx/rpc/krpc/ktor/client/KtorRpcClient;
public static final fun rpcConfig (Lio/ktor/client/request/HttpRequestBuilder;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun rpcConfig$default (Lio/ktor/client/request/HttpRequestBuilder;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
}
public abstract interface class kotlinx/rpc/krpc/ktor/client/KtorRpcClient : kotlinx/rpc/RpcClient {
- public abstract fun getWebSocketSession ()Lio/ktor/websocket/WebSocketSession;
-}
-
-public final class kotlinx/rpc/krpc/ktor/client/KtorRpcClient$DefaultImpls {
- public static fun callServerStreaming (Lkotlinx/rpc/krpc/ktor/client/KtorRpcClient;Lkotlinx/rpc/RpcCall;)Lkotlinx/coroutines/flow/Flow;
+ public abstract fun getWebSocketSession ()Lkotlinx/coroutines/Deferred;
}
diff --git a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/Krpc.kt b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/Krpc.kt
index aef216e9f..867a180ec 100644
--- a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/Krpc.kt
+++ b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/Krpc.kt
@@ -12,9 +12,6 @@ import kotlinx.rpc.krpc.KrpcConfigBuilder
internal val KrpcClientPluginAttributesKey = AttributeKey("KrpcClientPluginAttributesKey")
-@Deprecated("Use Krpc instead", ReplaceWith("Krpc"), level = DeprecationLevel.ERROR)
-public val RPC: ClientPlugin get() = Krpc
-
/**
* Ktor client plugin that allows to configure RPC globally for all instances obtained via [rpc] functions.
*/
@@ -22,11 +19,6 @@ public val Krpc: ClientPlugin = createClientPlugin("Kr
client.attributes.put(KrpcClientPluginAttributesKey, pluginConfig)
}
-@Deprecated("Use installKrpc instead", ReplaceWith("installKrpc"), level = DeprecationLevel.ERROR)
-public fun HttpClientConfig<*>.installRPC(configure: KrpcConfigBuilder.Client.() -> Unit = {}) {
- installKrpc(configure)
-}
-
/**
* Installs [WebSockets] and [Krpc] client plugins
*/
diff --git a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorClientDsl.kt b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorClientDsl.kt
index f54ee0a3c..8ed20197b 100644
--- a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorClientDsl.kt
+++ b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorClientDsl.kt
@@ -9,9 +9,7 @@ import io.ktor.client.plugins.*
import io.ktor.client.plugins.websocket.*
import io.ktor.client.request.*
import io.ktor.util.*
-import kotlinx.rpc.RpcClient
import kotlinx.rpc.krpc.KrpcConfigBuilder
-import kotlinx.rpc.krpc.rpcClientConfig
private val KrpcRequestConfigAttributeKey = AttributeKey Unit>(
name = "KrpcRequestConfigAttributeKey"
@@ -29,14 +27,17 @@ public fun HttpRequestBuilder.rpcConfig(configBuilder: KrpcConfigBuilder.Client.
}
/**
- * Configures [RpcClient] for the following path. Provides means for additional configuration via [block].
+ * Configures [KtorRpcClient] for the following path. Provides means for additional configuration via [block].
* Note that the [WebSockets] plugin is required for these calls.
*
* @param urlString The URL to use for the request.
- * @param block Optional configuration for the
- * @return An instance of [RpcClient] that is configured to send messages to the server.
+ * @param block Optional configuration for the [HttpRequestBuilder].
+ * @return An instance of [KtorRpcClient] that is configured to send messages to the server.
+ * The instance is cold and will establish WebSocket connection on the first request.
+ *
+ * @See [KtorRpcClient]
*/
-public suspend fun HttpClient.rpc(
+public fun HttpClient.rpc(
urlString: String,
block: HttpRequestBuilder.() -> Unit = {},
): KtorRpcClient {
@@ -47,30 +48,28 @@ public suspend fun HttpClient.rpc(
}
/**
- * Configures [RpcClient] for the following path. Provides means for additional configuration via [block].
+ * Configures [KtorRpcClient] for the following path. Provides means for additional configuration via [block].
* Note that the [WebSockets] plugin is required for these calls.
*
- * @param block Optional configuration for the
- * @return An instance of [RpcClient] that is configured to send messages to the server.
+ * @param block Optional configuration for the [HttpRequestBuilder].
+ * @return An instance of [KtorRpcClient] that is configured to send messages to the server.
+ * The instance is cold and will establish WebSocket connection on the first request.
+ *
+ * @See [KtorRpcClient]
*/
-public suspend fun HttpClient.rpc(
+public fun HttpClient.rpc(
block: HttpRequestBuilder.() -> Unit = {},
): KtorRpcClient {
pluginOrNull(WebSockets)
?: error("RPC for client requires $WebSockets plugin to be installed firstly")
- var requestConfigBuilder: KrpcConfigBuilder.Client.() -> Unit = {}
- val session = webSocketSession {
- block()
+ val pluginConfigBuilder = attributes.getOrNull(KrpcClientPluginAttributesKey)
- attributes.getOrNull(KrpcRequestConfigAttributeKey)?.let {
- requestConfigBuilder = it
+ return KtorKrpcClientImpl(pluginConfigBuilder) { configSetter ->
+ webSocketSession {
+ block()
+
+ attributes.getOrNull(KrpcRequestConfigAttributeKey)?.let { configSetter(it) }
}
}
-
- val pluginConfigBuilder = attributes.getOrNull(KrpcClientPluginAttributesKey)
- val rpcConfig = pluginConfigBuilder?.apply(requestConfigBuilder)?.build()
- ?: rpcClientConfig(requestConfigBuilder)
-
- return KtorKrpcClientImpl(session, rpcConfig)
}
diff --git a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorRpcClient.kt b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorRpcClient.kt
index 922f88441..9c3a52b35 100644
--- a/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorRpcClient.kt
+++ b/krpc/krpc-ktor/krpc-ktor-client/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/client/KtorRpcClient.kt
@@ -5,21 +5,52 @@
package kotlinx.rpc.krpc.ktor.client
import io.ktor.websocket.*
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.Deferred
import kotlinx.rpc.RpcClient
import kotlinx.rpc.krpc.KrpcConfig
+import kotlinx.rpc.krpc.KrpcConfigBuilder
+import kotlinx.rpc.krpc.KrpcTransport
import kotlinx.rpc.krpc.client.KrpcClient
import kotlinx.rpc.krpc.ktor.KtorTransport
+import kotlinx.rpc.krpc.rpcClientConfig
/**
* [RpcClient] implementation for Ktor, containing [webSocketSession] object,
* that is used to maintain connection.
+ *
+ * Client is cold, meaning the connection will be established on the first request.
+ * [webSocketSession] will be completed when the connection is established.
*/
public interface KtorRpcClient : RpcClient {
- public val webSocketSession: WebSocketSession
+ /**
+ * Cold [WebSocketSession] object. Instantiated when the connection is established on the first request.
+ */
+ public val webSocketSession: Deferred
}
internal class KtorKrpcClientImpl(
- override val webSocketSession: WebSocketSession,
- config: KrpcConfig.Client,
-): KrpcClient(config, KtorTransport(webSocketSession)), KtorRpcClient
+ private val pluginConfigBuilder: KrpcConfigBuilder.Client?,
+ private val webSocketSessionFactory: suspend (
+ configSetter: (KrpcConfigBuilder.Client.() -> Unit) -> Unit,
+ ) -> WebSocketSession,
+): KrpcClient(), KtorRpcClient {
+ private var requestConfigBuilder: KrpcConfigBuilder.Client.() -> Unit = {}
+ private val _webSocketSession = CompletableDeferred()
+ override val webSocketSession: Deferred = _webSocketSession
+
+ override suspend fun initializeTransport(): KrpcTransport {
+ val session = webSocketSessionFactory {
+ requestConfigBuilder = it
+ }
+
+ _webSocketSession.complete(session)
+ return KtorTransport(session)
+ }
+
+ override fun initializeConfig(): KrpcConfig.Client {
+ return pluginConfigBuilder?.apply(requestConfigBuilder)?.build()
+ ?: rpcClientConfig(requestConfigBuilder)
+ }
+}
diff --git a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts
index 89bf807cd..6e27be3eb 100644
--- a/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts
+++ b/krpc/krpc-ktor/krpc-ktor-core/build.gradle.kts
@@ -26,10 +26,17 @@ kotlin {
implementation(projects.krpc.krpcSerialization.krpcSerializationJson)
implementation(projects.krpc.krpcKtor.krpcKtorServer)
implementation(projects.krpc.krpcKtor.krpcKtorClient)
+ implementation(projects.krpc.krpcLogging)
implementation(libs.kotlin.test)
implementation(libs.ktor.server.netty)
implementation(libs.ktor.server.test.host)
+ implementation(libs.ktor.server.websockets)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.websockets)
+ implementation(libs.ktor.client.cio)
+ implementation(libs.logback.classic)
+ implementation(libs.coroutines.debug)
}
}
}
diff --git a/krpc/krpc-ktor/krpc-ktor-core/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/KtorTransport.kt b/krpc/krpc-ktor/krpc-ktor-core/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/KtorTransport.kt
index ed12adc72..b46bd644b 100644
--- a/krpc/krpc-ktor/krpc-ktor-core/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/KtorTransport.kt
+++ b/krpc/krpc-ktor/krpc-ktor-core/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/KtorTransport.kt
@@ -16,7 +16,7 @@ public class KtorTransport(
) : KrpcTransport, CoroutineScope by webSocketSession {
/**
- * Sends a single encoded RPC message over network (or any other medium) to a peer endpoint.
+ * Sends a single encoded RPC message over a network (or any other medium) to a peer endpoint.
*
* @param message a message to send. Either of string or binary type.
*/
@@ -33,9 +33,9 @@ public class KtorTransport(
}
/**
- * Suspends until next RPC message from a peer endpoint is received and then returns it.
+ * Suspends until the next RPC message from a peer endpoint is received and then returns it.
*
- * @return received RPC message.
+ * @return the received RPC message.
*/
override suspend fun receive(): KrpcTransportMessage {
return when (val message = webSocketSession.incoming.receive()) {
diff --git a/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt
index b7584d98a..9eb506b42 100644
--- a/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt
+++ b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/kotlin/kotlinx/rpc/krpc/ktor/KtorTransportTest.kt
@@ -6,11 +6,23 @@
package kotlinx.rpc.krpc.ktor
+import io.ktor.client.*
+import io.ktor.client.engine.cio.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
import io.ktor.server.application.*
+import io.ktor.server.engine.*
+import io.ktor.server.netty.*
+import io.ktor.server.response.*
+import io.ktor.server.routing.*
import io.ktor.server.testing.*
-import kotlinx.coroutines.cancel
-import kotlinx.rpc.RemoteService
+import kotlinx.coroutines.*
+import kotlinx.coroutines.debug.DebugProbes
+import kotlinx.coroutines.test.runTest
import kotlinx.rpc.annotations.Rpc
+import kotlinx.rpc.krpc.client.KrpcClient
+import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLogger
+import kotlinx.rpc.krpc.internal.logging.RpcInternalDumpLoggerContainer
import kotlinx.rpc.krpc.ktor.client.installKrpc
import kotlinx.rpc.krpc.ktor.client.rpc
import kotlinx.rpc.krpc.ktor.client.rpcConfig
@@ -19,16 +31,22 @@ import kotlinx.rpc.krpc.ktor.server.rpc
import kotlinx.rpc.krpc.serialization.json.json
import kotlinx.rpc.withService
import org.junit.Assert.assertEquals
-import kotlin.coroutines.CoroutineContext
+import org.junit.platform.commons.logging.Logger
+import org.junit.platform.commons.logging.LoggerFactory
+import java.net.ServerSocket
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlin.coroutines.cancellation.CancellationException
+import kotlin.test.Ignore
import kotlin.test.Test
+import kotlin.time.Duration.Companion.seconds
@Rpc
-interface NewService : RemoteService {
+interface NewService {
suspend fun echo(value: String): String
}
class NewServiceImpl(
- override val coroutineContext: CoroutineContext,
private val call: ApplicationCall,
) : NewService {
@Suppress("UastIncorrectHttpHeaderInspection")
@@ -38,6 +56,23 @@ class NewServiceImpl(
}
}
+@Rpc
+interface SlowService {
+ suspend fun verySlow(): String
+}
+
+class SlowServiceImpl : SlowService {
+ val received = CompletableDeferred()
+
+ override suspend fun verySlow(): String {
+ received.complete(Unit)
+
+ delay(Int.MAX_VALUE.toLong())
+
+ error("Must not be called")
+ }
+}
+
class KtorTransportTest {
@Test
fun testEcho() = testApplication {
@@ -54,7 +89,7 @@ class KtorTransportTest {
waitForServices = true
}
- registerService { NewServiceImpl(it, call) }
+ registerService { NewServiceImpl(call) }
}
}
@@ -77,7 +112,6 @@ class KtorTransportTest {
assertEquals("Hello, world!", firstActual)
- serviceWithGlobalConfig.cancel()
clientWithGlobalConfig.cancel()
val clientWithNoConfig = createClient {
@@ -98,7 +132,133 @@ class KtorTransportTest {
assertEquals("Hello, world!", secondActual)
- serviceWithLocalConfig.cancel()
clientWithNoConfig.cancel()
}
+
+ @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
+ @Test
+ @Ignore("Wait for Ktor fix (https://github.com/ktorio/ktor/pull/4927) or apply workaround if rejected")
+ fun testEndpointsTerminateWhenWsDoes() = runTest(timeout = 15.seconds) {
+ DebugProbes.install()
+
+ val logger = setupLogger()
+
+ val port: Int = findFreePort()
+
+ val newPool = Executors.newCachedThreadPool().asCoroutineDispatcher()
+
+ val serverReady = CompletableDeferred()
+ val dropServer = CompletableDeferred()
+
+ val service = SlowServiceImpl()
+
+ @Suppress("detekt.GlobalCoroutineUsage")
+ val serverJob = GlobalScope.launch(CoroutineName("server")) {
+ withContext(newPool) {
+ val server = embeddedServer(
+ factory = Netty,
+ port = port,
+ parentCoroutineContext = newPool,
+ ) {
+ install(Krpc)
+
+ routing {
+ get {
+ call.respondText("hello")
+ }
+
+ rpc("/rpc") {
+ rpcConfig {
+ serialization {
+ json()
+ }
+ }
+
+ registerService { service }
+ }
+ }
+ }.start(wait = false)
+
+ serverReady.complete(Unit)
+
+ dropServer.await()
+
+ server.stop(shutdownGracePeriod = 100L, shutdownTimeout = 100L, timeUnit = TimeUnit.MILLISECONDS)
+ }
+
+ logger.info { "Server stopped" }
+ }
+
+ val ktorClient = HttpClient(CIO) {
+ installKrpc {
+ serialization {
+ json()
+ }
+ }
+ }
+
+ serverReady.await()
+
+ assertEquals("hello", ktorClient.get("http://0.0.0.0:$port").bodyAsText())
+
+ val rpcClient = ktorClient.rpc("ws://0.0.0.0:$port/rpc")
+
+ launch {
+ try {
+ rpcClient.withService().verySlow()
+ error("Must not be called")
+ } catch (_: CancellationException) {
+ logger.info { "Cancellation exception caught for RPC request" }
+ ensureActive()
+ }
+ }
+
+ service.received.await()
+
+ logger.info { "Received RPC request" }
+
+ dropServer.complete(Unit)
+
+ logger.info { "Waiting for RPC client to complete" }
+
+ (rpcClient as KrpcClient).awaitCompletion()
+
+ logger.info { "RPC client completed" }
+
+ ktorClient.close()
+ newPool.close()
+
+ serverJob.cancel()
+ }
+
+ private fun findFreePort(): Int {
+ val port: Int
+ while (true) {
+ val socket = try {
+ ServerSocket(0)
+ } catch (_: Throwable) {
+ continue
+ }
+
+ port = socket.localPort
+ socket.close()
+ break
+ }
+ return port
+ }
+
+ private fun setupLogger(): Logger {
+ val logger = LoggerFactory.getLogger(KtorTransportTest::class.java)
+
+ RpcInternalDumpLoggerContainer.set(object : RpcInternalDumpLogger {
+
+ override val isEnabled: Boolean = true
+
+ override fun dump(vararg tags: String, message: () -> String) {
+ logger.info { "[${tags.joinToString()}] ${message()}" }
+ }
+ })
+
+ return logger
+ }
}
diff --git a/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/resources/logback.xml b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/resources/logback.xml
new file mode 100644
index 000000000..5ccb239b2
--- /dev/null
+++ b/krpc/krpc-ktor/krpc-ktor-core/src/jvmTest/resources/logback.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+ %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
+
+
+
+
+
+
+
+
diff --git a/krpc/krpc-ktor/krpc-ktor-server/api/krpc-ktor-server.api b/krpc/krpc-ktor/krpc-ktor-server/api/krpc-ktor-server.api
index 7ed6450b0..feb0180fb 100644
--- a/krpc/krpc-ktor/krpc-ktor-server/api/krpc-ktor-server.api
+++ b/krpc/krpc-ktor/krpc-ktor-server/api/krpc-ktor-server.api
@@ -1,6 +1,5 @@
public final class kotlinx/rpc/krpc/ktor/server/KrpcKt {
public static final fun getKrpc ()Lio/ktor/server/application/ApplicationPlugin;
- public static final fun getRPC ()Lio/ktor/server/application/ApplicationPlugin;
}
public final class kotlinx/rpc/krpc/ktor/server/KrpcRoute : io/ktor/server/websocket/DefaultWebSocketServerSession {
@@ -16,7 +15,7 @@ public final class kotlinx/rpc/krpc/ktor/server/KrpcRoute : io/ktor/server/webso
public fun getOutgoing ()Lkotlinx/coroutines/channels/SendChannel;
public fun getPingIntervalMillis ()J
public fun getTimeoutMillis ()J
- public final fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
+ public final fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)V
public final fun rpcConfig (Lkotlin/jvm/functions/Function1;)V
public fun send (Lio/ktor/websocket/Frame;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public fun setMasking (Z)V
diff --git a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/Krpc.kt b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/Krpc.kt
index 41ac1076c..f14dc06a7 100644
--- a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/Krpc.kt
+++ b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/Krpc.kt
@@ -11,9 +11,6 @@ import kotlinx.rpc.krpc.KrpcConfigBuilder
internal val KrpcServerPluginAttributesKey = AttributeKey("KrpcServerPluginAttributesKey")
-@Deprecated("Use Krpc instead", ReplaceWith("Krpc"), level = DeprecationLevel.ERROR)
-public val RPC: ApplicationPlugin get() = Krpc
-
/**
* Ktor server plugin that allows to configure RPC globally for all mounted servers.
*/
diff --git a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KrpcRoute.kt b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KrpcRoute.kt
index efff16c5e..bcd6e5088 100644
--- a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KrpcRoute.kt
+++ b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KrpcRoute.kt
@@ -8,15 +8,11 @@ import io.ktor.server.websocket.*
import kotlinx.rpc.RpcServer
import kotlinx.rpc.annotations.Rpc
import kotlinx.rpc.krpc.KrpcConfigBuilder
-import kotlin.coroutines.CoroutineContext
import kotlin.reflect.KClass
-@Deprecated("Use KrpcRoute instead", ReplaceWith("KrpcRoute"), level = DeprecationLevel.ERROR)
-public typealias RPCRoute = KrpcRoute
-
/**
- * RpcRoute class represents an RPC server that is mounted in Ktor routing.
- * This class provides API to register services and optionally setup configuration.
+ * [KrpcRoute] class represents an RPC server mounted in Ktor routing.
+ * This class provides an API to register services and optionally setup configuration.
*/
public class KrpcRoute(
webSocketSession: DefaultWebSocketServerSession
@@ -40,14 +36,14 @@ public class KrpcRoute(
* Service of any type should be unique on the server, but RpcServer does not specify the actual retention policy.
*
* @param Service the exact type of the server to be registered.
- * For example for service with `MyService` interface and `MyServiceImpl` implementation,
+ * For example, for service with `MyService` interface and `MyServiceImpl` implementation,
* type `MyService` should be specified explicitly.
* @param serviceKClass [KClass] of the [Service].
* @param serviceFactory function that produces the actual implementation of the service that will handle the calls.
*/
public fun <@Rpc Service : Any> registerService(
serviceKClass: KClass,
- serviceFactory: (CoroutineContext) -> Service,
+ serviceFactory: () -> Service,
) {
registrations.add { server ->
server.registerService(serviceKClass, serviceFactory)
@@ -59,12 +55,12 @@ public class KrpcRoute(
* Service of any type should be unique on the server, but RpcServer does not specify the actual retention policy.
*
* @param Service the exact type of the server to be registered.
- * For example for service with `MyService` interface and `MyServiceImpl` implementation,
+ * For example, for service with `MyService` interface and `MyServiceImpl` implementation,
* type `MyService` should be specified explicitly.
* @param serviceFactory function that produces the actual implementation of the service that will handle the calls.
*/
public inline fun <@Rpc reified Service : Any> registerService(
- noinline serviceFactory: (CoroutineContext) -> Service,
+ noinline serviceFactory: () -> Service,
) {
registerService(Service::class, serviceFactory)
}
diff --git a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KtorServerDsl.kt b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KtorServerDsl.kt
index 927e61a67..ae4e5e574 100644
--- a/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KtorServerDsl.kt
+++ b/krpc/krpc-ktor/krpc-ktor-server/src/commonMain/kotlin/kotlinx/rpc/krpc/ktor/server/KtorServerDsl.kt
@@ -54,6 +54,6 @@ private fun Route.createRpcServer(rpcRouteBuilder: KrpcRoute.() -> Unit) {
registration(server)
}
- server.coroutineContext.job.join()
+ server.internalScope.coroutineContext.job.join()
}
}
diff --git a/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormat.kt b/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormat.kt
index 3f1f129a7..5e8c060d7 100644
--- a/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormat.kt
+++ b/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormat.kt
@@ -10,9 +10,6 @@ import kotlinx.serialization.SerialFormat
import kotlinx.serialization.StringFormat
import kotlinx.serialization.modules.SerializersModule
-@Deprecated("Use KrpcSerialFormat instead", ReplaceWith("KrpcSerialFormat"), level = DeprecationLevel.ERROR)
-public typealias RPCSerialFormat = KrpcSerialFormat
-
/**
* [KrpcSerialFormat] interface defines an object which helps kRPC protocol to work with various serialization formats
*
@@ -35,13 +32,6 @@ public interface KrpcSerialFormat {
public fun FormatBuilder.applySerializersModule(serializersModule: SerializersModule)
}
-@Deprecated(
- "Use KrpcSerialFormatBuilder instead",
- ReplaceWith("KrpcSerialFormatBuilder"),
- level = DeprecationLevel.ERROR,
-)
-public typealias RPCSerialFormatBuilder = KrpcSerialFormatBuilder
-
/**
* Special wrapper class that is used to register serialization format in [KrpcSerialFormatConfiguration]
* Comes in two instances: [KrpcSerialFormatBuilder.Binary] and [KrpcSerialFormatBuilder.String]
diff --git a/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormatConfiguration.kt b/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormatConfiguration.kt
index 3e777102b..fa764ddc6 100644
--- a/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormatConfiguration.kt
+++ b/krpc/krpc-serialization/krpc-serialization-core/src/commonMain/kotlin/kotlinx/rpc/krpc/serialization/KrpcSerialFormatConfiguration.kt
@@ -4,13 +4,6 @@
package kotlinx.rpc.krpc.serialization
-@Deprecated(
- "Use KrpcSerialFormatConfiguration instead",
- ReplaceWith("KrpcSerialFormatConfiguration"),
- level = DeprecationLevel.ERROR,
-)
-public typealias RPCSerialFormatConfiguration = KrpcSerialFormatConfiguration
-
/**
* Special interface to configure serialization for a kRPC protocol in KrpcConfig
* ```kotlin
diff --git a/krpc/krpc-server/api/krpc-server.api b/krpc/krpc-server/api/krpc-server.api
index 41b38da45..7d1f9c37c 100644
--- a/krpc/krpc-server/api/krpc-server.api
+++ b/krpc/krpc-server/api/krpc-server.api
@@ -1,6 +1,9 @@
public abstract class kotlinx/rpc/krpc/server/KrpcServer : kotlinx/rpc/RpcServer, kotlinx/rpc/krpc/internal/KrpcEndpoint {
public fun (Lkotlinx/rpc/krpc/KrpcConfig$Server;Lkotlinx/rpc/krpc/KrpcTransport;)V
- public final fun getCoroutineContext ()Lkotlin/coroutines/CoroutineContext;
- public final fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)V
+ public final fun awaitCompletion (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
+ public final fun close (Ljava/lang/String;)V
+ public static synthetic fun close$default (Lkotlinx/rpc/krpc/server/KrpcServer;Ljava/lang/String;ILjava/lang/Object;)V
+ public fun deregisterService (Lkotlin/reflect/KClass;)V
+ public final fun registerService (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function0;)V
}
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt
index 8782071d4..898f3a266 100644
--- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/KrpcServer.kt
@@ -17,25 +17,15 @@ import kotlinx.rpc.krpc.internal.*
import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
import kotlinx.rpc.krpc.server.internal.KrpcServerConnector
import kotlinx.rpc.krpc.server.internal.KrpcServerService
-import kotlin.coroutines.CoroutineContext
+import kotlin.concurrent.Volatile
import kotlin.reflect.KClass
-@Deprecated("Use KrpcServer instead", ReplaceWith("KrpcServer"), level = DeprecationLevel.ERROR)
-public typealias KRPCServer = KrpcServer
-
/**
- * Default implementation of [RpcServer].
+ * kRPC implementation of the [RpcServer].
* Takes care of tracking requests and responses,
* serializing data, tracking streams, processing exceptions, and other protocol responsibilities.
* Routes resulting messages to the proper registered services.
- * Leaves out the delivery of encoded messages to the specific implementations.
- *
- * A simple example of how this server may be implemented:
- * ```kotlin
- * class MyTransport : KrpcTransport { /*...*/ }
- *
- * class MyServer(config: KrpcConfig.Server): KrpcServer(config, MyTransport())
- * ```
+ * Leaves out the delivery of encoded messages to the specific implementations with [KrpcTransport].
*
* @param config configuration provided for that specific server. Applied to all services that use this server.
* @param transport [KrpcTransport] instance that will be used to send and receive RPC messages.
@@ -46,8 +36,31 @@ public abstract class KrpcServer(
private val config: KrpcConfig.Server,
transport: KrpcTransport,
) : RpcServer, KrpcEndpoint {
- // we make a child here, so we can send cancellation messages before closing the connection
- final override val coroutineContext: CoroutineContext = SupervisorJob(transport.coroutineContext.job)
+
+ /**
+ * Close this server, removing all the services and stopping accepting messages.
+ */
+ public fun close(message: String? = null) {
+ internalScope.cancel(message ?: "Server closed")
+ }
+
+ /**
+ * Waits until the server is closed.
+ */
+ public suspend fun awaitCompletion() {
+ internalScope.coroutineContext.job.join()
+ }
+
+ /*
+ * #####################################################################
+ * # #
+ * # INTERNALS AHEAD #
+ * # #
+ * #####################################################################
+ */
+
+ @InternalRpcApi
+ public val internalScope: CoroutineScope = CoroutineScope(SupervisorJob(transport.coroutineContext.job))
private val logger = RpcInternalCommonLogger.logger(rpcInternalObjectId())
@@ -66,21 +79,19 @@ public abstract class KrpcServer(
final override var supportedPlugins: Set = emptySet()
private set
- private val rpcServices = RpcInternalConcurrentHashMap>()
-
- // compatibility with clients that do not have serviceId
- private var nullRpcServices = RpcInternalConcurrentHashMap>()
+ private val rpcServices = RpcInternalConcurrentHashMap>()
+ @Volatile
private var cancelledByClient = false
init {
- coroutineContext.job.invokeOnCompletion(onCancelling = true) {
+ internalScope.coroutineContext.job.invokeOnCompletion(onCancelling = true) {
if (!cancelledByClient) {
sendCancellation(CancellationType.ENDPOINT, null, null, closeTransportAfterSending = true)
}
}
- launch {
+ internalScope.launch(CoroutineName("krpc-server-generic-protocol-messages")) {
connector.subscribeToProtocolMessages(::handleProtocolMessage)
connector.subscribeToGenericMessages(::handleGenericMessage)
@@ -105,20 +116,14 @@ public abstract class KrpcServer(
final override fun <@Rpc Service : Any> registerService(
serviceKClass: KClass,
- serviceFactory: (CoroutineContext) -> Service,
+ serviceFactory: () -> Service,
) {
val descriptor = serviceDescriptorOf(serviceKClass)
- launch {
+ internalScope.launch(CoroutineName("krpc-server-service-$descriptor")) {
connector.subscribeToServiceMessages(descriptor.fqName) { message ->
- val rpcServerService = when (val id = message.serviceId) {
- null -> nullRpcServices.computeIfAbsent(descriptor.fqName) {
- createNewServiceInstance(descriptor, serviceFactory)
- }
-
- else -> rpcServices.computeIfAbsent(id) {
- createNewServiceInstance(descriptor, serviceFactory)
- }
+ val rpcServerService = rpcServices.computeIfAbsent(descriptor.fqName) {
+ createNewServiceInstance(descriptor, serviceFactory)
}
rpcServerService.accept(message)
@@ -126,23 +131,23 @@ public abstract class KrpcServer(
}
}
+ override fun <@Rpc Service : Any> deregisterService(serviceKClass: KClass) {
+ connector.unsubscribeFromServiceMessages(serviceDescriptorOf(serviceKClass).fqName)
+ rpcServices.remove(serviceDescriptorOf(serviceKClass).fqName)
+ }
+
private fun <@Rpc Service : Any> createNewServiceInstance(
descriptor: RpcServiceDescriptor,
- serviceFactory: (CoroutineContext) -> Service,
+ serviceFactory: () -> Service,
): KrpcServerService {
- val serviceInstanceContext = SupervisorJob(coroutineContext.job)
-
return KrpcServerService(
- service = serviceFactory(serviceInstanceContext),
+ service = serviceFactory(),
descriptor = descriptor,
config = config,
connector = connector,
- coroutineContext = serviceInstanceContext,
- ).apply {
- coroutineContext.job.invokeOnCompletion {
- connector.unsubscribeFromServiceMessages(descriptor.fqName)
- }
- }
+ supportedPlugins = supportedPlugins,
+ serverScope = internalScope,
+ )
}
@InternalRpcApi
@@ -151,25 +156,18 @@ public abstract class KrpcServer(
CancellationType.ENDPOINT -> {
cancelledByClient = true
- cancel("Server cancelled by client")
+ internalScope.cancel("Server cancelled by client")
rpcServices.clear()
}
- CancellationType.SERVICE -> {
- val serviceId = message[KrpcPluginKey.CLIENT_SERVICE_ID]?.toLongOrNull()
- ?: error("Expected CLIENT_SERVICE_ID for cancellation of type 'service' as Long value")
-
- rpcServices[serviceId]?.cancel("Service cancelled by client")
- }
-
CancellationType.REQUEST -> {
- val serviceId = message[KrpcPluginKey.CLIENT_SERVICE_ID]?.toLongOrNull()
- ?: error("Expected CLIENT_SERVICE_ID for cancellation of type 'request' as Long value")
+ val serviceType = message[KrpcPluginKey.CLIENT_SERVICE_ID]
+ ?: error("Expected CLIENT_SERVICE_ID for cancellation of type 'request'")
val callId = message[KrpcPluginKey.CANCELLATION_ID]
?: error("Expected CANCELLATION_ID for cancellation of type 'request'")
- rpcServices[serviceId]?.cancelRequest(callId, "Request cancelled by client")
+ rpcServices[serviceType]?.cancelRequestWithOptionalAck(callId, "Request cancelled by client")
}
else -> {
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt
index 804c9d249..886441247 100644
--- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerConnector.kt
@@ -33,8 +33,8 @@ internal class KrpcServerConnector private constructor(
}
)
- fun unsubscribeFromServiceMessages(serviceTypeString: String) {
- connector.unsubscribeFromMessages(MessageKey.Service(serviceTypeString))
+ fun unsubscribeFromServiceMessages(serviceTypeString: String, callback: () -> Unit = {}) {
+ connector.unsubscribeFromMessages(MessageKey.Service(serviceTypeString), callback)
}
suspend fun subscribeToServiceMessages(
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt
index c7d5532ed..5f4a10cb4 100644
--- a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/KrpcServerService.kt
@@ -11,39 +11,27 @@ import kotlinx.rpc.descriptor.RpcInvokator
import kotlinx.rpc.descriptor.RpcServiceDescriptor
import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
import kotlinx.rpc.krpc.KrpcConfig
-import kotlinx.rpc.krpc.callScoped
import kotlinx.rpc.krpc.internal.*
import kotlinx.rpc.krpc.internal.logging.RpcInternalCommonLogger
-import kotlinx.rpc.krpc.streamScopeOrNull
-import kotlinx.rpc.krpc.withServerStreamScope
import kotlinx.serialization.BinaryFormat
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerialFormat
import kotlinx.serialization.StringFormat
-import kotlin.coroutines.CoroutineContext
+import kotlinx.serialization.modules.SerializersModule
import kotlin.reflect.typeOf
internal class KrpcServerService<@Rpc T : Any>(
private val service: T,
private val descriptor: RpcServiceDescriptor,
- override val config: KrpcConfig.Server,
+ private val config: KrpcConfig.Server,
private val connector: KrpcServerConnector,
- coroutineContext: CoroutineContext,
-) : KrpcServiceHandler(), CoroutineScope {
- override val logger = RpcInternalCommonLogger.logger(rpcInternalObjectId(descriptor.fqName))
- override val sender: KrpcMessageSender get() = connector
- private val scope: CoroutineScope = this
-
- override val coroutineContext: CoroutineContext = coroutineContext.withServerStreamScope()
+ private val serverScope: CoroutineScope,
+ private val supportedPlugins: Set,
+) {
+ private val logger = RpcInternalCommonLogger.logger(rpcInternalObjectId(descriptor.fqName))
private val requestMap = RpcInternalConcurrentHashMap()
- init {
- coroutineContext.job.invokeOnCompletion {
- logger.trace { "Service completed with $it" }
- }
- }
-
suspend fun accept(message: KrpcCallMessage) {
val result = runCatching {
processMessage(message)
@@ -71,7 +59,7 @@ internal class KrpcServerService<@Rpc T : Any>(
connectionId = message.connectionId,
)
- sender.sendMessage(errorMessage)
+ connector.sendMessage(errorMessage)
}
}
@@ -97,37 +85,24 @@ internal class KrpcServerService<@Rpc T : Any>(
is KrpcCallMessage.StreamCancel -> {
// if no stream is present, it probably was already canceled
- getAndAwaitStreamContext(message)
- ?.cancelStream(message)
+ serverStreamContext.cancelStream(message)
}
is KrpcCallMessage.StreamFinished -> {
// if no stream is present, it probably was already finished
- getAndAwaitStreamContext(message)
- ?.closeStream(message)
+ serverStreamContext.closeStream(message)
}
is KrpcCallMessage.StreamMessage -> {
- requestMap[message.callId]?.streamContext?.apply {
- awaitInitialized().send(message, prepareSerialFormat(this))
- } ?: error("Invalid request call id: ${message.callId}")
+ serverStreamContext.send(message, serialFormat)
}
}
}
- private suspend fun getAndAwaitStreamContext(message: KrpcCallMessage): KrpcStreamContext? {
- return requestMap[message.callId]?.streamContext?.awaitInitialized()
- }
-
@Suppress("detekt.ThrowsCount", "detekt.LongMethod")
private fun handleCall(callData: KrpcCallMessage.CallData) {
val callId = callData.callId
- val streamContext = LazyKrpcStreamContext(streamScopeOrNull(scope)) {
- KrpcStreamContext(callId, config, callData.connectionId, callData.serviceId, it)
- }
- val serialFormat = prepareSerialFormat(streamContext)
-
val isMethod = when (callData.callType) {
KrpcCallMessage.CallType.Method -> true
KrpcCallMessage.CallType.Field -> false
@@ -135,6 +110,14 @@ internal class KrpcServerService<@Rpc T : Any>(
.endsWith("\$method") // compatibility with beta-4.2 clients
}
+ if (!isMethod) {
+ error(
+ "Service ${descriptor.fqName} doesn't support fields calls, " +
+ "but got a field call: ${callData.callableName} with callId $callId. " +
+ "Please, update the client version or change the call type to method."
+ )
+ }
+
val callableName = callData.callableName
.substringBefore('$') // compatibility with beta-4.2 clients
@@ -148,14 +131,16 @@ internal class KrpcServerService<@Rpc T : Any>(
val data = if (isMethod) {
val serializerModule = serialFormat.serializersModule
val paramsSerializer = serializerModule.rpcSerializerForType(callable.dataType)
- decodeMessageData(serialFormat, paramsSerializer, callData)
+ serverStreamContext.scoped(callId) {
+ decodeMessageData(serialFormat, paramsSerializer, callData)
+ }
} else {
null
}
var failure: Throwable? = null
- val requestJob = launch(start = CoroutineStart.LAZY) {
+ val requestJob = serverScope.launch(start = CoroutineStart.LAZY) {
try {
val markedNonSuspending = callData.pluginParams.orEmpty()
.contains(KrpcPluginKey.NON_SUSPENDING_SERVER_FLOW_MARKER)
@@ -171,13 +156,7 @@ internal class KrpcServerService<@Rpc T : Any>(
val value = when (val invokator = callable.invokator) {
is RpcInvokator.Method -> {
- callScoped(callId) {
- invokator.call(service, data)
- }
- }
-
- is RpcInvokator.Field -> {
- invokator.call(service)
+ invokator.call(service, data)
}
}.let { interceptedValue ->
// KRPC-173
@@ -216,27 +195,17 @@ internal class KrpcServerService<@Rpc T : Any>(
cause = serializedCause,
connectionId = callData.connectionId,
serviceId = callData.serviceId,
- ).also { sender.sendMessage(it) }
+ ).also { connector.sendMessage(it) }
}
- if (failure == null) {
- streamContext.valueOrNull?.apply {
- launchIf({ incomingHotFlowsAvailable }) {
- handleIncomingHotFlows(it)
- }
-
- launchIf({ outgoingStreamsAvailable }) {
- handleOutgoingStreams(it, serialFormat, descriptor.fqName)
- }
- } ?: run {
- cancelRequest(callId, fromJob = true)
- }
- } else {
+ if (failure != null) {
cancelRequest(callId, "Server request failed", failure, fromJob = true)
+ } else {
+ cancelRequest(callId, fromJob = true)
}
}
- requestMap[callId] = RpcRequest(requestJob, streamContext)
+ requestMap[callId] = RpcRequest(requestJob, serverStreamContext)
requestJob.start()
}
@@ -275,7 +244,7 @@ internal class KrpcServerService<@Rpc T : Any>(
}
}
- sender.sendMessage(result)
+ connector.sendMessage(result)
}
private suspend fun sendFlowMessages(
@@ -352,18 +321,40 @@ internal class KrpcServerService<@Rpc T : Any>(
fromJob: Boolean = false,
) {
requestMap.remove(callId)?.cancelAndClose(callId, message, cause, fromJob)
+ }
- // acknowledge the cancellation
- sender.sendMessage(
- KrpcGenericMessage(
- connectionId = null,
- pluginParams = mapOf(
- KrpcPluginKey.GENERIC_MESSAGE_TYPE to KrpcGenericMessage.CANCELLATION_TYPE,
- KrpcPluginKey.CANCELLATION_TYPE to CancellationType.CANCELLATION_ACK.toString(),
- KrpcPluginKey.CANCELLATION_ID to callId,
+ suspend fun cancelRequestWithOptionalAck(
+ callId: String,
+ message: String? = null,
+ cause: Throwable? = null,
+ ) {
+ cancelRequest(callId, message, cause, fromJob = false)
+
+ if (!supportedPlugins.contains(KrpcPlugin.NO_ACK_CANCELLATION)) {
+ connector.sendMessage(
+ KrpcGenericMessage(
+ connectionId = null,
+ pluginParams = mapOf(
+ KrpcPluginKey.GENERIC_MESSAGE_TYPE to KrpcGenericMessage.CANCELLATION_TYPE,
+ KrpcPluginKey.CANCELLATION_TYPE to CancellationType.CANCELLATION_ACK.toString(),
+ KrpcPluginKey.CANCELLATION_ID to callId,
+ )
)
)
- )
+ }
+ }
+
+ private val serverStreamContext: ServerStreamContext = ServerStreamContext()
+
+ private val serialFormat: SerialFormat by lazy {
+ val module = SerializersModule {
+ contextual(Flow::class) {
+ @Suppress("UNCHECKED_CAST")
+ ServerStreamSerializer(serverStreamContext, it.first() as KSerializer)
+ }
+ }
+
+ config.serialFormatInitializer.applySerializersModuleAndBuild(module)
}
companion object {
@@ -373,7 +364,7 @@ internal class KrpcServerService<@Rpc T : Any>(
}
}
-internal class RpcRequest(val handlerJob: Job, val streamContext: LazyKrpcStreamContext) {
+internal class RpcRequest(val handlerJob: Job, val streamContext: ServerStreamContext) {
suspend fun cancelAndClose(
callId: String,
message: String? = null,
@@ -390,13 +381,6 @@ internal class RpcRequest(val handlerJob: Job, val streamContext: LazyKrpcStream
handlerJob.join()
}
- val ctx = streamContext.valueOrNull
- if (ctx == null) {
- streamContext.streamScopeOrNull
- ?.cancelRequestScopeById(callId, message ?: "Scope cancelled", cause)
- ?.join()
- } else {
- ctx.cancel(message ?: "Request cancelled", cause)?.join()
- }
+ streamContext.removeCall(callId, cause)
}
}
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt
new file mode 100644
index 000000000..8e2f01b73
--- /dev/null
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamContext.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.server.internal
+
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.flow
+import kotlinx.rpc.internal.utils.map.RpcInternalConcurrentHashMap
+import kotlinx.rpc.internal.utils.thread.RpcInternalThreadLocal
+import kotlinx.rpc.krpc.internal.KrpcCallMessage
+import kotlinx.rpc.krpc.internal.decodeMessageData
+import kotlinx.rpc.krpc.internal.deserialize
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialFormat
+
+internal class ServerStreamContext {
+ private val currentCallId = RpcInternalThreadLocal()
+
+ fun scoped(callId: String, body: () -> T): T {
+ try {
+ currentCallId.set(callId)
+ return body()
+ } finally {
+ currentCallId.remove()
+ }
+ }
+
+ private val streams = RpcInternalConcurrentHashMap>()
+
+ suspend fun send(message: KrpcCallMessage.StreamMessage, serialFormat: SerialFormat) {
+ val call = streams[message.callId]?.get(message.streamId) ?: return
+ val data = scoped(message.streamId) {
+ decodeMessageData(serialFormat, call.elementSerializer, message)
+ }
+ call.channel.send(data)
+ }
+
+ suspend fun cancelStream(message: KrpcCallMessage.StreamCancel) {
+ streams[message.callId]?.get(message.streamId)?.channel?.send(StreamCancel(message.cause.deserialize()))
+ }
+
+ suspend fun closeStream(message: KrpcCallMessage.StreamFinished) {
+ streams[message.callId]?.get(message.streamId)?.channel?.send(StreamEnd)
+ }
+
+ fun removeCall(callId: String, cause: Throwable?) {
+ streams.remove(callId)?.values?.forEach {
+ it.channel.close(cause)
+ }
+ }
+
+ fun prepareClientStream(streamId: String, elementKind: KSerializer): Flow {
+ val callId = currentCallId.get() ?: error("No call id")
+
+ val channel = Channel(Channel.UNLIMITED)
+
+ @Suppress("UNCHECKED_CAST")
+ val map = streams.computeIfAbsent(callId) { RpcInternalConcurrentHashMap() }
+ map[streamId] = StreamCall(callId, streamId, channel, elementKind)
+
+ fun onClose() {
+ channel.cancel()
+ map.remove(streamId)
+ }
+
+ val flow = flow {
+ for (message in channel) {
+ when (message) {
+ is StreamCancel -> {
+ onClose()
+ throw message.cause ?: streamCanceled()
+ }
+
+ is StreamEnd -> {
+ onClose()
+
+ return@flow
+ }
+
+ else -> {
+ emit(message)
+ }
+ }
+ }
+ }
+
+ return flow
+ }
+
+ private fun streamCanceled(): Throwable = NoSuchElementException("Stream canceled")
+}
+
+private data class StreamCancel(val cause: Throwable? = null)
+private data object StreamEnd
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamSerializer.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamSerializer.kt
new file mode 100644
index 000000000..be9b292bc
--- /dev/null
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/ServerStreamSerializer.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023-2024 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+package kotlinx.rpc.krpc.server.internal
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.rpc.krpc.internal.StreamSerializer
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.*
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+
+internal class ServerStreamSerializer(
+ val context: ServerStreamContext,
+ val elementType: KSerializer,
+) : KSerializer>, StreamSerializer() {
+ override val descriptor: SerialDescriptor by lazy {
+ buildClassSerialDescriptor("ServerStreamSerializer") {
+ element(STREAM_ID_SERIAL_NAME, streamIdDescriptor)
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): Flow<*> {
+ val streamId = decoder.decodeString()
+
+ return context.prepareClientStream(streamId, elementType)
+ }
+
+ override fun serialize(encoder: Encoder, value: Flow<*>) {
+ error("This method must not be called. Please report to the developer.")
+ }
+}
diff --git a/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/StreamCall.kt b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/StreamCall.kt
new file mode 100644
index 000000000..9ff3fb526
--- /dev/null
+++ b/krpc/krpc-server/src/commonMain/kotlin/kotlinx/rpc/krpc/server/internal/StreamCall.kt
@@ -0,0 +1,15 @@
+/*
+ * 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.server.internal
+
+import kotlinx.coroutines.channels.Channel
+import kotlinx.serialization.KSerializer
+
+internal data class StreamCall(
+ val callId: String,
+ val streamId: String,
+ val channel: Channel,
+ val elementSerializer: KSerializer,
+)
diff --git a/krpc/krpc-test/build.gradle.kts b/krpc/krpc-test/build.gradle.kts
index 0ba1b3566..ba1b5a810 100644
--- a/krpc/krpc-test/build.gradle.kts
+++ b/krpc/krpc-test/build.gradle.kts
@@ -28,6 +28,7 @@ kotlin {
api(projects.krpc.krpcCore)
api(projects.krpc.krpcServer)
api(projects.krpc.krpcClient)
+ api(projects.krpc.krpcLogging)
implementation(projects.krpc.krpcSerialization.krpcSerializationJson)
@@ -39,6 +40,7 @@ kotlin {
jvmMain {
dependencies {
+ implementation(libs.coroutines.debug)
implementation(libs.kotlin.test.junit)
}
}
@@ -101,15 +103,3 @@ tasks.register("moveToGold") {
}
}
}
-
-rpc {
- strict {
- stateFlow = RpcStrictMode.NONE
- sharedFlow = RpcStrictMode.NONE
- nestedFlow = RpcStrictMode.NONE
- streamScopedFunctions = RpcStrictMode.NONE
- suspendingServerStreaming = RpcStrictMode.NONE
- notTopLevelServerFlow = RpcStrictMode.NONE
- fields = RpcStrictMode.NONE
- }
-}
diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestClient.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestClient.kt
index 5799f4328..8008a31be 100644
--- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestClient.kt
+++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestClient.kt
@@ -6,6 +6,7 @@ package kotlinx.rpc.krpc.test
import kotlinx.rpc.krpc.KrpcConfig
import kotlinx.rpc.krpc.KrpcTransport
+import kotlinx.rpc.krpc.client.InitializedKrpcClient
import kotlinx.rpc.krpc.client.KrpcClient
/**
@@ -17,4 +18,4 @@ import kotlinx.rpc.krpc.client.KrpcClient
class KrpcTestClient(
config: KrpcConfig.Client,
transport: KrpcTransport,
-) : KrpcClient(config, transport)
+) : InitializedKrpcClient(config, transport)
diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt
index ac23ea832..45aca6030 100644
--- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt
+++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestService.kt
@@ -5,9 +5,6 @@
package kotlinx.rpc.krpc.test
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharedFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.rpc.RemoteService
import kotlinx.rpc.annotations.Rpc
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
@@ -45,7 +42,7 @@ data class LocalDateTime(
@Suppress("detekt.TooManyFunctions")
@Rpc
-interface KrpcTestService : RemoteService {
+interface KrpcTestService {
fun nonSuspendFlow(): Flow
fun nonSuspendFlowErrorOnEmit(): Flow
fun nonSuspendFlowErrorOnReturn(): Flow
@@ -73,29 +70,15 @@ interface KrpcTestService : RemoteService {
): String
suspend fun incomingStreamSyncCollect(arg1: Flow): Int
- suspend fun incomingStreamAsyncCollect(arg1: Flow): Int
- suspend fun outgoingStream(): Flow
- suspend fun bidirectionalStream(arg1: Flow): Flow
- suspend fun echoStream(arg1: Flow): Flow
+ suspend fun incomingStreamSyncCollectMultiple(arg1: Flow, arg2: Flow, arg3: Flow): Int
+ fun outgoingStream(): Flow
+ fun bidirectionalStream(arg1: Flow): Flow
+ fun echoStream(arg1: Flow): Flow
suspend fun streamInDataClass(payloadWithStream: PayloadWithStream): Int
- suspend fun streamInStream(payloadWithStream: Flow): Int
- suspend fun streamOutDataClass(): PayloadWithStream
- suspend fun streamOfStreamsInReturn(): Flow>
- suspend fun streamOfPayloadsInReturn(): Flow
-
- suspend fun streamInDataClassWithStream(payloadWithPayload: PayloadWithPayload): Int
- suspend fun streamInStreamWithStream(payloadWithPayload: Flow): Int
- suspend fun returnPayloadWithPayload(): PayloadWithPayload
- suspend fun returnFlowPayloadWithPayload(): Flow
-
- suspend fun bidirectionalFlowOfPayloadWithPayload(
- payloadWithPayload: Flow
- ): Flow
-
- suspend fun getNInts(n: Int): Flow
- suspend fun getNIntsBatched(n: Int): Flow>
+ fun getNInts(n: Int): Flow
+ fun getNIntsBatched(n: Int): Flow>
suspend fun bytes(byteArray: ByteArray)
suspend fun nullableBytes(byteArray: ByteArray?)
@@ -107,39 +90,11 @@ interface KrpcTestService : RemoteService {
suspend fun nullableInt(v: Int?): Int?
suspend fun nullableList(v: List?): List?
- suspend fun delayForever(): Flow
+ fun delayForever(): Flow
suspend fun answerToAnything(arg: String): Int
suspend fun krpc173()
fun unitFlow(): Flow
-
- val plainFlowOfInts : Flow
-
- val plainFlowOfFlowsOfInts : Flow>
-
- val plainFlowOfFlowsOfFlowsOfInts : Flow>>
-
- val sharedFlowOfInts : SharedFlow
-
- val sharedFlowOfFlowsOfInts : SharedFlow>
-
- val sharedFlowOfFlowsOfFlowsOfInts : SharedFlow>>
-
- val stateFlowOfInts : StateFlow
-
- val stateFlowOfFlowsOfInts : StateFlow>
-
- val stateFlowOfFlowsOfFlowsOfInts : StateFlow>>
-
- suspend fun emitNextForStateFlowOfInts(value: Int)
-
- suspend fun emitNextForStateFlowOfFlowsOfInts(value: Int)
-
- suspend fun emitNextForStateFlowOfFlowsOfFlowsOfInts(value: Int)
-
- suspend fun sharedFlowInFunction(sharedFlow: SharedFlow): StateFlow
-
- suspend fun stateFlowInFunction(stateFlow: StateFlow): StateFlow
}
diff --git a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt
index 014fc62d6..2ec18fb82 100644
--- a/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt
+++ b/krpc/krpc-test/src/commonMain/kotlin/kotlinx/rpc/krpc/test/KrpcTestServiceBackend.kt
@@ -6,18 +6,14 @@ package kotlinx.rpc.krpc.test
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
+import kotlinx.coroutines.test.TestScope
import kotlinx.serialization.Serializable
-import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.resumeWithException
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
@OptIn(ExperimentalCoroutinesApi::class)
-class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) : KrpcTestService {
- companion object {
- const val SHARED_FLOW_REPLAY = 5
- }
-
+class KrpcTestServiceBackend : KrpcTestService {
override fun nonSuspendFlow(): Flow {
return flow {
repeat(10) {
@@ -133,27 +129,23 @@ class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) :
return arg1.count()
}
- val incomingStreamAsyncCollectLatch = CompletableDeferred()
-
- @OptIn(DelicateCoroutinesApi::class)
- override suspend fun incomingStreamAsyncCollect(arg1: Flow): Int {
- @Suppress("detekt.GlobalCoroutineUsage")
- GlobalScope.launch {
- assertContentEquals(listOf("test1", "test2", "test3"), arg1.toList())
- incomingStreamAsyncCollectLatch.complete(Unit)
- }
- return 5
+ override suspend fun incomingStreamSyncCollectMultiple(
+ arg1: Flow,
+ arg2: Flow,
+ arg3: Flow,
+ ): Int {
+ return arg1.count() + arg2.count() + arg3.count()
}
- override suspend fun outgoingStream(): Flow {
+ override fun outgoingStream(): Flow {
return flow { emit("a"); emit("b"); emit("c") }
}
- override suspend fun bidirectionalStream(arg1: Flow): Flow {
+ override fun bidirectionalStream(arg1: Flow): Flow {
return arg1.map { it.reversed() }
}
- override suspend fun echoStream(arg1: Flow): Flow = flow {
+ override fun echoStream(arg1: Flow): Flow = flow {
arg1.collect {
emit(it)
}
@@ -163,55 +155,7 @@ class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) :
return payloadWithStream.payload.length + payloadWithStream.stream.count()
}
- // necessary for older Kotlin versions
- @Suppress("UnnecessaryOptInAnnotation")
- @OptIn(FlowPreview::class)
- override suspend fun streamInStream(payloadWithStream: Flow): Int {
- return payloadWithStream.flatMapConcat { it.stream }.count()
- }
-
- override suspend fun streamOutDataClass(): PayloadWithStream {
- return payload()
- }
-
- override suspend fun streamOfStreamsInReturn(): Flow> {
- return flow {
- emit(flow { emit("a"); emit("b"); emit("c") })
- emit(flow { emit("1"); emit("2"); emit("3") })
- }
- }
-
- override suspend fun streamOfPayloadsInReturn(): Flow {
- return payloadStream()
- }
-
- override suspend fun streamInDataClassWithStream(payloadWithPayload: PayloadWithPayload): Int {
- assertContentEquals(KrpcTransportTestBase.expectedPayloadWithPayload(10), payloadWithPayload.collect())
- return 5
- }
-
- override suspend fun streamInStreamWithStream(payloadWithPayload: Flow): Int {
- payloadWithPayload.collectIndexed { index, payload ->
- assertContentEquals(KrpcTransportTestBase.expectedPayloadWithPayload(index), payload.collect())
- }
- return 5
- }
-
- override suspend fun returnPayloadWithPayload(): PayloadWithPayload {
- return payloadWithPayload()
- }
-
- override suspend fun returnFlowPayloadWithPayload(): Flow {
- return payloadWithPayloadStream()
- }
-
- override suspend fun bidirectionalFlowOfPayloadWithPayload(
- payloadWithPayload: Flow
- ): Flow {
- return payloadWithPayload
- }
-
- override suspend fun getNInts(n: Int): Flow {
+ override fun getNInts(n: Int): Flow {
return flow {
for (it in 1..n) {
emit(it)
@@ -219,7 +163,7 @@ class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) :
}
}
- override suspend fun getNIntsBatched(n: Int): Flow> {
+ override fun getNIntsBatched(n: Int): Flow> {
return flow {
for (it in (1..n).chunked(1000)) {
emit(it)
@@ -264,7 +208,7 @@ class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) :
override suspend fun nullableInt(v: Int?): Int? = v
override suspend fun nullableList(v: List?): List? = v
- override suspend fun delayForever(): Flow = flow {
+ override fun delayForever(): Flow = flow {
emit(true)
delay(Int.MAX_VALUE.toLong())
}
@@ -287,68 +231,8 @@ class KrpcTestServiceBackend(override val coroutineContext: CoroutineContext) :
emit(Unit)
}
}
-
- override val plainFlowOfInts: Flow = plainFlow { it }
-
- override val plainFlowOfFlowsOfInts: Flow> = plainFlow { plainFlow { i -> i } }
-
- override val plainFlowOfFlowsOfFlowsOfInts: Flow>> = plainFlow { plainFlow { plainFlow { i -> i } } }
-
- override val sharedFlowOfInts: SharedFlow =
- sharedFlowOfT { it }
-
- override val sharedFlowOfFlowsOfInts: SharedFlow> =
- sharedFlowOfT { sharedFlowOfT { it } }
-
- override val sharedFlowOfFlowsOfFlowsOfInts: SharedFlow>> =
- sharedFlowOfT { sharedFlowOfT { sharedFlowOfT { it } } }
-
- override val stateFlowOfInts: MutableStateFlow =
- stateFlowOfT { it }
-
- override val stateFlowOfFlowsOfInts: MutableStateFlow> =
- stateFlowOfT { stateFlowOfT { it } }
-
- override val stateFlowOfFlowsOfFlowsOfInts: MutableStateFlow>> =
- stateFlowOfT { stateFlowOfT { stateFlowOfT { it } } }
-
- override suspend fun emitNextForStateFlowOfInts(value: Int) {
- stateFlowOfInts.emit(value)
- }
-
- override suspend fun emitNextForStateFlowOfFlowsOfInts(value: Int) {
- stateFlowOfFlowsOfInts.value.emit(value)
- stateFlowOfFlowsOfInts.emit(MutableStateFlow(value))
- }
-
- override suspend fun emitNextForStateFlowOfFlowsOfFlowsOfInts(value: Int) {
- stateFlowOfFlowsOfFlowsOfInts.value.value.emit(value)
- stateFlowOfFlowsOfFlowsOfInts.value.emit(MutableStateFlow(value))
- stateFlowOfFlowsOfFlowsOfInts.emit(MutableStateFlow(MutableStateFlow(value)))
- }
-
- override suspend fun sharedFlowInFunction(sharedFlow: SharedFlow