All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.com.caesarealabs.rpc4k.runtime.implementation.GeneratedCodeUtils.kt Maven / Gradle / Ivy

package com.caesarealabs.rpc4k.runtime.implementation

import com.benasher44.uuid.uuid4
import com.caesarealabs.logging.Logging
import com.caesarealabs.rpc4k.runtime.api.*
import com.caesarealabs.rpc4k.runtime.implementation.serializers.TupleSerializer
import com.caesarealabs.rpc4k.runtime.user.EventSubscription
import com.caesarealabs.rpc4k.runtime.user.RPCContext
import kotlinx.coroutines.flow.map
import kotlinx.serialization.KSerializer
import kotlinx.serialization.SerializationException

/**
 * These functions are used by generated code and code that interacts with them
 */
//TO DO: consider making the C2S functions be in a instance method, this would allow storing the format and client and reduce codegen.
public object GeneratedCodeUtils {


    /**
     * Sends a value and returns the result
     */
    public suspend fun  request(
        client: RpcClient,
        format: SerializationFormat,
        methodName: String,
        args: List,
        argSerializers: List>,
        responseSerializer: KSerializer<*>
    ): T {
        val rpc = Rpc(methodName, args)
        val result = client.send(rpc, format, argSerializers)
        return format.decode(responseSerializer, result) as T
    }

    /**
     * Sends a value, not caring about the result
     */
    public suspend fun send(
        client: RpcClient, format: SerializationFormat, methodName: String, args: List,
        argSerializers: List>
    ) {
        val rpc = Rpc(methodName, args)
        client.send(rpc, format, argSerializers)
    }

    /**
     * Creates a new [EventSubscription] that is a _cold_ flow that allows listening to an event.
     * @param target Note that here we don't have an issue with 'empty string' conflicting with 'no target' because
     * the server already defines when a target is necessary. When a target is needed, empty string is interpreted as empty string,
     * when a target is not needed, empty string, null, or anything else will be treated as 'no target'.
     */
    public fun  coldEventFlow(
        client: RpcClient,
        format: SerializationFormat,
        event: String,
        args: List<*>,
        argSerializers: List>,
        eventSerializer: KSerializer,
        target: Any? = null
    ): EventSubscription {
        val listenerId = uuid4().toString()
        val payload = format.encode(TupleSerializer(argSerializers), args)
        val subscriptionMessage = C2SEventMessage.Subscribe(event = event, listenerId = listenerId, payload, target?.toString())
        val unsubMessage = C2SEventMessage.Unsubscribe(event = event, listenerId = listenerId)
        val flow = client.events.createFlow(subscriptionMessage.toByteArray(), unsubMessage.toByteArray(), listenerId)
            .map { format.decode(eventSerializer, it) }
        return EventSubscription(listenerId, flow)
    }

    /**
     * Called by the generated Router, to respond to the client after receiving a request.
     * [argDeserializers], [resultSerializer], and [respondMethod] are unique for each procedure
     * and so need special codegen to generate them.
     */
    public suspend fun  respond(
        config: HandlerConfig<*>,
        request: ByteArray,
        argDeserializers: List>,
        resultSerializer: KSerializer,
        context: RPCContext,
        respondMethod: suspend RPCContext.(args: List<*>) -> T
    ): ByteArray {
        val parsed = try {
            Rpc.fromByteArray(request, config.format, argDeserializers)
        } catch (e: SerializationException) {
            throw InvalidRpcRequestException("Malformed request arguments: ${e.message}", e)
        }

        context.logData("Parameters") { parsed.arguments }
        val result = with(context) { respondMethod(parsed.arguments) }
        context.logData("Response") { result }
        val response = config.format.encode(resultSerializer, result)

        return response
    }

    public suspend fun  invokeEvent(
        config: HandlerConfig,
        eventName: String,
        subArgDeserializers: List>,
        resultSerializer: KSerializer,
        context: RPCContext,
        /**
         * The actors that actually produced this event, and will not want to get updated that this event occurred, because they
         * already updated the outcome of said event in memory.
         */
        participants: Set,
        /**
         * Important - pass null when targets are not used in the event,
         * pass .toString() when targets are used in the event. The null value should be equivalent to the "null" value, when targets are relevant.
         */
        target: String? = null,
        handle: suspend (subArgs: List<*>) -> R
    ) {
        val match = config.eventManager.match(eventName, target)
//        config.logging.wrapCall(eventName) {
        context.logInfo { "Invoking event $eventName" }
        for ((i, subscriber) in match.withIndex()) {
            // Don't send events to participants
            if (subscriber.info.listenerId in participants) continue

            val parsed = config.format.decode(TupleSerializer(subArgDeserializers), subscriber.info.data)

//            logData("Listener ID $i") { subscriber.info.listenerId }
//            logData("Subscription Data $i") { parsed }

//            logInfo { "Processing subscription ${subscriber.info.listenerId}" }
            val handled = handle(parsed)
//            logData("Event $i") { handled }
            context.logVerbose {
                "Dispatching event $eventName to listener num $i, with ID ${subscriber.info.listenerId}" +
                    ", subscription data $parsed, and resulting event $handled"
            }
            val bytes = config.format.encode(resultSerializer, handled)
            val fullMessage = S2CEventMessage.Emitted(subscriber.info.listenerId, bytes).toByteArray()
            config.sendOrDrop(subscriber.connection, fullMessage, context)
        }
//        }

    }
}

/**
 * Will send the [bytes] to the [connection], dropping it if it cannot be reached
 */
internal suspend fun  HandlerConfig.sendOrDrop(connection: EventConnection, bytes: ByteArray, logging: Logging) {
    val clientExists = messageLauncher.send(connection, bytes)
    if (!clientExists) {
        logging.logInfo { "Dropping connection ${connection.id} as it cannot be reached" }
        eventManager.dropClient(connection)
    }
}





© 2015 - 2024 Weber Informatics LLC | Privacy Policy