diff options
author | therealbush <therealbush@users.noreply.github.com> | 2022-04-23 15:22:18 -0700 |
---|---|---|
committer | therealbush <therealbush@users.noreply.github.com> | 2022-04-23 15:22:18 -0700 |
commit | c12696bd528e891b9da126d3b92f3db089b4b6e6 (patch) | |
tree | 6683284c29f6dd7b9d65fae881527e05b54b3a84 /src/main/kotlin/me/bush/eventbuskotlin | |
parent | 1cb96200f8844e9192f55037cfc11bfb42e1d94f (diff) | |
download | eventbus-kotlin-c12696bd528e891b9da126d3b92f3db089b4b6e6.tar.gz eventbus-kotlin-c12696bd528e891b9da126d3b92f3db089b4b6e6.tar.bz2 eventbus-kotlin-c12696bd528e891b9da126d3b92f3db089b4b6e6.zip |
some changes
Diffstat (limited to 'src/main/kotlin/me/bush/eventbuskotlin')
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/CancelledState.kt | 70 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/Config.kt | 50 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/Event.kt | 40 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/EventBus.kt | 111 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/Listener.kt | 72 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/ListenerGroup.kt | 89 | ||||
-rw-r--r-- | src/main/kotlin/me/bush/eventbuskotlin/Util.kt | 77 |
7 files changed, 509 insertions, 0 deletions
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<KClass<*>, 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<Boolean>() } + .filterIsInstance<KMutableProperty<*>>().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<KClass<*>, ListenerGroup>() + private val subscribers = ConcurrentHashMap.newKeySet<Any>() + private val cache = ConcurrentHashMap<Any, List<Listener>?>() + + /** + * 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 <reified T : Any> 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 <T : Any> listener( + type: Class<T>, + 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<T> +) = 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<Listener>() + val parallel = CopyOnWriteArrayList<Listener>() + + /** + * 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 <R> listWith(listener: Listener, block: (MutableList<Listener>) -> 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 <T : Any> KClass<T>.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 <R> KCallable<R>.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<Listener>() && it.valueParameters.isEmpty() + } as Sequence<KCallable<Listener>> + +/** + * 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<EventListener>() } + .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 |