Skip to content

Commit

Permalink
Start work on filtered events.
Browse files Browse the repository at this point in the history
  • Loading branch information
LambdAurora committed Nov 11, 2024
1 parent 9af7a0f commit 6bcb821
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 72 deletions.
2 changes: 1 addition & 1 deletion build_logic/src/main/kotlin/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ data class Developer(val name: String, val email: String)

object Constants {
const val GROUP = "dev.yumi.commons"
const val VERSION = "1.0.0-alpha.1"
const val VERSION = "1.0.0-alpha.2"
const val JAVA_VERSION = 17

const val PROJECT_NAME = "Yumi Commons"
Expand Down
82 changes: 58 additions & 24 deletions libraries/event/src/main/java/dev/yumi/commons/event/Event.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
package dev.yumi.commons.event;

import dev.yumi.commons.collections.toposort.NodeSorting;
import dev.yumi.commons.collections.toposort.SortableNode;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;

Expand All @@ -52,7 +54,7 @@
* of implementing an invoker and only allows listener implementations to be done by implementing an interface onto a
* class or extending a class.
* <p>
* An Event can have phases, each listener is attributed to a phase ({@link Event#getDefaultPhaseId()} if unspecified),
* An Event can have phases, each listener is attributed to a phase ({@link Event#defaultPhaseId()} if unspecified),
* and each phase can have a defined ordering. Each event phase is identified, ordering is done
* by explicitly stating that event phase {@code A} will run before event phase {@code B}, for example.
* See {@link Event#addPhaseOrdering(Comparable, Comparable)} for more information.
Expand Down Expand Up @@ -114,11 +116,13 @@
* }</pre>
*
* @param <I> the phase identifier type
* @param <T> the type of the invoker used to execute an event and the type of the listener
* @param <T> the type of the invoker used to execute an event and the type of the listeners
* @version 1.0.0
* @since 1.0.0
*/
public final class Event<I extends Comparable<? super I>, T> {
public sealed class Event<I extends Comparable<? super I>, T>
implements InvokableEvent<T>
permits FilteredEvent {
/**
* The type of listener of this event.
*/
Expand All @@ -130,24 +134,24 @@ public final class Event<I extends Comparable<? super I>, T> {
/**
* The function used to generate the implementation of the invoker to call the listeners.
*/
private final Function<T[], T> invokerFactory;
private final Lock lock = new ReentrantLock();
final Function<T[], T> invokerFactory;
final Lock lock = new ReentrantLock();
/**
* The invoker to execute the callbacks.
*/
private volatile T invoker;
/**
* The registered listeners.
*/
private T[] listeners;
T[] listeners;
/**
* The registered event phases.
*/
private final Map<I, EventPhaseData<I, T>> phases = new LinkedHashMap<>();
final Map<I, PhaseData<I, T>> phases = new LinkedHashMap<>();
/**
* The event phases sorted in a way that satisfies dependencies.
*/
private final List<EventPhaseData<I, T>> sortedPhases = new ArrayList<>();
final List<PhaseData<I, T>> sortedPhases = new ArrayList<>();

@SuppressWarnings("unchecked")
Event(
Expand All @@ -170,15 +174,15 @@ public final class Event<I extends Comparable<? super I>, T> {
* {@return the class of the kind of listeners accepted by this event}
*/
@Contract(pure = true)
public @NotNull Class<? super T> getType() {
public @NotNull Class<? super T> type() {
return this.type;
}

/**
* {@return the default phase identifier of this event}
*/
@Contract(pure = true)
public @NotNull I getDefaultPhaseId() {
public @NotNull I defaultPhaseId() {
return this.defaultPhaseId;
}

Expand Down Expand Up @@ -235,44 +239,42 @@ public void addPhaseOrdering(@NotNull I firstPhase, @NotNull I secondPhase) {
var first = this.getOrCreatePhase(firstPhase, false);
var second = this.getOrCreatePhase(secondPhase, false);

EventPhaseData.link(first, second);
NodeSorting.sort(this.sortedPhases, "event phases");
PhaseData.link(first, second);
this.sortPhases();
this.rebuildInvoker(this.listeners.length);
} finally {
this.lock.unlock();
}
}

/**
* {@return the invoker instance used to execute this event}
* <p>
* The result of this method should not be stored since the invoker may become invalid
* at any time. Always call this method when you intend to execute an event.
*/
@Contract(pure = true)
@Override
public @NotNull T invoker() {
return this.invoker;
}

/* Implementation */

private EventPhaseData<I, T> getOrCreatePhase(I id, boolean sortIfCreate) {
PhaseData<I, T> getOrCreatePhase(@NotNull I id, boolean sortIfCreate) {
var phase = this.phases.get(id);

if (phase == null) {
phase = new EventPhaseData<>(id, this.type);
phase = new PhaseData<>(id, this.type);
this.phases.put(id, phase);
this.sortedPhases.add(phase);

if (sortIfCreate) {
NodeSorting.sort(this.sortedPhases, "event phases");
this.sortPhases();
}
}

return phase;
}

private void rebuildInvoker(int newLength) {
void sortPhases() {
NodeSorting.sort(this.sortedPhases, "event phases");
}

void rebuildInvoker(int newLength) {
if (this.sortedPhases.size() == 1) {
// There's a single phase, so we can directly use its listeners.
this.listeners = this.sortedPhases.get(0).listeners;
Expand All @@ -297,7 +299,7 @@ private void rebuildInvoker(int newLength) {
this.update();
}

private void update() {
void update() {
// Make a copy of the array given to the invoker factory so the entries cannot be mutated.
this.invoker = this.invokerFactory.apply(
Arrays.copyOf(this.listeners, this.listeners.length)
Expand All @@ -315,4 +317,36 @@ public String toString() {
", sortedPhases=" + this.sortedPhases +
'}';
}

/**
* Represents data for a specific event phase.
*
* @param <I> the phase identifier type
* @param <T> the type of the listeners
*/
@ApiStatus.Internal
static sealed class PhaseData<I, T> extends SortableNode<I, PhaseData<I, T>>
permits FilteredEvent.FilteredPhaseData {
private final I id;
T[] listeners;

@SuppressWarnings("unchecked")
PhaseData(@NotNull I id, @NotNull Class<? super T> listenerType) {
Objects.requireNonNull(id);

this.id = id;
this.listeners = (T[]) Array.newInstance(listenerType, 0);
}

@Override
public @NotNull I getId() {
return this.id;
}

void addListener(@NotNull T listener) {
int oldLength = this.listeners.length;
this.listeners = Arrays.copyOf(this.listeners, oldLength + 1);
this.listeners[oldLength] = listener;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,16 @@ public EventManager(@NotNull I defaultPhaseId, @NotNull Function<String, I> phas
return event;
}

public <T, C> @NotNull FilteredEvent<I, T, C> createFiltered(@NotNull Class<? super T> type) {
return this.createFiltered(type, new DefaultInvokerFactory<>(type));
}

public <T, C> @NotNull FilteredEvent<I, T, C> createFiltered(@NotNull Class<? super T> type, @NotNull Function<T[], T> implementation) {
var event = new FilteredEvent<I, T, C>(type, this.defaultPhaseId, implementation);
this.creationEvent.invoker().onEventCreation(this, event);
return event;
}

/**
* Registers the listener to the specified events.
* <p>
Expand All @@ -296,20 +306,20 @@ public final void listenAll(Object listener, Event<I, ?>... events) {

// Check whether we actually can register stuff. We only commit the registration if all events can.
for (var event : events) {
if (!event.getType().isAssignableFrom(listener.getClass())) {
if (!event.type().isAssignableFrom(listener.getClass())) {
throw new IllegalArgumentException("Given object " + listener + " is not a listener of event " + event);
}

if (event.getType().getTypeParameters().length > 0) {
if (event.type().getTypeParameters().length > 0) {
throw new IllegalArgumentException("Cannot register a listener for the event " + event + " which is using generic parameters with listenAll.");
}

listenedPhases.putIfAbsent(event.getType(), this.defaultPhaseId);
listenedPhases.putIfAbsent(event.type(), this.defaultPhaseId);
}

// We can register, so we do!
for (var event : events) {
((Event) event).register(listenedPhases.get(event.getType()), listener);
((Event) event).register(listenedPhases.get(event.type()), listener);
}
}

Expand Down

This file was deleted.

Loading

0 comments on commit 6bcb821

Please sign in to comment.