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

com.freya02.botcommands.api.core.EventDispatcher.kt Maven / Gradle / Ivy

package com.freya02.botcommands.api.core

import com.freya02.botcommands.api.core.annotations.BEventListener
import com.freya02.botcommands.api.core.events.BEvent
import com.freya02.botcommands.api.core.exceptions.InitializationException
import com.freya02.botcommands.api.core.service.annotations.BService
import com.freya02.botcommands.internal.BContextImpl
import com.freya02.botcommands.internal.core.ClassPathFunction
import com.freya02.botcommands.internal.core.EventHandlerFunction
import com.freya02.botcommands.internal.core.requiredFilter
import com.freya02.botcommands.internal.core.service.FunctionAnnotationsMap
import com.freya02.botcommands.internal.core.toClassPathFunctions
import com.freya02.botcommands.internal.throwInternal
import com.freya02.botcommands.internal.throwUser
import com.freya02.botcommands.internal.unreflect
import com.freya02.botcommands.internal.utils.FunctionFilter
import com.freya02.botcommands.internal.utils.ReflectionUtils.declaringClass
import com.freya02.botcommands.internal.utils.ReflectionUtils.nonInstanceParameters
import com.freya02.botcommands.internal.utils.ReflectionUtils.shortSignature
import com.freya02.botcommands.internal.utils.withFilter
import dev.minn.jda.ktx.events.CoroutineEventManager
import kotlinx.coroutines.*
import mu.KotlinLogging
import net.dv8tion.jda.api.events.Event
import net.dv8tion.jda.api.events.GenericEvent
import java.lang.reflect.InvocationTargetException
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.reflect.KClass
import kotlin.reflect.full.callSuspend
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.functions
import kotlin.reflect.jvm.jvmErasure
import kotlin.time.Duration
import kotlin.time.toDuration
import kotlin.time.toDurationUnit

// https://discord.com/channels/125227483518861312/125227483518861312/1114953133722980453
internal class ConcurrentTreeSet {
    // Only protect modification operations, traversal is fine
    private val lock = ReentrantLock()
    private var set: MutableSet = TreeSet()

    fun add(t: T): Boolean = lock.withLock {
        val newSet = TreeSet(set)
        return newSet.add(t).also {
            this.set = newSet
        }
    }

    fun remove(t: T): Boolean = lock.withLock {
        val newSet = TreeSet(set)
        return newSet.remove(t).also {
            this.set = newSet
        }
    }

    inline fun  map(block: (T) -> R): List = set.map(block)

    fun removeAll(removedSet: ConcurrentTreeSet): Boolean = lock.withLock {
        val newSet = TreeSet(set)
        return newSet.removeAll(removedSet.set).also {
            this.set = newSet
        }
    }

    inline fun forEach(block: (T) -> Unit) = set.forEach(block)
}

private typealias EventMap = MutableMap, ConcurrentTreeSet>

