From b49065b50edd48d0f335e17bcf6868dc2f916ad5 Mon Sep 17 00:00:00 2001 From: therealbush Date: Thu, 14 Apr 2022 00:31:16 -0700 Subject: soon (lol) --- src/main/kotlin/me/bush/illnamethislater/Config.kt | 10 ++--- .../kotlin/me/bush/illnamethislater/EventBus.kt | 47 ++++++++++++++-------- .../me/bush/illnamethislater/EventListener.kt | 6 +++ .../kotlin/me/bush/illnamethislater/Listener.kt | 2 +- .../me/bush/illnamethislater/ListenerGroup.kt | 41 +++++++------------ .../kotlin/me/bush/illnamethislater/ReflectUtil.kt | 45 ++++++++++++++------- 6 files changed, 87 insertions(+), 64 deletions(-) create mode 100644 src/main/kotlin/me/bush/illnamethislater/EventListener.kt (limited to 'src/main/kotlin/me/bush/illnamethislater') diff --git a/src/main/kotlin/me/bush/illnamethislater/Config.kt b/src/main/kotlin/me/bush/illnamethislater/Config.kt index ef946c5..31c2b63 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Config.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Config.kt @@ -1,5 +1,6 @@ package me.bush.illnamethislater +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger @@ -29,7 +30,7 @@ data class Config( * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ - val parallelContext: CoroutineContext = Dispatchers.Default, + val parallelScope: CoroutineScope = CoroutineScope(Dispatchers.Default), /** * Whether this [EventBus] should try to find a "cancelled" field in events being listened for that @@ -40,10 +41,7 @@ data class Config( val thirdPartyCompatibility: Boolean = true, /** - * Whether parallel listeners should be called before or after sequential listeners. Parallel listeners - * will always finish before sequential listeners are called, or before [EventBus.post] returns. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * todo doc */ - val parallelFirst: Boolean = true + val annotationRequired: Boolean = false ) diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt index 5b278e3..7affa4a 100644 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt @@ -1,6 +1,10 @@ package me.bush.illnamethislater +import kotlinx.coroutines.delay import kotlin.reflect.KClass +import kotlin.reflect.full.companionObject +import kotlin.reflect.full.companionObjectInstance +import kotlin.reflect.full.hasAnnotation /** * [A simple event dispatcher.](https://github.com/therealbush/eventbus-kotlin#tododothething) @@ -12,23 +16,37 @@ class EventBus(private val config: Config = Config()) { private val listeners = hashMapOf, ListenerGroup>() private val subscribers = hashMapOf>() + /** + * Returns the current count of active subscribers. + */ + val subscriberCount get() = subscribers.size + + /** + * Returns the current count of all listeners, regardless of type. + */ + val listenerCount get() = listeners.values.sumOf { it.parallel.size + it.sequential.size } + /** * Searches [subscriber] for members that return [Listener] and registers them. * * This will not find top level listeners, use [register] instead. * - * Returns `false` if [subscriber] was already subscribed, `true` otherwise. + * Returns `true` if [subscriber] was successfully subscribed, + * `false` if it was already subscribed, or could not be. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun subscribe(subscriber: Any): Boolean { return if (subscriber in subscribers) false else runCatching { - // Register every listener into a group, but also - // keep a separate list just for this subscriber. - subscribers[subscriber] = subscriber::class.listeners.map { member -> - register(member.handleCall(subscriber).also { it.subscriber = subscriber }) - }.toList() + // Keep a separate list just for this subscriber. + subscribers[subscriber] = subscriber::class.listeners + .filter { !config.annotationRequired || it.hasAnnotation() }.map { member -> + // Register listener to a group. + println("${member.name}, ${member.returnType}") + member.parameters.forEach { println(it) } + register(member.handleCall(subscriber).also { it.subscriber = subscriber }) + }.toList() true }.getOrElse { config.logger.error("Unable to register listeners for subscriber $subscriber", it) @@ -41,18 +59,13 @@ class EventBus(private val config: Config = Config()) { * * This will not remove top level listeners, use [unregister] instead. * - * Returns `true` if [subscriber] was subscribed, `false` otherwise. + * Returns `true` if [subscriber] was subscribed. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun unsubscribe(subscriber: Any): Boolean { val contained = subscriber in subscribers - // Unregister every listener for this subscriber, - // and return null so the map entry is removed. - subscribers.computeIfPresent(subscriber) { _, listeners -> - listeners.forEach { unregister(it) } - null - } + subscribers.remove(subscriber)?.forEach { unregister(it) } return contained } @@ -69,7 +82,7 @@ class EventBus(private val config: Config = Config()) { } /** - * Unregisters a [Listener] from this [EventBus]. + * Unregisters a [Listener] from this [EventBus]. Returns `true` if [Listener] was registered. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ @@ -115,11 +128,13 @@ class EventBus(private val config: Config = Config()) { * String: 3, 0 */ fun debugInfo() { - config.logger.info("Subscribers: ${subscribers.size}") + config.logger.info("Subscribers: ${subscribers.keys.size}") val sequential = listeners.values.sumOf { it.sequential.size } val parallel = listeners.values.sumOf { it.parallel.size } config.logger.info("Listeners: $sequential sequential, $parallel parallel") - listeners.values.sortedByDescending { it.sequential.size + it.parallel.size }.forEach { it.debugInfo() } + listeners.values.sortedByDescending { it.sequential.size + it.parallel.size }.forEach { + config.logger.info(it.toString()) + } } } diff --git a/src/main/kotlin/me/bush/illnamethislater/EventListener.kt b/src/main/kotlin/me/bush/illnamethislater/EventListener.kt new file mode 100644 index 0000000..e96b1db --- /dev/null +++ b/src/main/kotlin/me/bush/illnamethislater/EventListener.kt @@ -0,0 +1,6 @@ +package me.bush.illnamethislater + +/** + * todo docs + */ +annotation class EventListener diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt index b2bb2ce..c9251d2 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt @@ -70,4 +70,4 @@ fun listener( // This might introduce some overhead, but its worth // not manually having to return "Kotlin.UNIT" from every Java listener. listener: Consumer -) = Listener({ event: T -> listener.accept(event) }, type.kotlin, priority, parallel, receiveCancelled) +) = Listener(listener::accept, type.kotlin, priority, parallel, receiveCancelled) diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt index fdcc7bc..b8116cb 100644 --- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt +++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt @@ -1,7 +1,6 @@ package me.bush.illnamethislater -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.* import java.util.concurrent.CopyOnWriteArrayList import kotlin.reflect.KClass @@ -23,9 +22,10 @@ internal class ListenerGroup( * Adds [listener] to this [ListenerGroup], and sorts its list. */ fun register(listener: Listener) { - with(if (listener.parallel) parallel else sequential) { - add(listener) - sortedByDescending { it.priority } + (if (listener.parallel) parallel else sequential).let { + if (it.addIfAbsent(listener)) { + it.sortedByDescending(Listener::priority) + } } } @@ -41,36 +41,25 @@ internal class ListenerGroup( * Posts an event to every listener. Returns true of the event was cancelled. */ fun post(event: Any): Boolean { - if (config.parallelFirst) postParallel(event) sequential.forEach { if (it.receiveCancelled || !cancelledState.isCancelled(event)) { it.listener(event) } } - if (!config.parallelFirst) postParallel(event) - return cancelledState.isCancelled(event) - } - - /** - * Posts an event to all parallel listeners. Cancel state of the event is checked once before - * posting the event as opposed to before calling each listener, to avoid inconsistencies. - */ - private fun postParallel(event: Any) { - if (parallel.isEmpty()) return - // We check this once, because listener order is not consistent - val cancelled = cancelledState.isCancelled(event) - // Credit to KB for the idea - runBlocking(config.parallelContext) { + if (parallel.isNotEmpty()) { + // We check this once, because listener order is not guaranteed. + val cancelled = cancelledState.isCancelled(event) + // Credit to KB for the idea parallel.forEach { - if (it.receiveCancelled || !cancelled) launch { - it.listener(event) + if (it.receiveCancelled || !cancelled) { + config.parallelScope.launch { + it.listener(event) + } } } } + return cancelledState.isCancelled(event) } - /** - * Logs information about this [ListenerGroup]. - */ - fun debugInfo() = config.logger.info("${type.simpleName}: ${sequential.size}, ${parallel.size}") + override fun toString() = "${type.simpleName}: ${sequential.size}, ${parallel.size}" } diff --git a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt index ba42f76..2b16379 100644 --- a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt +++ b/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt @@ -1,15 +1,15 @@ package me.bush.illnamethislater import java.lang.reflect.Modifier -import kotlin.reflect.KCallable -import kotlin.reflect.KClass -import kotlin.reflect.KProperty +import kotlin.reflect.* import kotlin.reflect.full.allSuperclasses import kotlin.reflect.full.declaredMembers +import kotlin.reflect.full.valueParameters +import kotlin.reflect.full.withNullability import kotlin.reflect.jvm.isAccessible import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaGetter -import kotlin.reflect.typeOf +import kotlin.reflect.jvm.javaMethod // by bush, unchanged since 1.0.0 @@ -22,26 +22,41 @@ internal val KClass.allMembers /** * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly. - * I am not aware of a better alternative that works with `object` classes. + * + * I have tried to check if the callable needs a receiver, and have left my code + * below, but for some reason a property (private, no getter) of a companion object + * (which is static in bytecode) requires a receiver, while an identical property in a + * non companion object does not, and will throw if one is passed. + * + * I am not aware of a way to check if a property belongs to a companion object, is static, + * or requires certain arguments. (instanceParameter exists, but it will throw if it is an argument) + * I thought maybe I was using the wrong methods, but apart from KProperty#get, (which is only for + * properties, and only accepts arguments of type `Nothing` when `T` is star projected or covariant) + * I could not find any other way to do this, not even on StackOverFlow. + * + * Funny how this solution is 1/10th the lines and always works. */ internal fun KCallable.handleCall(receiver: Any? = null): R { isAccessible = true - return if (static) call() else call(receiver) + return runCatching { call(receiver) }.getOrElse { call() } } -/** - * Checks if the calling [KCallable] is a static java field. Because Kotlin likes to be funny, properties - * belonging to `object` classes are static, but their getters are not. If there is a getter (the property - * is not private), we will be accessing that, otherwise we check if the field is static with Java reflection. - * This also lets us support static listeners in Java code. +/* +internal val KCallable<*>.isJvmStatic + get() = when (this) { + is KFunction -> Modifier.isStatic(javaMethod?.modifiers ?: 0) + is KProperty -> this.javaGetter == null && Modifier.isStatic(javaField?.modifiers ?: 0) + else -> false + } */ -internal val KCallable<*>.static - get() = if (this !is KProperty<*> || javaGetter != null) false - else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false /** * Finds all members of return type [Listener]. (properties and methods) */ @Suppress("UNCHECKED_CAST") // This cannot fail internal inline val KClass<*>.listeners - get() = allMembers.filter { it.returnType == typeOf() } as Sequence> + // Force nullability to false, so this will detect listeners in Java + // with "!" nullability. Also make sure there are no parameters. + get() = allMembers.filter { + it.returnType.withNullability(false) == typeOf() && it.valueParameters.isEmpty() + } as Sequence> -- cgit