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

commonMain.com.caesarealabs.rpc4k.runtime.user.components.MemoryRpc.kt Maven / Gradle / Ivy

package com.caesarealabs.rpc4k.runtime.user.components

import com.benasher44.uuid.uuid4
import com.caesarealabs.logging.LoggingFactory
import com.caesarealabs.logging.PrintLoggingFactory
import com.caesarealabs.rpc4k.runtime.api.*
import com.caesarealabs.rpc4k.runtime.implementation.RpcResult
import com.caesarealabs.rpc4k.runtime.user.Rpc4kIndex
import com.caesarealabs.rpc4k.runtime.user.startRpc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.serialization.KSerializer
import kotlin.time.Duration

// A global map used to store active MemoryMulticallServer by their 'port'
private val memoryServerRegistry = mutableMapOf()


/**
 * Simple implementation of a [DedicatedServer] that handles everything in-memory without any need for HTTP and such
 * @param port While this doesn't actually open a port on the network, this number is used as an identifier for the server,
 * so that clients may connect to this server specifically and not other servers running in the same process.
 * @param emulatedLatency If specified, server will wait this much time when responding. This allows emulating the latency of a real server
 */
public class MemoryDedicatedServer(
    private val port: Int,
    private val emulatedLatency: Duration? = null
) : DedicatedServer {
    private val connections = mutableMapOf()
    private var config: ServerConfig? = null
    private val scope = CoroutineScope(Dispatchers.Default)

    private suspend fun emulateLatency() {
        if (emulatedLatency != null) delay(emulatedLatency)
    }

    private fun getConfig() =
        config ?: error("Attempt to respond with server that has not been started with start()")

    /**
     * Emulates the handling of a request by a server.
     * Will run the processing in its own coroutine.
     */
    internal suspend fun respond(rpcRequest: ByteArray): RpcResult = scope.async {
        emulateLatency()
        RpcServerUtils.routeCall(rpcRequest, getConfig())
    }.await()

    /**
     * Emulates the handling of a subscription / unsubscription by a server
     */
    internal suspend fun acceptEventMessage(message: ByteArray, session: MemoryEventClient) {
        emulateLatency()
        getConfig().acceptEventSubscription(message, session.connection)
    }

    internal fun connect(client: MemoryEventClient) {
        connections[client.connection] = client
    }

    /**
     * Currently unused
     */
    internal fun disconnect(client: MemoryEventClient) {
        connections.remove(client.connection)
    }

    override fun start(config: ServerConfig, wait: Boolean) {
        this.config = config
        memoryServerRegistry[port] = this
    }

    override fun stop() {
        memoryServerRegistry.remove(port)
    }

    override suspend fun send(connection: EventConnection, bytes: ByteArray): Boolean {
        emulateLatency()
        val session = connections[connection] ?: return false
        session.handleMessage(S2CEventMessage.fromByteArray(bytes))
        return true
    }
}

/**
 * Simple implementation of a [RpcClient] that handles everything in-memory without any need for HTTP and such
 * @param port While this doesn't actually open a port on the network, this number is used as an identifier for the server,
 * so that clients may connect to this server specifically and not other servers running in the same process.
 */
public class MemoryRpcClient(private val port: Int) : RpcClient {
    override suspend fun send(
        rpc: Rpc,
        format: SerializationFormat,
        serializers: List>
    ): ByteArray {
        val data = rpc.toByteArray(format, serializers)
        val server = memoryServerRegistry[port] ?: error("Cannot find started memory server at port $port")
        when (val response = server.respond(data)) {
            // The last 2 parameters don't mean much here
            is RpcResult.Error -> throw RpcResponseException(
                response.message,
                rpc,
                format,
                this,
                response.message,
                0
            )

            is RpcResult.Success -> return response.bytes
        }
    }

    override val events: EventClient = MemoryEventClient(port)

}

internal class MemoryEventClient(private val port: Int) : AbstractEventClient() {
    // Used as an identifier for the connection to the server
    internal val connection = EventConnection(uuid4().toString())
    private var connected = false
    override suspend fun send(message: ByteArray) {
        val server = memoryServerRegistry[port] ?: error("Cannot find started memory server at port $port")
        if (!connected) server.connect(this)
        server.acceptEventMessage(message, this)
    }
}

public fun  Rpc4kIndex<*, C, *>.memoryClient(
    port: Int,
    format: SerializationFormat = JsonFormat()
): C {
    return createNetworkClient(MemoryRpcClient(port), format)
}

/**
 * Configure and start an RPC server with one call from the [Rpc4kIndex]
 */
public fun  Rpc4kIndex.startMemory(
    format: SerializationFormat = JsonFormat(),
    eventManager: EventManager = MemoryEventManager(),
    wait: Boolean = true,
    port: Int = PortPool.get(),
    logging: LoggingFactory = PrintLoggingFactory,
    emulatedLatency: Duration? = null,
    service: (I) -> S
): TypedServerConfig = MemoryDedicatedServer(port, emulatedLatency)
    .startRpc(this, format, eventManager, logging, wait, service)





© 2015 - 2024 Weber Informatics LLC | Privacy Policy