@BService
class EventDispatcher internal constructor(
    private val context: BContextImpl,
    private val eventTreeService: EventTreeService,
    functionAnnotationsMap: FunctionAnnotationsMap
) {
    private val logger = KotlinLogging.logger { }
    private val eventManager: CoroutineEventManager = context.eventManager

    private val map: EventMap = ConcurrentHashMap()
    private val listeners: MutableMap, EventMap> = ConcurrentHashMap()

    init {
        functionAnnotationsMap
            .getFunctionsWithAnnotation()
            .addAsEventListeners()

        //This could dispatch to multiple listeners, timeout must be handled on a per-listener basis manually
        // as jda-ktx takes this group of listeners as only being one.
        eventManager.listener(timeout = Duration.INFINITE) {
            dispatchEvent(it)
        }
    }

    fun addEventListener(listener: Any) {
        listener::class
            .functions
            .withFilter(FunctionFilter.annotation())
            .toClassPathFunctions(listener)
            .addAsEventListeners()
    }

    fun removeEventListener(listener: Any) {
        listeners[listener::class.java]?.let { instanceMap ->
            instanceMap.forEach { (kClass, functions) ->
                val functionMap = map[kClass]
                    ?: throwInternal("Listener was registered without having its functions added to the listener map")
                functionMap.removeAll(functions)
            }
        }
    }

    @JvmSynthetic
    suspend fun dispatchEvent(event: Any) {
        // No need to check for `event` type as if it's in the map, then it's recognized
        val handlers = map[event::class] ?: return

        handlers.forEach { preboundFunction ->
            if (preboundFunction.isAsync) {
                context.coroutineScopesConfig.eventDispatcherScope.launch {
                    runEventHandler(preboundFunction, event)
                }
            } else {
                runEventHandler(preboundFunction, event)
            }
        }
    }

    @JvmName("dispatchEvent")
    fun dispatchEventJava(event: Any) = runBlocking { dispatchEvent(event) }

    fun dispatchEventAsync(event: Any): List> {
        // Try not to switch context on non-handled events
        // No need to check for `event` type as if it's in the map, then it's recognized
        val handlers = map[event::class] ?: return emptyList()

        val scope = context.coroutineScopesConfig.eventDispatcherScope
        return handlers.map { preboundFunction ->
            scope.async { runEventHandler(preboundFunction, event) }
        }
    }

    private suspend fun runEventHandler(eventHandlerFunction: EventHandlerFunction, event: Any) {
        try {
            val (instance, function) = eventHandlerFunction.classPathFunction

            /**
             * See [CoroutineEventManager.handle]
             */
            val actualTimeout = eventHandlerFunction.timeout
            if (actualTimeout.isPositive() && actualTimeout.isFinite()) {
                // Timeout only works when the continuations implement a cancellation handler
                val result = withTimeoutOrNull(actualTimeout.inWholeMilliseconds) {
                    function.callSuspend(instance, event, *eventHandlerFunction.parameters)
                }
                if (result == null) {
                    logger.debug("Event of type ${event.javaClass.simpleName} timed out.")
                }
            } else {
                function.callSuspend(instance, event, *eventHandlerFunction.parameters)
            }
        } catch (e: InvocationTargetException) {
            when (e.cause) {
                is InitializationException -> throw e.cause!!
                else -> printException(eventHandlerFunction, e)
            }
        } catch (e: Throwable) {
            printException(eventHandlerFunction, e)
        }
    }

    private fun Collection.addAsEventListeners() = this
        .requiredFilter(FunctionFilter.nonStatic())
        .requiredFilter(FunctionFilter.firstArg(GenericEvent::class, BEvent::class))
        .forEach { classPathFunc ->
            val function = classPathFunc.function
            val annotation = function.findAnnotation()
                ?: throwInternal(function, "Function was asserted to have BEventListener but it was not found")

            val parameters = function.nonInstanceParameters

            val eventErasure = parameters.first().type.jvmErasure
            val eventParametersErasures = parameters.drop(1).map { it.type.jvmErasure }
                // The main risk was with injected services, as they may not be available at that point,
                // but they are pretty much limited to objects manually added by the framework, before the service loading occurs
                .onEach {
                    context.serviceContainer.canCreateService(it)?.let { errorMessage ->
                        throwUser(
                            classPathFunc.function,
                            "Unable to register event listener due to an unavailable service: $errorMessage"
                        )
                    }
                }
            val eventHandlerFunction = EventHandlerFunction(classPathFunction = classPathFunc,
                isAsync = annotation.async,
                timeout = getTimeout(annotation),
                priority = annotation.priority,
                parametersBlock = {
                    //Getting services is delayed until execution, as to ensure late services can be used in listeners
                    context.serviceContainer.getParameters(eventParametersErasures).toTypedArray()
                })

            classPathFunc.function.declaringClass.java.let { clazz ->
                val instanceMap = listeners.computeIfAbsent(clazz) { hashMapOf() }

                (eventTreeService.getSubclasses(eventErasure) + eventErasure).forEach {
                    instanceMap.computeIfAbsent(it) { ConcurrentTreeSet() }.add(eventHandlerFunction)
                }
            }

            (eventTreeService.getSubclasses(eventErasure) + eventErasure).forEach {
                map.computeIfAbsent(it) { ConcurrentTreeSet() }.add(eventHandlerFunction)
            }
        }

    private fun getTimeout(annotation: BEventListener): Duration {
        if (annotation.timeout < 0) return Duration.INFINITE

        return annotation.timeout.toDuration(annotation.timeoutUnit.toDurationUnit()).let {
            when {
                it.isPositive() && it.isFinite() -> it
                else -> eventManager.timeout
            }
        }
    }

    private fun printException(eventHandlerFunction: EventHandlerFunction, e: Throwable) = logger.error(
        "An exception occurred while dispatching an event for ${eventHandlerFunction.classPathFunction.function.shortSignature}",
        e.unreflect()
    )
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy