From c12696bd528e891b9da126d3b92f3db089b4b6e6 Mon Sep 17 00:00:00 2001 From: therealbush Date: Sat, 23 Apr 2022 15:22:18 -0700 Subject: some changes --- README.md | 198 ++++++++++++++++++++- .../me/bush/eventbuskotlin/CancelledState.kt | 70 ++++++++ src/main/kotlin/me/bush/eventbuskotlin/Config.kt | 50 ++++++ src/main/kotlin/me/bush/eventbuskotlin/Event.kt | 40 +++++ src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt | 111 ++++++++++++ src/main/kotlin/me/bush/eventbuskotlin/Listener.kt | 72 ++++++++ .../kotlin/me/bush/eventbuskotlin/ListenerGroup.kt | 89 +++++++++ src/main/kotlin/me/bush/eventbuskotlin/Util.kt | 77 ++++++++ .../me/bush/illnamethislater/CancelledState.kt | 57 ------ src/main/kotlin/me/bush/illnamethislater/Config.kt | 47 ----- src/main/kotlin/me/bush/illnamethislater/Event.kt | 34 ---- .../kotlin/me/bush/illnamethislater/EventBus.kt | 137 -------------- .../me/bush/illnamethislater/EventListener.kt | 6 - .../kotlin/me/bush/illnamethislater/Listener.kt | 73 -------- .../me/bush/illnamethislater/ListenerGroup.kt | 65 ------- .../kotlin/me/bush/illnamethislater/ReflectUtil.kt | 60 ------- src/test/java/JavaTest.java | 37 ++-- src/test/kotlin/KotlinTest.kt | 76 ++++++-- 18 files changed, 788 insertions(+), 511 deletions(-) create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/CancelledState.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/Config.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/Event.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/Listener.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/ListenerGroup.kt create mode 100644 src/main/kotlin/me/bush/eventbuskotlin/Util.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/CancelledState.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/Config.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/Event.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/EventBus.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/EventListener.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/Listener.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt delete mode 100644 src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt diff --git a/README.md b/README.md index e43d9f5..c196ba2 100644 --- a/README.md +++ b/README.md @@ -1 +1,197 @@ -readme soon +# EVENTBUS + +lines of code +code size +[![](https://jitpack.io/v/therealbush/eventbus-kotlin.svg)](https://jitpack.io/#therealbush/eventbus-kotlin) +[![](https://jitpack.io/v/therealbush/eventbus-kotlin/month.svg)](https://jitpack.io/#therealbush/eventbus-kotlin)
+ +*A simple, thread safe, and fast event dispatcher for Kotlin/JVM and Java.* + +## Features + +#### Simple to Use + +Simple setup, and easy to learn, logical API + +#### Thread Safe + +Non locking event posting, and minimally locking registering/subscribing make this event dispatcher fit for use in +highly concurrent applications. + +#### Fast + +Because there is no reflection during event posting, and subscribers are cached after the first use of reflection, this +event dispatcher is much faster than other reflection based alternatives. + +#### Flexible + +This event dispatcher supports third party events, such as those used in MinecraftForge, and uses the Unsafe API to get +the value of a "cancelled" property at the same speed as direct access. + +#### Parallel Event Posting + +Listeners can be parallel, and they will be called on a Coroutine in the background. This is useful for heavy operations +that may slow down the posting thread. + +## Usage + +### Adding to your project: + +If you have not already, add Jitpack as a repository: + +```groovy +repositories { + maven { url 'https://jitpack.io' } +} +``` + +Add the release of your choice in the dependencies block: + +```groovy +dependencies { + implementation 'com.github.therealbush:eventbus-kotlin:1.0.0' +} +``` + +### Creating an EventBus: + +`EventBus` can take a `Config` as an argument, however it is not required. + +`Config` has four parameters. The following are the default arguments: + +```kotlin +Config( + logger = LogManager.getLogger("Eventbus"), + parallelScope = CoroutineScope(Dispatchers.Default), + thirdPartyCompatibility = true, + annotationRequired = false +) +``` + +#### logger + +The `Logger` this `EventBus` will use to log errors, or `EventBus#debug` + +#### parallelScope + +The `CoroutineScope` to use when posting events to parallel listeners. The default value will work just fine, but you +can specify a custom scope if desired. + +[What is a Coroutine?](https://kotlinlang.org/docs/coroutines-overview.html) + +#### thirdpartyCompatibility + +Whether this `EventBus` should try to find a "cancelled" field in events being listened for that are not a subclass +of `Event`. This is experimental, and should be set to `false` if problems arise. + +#### annotationRequired + +Whether listeners need to be annotated with `@EventListener` to be subscribed to this +`EventBus`. This has no effect on anything else, and is just to improve code readability. + +### Creating an Event: + +Any class can be posted to an `EventBus`, but if you wish to use the base event class, extend `Event`, and implement the +property `cancellable`. + +```kotlin +class SimpleEvent : Event() { + override val cancellable = true +} +``` + +```java +public class SimpleEvent extends Event { + @Override + protected boolean getCancellable() { + return false; + } +} +``` + +### Creating a Listener: + +Listeners are created by using the `listener` function: + +```kotlin +listener(priority = 0, parallel = false, receiveCancelled = false) { + ...listener body ... +} +``` + +```java +listener(EventType.class,0,false,false,event->{ + ...listener body... + }); +``` + +Listeners can be registered either directly with `EventBus#register`, or subscribed by returning them from a function or +property and subscribing the object they belong to with `EventBus#subscribe`. + +The following are all valid. Listeners should be public, but they don't need to be. + +```kotlin +val listener0 = listener { + ...listener body ... +} + +val listener1 + get() = listener { + ...listener body ... + } + +fun listener2() = listener { + ...listener body ... +} +``` + +```java +public Listener listener0=listener(EventType.class,event->{ + ...listener body... + }); + +public Listener listener1(){ + return listener(EventType.class,event->{ + ...listener body... + }); + } +``` + +#### priority + +The priority of this `Listener`. Listeners with a higher priority will receive events before listeners with a lower +priority. + +#### parallel + +Whether this `Listener` should be called on the thread that called `EventBus#post`, or on the +`CoroutineScope` in `Config#parallelScope`. `EventBus#post` will not wait for parallel listeners to complete. + +*Currently, there is no way to get a reference to the `Job` created by `launch` when posting to parallel listeners, and +listeners are not `suspend` lambdas. This may change in the future.* + +#### receiveCancelled + +Whether this `Listener` should receive events that have been cancelled. This will work on third party events +if `Config#thirdPartyCompatibility` is enabled. + +### Subscribing an Object: + +Calling `EventBus#subscribe` and `EventBus#unsubscribe` with an object will add and remove listeners belonging to that +object from the EventBus. Only listeners in subscribed objects will receive events. + +Companion objects and singleton object classes can be subscribed, but subscribing a KClass will not work. + +### Posting an Event: + +Calling `EventBus#post` will post an event to every listener with an **exactly** matching event type. For example, if +event B extends event A, and event A is posted, B listeners will not receive it. + +Events are **not** queued: only listeners currently subscribed will be called. + +`EventBus#post` will return true if the posted event is cancelled after posting it to sequential listeners. Event cancel +state is checked once before posting to parallel listeners, because order is not guaranteed. + +### Still Confused? + +Read [this](https://github.com/therealbush/eventbus-kotlin/tree/master/src/test) for usage examples. diff --git a/src/main/kotlin/me/bush/eventbuskotlin/CancelledState.kt b/src/main/kotlin/me/bush/eventbuskotlin/CancelledState.kt new file mode 100644 index 0000000..861b9fd --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/CancelledState.kt @@ -0,0 +1,70 @@ +package me.bush.eventbuskotlin + +import sun.misc.Unsafe +import java.lang.reflect.Modifier +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KClass +import kotlin.reflect.KMutableProperty +import kotlin.reflect.full.declaredMembers +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.withNullability +import kotlin.reflect.jvm.javaField +import kotlin.reflect.typeOf + +/** + * A simple SAM interface for determining if an event (or any class) is cancellable. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @author bush + * @since 1.0.0 + */ +internal fun interface CancelledState { + + /** + * [event] should only ever be of the type that was passed + * to [CancelledState.invoke], **or this will throw.** + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @return `true` if [event] is cancelled, `false` otherwise. + */ + fun isCancelled(event: Any): Boolean + + companion object { + private val UNSAFE = runCatching { + Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe + }.onFailure { + LOGGER.error("Could not obtain Unsafe instance. Will not be able to determine external cancel state.") + }.getOrNull() // soy jvm + private val NAMES = arrayOf("canceled", "cancelled") + private val NOT_CANCELLABLE = CancelledState { false } + private val CACHE = ConcurrentHashMap, CancelledState>() + + /** + * Creates a [CancelledState] object for events of class [type]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + operator fun invoke(type: KClass<*>, config: Config): CancelledState = CACHE.getOrPut(type) { + // Default implementation for our event class. + if (type.isSubclassOf(Event::class)) CancelledState { (it as Event).cancelled } + // If compatibility is disabled. + else if (!config.thirdPartyCompatibility) NOT_CANCELLABLE + // Find a field named "cancelled" or "canceled" that is a boolean, and has a backing field. + else type.allMembers.filter { it.name in NAMES && it.returnType.withNullability(false) == typeOf() } + .filterIsInstance>().filter { it.javaField != null }.toList().let { + if (it.isEmpty() || UNSAFE == null) NOT_CANCELLABLE else { + if (it.size != 1) config.logger.warn("Multiple possible cancel fields found for event $type") + val offset = it[0].javaField!!.let { field -> + if (Modifier.isStatic(field.modifiers)) + UNSAFE.staticFieldOffset(field) + else UNSAFE.objectFieldOffset(field) + } + // This is the same speed as direct access, plus one JNI call. + CancelledState { event -> UNSAFE.getBoolean(event, offset) } + } + } + } + } +} diff --git a/src/main/kotlin/me/bush/eventbuskotlin/Config.kt b/src/main/kotlin/me/bush/eventbuskotlin/Config.kt new file mode 100644 index 0000000..f0e1120 --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/Config.kt @@ -0,0 +1,50 @@ +package me.bush.eventbuskotlin + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import org.apache.logging.log4j.Logger + + +/** + * A class containing configuration options for an [EventBus]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @author bush + * @since 1.0.0 + */ +data class Config( + + /** + * The logger this [EventBus] will use to log errors, or [EventBus.debug] + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + val logger: Logger = LOGGER, + + /** + * The [CoroutineScope] to use when posting events to parallel listeners. The default + * value will work just fine, but you can specify a custom scope if desired. + * + * [What is a Coroutine?](https://kotlinlang.org/docs/coroutines-overview.html) + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + val parallelScope: CoroutineScope = CoroutineScope(Dispatchers.Default), + + /** + * Whether this [EventBus] should try to find a "cancelled" field in events being listened for that + * are not a subclass of [Event]. This is experimental, and should be set to `false` if problems arise. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + val thirdPartyCompatibility: Boolean = true, + + /** + * Whether listeners need to be annotated with [EventListener] to be subscribed to this [EventBus]. + * This has no effect on anything else, and is just to improve code readability. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + val annotationRequired: Boolean = false +) diff --git a/src/main/kotlin/me/bush/eventbuskotlin/Event.kt b/src/main/kotlin/me/bush/eventbuskotlin/Event.kt new file mode 100644 index 0000000..50f1131 --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/Event.kt @@ -0,0 +1,40 @@ +package me.bush.eventbuskotlin + +/** + * A base class for events that can be cancelled. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#ththingtodo) + * + * @author bush + * @since 1.0.0 + */ +abstract class Event { + + /** + * Whether this event is cancelled or not. If it is, only future listeners with + * [Listener.receiveCancelled] will receive it. However, it can be set back to + * `false`, and listeners will be able to receive it again. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + var cancelled = false + set(value) { + if (cancellable) field = value + } + + /** + * Determines if this event can be [cancelled]. This does not have to return a constant value. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + protected abstract val cancellable: Boolean + + /** + * Sets [cancelled] to true. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + fun cancel() { + cancelled = true + } +} diff --git a/src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt b/src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt new file mode 100644 index 0000000..eeb887d --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt @@ -0,0 +1,111 @@ +package me.bush.eventbuskotlin + +import kotlinx.coroutines.launch +import java.util.concurrent.ConcurrentHashMap +import kotlin.reflect.KClass + +/** + * [A simple, thread safe, and fast event dispatcher for Kotlin/JVM and Java.](https://github.com/therealbush/eventbus-kotlin) + * + * @author bush + * @since 1.0.0 + */ +class EventBus(private val config: Config = Config()) { + private val listeners = ConcurrentHashMap, ListenerGroup>() + private val subscribers = ConcurrentHashMap.newKeySet() + private val cache = ConcurrentHashMap?>() + + /** + * Searches [subscriber] for members that return [Listener] and registers them, if + * [Config.annotationRequired] is false, or they are annotated with [EventListener]. + * + * This will not find top level members, use [register] instead. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @return `true` if [subscriber] was successfully subscribed, + * `false` if it was already subscribed, or could not be. + */ + fun subscribe(subscriber: Any): Boolean = subscribers.add(subscriber).also { + if (it) cache.computeIfAbsent(subscriber) { + getListeners(subscriber, config) + }?.forEach(::register) ?: return false + } + + /** + * Unregisters all listeners belonging to [subscriber]. + * + * This will not remove top level listeners, use [unregister] instead. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @return `true` if [subscriber] was successfully unsubscribed, `false` if it was not subscribed. + */ + fun unsubscribe(subscriber: Any): Boolean = subscribers.remove(subscriber).also { + if (it) cache[subscriber]?.forEach(::unregister) + } + + /** + * Registers a [Listener] to this [EventBus]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + fun register(listener: Listener): Boolean = listeners.computeIfAbsent(listener.type) { + ListenerGroup(it, config) + }.register(listener) + + /** + * Unregisters a [Listener] from this [EventBus]. Returns `true` if [Listener] was registered. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + fun unregister(listener: Listener): Boolean = listeners[listener.type]?.let { + val contained = it.unregister(listener) + if (it.parallel.isEmpty() && it.sequential.isEmpty()) { + listeners.remove(listener.type) + } + contained + } ?: false + + /** + * Posts an [event] to every listener that accepts its type. + * + * Events are **not** queued: only listeners currently subscribed will be called. + * + * If [event] is a subclass of [Event], or has a field-backed mutable boolean property + * named "cancelled" or "canceled" and [Config.thirdPartyCompatibility] is `true`, + * only future listeners with [Listener.receiveCancelled] will receive [event] + * while that property is `true`. + * + * Sequential listeners are called in the order of [Listener.priority], and parallel listeners + * are called after using [launch]. This method will not wait for parallel listeners to complete. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ + fun post(event: Any): Boolean = listeners[event::class]?.post(event) ?: false + + /** + * Logs the subscriber count, total listener count, and listener count for every event type with at + * least one subscriber to [Config.logger]. Per-event counts are sorted from greatest to least listeners. + * + * **This may cause a [ConcurrentModificationException] if [register] or [subscribe] is called in parallel.** + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * ``` + * Subscribers: 5 + * Listeners: 8 sequential, 21 parallel + * BushIsSoCool: 4, 9 + * OtherEvent: 1, 10 + * String: 3, 0 + */ + fun debug() { + config.logger.info("Subscribers: ${subscribers.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 { + config.logger.info(it.toString()) + } + } +} + diff --git a/src/main/kotlin/me/bush/eventbuskotlin/Listener.kt b/src/main/kotlin/me/bush/eventbuskotlin/Listener.kt new file mode 100644 index 0000000..bb78b75 --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/Listener.kt @@ -0,0 +1,72 @@ +package me.bush.eventbuskotlin + +import java.util.function.Consumer +import kotlin.reflect.KClass + +/** + * This class is not intended to be used externally, use [listener] instead. You *could* use this, + * and it would work fine however you would have to specify the type explicitly. (ew!) + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @author bush + * @since 1.0.0 + */ +class Listener @PublishedApi internal constructor( + listener: (Nothing) -> Unit, + internal val type: KClass<*>, + internal val priority: Int = 0, + internal val parallel: Boolean = false, + internal val receiveCancelled: Boolean = false +) { + @Suppress("UNCHECKED_CAST") + // Generics have no benefit here. + internal val listener = listener as (Any) -> Unit + internal var subscriber: Any? = null +} + +/** + * Creates a listener that can be held in a variable or returned from a function + * or getter belonging to an object to be subscribed with [EventBus.subscribe], + * or directly registered to an [EventBus] with [EventBus.register]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @param T The **exact** (no inheritance) type of event to listen for. + * @param priority The priority of this listener, high to low. + * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. + * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. + * @param listener The body of the listener that will be invoked. + */ +inline fun listener( + priority: Int = 0, + parallel: Boolean = false, + receiveCancelled: Boolean = false, + noinline listener: (T) -> Unit +) = Listener(listener, T::class, priority, parallel, receiveCancelled) + +/** + * **This function is intended for use in Java only.** + * + * Creates a listener that can be held in a variable or returned from a function + * or getter belonging to an object to be subscribed with [EventBus.subscribe], + * or directly registered to an [EventBus] with [EventBus.register]. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * + * @param type The **exact** (no inheritance) type of event to listen for. + * @param priority The priority of this listener, high to low. + * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. + * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. + * @param listener The body of the listener that will be invoked. + */ +@JvmOverloads +fun listener( + type: Class, + priority: Int = 0, + parallel: Boolean = false, + receiveCancelled: Boolean = false, + // This might introduce some overhead, but its worth + // not manually having to return "Unit.INSTANCE" from every Java listener. + listener: Consumer +) = Listener(listener::accept, type.kotlin, priority, parallel, receiveCancelled) diff --git a/src/main/kotlin/me/bush/eventbuskotlin/ListenerGroup.kt b/src/main/kotlin/me/bush/eventbuskotlin/ListenerGroup.kt new file mode 100644 index 0000000..d4e9697 --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/ListenerGroup.kt @@ -0,0 +1,89 @@ +package me.bush.eventbuskotlin + +import kotlinx.coroutines.launch +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.reflect.KClass + +/** + * A class for storing and handling listeners of the same [type]. + * + * @author bush + * @since 1.0.0 + */ +internal class ListenerGroup( + private val type: KClass<*>, + private val config: Config +) { + private val cancelledState = CancelledState(type, config) + val sequential = CopyOnWriteArrayList() + val parallel = CopyOnWriteArrayList() + + /** + * Registers a listener. + * + * @return `false` if [listener] was already registered, `true` otherwise. + */ + fun register(listener: Listener): Boolean = listWith(listener) { + var position = it.size + // Sorted insertion and duplicates check + it.forEachIndexed { index, other -> + if (listener == other) return false + if (listener.priority > other.priority && position == it.size) { + position = index + } + } + it.add(position, listener) + true + } + + /** + * Unregisters a listener. + * + * @return `false` if [listener] was not already registered, `true` otherwise. + */ + fun unregister(listener: Listener): Boolean = listWith(listener) { + it.remove(listener) + } + + /** + * Posts an event to every listener in this group. [event] must be the same type as [type]. + * + * @return `true` if [event] was cancelled by sequential listeners, `false` otherwise. + */ + fun post(event: Any): Boolean { + sequential.forEach { + if (it.receiveCancelled || !cancelledState.isCancelled(event)) { + it.listener(event) + } + } + // We check this once, because parallel listener order is not guaranteed. + val cancelled = cancelledState.isCancelled(event) + if (parallel.isNotEmpty()) { + parallel.forEach { + if (it.receiveCancelled || !cancelled) { + config.parallelScope.launch { + it.listener(event) + } + } + } + } + return cancelled + } + + /** + * Convenience method to perform an action on the list [listener] belongs to within a synchronized block. + * + * We are synchronizing a COWArrayList because multiple concurrent mutations can cause IndexOutOfBoundsException. + * If we were to use a normal ArrayList we would have to synchronize [post], however posting performance + * is much more valuable than [register]/[unregister] performance. + */ + private inline fun listWith(listener: Listener, block: (MutableList) -> R): R { + return (if (listener.parallel) parallel else sequential).let { + synchronized(it) { + block(it) + } + } + } + + override fun toString() = "${type.simpleName}: ${sequential.size}, ${parallel.size}" +} diff --git a/src/main/kotlin/me/bush/eventbuskotlin/Util.kt b/src/main/kotlin/me/bush/eventbuskotlin/Util.kt new file mode 100644 index 0000000..edad824 --- /dev/null +++ b/src/main/kotlin/me/bush/eventbuskotlin/Util.kt @@ -0,0 +1,77 @@ +package me.bush.eventbuskotlin + +import org.apache.logging.log4j.LogManager +import kotlin.reflect.KCallable +import kotlin.reflect.KClass +import kotlin.reflect.full.* +import kotlin.reflect.jvm.isAccessible +import kotlin.reflect.typeOf + +// by bush, unchanged since 1.0.0 + +internal val LOGGER = LogManager.getLogger("EventBus") + +/** + * Using [KClass.members] only returns public members, and using [KClass.declaredMembers] + * doesn't return inherited members. This returns all members, private and inherited. + */ +internal val KClass.allMembers + get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence() + +/** + * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly. + * + * 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 runCatching { call(receiver) }.getOrElse { call() } +} + +/* +private 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 + } + */ + +/** + * Finds all members of return type [Listener]. (properties and methods) + */ +@Suppress("UNCHECKED_CAST") // This cannot fail +private inline val KClass<*>.listeners + get() = allMembers.filter { + // Set nullability to false, so this will detect listeners in Java + // with "!" nullability. Also make sure there are no parameters. + it.returnType.withNullability(false) == typeOf() && it.valueParameters.isEmpty() + } as Sequence> + +/** + * Finds all listeners in [subscriber]. + * + * @return A list of listeners belonging to [subscriber], or null if an exception is caught. + */ +internal fun getListeners(subscriber: Any, config: Config) = runCatching { + subscriber::class.listeners.filter { !config.annotationRequired || it.hasAnnotation() } + .map { member -> member.handleCall(subscriber).also { it.subscriber = subscriber } }.toList() +}.onFailure { config.logger.error("Unable to register listeners for subscriber $subscriber", it) }.getOrNull() + +/** + * An annotation that must be used to identify listeners if [Config.annotationRequired] is `true`. + * + * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + */ +annotation class EventListener diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt deleted file mode 100644 index d6ff39d..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt +++ /dev/null @@ -1,57 +0,0 @@ -package me.bush.illnamethislater - -import sun.misc.Unsafe -import java.lang.reflect.Modifier -import kotlin.reflect.KClass -import kotlin.reflect.KMutableProperty -import kotlin.reflect.full.declaredMembers -import kotlin.reflect.full.isSubclassOf -import kotlin.reflect.jvm.javaField -import kotlin.reflect.typeOf - -/** - * A simple SAM interface for determining if an event (or any class) is cancellable. - * - * @author bush - * @since 1.0.0 - */ -internal fun interface CancelledState { - - /** - * Returns whether [event] is cancelled or not. [event] should only ever be of the type - * that was passed to [CancelledState.of], **or this will crash.** - */ - fun isCancelled(event: Any): Boolean - - companion object { - private val UNSAFE = runCatching { - Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe - }.getOrNull() // soy jvm - private val CANCELLED_NAMES = arrayOf("canceled", "cancelled") - private val NOT_CANCELLABLE = CancelledState { false } - private val OFFSETS = hashMapOf, Long>() - - /** - * Creates a [CancelledState] object for events of class [type]. - */ - fun of(type: KClass<*>, config: Config): CancelledState { - // Default impl for our event class. - if (type.isSubclassOf(Event::class)) return CancelledState { (it as Event).cancelled } - // If compat is disabled. - if (!config.thirdPartyCompatibility) return NOT_CANCELLABLE - // Find a field named "cancelled" or "canceled" that is a boolean, and has a backing field. - type.allMembers.filter { it.name in CANCELLED_NAMES && it.returnType == typeOf() } - .filterIsInstance>().filter { it.javaField != null }.toList().let { - if (it.isEmpty() || UNSAFE == null) return NOT_CANCELLABLE - if (it.size != 1) config.logger.warn("Multiple possible cancel fields found for event type $type") - it[0].javaField!!.let { field -> - if (Modifier.isStatic(field.modifiers)) OFFSETS[type] = UNSAFE.staticFieldOffset(field) - else OFFSETS[type] = UNSAFE.objectFieldOffset(field) - } - // This is the same speed as direct access, plus one JNI call and hashmap access. - // If you are familiar with C, this is essentially the same idea as pointers. - return CancelledState { event -> UNSAFE.getBoolean(event, OFFSETS[type]!!) } - } - } - } -} diff --git a/src/main/kotlin/me/bush/illnamethislater/Config.kt b/src/main/kotlin/me/bush/illnamethislater/Config.kt deleted file mode 100644 index 31c2b63..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/Config.kt +++ /dev/null @@ -1,47 +0,0 @@ -package me.bush.illnamethislater - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.Logger -import kotlin.coroutines.CoroutineContext - - -/** - * A class containing configuration options for an [EventBus]. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - * - * @author bush - * @since 1.0.0 - */ -data class Config( - - /** - * The logger this [EventBus] will use to log errors, or log [EventBus.debugInfo] - */ - val logger: Logger = LogManager.getLogger("Eventbus"), - - /** - * The [CoroutineContext] to use when posting events to parallel listeners. The default - * value will work just fine, but you can specify a custom context if desired. - * - * [What is a Coroutine Context?](https://kotlinlang.org/docs/coroutine-context-and-dispatchers.html) - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - */ - val parallelScope: CoroutineScope = CoroutineScope(Dispatchers.Default), - - /** - * Whether this [EventBus] should try to find a "cancelled" field in events being listened for that - * are not a subclass of [Event]. This is experimental, and should be set to `false` if problems arise. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - */ - val thirdPartyCompatibility: Boolean = true, - - /** - * todo doc - */ - val annotationRequired: Boolean = false -) diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt deleted file mode 100644 index 2881c0e..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/Event.kt +++ /dev/null @@ -1,34 +0,0 @@ -package me.bush.illnamethislater - -/** - * A base class for events that can be cancelled. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#ththingtodo) - * - * @author bush - * @since 1.0.0 - */ -abstract class Event { - - /** - * Whether this event is cancelled or not. If it is, only future listeners with - * [Listener.receiveCancelled] will receive it. However, it can be set back to - * `false`, and listeners will be able to receive it again. - */ - var cancelled = false - set(value) { - if (cancellable) field = value - } - - /** - * Determines if this event can be [cancelled]. This does not have to return a constant value. - */ - protected abstract val cancellable: Boolean - - /** - * Sets [cancelled] to true. - */ - fun cancel() { - cancelled = true - } -} diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt deleted file mode 100644 index ddc72ef..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ /dev/null @@ -1,137 +0,0 @@ -package me.bush.illnamethislater - -import kotlin.reflect.KClass -import kotlin.reflect.full.hasAnnotation - -/** - * [A simple event dispatcher.](https://github.com/therealbush/eventbus-kotlin#tododothething) - * - * @author bush - * @since 1.0.0 - */ -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 `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 { - // 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) - false - } - } - - /** - * Unregisters all listeners belonging to [subscriber]. - * - * This will not remove top level listeners, use [unregister] instead. - * - * 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 - subscribers.remove(subscriber)?.forEach { unregister(it) } - return contained - } - - /** - * Registers a [Listener] to this [EventBus]. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - */ - fun register(listener: Listener): Listener { - listeners.computeIfAbsent(listener.type) { - ListenerGroup(it, config) - }.register(listener) - return listener - } - - /** - * Unregisters a [Listener] from this [EventBus]. Returns `true` if [Listener] was registered. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - */ - fun unregister(listener: Listener): Boolean { - return listeners[listener.type]?.let { - val contained = it.unregister(listener) - if (it.parallel.isEmpty() && it.sequential.isEmpty()) { - listeners.remove(listener.type) - } - contained - } ?: false - } - - /** - * Posts an [event] to every listener that accepts its type. - * - * Events are **not** queued: only listeners subscribed currently will be called. - * - * If [event] is a subclass of [Event], or has a field-backed mutable boolean property - * named "cancelled" or "canceled" and [Config.thirdPartyCompatibility] is `true`, - * it can be cancelled by a listener, and only future listeners with [Listener.receiveCancelled] - * will receive it. - * - * Sequential listeners are called in the order of [Listener.priority], and parallel - * listeners are called before or after, depending on the value of [Config.parallelFirst]. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - */ - fun post(event: Any) = listeners[event::class]?.post(event) ?: false - - /** - * Logs the subscriber count, total listener count, and listener count for every event type with at - * least one subscriber to [Config.logger]. Per-event counts are sorted from greatest to least listeners. - * - * **This may cause a [ConcurrentModificationException] if [register] or [subscribe] is called in parallel.** - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - * ``` - * Subscribers: 5 - * Listeners: 8 sequential, 21 parallel - * BushIsSoCool: 4, 9 - * OtherEvent: 1, 10 - * String: 3, 0 - */ - fun debugInfo() { - 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 { - 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 deleted file mode 100644 index e96b1db..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/EventListener.kt +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index c9251d2..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ /dev/null @@ -1,73 +0,0 @@ -package me.bush.illnamethislater - -import java.util.function.Consumer -import kotlin.reflect.KClass - -/** - * This class is not intended to be used externally, use [listener] instead. You *could* use this, - * and it would work fine however you would have to specify the type explicitly. (ew!) - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - * - * @author bush - * @since 1.0.0 - */ -class Listener @PublishedApi internal constructor( - listener: (Nothing) -> Unit, - internal val type: KClass<*>, - internal val priority: Int = 0, - internal val parallel: Boolean = false, - internal val receiveCancelled: Boolean = false -) { - @Suppress("UNCHECKED_CAST") - // Generics have no benefit here, - // it is easier just to force cast. - internal val listener = listener as (Any) -> Unit - internal var subscriber: Any? = null -} - -/** - * Creates a listener that can be held in a variable or returned from a function - * or getter belonging to an object to be subscribed with [EventBus.subscribe], - * or directly registered to an [EventBus] with [EventBus.register]. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - * - * @param T The **exact** (no inheritance) type of event to listen for. - * @param priority The priority of this listener, high to low. - * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. - * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. - * @param listener The body of the listener that will be invoked. - */ -inline fun listener( - priority: Int = 0, - parallel: Boolean = false, - receiveCancelled: Boolean = false, - noinline listener: (T) -> Unit -) = Listener(listener, T::class, priority, parallel, receiveCancelled) - -/** - * **This function is intended for use in Java code.** - * - * Creates a listener that can be held in a variable or returned from a function - * or getter belonging to an object to be subscribed with [EventBus.subscribe], - * or directly registered to an [EventBus] with [EventBus.register]. - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) - * - * @param type The **exact** (no inheritance) type of event to listen for. - * @param priority The priority of this listener, high to low. - * @param parallel If a listener should be invoked in parallel with other parallel listeners, or sequentially. - * @param receiveCancelled If a listener should receive events that have been cancelled by previous listeners. - * @param listener The body of the listener that will be invoked. - */ -@JvmOverloads -fun listener( - type: Class, - priority: Int = 0, - parallel: Boolean = false, - receiveCancelled: Boolean = false, - // This might introduce some overhead, but its worth - // not manually having to return "Kotlin.UNIT" from every Java listener. - listener: Consumer -) = 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 deleted file mode 100644 index 24a1ba3..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt +++ /dev/null @@ -1,65 +0,0 @@ -package me.bush.illnamethislater - -import kotlinx.coroutines.launch -import java.util.concurrent.CopyOnWriteArrayList -import kotlin.reflect.KClass - -/** - * A class for storing and handling listeners. - * - * @author bush - * @since 1.0.0 - */ -internal class ListenerGroup( - private val type: KClass<*>, - private val config: Config -) { - private val cancelledState = CancelledState.of(type, config) - val sequential = CopyOnWriteArrayList() - val parallel = CopyOnWriteArrayList() - - /** - * Adds [listener] to this [ListenerGroup], and sorts its list. - */ - fun register(listener: Listener) { - (if (listener.parallel) parallel else sequential).let { - if (it.addIfAbsent(listener)) { - it.sortedByDescending(Listener::priority) - } - } - } - - /** - * Removes [listener] from this [ListenerGroup]. - */ - fun unregister(listener: Listener): Boolean { - return if (listener.parallel) parallel.remove(listener) - else sequential.remove(listener) - } - - /** - * Posts an event to every listener. Returns true of the event was cancelled. - */ - fun post(event: Any): Boolean { - sequential.forEach { - if (it.receiveCancelled || !cancelledState.isCancelled(event)) { - it.listener(event) - } - } - 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) { - config.parallelScope.launch { - it.listener(event) - } - } - } - } - return cancelledState.isCancelled(event) - } - - 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 deleted file mode 100644 index 7f3618c..0000000 --- a/src/main/kotlin/me/bush/illnamethislater/ReflectUtil.kt +++ /dev/null @@ -1,60 +0,0 @@ -package me.bush.illnamethislater - -import kotlin.reflect.KCallable -import kotlin.reflect.KClass -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.typeOf - -// by bush, unchanged 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. - */ -internal val KClass.allMembers - get() = (declaredMembers + allSuperclasses.flatMap { it.declaredMembers }).asSequence() - -/** - * Checks if a [KCallable] is static on the jvm, and handles invocation accordingly. - * - * 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 runCatching { call(receiver) }.getOrElse { call() } -} - -/* -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 - } - */ - -/** - * Finds all members of return type [Listener]. (properties and methods) - */ -@Suppress("UNCHECKED_CAST") // This cannot fail -internal inline val KClass<*>.listeners - // 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> diff --git a/src/test/java/JavaTest.java b/src/test/java/JavaTest.java index 8518315..7d6f192 100644 --- a/src/test/java/JavaTest.java +++ b/src/test/java/JavaTest.java @@ -1,5 +1,5 @@ -import me.bush.illnamethislater.EventBus; -import me.bush.illnamethislater.Listener; +import me.bush.eventbuskotlin.Event;import me.bush.eventbuskotlin.EventBus; +import me.bush.eventbuskotlin.Listener; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -10,32 +10,17 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.TestInstance.Lifecycle; -import static me.bush.illnamethislater.ListenerKt.listener; +import static me.bush.eventbuskotlin.ListenerKt.listener; /** - * I was getting NCDFE when trying to load this class - * from the other test and I don't care enough to fix it. - * * @author bush * @since 1.0.0 */ @TestInstance(Lifecycle.PER_CLASS) public class JavaTest { - public static Listener someStaticListenerField = listener(SimpleEvent.class, event -> { - event.setCount(event.getCount() + 1); - }); private final Logger logger = LogManager.getLogger(); - public Listener someInstanceListenerField = listener(SimpleEvent.class, event -> { - event.setCount(event.getCount() + 1); - }); private EventBus eventBus; - public static Listener someStaticListenerMethod() { - return listener(SimpleEvent.class, event -> { - event.setCount(event.getCount() + 1); - }); - } - @BeforeAll public void setup() { Configurator.setRootLevel(Level.ALL); @@ -52,9 +37,23 @@ public class JavaTest { Assertions.assertEquals(event.getCount(), 4); } + public Listener someInstanceListenerField = listener(SimpleEvent.class, event -> { + event.setCount(event.getCount() + 1); + }); + + public static Listener someStaticListenerField = listener(SimpleEvent.class, event -> { + event.setCount(event.getCount() + 1); + }); + public Listener someInstanceListenerMethod() { return listener(SimpleEvent.class, event -> { event.setCount(event.getCount() + 1); }); } -} + + public static Listener someStaticListenerMethod() { + return listener(SimpleEvent.class, event -> { + event.setCount(event.getCount() + 1); + }); + } + } diff --git a/src/test/kotlin/KotlinTest.kt b/src/test/kotlin/KotlinTest.kt index 3670703..136e7c8 100644 --- a/src/test/kotlin/KotlinTest.kt +++ b/src/test/kotlin/KotlinTest.kt @@ -1,7 +1,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking -import me.bush.illnamethislater.* +import kotlinx.coroutines.launch +import me.bush.eventbuskotlin.* import org.apache.logging.log4j.Level import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.core.config.Configurator @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import java.util.concurrent.atomic.AtomicInteger import kotlin.random.Random /** @@ -41,7 +42,7 @@ class KotlinTest { //////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Test - fun `test listener priority and ability to cancel events or receive cancelled events`() { + fun `test priority and ability to cancel events or receive cancelled events`() { eventBus.subscribe(this) val event = SimpleEvent() eventBus.post(event) @@ -130,25 +131,75 @@ class KotlinTest { @Test fun `test parallel event posting`() { - runBlocking { - sus() + val listeners = mutableListOf() + repeat(10) { + // Not sure what to test + listeners += listener(parallel = true) { + logger.info("Thread:" + Thread.currentThread().name) + } } - } + listeners.forEach { eventBus.register(it) } + eventBus.post(Unit) + listeners.forEach { eventBus.unregister(it) } - suspend fun sus() { - println() - } + /* I'm not sure what else to test for this, but I'm also not really happy with parallel event posting yet. TODO + + [DefaultDispatcher-worker-5 @coroutine#5] INFO Kotlin Test - DefaultDispatcher-worker-5 @coroutine#5 + [DefaultDispatcher-worker-2 @coroutine#2] INFO Kotlin Test - DefaultDispatcher-worker-2 @coroutine#2 + [DefaultDispatcher-worker-1 @coroutine#1] INFO Kotlin Test - DefaultDispatcher-worker-1 @coroutine#1 + [DefaultDispatcher-worker-4 @coroutine#4] INFO Kotlin Test - DefaultDispatcher-worker-4 @coroutine#4 + [DefaultDispatcher-worker-7 @coroutine#7] INFO Kotlin Test - DefaultDispatcher-worker-7 @coroutine#7 + [DefaultDispatcher-worker-6 @coroutine#6] INFO Kotlin Test - DefaultDispatcher-worker-6 @coroutine#6 + [DefaultDispatcher-worker-8 @coroutine#8] INFO Kotlin Test - DefaultDispatcher-worker-8 @coroutine#8 + [DefaultDispatcher-worker-9 @coroutine#9] INFO Kotlin Test - DefaultDispatcher-worker-9 @coroutine#9 + [DefaultDispatcher-worker-3 @coroutine#3] INFO Kotlin Test - DefaultDispatcher-worker-3 @coroutine#3 + [DefaultDispatcher-worker-10 @coroutine#10] INFO Kotlin Test - DefaultDispatcher-worker-10 @coroutine#10 */ - fun sussy() { - println() } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////// @Test - fun `call every method on multiple threads concurrently to ensure no CME is thrown`() { + fun `test thread safety`() { + val listeners = mutableListOf() + repeat(10) { + listeners += listener(parallel = false) { + doStuff() + } + listeners += listener(parallel = true) { + doStuff() + } + } + listeners.forEach { eventBus.register(it) } + eventBus.debug() + CoroutineScope(Dispatchers.Default).launch { + repeat(100) { + launch { + doStuff() + eventBus.post(Any()) + } + } + } + Thread.sleep(2000) + Assertions.assertEquals(2100, counter.get()) + listeners.forEach { eventBus.unregister(it) } + eventBus.unregister(dummy) + eventBus.unsubscribe(this) + // Should be empty + eventBus.debug() + } + + private val dummy = listener {} + + private var counter = AtomicInteger() + private fun doStuff() { + eventBus.unsubscribe(this) + eventBus.subscribe(this) + eventBus.unregister(dummy) + eventBus.register(dummy) + counter.getAndIncrement() } //////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -181,6 +232,7 @@ class KotlinTest { eventBus.subscribe(this) eventBus.post(Unit) Assertions.assertTrue(called) + eventBus.unsubscribe(this) } var called = false -- cgit