Skip to content
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
<!-- NOTE: The overview, basic example, and note on nullability are also included in
`src/main/java/module-info.java` -->

# EventBus

A flexible, high-performance, thread-safe subscriber-publisher framework designed with modern Java in mind.
Expand All @@ -14,13 +17,13 @@ First, add the Forge Maven repository and the EventBus dependency to your projec
```gradle
repositories {
maven {
name = "Forge"
url = "https://maven.minecraftforge.net"
name = 'Forge'
url = 'https://maven.minecraftforge.net'
}
}

dependencies {
implementation "net.minecraftforge:eventbus:<version>"
implementation 'net.minecraftforge:eventbus:<version>'
}
```

Expand All @@ -47,11 +50,11 @@ Browse the `net.minecraftforge.eventbus.api` package and read the Javadocs for m
examples, check out Forge's extensive use of EventBus [here][Forge usages].

## Nullability
The entirety of EventBus' API is `@NullMarked` and compliant with the [jSpecify specification](https://jspecify.dev/) -
this means that everything is non-null by default unless otherwise specified.
The entirety of EventBus' API is `@NullMarked` and compliant with the [jSpecify specification](https://jspecify.dev/).
This means that everything is non-null by default unless otherwise specified.

Attempting to pass a `null` value to a method param that isn't explicitly marked as `@Nullable` is an unsupported
operation and won't be considered a breaking change if a future version throws an exception in such cases when it didn't
Attempting to pass a `null` value to a method param that isn't explicitly marked as `@Nullable` is an *unsupported
operation* and won't be considered a breaking change if a future version throws an exception in such cases when it didn't
before.

