diff options
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | build.gradle.kts | 4 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Event.kt | 16 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/EventBus.kt | 95 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Listener.kt | 49 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/ListenerList.kt | 43 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/illnamethislater/Util.kt | 26 | ||||
-rw-r--r-- | src/test/kotlin/Main.kt | 58 |
8 files changed, 183 insertions, 110 deletions
@@ -1,2 +1,4 @@ #EVENTBUS-KOTLIN this isnt done yet i just wanted to get the gh up + +still not done dfsdofdfomsdkflmsdf diff --git a/build.gradle.kts b/build.gradle.kts index b882228..a1450bd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,9 @@ dependencies { implementation("org.apache.logging.log4j:log4j-api:2.15.0") implementation("org.apache.logging.log4j:log4j-core:2.15.0") + // Additional Kotlin libraries required for some features implementation("org.jetbrains.kotlin:kotlin-reflect:1.6.10") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0") } tasks.test { @@ -28,4 +30,4 @@ tasks.test { tasks.withType<KotlinCompile> { kotlinOptions.jvmTarget = "1.8" -}
\ No newline at end of file +} diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt index 2430067..39360eb 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Event.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt @@ -3,9 +3,17 @@ package me.bush.illnamethislater /** * A base class for events that can be cancelled. * + * If [cancellable] is true, your event can be [cancelled]. + * + * If [cancelled] is true, listeners with lower priority will not receive it unless: + * * They have [Listener.receiveCancelled] set to `true`. + * * A future listener with [Listener.receiveCancelled] sets [cancelled] to `false` + * * @author bush - * @since 3/13/2022 + * @since 1.0.0 */ +// TODO: 3/27/2022 ducks or a way to cancel anything (can't put a custom annotation on forge events) +// store cancellable info at subscribe time, do not calculate it in post abstract class Event { var cancelled = false set(value) { @@ -14,5 +22,7 @@ abstract class Event { abstract val cancellable: Boolean - fun cancel() { cancelled = false } -}
\ No newline at end of file + fun cancel() { + cancelled = false + } +} diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt index f35b65d..4de6bc5 100644 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt @@ -2,81 +2,84 @@ package me.bush.illnamethislater import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger -import java.util.UUID -import java.util.concurrent.atomic.AtomicLong import kotlin.reflect.KClass -import kotlin.reflect.full.findAnnotation /** + * [A simple event dispatcher](http://github.com/therealbush/eventbus-kotlin) + * * @author bush - * @since 3/13/2022 + * @since 1.0.0 */ -class EventBus(private val logger: Logger = LogManager.getLogger(), private val strict: Boolean = true) { - private val listeners = hashMapOf<KClass<*>, MutableList<Listener>>() - private val subscribers = hashSetOf<Any>() - - // TODO: 3/26/2022 coroutine shit idk +class EventBus(private val logger: Logger = LogManager.getLogger()) { + private val listeners = hashMapOf<KClass<*>, ListenerList>() + private val subscribers = mutableSetOf<Any>() /** * blah blah annotation properties override listener properties */ - fun subscribe(subscriber: Any) { - if (subscribers.add(subscriber)) { - subscriber::class.allMembers.filterReturnType<Listener>().forEach { property -> - register(property.handleCall(subscriber).apply { - // If the annotation is present, update listener settings from it - // If it is not, and we are in strict mode, continue to the next property - property.findAnnotation<EventListener>()?.let { - priority = it.priority - receiveCancelled = it.receiveCancelled - } ?: if (strict) return@forEach - }, subscriber) + infix fun subscribe(subscriber: Any): Boolean { + return if (subscriber in subscribers) false + else runCatching { + subscriber::class.listeners.forEach { + register(it.handleCall(subscriber), subscriber) } + true + }.getOrElse { + logger.error("Unable to register listeners for subscriber $subscriber", it) + false } } /** + * Registers a listener (which may not belong to any subscriber) to this [EventBus]. If no object + * is given, a key will be returned which can be used in [unsubscribe] to remove the listener. * + * The */ - fun register(listener: Listener, subscriber: Any = ListenerKey()): Any { - putListener(listener.also { it.subscriber = subscriber }) + fun register(listener: Listener, subscriber: Any = Any()): Any { + listeners.getOrPut(listener.type) { ListenerList() } + .add(listener.also { it.subscriber = subscriber }) + subscribers += subscriber return subscriber } /** - * Puts a listener into its respective list and sorts the list. + * */ - private fun putListener(listener: Listener) { - listeners.getOrPut(listener.type) { mutableListOf() }.run { - add(listener) - sortBy(Listener::priority) + // doc + infix fun unsubscribe(subscriber: Any) = subscribers.remove(subscriber).apply { + if (this) listeners.entries.removeIf { + it.value.removeFrom(subscriber) + it.value.isEmpty } } /** - * + * Posts an event. */ - fun unsubscribe(subscriber: Any) { - if (subscribers.remove(subscriber)) { - listeners.values.forEach { it.removeIf(subscriber::equals) } - } - } + // doc + infix fun post(event: Any) = listeners[event::class]?.post(event) /** - * ur mom + * Logs the subscriber count, total listener count, and listener count + * for every event type with at least one subscriber to [logger]. + * Per-event counts are sorted from greatest to least listeners. + * ``` + * Subscribers: 5 + * Listeners: 8 sequential, 4 parallel + * SomeInnerClass: 4, 2 + * OtherEvent: 3, 1 + * String: 1, 1 */ - fun post(event: Any) = listeners[event::class].let { list -> - if (list == null || list.isEmpty()) false - else if (event is Event) { - list.forEach { - if (!event.cancelled || it.receiveCancelled) { - it.listener(event) - } + fun debugInfo() { + logger.info(StringBuilder().apply { + append("\nSubscribers: ${subscribers.size}") + val sequential = listeners.values.sumOf { it.sequential.size } + val parallel = listeners.values.sumOf { it.parallel.size } + append("\nListeners: $sequential sequential, $parallel parallel") + listeners.entries.sortedByDescending { it.value.sequential.size + it.value.parallel.size }.forEach { + append("\n${it.key.simpleName}: ${it.value.sequential.size}, ${it.value.parallel.size}") } - event.cancelled - } else { - list.forEach { it.listener(event) } - false - } + }.toString()) } } diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt index f4626ca..9e7e25e 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt @@ -2,50 +2,33 @@ package me.bush.illnamethislater import kotlin.reflect.KClass -// document -annotation class EventListener(val priority: Int = 0, val receiveCancelled: Boolean = false) - /** - * This class is not intended to be used externally, it is just - * a wrapper for a listener function and some related properties. + * This class is not intended to be used externally, use [listener] instead. * - * Use [listener] instead. + * You *could* use this, and it would work fine, however + * [listener] lets us use reified types for a cleaner look. * * @author bush - * @since 3/13/2022 + * @since 1.0.0 */ class Listener @PublishedApi internal constructor( - internal val type: KClass<*>, - internal val listener: (Any) -> Unit, + listener: (Nothing) -> Unit, internal var priority: Int = 0, - internal var receiveCancelled: Boolean = false + internal var parallel: Boolean = false, + internal var receiveCancelled: Boolean = false, + internal val type: KClass<*> ) { - internal var subscriber: Any? = null + @Suppress("UNCHECKED_CAST") + internal val listener = listener as (Any) -> Unit + internal lateinit var subscriber: Any } -@Suppress("UNCHECKED_CAST") -inline fun <reified T : Any> listener( - noinline listener: (T) -> Unit -) = Listener(T::class, listener as (Any) -> Unit) - -@Suppress("UNCHECKED_CAST") -inline fun <reified T : Any> listener( - priority: Int, - noinline listener: (T) -> Unit -) = Listener(T::class, listener as (Any) -> Unit, priority) - -@Suppress("UNCHECKED_CAST") -inline fun <reified T : Any> listener( - receiveCancelled: Boolean, - noinline listener: (T) -> Unit -) = Listener(T::class, listener as (Any) -> Unit, receiveCancelled = receiveCancelled) - -// The above aren't needed as the next is functionally identical, but they -// let us avoid (imo ugly) named parameters or a lambda inside parentheses. - -@Suppress("UNCHECKED_CAST") +/** + * + */ inline fun <reified T : Any> listener( priority: Int = 0, + parallel: Boolean = false, receiveCancelled: Boolean = false, noinline listener: (T) -> Unit -) = Listener(T::class, listener as (Any) -> Unit, priority, receiveCancelled) +) = Listener(listener, priority, parallel, receiveCancelled, T::class) diff --git a/src/main/kotlin/me/bush/illnamethislater/ListenerList.kt b/src/main/kotlin/me/bush/illnamethislater/ListenerList.kt new file mode 100644 index 0000000..9bebb87 --- /dev/null +++ b/src/main/kotlin/me/bush/illnamethislater/ListenerList.kt @@ -0,0 +1,43 @@ +package me.bush.illnamethislater + +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.concurrent.CopyOnWriteArrayList + +/** + * @author bush + * @since 1.0.0 + */ +internal class ListenerList { + val sequential = CopyOnWriteArrayList<Listener>() + val parallel = CopyOnWriteArrayList<Listener>() + + val isEmpty get() = sequential.isEmpty() && parallel.isEmpty() + + fun add(listener: Listener) { + if (listener.parallel) parallel += listener + else sequential += listener + } + + fun removeFrom(subscriber: Any) { + sequential.removeIf(subscriber::equals) + parallel.removeIf(subscriber::equals) + } + + fun post(event: Any): Boolean { + // todo thsi + parallel.run { + if (isNotEmpty()) runBlocking { + forEach { // credit kami blue for the idea + launch { it.listener(event) } + } + } + } + sequential.run { + if (isNotEmpty()) forEach { + it.listener(event) + } + } + return false + } +} diff --git a/src/main/kotlin/me/bush/illnamethislater/Util.kt b/src/main/kotlin/me/bush/illnamethislater/Util.kt index 6c0eb9a..5285718 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Util.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Util.kt @@ -12,10 +12,10 @@ import kotlin.reflect.full.starProjectedType import kotlin.reflect.jvm.javaField import kotlin.reflect.jvm.javaGetter -// @author bush -// @since 3/13/2022 +// author bush +// since 1.0.0 -/** +/* * Using [KClass.members] only returns public members, and * using [KClass.declaredMembers] doesn't return inherited * members. This returns all members, private and inherited. @@ -23,14 +23,14 @@ import kotlin.reflect.jvm.javaGetter internal val <T : Any> KClass<T>.allMembers get() = declaredMembers + allSuperclasses.flatMap { it.declaredMembers } -/** +/* * 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. */ internal fun <R> KCallable<R>.handleCall(receiver: Any) = if (static) call() else call(receiver) -/** +/* * Checks if the calling [KCallable] is a static java field. * * Because kotlin likes to be funny, properties belonging to @@ -45,13 +45,11 @@ internal val KCallable<*>.static get() = if (this !is KProperty<*> || javaGetter != null) false else javaField?.let { Modifier.isStatic(it.modifiers) } ?: false -@Suppress("UNCHECKED_CAST") // This cannot fail -internal inline fun <reified T : Any> List<KCallable<*>>.filterReturnType() = - filter { it.returnType == T::class.starProjectedType } as List<KCallable<T>> - -/** - * A simple class returned as a "key" for listeners that are not - * members of a class, just to make its intentions clearer. This - * can be used in [EventBus.unsubscribe] to remove the listener. +/* + * Finds all listeners in a class. (properties and methods) */ -internal class ListenerKey
\ No newline at end of file +@Suppress("UNCHECKED_CAST") // This cannot fail +internal inline val KClass<*>.listeners + get() = allMembers.asSequence().filter { + it.returnType == Listener::class.starProjectedType + } as Sequence<KCallable<Listener>> diff --git a/src/test/kotlin/Main.kt b/src/test/kotlin/Main.kt index 0933c97..dc6e194 100644 --- a/src/test/kotlin/Main.kt +++ b/src/test/kotlin/Main.kt @@ -1,26 +1,58 @@ import me.bush.illnamethislater.* +import org.apache.logging.log4j.Level +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.core.config.Configurator /** * @author bush - * @since 3/13/2022 + * @since 1.0.0 */ fun main() { - EventBus().run { + Configurator.setRootLevel(Level.INFO) - subscribe(Subscriber()) +// EventBus().run { +// +// subscribe(Subscriber()) +// +// post("String") +// +// val key = register(listener<Int> { +// println(it) +// }) +// +// val topLevelListenerKey = register(topLevelListener()) +// +// unsubscribe(key) +// +// unsubscribe(topLevelListenerKey) +// +// debugInfo() +// } - post("Object()") + val not = NotDuck() + not.wtf() + doDuck(not) + //doDuck(Any()) +} + +fun topLevelListener() = listener<Int> { println("topLevelListener(): $it") } - register(listener<String> { - println("it") - }) +class Subscriber { + + val listener0 get() = listener<String>(500, true, false) { + println(it.uppercase()) } } -class Subscriber { -// clean up and writr tests - @EventListener - fun onDeez() = listener(listener = { e: String -> - println(e.uppercase()) - }, receiveCancelled = false, ) +fun doDuck(any: Any) { +} + +class NotDuck { + fun wtf() { + println("wtf") + } +} + +interface Duck { + fun wtf() } |