From b196c7c2fe06d84e862648a8bf25058f72c6c0b1 Mon Sep 17 00:00:00 2001 From: therealbush Date: Sat, 2 Apr 2022 00:17:29 -1000 Subject: Nearly done --- .../me/bush/illnamethislater/CancelledState.kt | 8 +- src/main/kotlin/me/bush/illnamethislater/Event.kt | 2 +- .../kotlin/me/bush/illnamethislater/EventBus.kt | 85 ++++++++++++++-------- .../kotlin/me/bush/illnamethislater/Listener.kt | 40 ++++++++-- .../me/bush/illnamethislater/ListenerGroup.kt | 12 +-- 5 files changed, 97 insertions(+), 50 deletions(-) (limited to 'src/main/kotlin/me/bush') diff --git a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt index 80022ba..d6ff39d 100644 --- a/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt +++ b/src/main/kotlin/me/bush/illnamethislater/CancelledState.kt @@ -19,12 +19,14 @@ 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 cause an error. + * that was passed to [CancelledState.of], **or this will crash.** */ fun isCancelled(event: Any): Boolean companion object { - private val UNSAFE = Unsafe::class.declaredMembers.single { it.name == "theUnsafe" }.handleCall() as Unsafe + 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>() @@ -40,7 +42,7 @@ internal fun interface CancelledState { // 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()) return NOT_CANCELLABLE + 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) diff --git a/src/main/kotlin/me/bush/illnamethislater/Event.kt b/src/main/kotlin/me/bush/illnamethislater/Event.kt index 476a34a..2881c0e 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Event.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Event.kt @@ -23,7 +23,7 @@ abstract class Event { /** * Determines if this event can be [cancelled]. This does not have to return a constant value. */ - abstract val cancellable: Boolean + protected abstract val cancellable: Boolean /** * Sets [cancelled] to true. diff --git a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt index 6b9b0bf..5b278e3 100644 --- a/src/main/kotlin/me/bush/illnamethislater/EventBus.kt +++ b/src/main/kotlin/me/bush/illnamethislater/EventBus.kt @@ -3,31 +3,32 @@ package me.bush.illnamethislater import kotlin.reflect.KClass /** - * A simple event dispatcher - * - * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) + * [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 = mutableSetOf() + private val subscribers = hashMapOf>() /** - * doc + * 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. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun subscribe(subscriber: Any): Boolean { return if (subscriber in subscribers) false else runCatching { - subscriber::class.listeners.forEach { member -> - register(member.handleCall(subscriber).also { - it.subscriber = subscriber - }) - } - subscribers.add(subscriber) + // 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() true }.getOrElse { config.logger.error("Unable to register listeners for subscriber $subscriber", it) @@ -36,40 +37,64 @@ class EventBus(private val config: Config = Config()) { } /** - * doc + * Unregisters all listeners belonging to [subscriber]. + * + * This will not remove top level listeners, use [unregister] instead. + * + * Returns `true` if [subscriber] was subscribed, `false` otherwise. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ fun unsubscribe(subscriber: Any): Boolean { - return subscribers.remove(subscriber).also { contains -> - if (contains) listeners.entries.removeIf { - it.value.unsubscribe(subscriber) - it.value.sequential.isEmpty() && it.value.parallel.isEmpty() - } + 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 } + return contained } /** - * 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. + * Registers a [Listener] to this [EventBus]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ - fun register(listener: Listener) = listener.also { - listeners.computeIfAbsent(it.type) { type -> ListenerGroup(type, config) }.register(it) + fun register(listener: Listener): Listener { + listeners.computeIfAbsent(listener.type) { + ListenerGroup(it, config) + }.register(listener) + return listener } /** - * doc + * Unregisters a [Listener] from this [EventBus]. * * [Information and examples](https://github.com/therealbush/eventbus-kotlin#tododothething) */ - fun unregister(listener: Listener) = listener.also { - listeners[it.type]?.unregister(it) + 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. doc + * 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) */ @@ -79,13 +104,15 @@ class EventBus(private val config: Config = Config()) { * 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, 4 parallel - * BushIsSoCool: 4, 2 - * OtherEvent: 3, 1 - * String: 1, 1 + * Listeners: 8 sequential, 21 parallel + * BushIsSoCool: 4, 9 + * OtherEvent: 1, 10 + * String: 3, 0 */ fun debugInfo() { config.logger.info("Subscribers: ${subscribers.size}") diff --git a/src/main/kotlin/me/bush/illnamethislater/Listener.kt b/src/main/kotlin/me/bush/illnamethislater/Listener.kt index 7105baf..b2bb2ce 100644 --- a/src/main/kotlin/me/bush/illnamethislater/Listener.kt +++ b/src/main/kotlin/me/bush/illnamethislater/Listener.kt @@ -1,5 +1,6 @@ package me.bush.illnamethislater +import java.util.function.Consumer import kotlin.reflect.KClass /** @@ -26,16 +27,15 @@ class Listener @PublishedApi internal constructor( } /** - * Creates a listener that can be held in a variable, returned from - * a function or getter, or directly registered to an Eventbus. + * 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 type of event to listen for. Inheritance has no effect here. - * If [T] is a base class, subclass events will not be received. - * @param priority The priority of this listener. This can be any integer. - * Listeners with a higher [priority] will be invoked first. - * @param parallel If a listener should be invoked in parallel with other [parallel] listeners, or sequentially. todo finish parallel + * @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. */ @@ -45,3 +45,29 @@ inline fun listener( 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({ event: T -> listener.accept(event) }, 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 7b2a039..fdcc7bc 100644 --- a/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt +++ b/src/main/kotlin/me/bush/illnamethislater/ListenerGroup.kt @@ -32,19 +32,11 @@ internal class ListenerGroup( /** * Removes [listener] from this [ListenerGroup]. */ - fun unregister(listener: Listener) { - if (listener.parallel) parallel.remove(listener) + fun unregister(listener: Listener): Boolean { + return if (listener.parallel) parallel.remove(listener) else sequential.remove(listener) } - /** - * Removes every listener whose subscriber is [subscriber]. - */ - fun unsubscribe(subscriber: Any) { - parallel.removeIf { it.subscriber == subscriber } - sequential.removeIf { it.subscriber == subscriber } - } - /** * Posts an event to every listener. Returns true of the event was cancelled. */ -- cgit