## Contributing
Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ plugins {
id 'org.gradlex.extra-java-module-info' version '1.11'
id 'net.minecraftforge.gradleutils' version '2.4.13'
id 'net.minecraftforge.licenser' version '1.1.1'
alias libs.plugins.javadoc.links

// Enforce jSpecify annotations at compile-time
id 'net.ltgt.errorprone' version '4.1.0'
Expand All @@ -22,6 +23,7 @@ java {
toolchain.languageVersion = JavaLanguageVersion.of(21)
modularity.inferModulePath = true
withSourcesJar()
withJavadocJar()
}

repositories {
Expand All @@ -31,6 +33,7 @@ repositories {

dependencies {
api libs.jspecify.annotations
compileOnly libs.jetbrains.annotations
errorprone libs.errorprone.core
errorprone libs.nullaway
}
Expand All @@ -43,6 +46,13 @@ changelog {
from '1.0.0'
}

tasks.withType(Javadoc).configureEach {
options { StandardJavadocDocletOptions options ->
options.windowTitle = 'EventBus ' + project.version
options.tags 'apiNote:a:API Note:', 'implNote:a:Implementation Note:', 'implSpec:a:Implementation Specification:'
}
}

tasks.withType(JavaCompile).configureEach {
// Set up compile-time enforcement of the jSpecify spec via ErrorProne and NullAway
options.errorprone { ErrorProneOptions errorProne ->
Expand Down
4 changes: 4 additions & 0 deletions eventbus-jmh/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ extraJavaModuleInfo {
automaticModule('net.sf.jopt-simple:jopt-simple', 'jopt.simple')
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

tasks.register('aggregateJmh', AggregateJmh) {
if (rootProject.file('jmh_data_input.json').exists())
inputData = rootProject.file('jmh_data_input.json')
Expand Down
4 changes: 4 additions & 0 deletions eventbus-test-jar/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ license {
newLine = false
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

// Hack eclipse into knowing that the gradle deps are modules
eclipse.classpath {
containers 'org.eclipse.buildship.core.gradleclasspathcontainer'
Expand Down
4 changes: 4 additions & 0 deletions eventbus-test/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ extraJavaModuleInfo {
failOnMissingModuleInfo = false
}

tasks.named('javadoc', Javadoc) {
enabled = false
}

tasks.named('test', Test) {
useJUnitPlatform()
}
Expand Down
3 changes: 3 additions & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ plugins {
dependencyResolutionManagement {
versionCatalogs {
libs {
plugin 'javadoc-links', 'io.freefair.javadoc-links' version '8.13.1'

// https://mvnrepository.com/artifact/org.jspecify/jspecify
library('jspecify-annotations', 'org.jspecify', 'jspecify') version '1.0.0'
library 'jetbrains-annotations', 'org.jetbrains', 'annotations' version '26.0.2'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing mvnrepository link comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please approve #73 first.


// https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core
library('errorprone-core', 'com.google.errorprone', 'error_prone_core') version '2.36.0'
Expand Down
47 changes: 45 additions & 2 deletions src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,53 @@
*/
import org.jspecify.annotations.NullMarked;

/**
* EventBus is a flexible, high-performance, thread-safe subscriber-publisher framework designed with modern Java in
* mind.
*
* <h2>Overview</h2>
* <p>The core functionality of EventBus is to provide a simple and efficient way to handle
* {@linkplain net.minecraftforge.eventbus.api.event events} in a decoupled manner.</p>
* <p>Each event may have one or more {@linkplain net.minecraftforge.eventbus.api.bus.EventBus buses} associated with
* it, which are responsible for managing {@linkplain net.minecraftforge.eventbus.api.listener.EventListener listeners}
* and dispatching instances of the event object to them. To maximise performance, the underlying implementation is
* tailored on the fly based on the event's type,
* {@linkplain net.minecraftforge.eventbus.api.event.characteristic characteristics}, inheritance chain and the number
* and type of listeners registered to the bus.</p>
*
* <h2>Example</h2>
* <p>Here is a basic usage example of EventBus in action:</p>
* {@snippet :
* import net.minecraftforge.eventbus.api.event.RecordEvent;
* import net.minecraftforge.eventbus.api.bus.EventBus;
*
* // Define an event and a bus for it
* record PlayerLoggedInEvent(String username) implements RecordEvent {
* public static final EventBus<PlayerLoggedInEvent> BUS = EventBus.create(PlayerLoggedInEvent.class);
* }
*
* // Register an event listener
* PlayerLoggedInEvent.BUS.addListener(event -> System.out.println("Player logged in: " + event.username()));
*
* // Post an event to the registered listeners
* PlayerLoggedInEvent.BUS.post(new PlayerLoggedInEvent("Paint_Ninja"));
*}
* <p>There are several more example usages within the JavaDocs of the different packages and classes in this API
* module. These examples are non-exhaustive, but provide a good basis on which to build your usage of EventBus.</p>
*
* <h2>Nullability</h2>
* <p>The entirety of EventBus' API is {@link org.jspecify.annotations.NullMarked @NullMarked} and compliant with the
* <a href="https://jspecify.dev/">jSpecify specification</a>. This means that everything is
* {@linkplain org.jspecify.annotations.NonNull non-null} by default unless otherwise specified.</p>
* <p>Attempting to pass a {@code null} value to a method param that isn't explicitly marked as
* {@link org.jspecify.annotations.Nullable @Nullable} is an <i>unsupported operation</i> and won't be considered a
* breaking change if a future version throws an exception in such cases when it didn't before.</p>
*/
@NullMarked
module net.minecraftforge.eventbus {
requires java.logging;
requires org.jspecify;
requires java.logging; // Logging
requires org.jspecify; // Nullability
requires static org.jetbrains.annotations; // Other Static Analysis

exports net.minecraftforge.eventbus.api.bus;
exports net.minecraftforge.eventbus.api.event;
Expand Down
146 changes: 126 additions & 20 deletions src/main/java/net/minecraftforge/eventbus/api/bus/BusGroup.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,89 +4,195 @@
*/
package net.minecraftforge.eventbus.api.bus;

import net.minecraftforge.eventbus.internal.Event;
import net.minecraftforge.eventbus.api.listener.EventListener;
import net.minecraftforge.eventbus.api.listener.SubscribeEvent;
import net.minecraftforge.eventbus.internal.BusGroupImpl;
import net.minecraftforge.eventbus.internal.Event;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;

import java.lang.invoke.MethodHandles;
import java.util.Collection;

/**
* A collection of {@link EventBus} instances that are grouped together for easier management.
* A bus group is a collection of {@link EventBus} instances that are grouped together for easier management.
* <p>Using a bus group allows consumers to manage all of their related event buses without needing to manually manage
* each one.</p>
*
* <h2>Example</h2>
* <p>Here is a small example showing the creation and disposal of a bus group.</p>
* {@snippet :
* import net.minecraftforge.eventbus.api.bus.BusGroup;
* import net.minecraftforge.eventbus.api.bus.EventBus;
* import net.minecraftforge.eventbus.api.event.RecordEvent;
* import net.minecraftforge.eventbus.api.listener.SubscribeEvent;
*
* import java.lang.invoke.MethodHandles;
*
* public class MyClass {
* public static final BusGroup BUS_GROUP = BusGroup.create("MyProject", RecordEvent.class);
*
* public record MyEvent(String message) implements RecordEvent {
* public static final EventBus<MyEvent> BUS = EventBus.create(BUS_GROUP, MyEvent.class);
* }
*
* @SubscribeEvent
* private static void onMyEvent(MyEvent event) {
* System.out.println("Received event: " + event.message());
* }
*
* // if we only have one listener in our class, EventBus will throw an exception saying you should use BusGroup#addListener instead
* @SubscribeEvent
* private static void alsoOnMyEvent(MyEvent event) {
* System.out.println("Double checking, received event: " + event.message());
* }
*
* // begin program!
* public static void run() {
* // the bus group is already started! no need to call startup() on it.
*
* // MethodHandles.lookup() gives EventBus the ability to get method references
* // for all the @SubscribeEvent methods in this class.
* BUS_GROUP.register(MethodHandles.lookup(), MyClass.class);
* }
*
* // close program!
* public static void shutdown() {
* // dispose will shutdown and then dispose this bus group
* // consider it "freed memory" that should not be reused
* BUS_GROUP.dispose();
* }
* }
*}
*/
public sealed interface BusGroup permits BusGroupImpl {
/**
* The default bus group, which is used when an {@linkplain EventBus event bus} is created without specifying a
* group.
*
* @apiNote If you require tight controls over your event buses, you should create your own bus group instead. This
* bus group can be used and mutated by other consumers within the same environment.
* @see EventBus#create(Class)
*/
BusGroup DEFAULT = create("default");

/**
* Creates a new bus group with the given name.
* <p>The name for this bus group <i>must be unique.</i> An attempt to create a bus group with a name that is
* already in use will result in an {@link IllegalArgumentException}. If you must create a new bus group with a name
* that is in use, the relevant bus group must be {@linkplain #dispose() disposed}.</p>
*
* @param name The name
* @return The new bus group
* @throws IllegalArgumentException If the name is already in use by another bus group
* @apiNote To enforce a base type with your bus group, use {@linkplain #create(String, Class)}.
*/
static BusGroup create(String name) {
return new BusGroupImpl(name, Event.class);
}

/**
* Creates a new bus group with the given name.
* <p>The given base type will enforce that all {@linkplain EventBus event buses} created within this group inherit
* it.</p>
* <p>The name for this bus group <i>must be unique.</i> An attempt to create a bus group with a name that is
* already in use will result in an {@link IllegalArgumentException}. If you must create a new bus group with a name
* that is in use, the relevant bus group must be {@linkplain #dispose() disposed}.</p>
*
* @param name The name
* @return The new bus group
* @throws IllegalArgumentException If the name is already in use by another bus group
*/
static BusGroup create(String name, Class<?> baseType) {
return new BusGroupImpl(name, baseType);
}

/**
* The unique name of this BusGroup.
* <p>The uniqueness of this name is enforced when the bus group is {@linkplain #create(String) created}.</p>
*/
@Contract(pure = true)
String name();

/**
* Starts up all EventBus instances associated with this BusGroup, allowing events to be posted again after a
* Starts up all EventBus instances associated with this bus group, allowing events to be posted again after a
* previous call to {@link #shutdown()}.
* <p>Calling this method without having previously called {@link #shutdown()} will have no effect.</p>
*/
void startup();

/**
* Shuts down all EventBus instances associated with this BusGroup, preventing any further events from being posted
* Shuts down all EventBus instances associated with this bus group, preventing any further events from being posted
* until {@link #startup()} is called.
* <p>Calling this method without having previously called {@link #startup()} will have no effect.</p>
*
* @apiNote If you need to destroy this bus group and free up the resources it uses, use {@link #dispose()}.
*/
void shutdown();

/**
* Shuts down all EventBus instances associated with this BusGroup, unregisters all listeners and frees resources
* no longer needed.
* <p>Warning: This is a destructive operation - this BusGroup should not be used again after calling this method.</p>
* {@linkplain #shutdown() Shuts down} all EventBus instances associated with this bus group,
* {@linkplain #unregister(Collection) unregisters} all listeners and frees resources no longer needed.
* <p><strong>This will effectively destroy this bus group.</strong> It should not be used again after calling this
* method.</p>
*
* @apiNote If you plan on using this bus group again, use {@link #shutdown()} instead.
*/
void dispose();

/**
* Experimental feature - may be removed, renamed or otherwise changed without notice.
* <p>Trims the backing lists of all EventBus instances associated with this BusGroup to free up resources.</p>
* <p>Warning: This is only intended to be called <b>once</b> after all listeners are registered - calling this
* Trims the backing lists of all EventBus instances associated with this BusGroup to free up resources.
* <p>This is only intended to be called <strong>once</strong> after all listeners are registered. Calling this
* repeatedly may hurt performance.</p>
*
* @apiNote <strong>This is an experimental feature!</strong> It may be removed, renamed or otherwise changed
* without notice.
*/
@ApiStatus.Experimental
void trim();

/**
* Registers all static methods annotated with {@link SubscribeEvent} in the given class.
* Registers all <i>static</i> methods annotated with {@link SubscribeEvent} in the given class.
* <p>This is done by getting method references for those methods using the given
* {@linkplain MethodHandles.Lookup method handles lookup}. This lookup <strong>must be acquiored from
* {@link MethodHandles#lookup()}.</strong> Using {@link MethodHandles#publicLookup()} is unsupported because it
* doesn't work with {@link java.lang.invoke.LambdaMetafactory} as it could allow for access to private fields
* through inner class generation.</p>
*
* @param callerLookup {@code MethodHandles.lookup()} from the class containing listeners
* @param callerLookup {@link MethodHandles#lookup()} from the class containing listeners
* @param utilityClassWithStaticListeners the class containing the static listeners
* @return A collection of the registered listeners, which can be used to optionally unregister them later
*
* @apiNote This method only registers static listeners.
* <p>If you want to register both instance and static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Object)} instead.</p>
* <p>If you want to register both instance and static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Object)} instead.</p>
*/
Collection<EventListener> register(MethodHandles.Lookup callerLookup, Class<?> utilityClassWithStaticListeners);

/**
* Registers all methods annotated with {@link SubscribeEvent} in the given object.
* <p>Both the static <i>and</i> instance methods for the given object are registered. Keep in mind that, unlike
* with {@link #register(MethodHandles.Lookup, Class)}, you will need to register each object instance of the class
* using this method.</p>
* <p>This is done by getting method references for those methods using the given
* {@linkplain MethodHandles.Lookup method handles lookup}. This lookup <strong>must be acquiored from
* {@link MethodHandles#lookup()}.</strong> Using {@link MethodHandles#publicLookup()} is unsupported because it
* doesn't work with {@link java.lang.invoke.LambdaMetafactory} as it could allow for access to private fields
* through inner class generation.</p>
*
* @param callerLookup {@code MethodHandles.lookup()} from the class containing the listeners
* @param listener the object containing the static and/or instance listeners
* @param listener the object containing the static and/or instance listeners
* @return A collection of the registered listeners, which can be used to optionally unregister them later
*
* @apiNote If you know all the listeners are static methods, use
* {@link BusGroup#register(MethodHandles.Lookup, Class)} instead for better registration performance.
* {@link BusGroup#register(MethodHandles.Lookup, Class)} instead for better registration performance.
*/
Collection<EventListener> register(MethodHandles.Lookup callerLookup, Object listener);

/**
* Unregisters the given listeners from this BusGroup.
* Unregisters the given listeners from this bus group.
*
* @param listeners A collection of listeners to unregister, obtained from
* {@link #register(MethodHandles.Lookup, Class)} or {@link #register(MethodHandles.Lookup, Object)}
* {@link #register(MethodHandles.Lookup, Class)} or
* {@link #register(MethodHandles.Lookup, Object)}
*/
void unregister(Collection<EventListener> listeners);
}
Loading