From be6c1fefbf3e9f1a570a5031168dbd9eed94152a Mon Sep 17 00:00:00 2001 From: javasabr Date: Sat, 30 May 2026 17:03:15 +0200 Subject: [PATCH 1/5] work on eventbus implementation --- rlib-eventbus/build.gradle | 8 + .../java/javasabr/rlib/eventbus/EventBus.java | 33 +++ .../rlib/eventbus/EventBusFactory.java | 17 ++ .../rlib/eventbus/impl/DefaultEventBus.java | 87 ++++++ .../eventbus/impl/DefaultTypeIdFactory.java | 52 ++++ .../rlib/eventbus/impl/package-info.java | 4 + .../javasabr/rlib/eventbus/package-info.java | 4 + .../rlib/eventbus/DefaultEventBusTest.java | 277 ++++++++++++++++++ settings.gradle | 3 +- 9 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 rlib-eventbus/build.gradle create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/package-info.java create mode 100644 rlib-eventbus/src/main/java/javasabr/rlib/eventbus/package-info.java create mode 100644 rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java diff --git a/rlib-eventbus/build.gradle b/rlib-eventbus/build.gradle new file mode 100644 index 00000000..81ce893e --- /dev/null +++ b/rlib-eventbus/build.gradle @@ -0,0 +1,8 @@ +plugins { + id("configure-java") + id("configure-publishing") +} + +dependencies { + api projects.rlibCollections +} diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java new file mode 100644 index 00000000..15dc6801 --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java @@ -0,0 +1,33 @@ +package javasabr.rlib.eventbus; + +import java.util.function.Consumer; + +public interface EventBus { + + > void registerConsumer(TypeId typeId, Consumer consumer); + + > void unregisterConsumer(TypeId typeId, Consumer consumer); + + void sendSync(Event event); + + void sendAsync(Event event); + + interface TypeIdSet { + } + + interface TypeIdFactory { + + > TypeId createFor(Class eventType); + } + + interface TypeId> { + + Class eventType(); + int id(); + } + + interface Event { + + TypeId> typeId(); + } +} diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java new file mode 100644 index 00000000..cb40e359 --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java @@ -0,0 +1,17 @@ +package javasabr.rlib.eventbus; + +import javasabr.rlib.eventbus.EventBus.TypeIdFactory; +import javasabr.rlib.eventbus.EventBus.TypeIdSet; +import javasabr.rlib.eventbus.impl.DefaultEventBus; +import javasabr.rlib.eventbus.impl.DefaultTypeIdFactory; + +public class EventBusFactory { + + public static TypeIdFactory createTypeIdFactory(Class typeIdSetType) { + return DefaultTypeIdFactory.INSTANCE.typedTypeIdFactory(typeIdSetType); + } + + public static EventBus eventBus(@SuppressWarnings("unused") TypeIdFactory typeIdFactory) { + return new DefaultEventBus<>(); + } +} diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java new file mode 100644 index 00000000..5112ff4d --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java @@ -0,0 +1,87 @@ +package javasabr.rlib.eventbus.impl; + +import java.util.concurrent.Executor; +import java.util.concurrent.ForkJoinPool; +import java.util.function.Consumer; +import javasabr.rlib.collections.array.ArrayFactory; +import javasabr.rlib.collections.array.MutableArray; +import javasabr.rlib.collections.array.UnsafeMutableArray; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import javasabr.rlib.collections.dictionary.MutableRefToRefDictionary; +import javasabr.rlib.eventbus.EventBus; +import lombok.AccessLevel; +import lombok.experimental.FieldDefaults; + +@FieldDefaults(level = AccessLevel.PRIVATE) +public class DefaultEventBus implements EventBus { + + final MutableRefToRefDictionary>, TypeId>> knownEventTypes; + final UnsafeMutableArray>> consumers; + final Executor asyncExecutor; + + public DefaultEventBus() { + this(ForkJoinPool.commonPool()); + } + + public DefaultEventBus(Executor asyncExecutor) { + MutableArray>> consumers = ArrayFactory.copyOnModifyArray(MutableArray.class); + this.consumers = consumers.asUnsafe(); + this.knownEventTypes = DictionaryFactory.mutableRefToRefDictionary(); + this.asyncExecutor = asyncExecutor; + } + + @Override + public > void registerConsumer(TypeId typeId, Consumer consumer) { + registerTypeId(typeId); + consumers.unsafeGet(typeId.id()).add(consumer); + } + + @Override + public > void unregisterConsumer(TypeId typeId, Consumer consumer) { + if (typeId.id() < 0) { + throw new IllegalArgumentException("Unexpected typeId:[%s]".formatted(typeId)); + } + if (consumers.size() > typeId.id()) { + consumers.unsafeGet(typeId.id()).remove(consumer); + } + } + + @Override + public void sendSync(Event event) { + UnsafeMutableArray> eventConsumers; + try { + eventConsumers = consumers.unsafeGet(event.typeId().id()); + } catch (ArrayIndexOutOfBoundsException e) { + registerTypeId(event.typeId()); + sendSync(event); + return; + } + //noinspection unchecked we control it during registering + for (var consumer : (Consumer>[]) eventConsumers.wrapped()) { + consumer.accept(event); + } + } + + @Override + public void sendAsync(Event event) { + asyncExecutor.execute(() -> sendSync(event)); + } + + private synchronized void registerTypeId(TypeId> typeId) { + if (typeId.id() < 0) { + throw new IllegalArgumentException("Unexpected typeId:[%s]".formatted(typeId)); + } + Class> eventType = typeId.eventType(); + TypeId> alreadyRegistered = knownEventTypes.get(eventType); + if (alreadyRegistered != null && alreadyRegistered != typeId) { + throw new IllegalStateException( + "EventType:[%s] is already registered with typeId:[%s], but now received typeId:[%s]".formatted(eventType, alreadyRegistered, typeId)); + } + // we need synchronize only on write because read is fully thread safe + while (typeId.id() >= consumers.size()) { + MutableArray> eventConsumers = ArrayFactory.copyOnModifyArray(Consumer.class); + consumers.add(eventConsumers.asUnsafe()); + } + knownEventTypes.put(eventType, typeId); + } +} diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java new file mode 100644 index 00000000..dc13696c --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java @@ -0,0 +1,52 @@ +package javasabr.rlib.eventbus.impl; + +import java.util.concurrent.atomic.AtomicInteger; +import javasabr.rlib.collections.dictionary.DictionaryFactory; +import javasabr.rlib.collections.dictionary.MutableRefToRefDictionary; +import javasabr.rlib.eventbus.EventBus.Event; +import javasabr.rlib.eventbus.EventBus.TypeId; +import javasabr.rlib.eventbus.EventBus.TypeIdFactory; +import javasabr.rlib.eventbus.EventBus.TypeIdSet; + +public class DefaultTypeIdFactory { + + public static final DefaultTypeIdFactory INSTANCE = new DefaultTypeIdFactory(); + + MutableRefToRefDictionary, TypedTypeIdFactory> knownFactories; + + private DefaultTypeIdFactory() { + this.knownFactories = DictionaryFactory.mutableRefToRefDictionary(); + } + + public synchronized TypeIdFactory typedTypeIdFactory(Class typeIdSetType) { + //noinspection unchecked + return (TypeIdFactory) knownFactories.getOrCompute(typeIdSetType, TypedTypeIdFactory::new); + } + + private static class TypedTypeIdFactory implements TypeIdFactory { + + MutableRefToRefDictionary, TypeIdImpl> knownTypes; + AtomicInteger typeIdFactory; + + private TypedTypeIdFactory() { + this.knownTypes = DictionaryFactory.mutableRefToRefDictionary(); + this.typeIdFactory = new AtomicInteger(0); + } + + @Override + public synchronized > TypeId createFor(Class eventType) { + TypeIdImpl exist = knownTypes.get(eventType); + if (exist != null) { + //noinspection unchecked it's checked during creation + return (TypeId) exist; + } + var newTypeId = new TypeIdImpl<>(eventType, typeIdFactory.incrementAndGet()); + knownTypes.put(eventType, newTypeId); + return newTypeId; + } + } + + private record TypeIdImpl>( + Class eventType, + int id) implements TypeId {} +} diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/package-info.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/package-info.java new file mode 100644 index 00000000..3281ef41 --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.rlib.eventbus.impl; + +import org.jspecify.annotations.NullMarked; diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/package-info.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/package-info.java new file mode 100644 index 00000000..8af2dbc8 --- /dev/null +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package javasabr.rlib.eventbus; + +import org.jspecify.annotations.NullMarked; diff --git a/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java b/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java new file mode 100644 index 00000000..660d84d1 --- /dev/null +++ b/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java @@ -0,0 +1,277 @@ +package javasabr.rlib.eventbus; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import javasabr.rlib.collections.array.ArrayFactory; +import javasabr.rlib.collections.array.MutableArray; +import javasabr.rlib.eventbus.EventBus.TypeId; +import javasabr.rlib.eventbus.EventBus.TypeIdFactory; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class DefaultEventBusTest { + + public static class TestTypeIdSet implements EventBus.TypeIdSet { + } + public static class Test2TypeIdSet implements EventBus.TypeIdSet { + } + + static final TypeIdFactory typeIdFactory = EventBusFactory.createTypeIdFactory(TestTypeIdSet.class); + static final TypeIdFactory typeIdFactory2 = EventBusFactory.createTypeIdFactory(Test2TypeIdSet.class); + + public record EventA(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = typeIdFactory.createFor(EventA.class); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + public record EventB(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = typeIdFactory.createFor(EventB.class); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + public record EventC(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = typeIdFactory.createFor(EventC.class); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + public record EventD(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = typeIdFactory2.createFor(EventD.class); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + private record CustomTypeId>( + Class eventType, + int id) implements TypeId { + } + + public record EventWithNegativeTypeId(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = + new CustomTypeId<>(EventWithNegativeTypeId.class, -1); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + public record EventWithOutOfRangeTypeId(String value) implements EventBus.Event { + + static final TypeId TYPE_ID = + new CustomTypeId<>(EventWithOutOfRangeTypeId.class, 100); + + @Override + public TypeId typeId() { + return TYPE_ID; + } + } + + @Test + void shouldCorrectlyDeliverEvents() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + + MutableArray receivedEventsA1 = ArrayFactory.mutableArray(EventA.class); + MutableArray receivedEventsA2 = ArrayFactory.mutableArray(EventA.class); + MutableArray receivedEventsB1 = ArrayFactory.mutableArray(EventB.class); + MutableArray receivedEventsC1 = ArrayFactory.mutableArray(EventC.class); + MutableArray receivedEventsC2 = ArrayFactory.mutableArray(EventC.class); + MutableArray receivedEventsC3 = ArrayFactory.mutableArray(EventC.class); + + eventBus.registerConsumer(EventA.TYPE_ID, receivedEventsA1::add); + eventBus.registerConsumer(EventA.TYPE_ID, eventA -> { + receivedEventsA1.add(eventA); + receivedEventsA2.add(eventA); + }); + eventBus.registerConsumer(EventB.TYPE_ID, receivedEventsB1::add); + eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC1::add); + eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC2::add); + eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC3::add); + + // when: + eventBus.sendSync(new EventA("event_a_1")); + eventBus.sendSync(new EventA("event_a_2")); + eventBus.sendSync(new EventB("event_b_1")); + eventBus.sendSync(new EventC("event_c_1")); + eventBus.sendSync(new EventC("event_c_2")); + eventBus.sendSync(new EventC("event_c_3")); + + // then: + assertThat(receivedEventsA1).containsExactly( + new EventA("event_a_1"), + new EventA("event_a_1"), + new EventA("event_a_2"), + new EventA("event_a_2")); + assertThat(receivedEventsA2).containsExactly( + new EventA("event_a_1"), + new EventA("event_a_2")); + assertThat(receivedEventsB1).containsExactly( + new EventB("event_b_1")); + assertThat(receivedEventsC1).containsExactly( + new EventC("event_c_1"), + new EventC("event_c_2"), + new EventC("event_c_3")); + assertThat(receivedEventsC2).containsExactly( + new EventC("event_c_1"), + new EventC("event_c_2"), + new EventC("event_c_3")); + assertThat(receivedEventsC3).containsExactly( + new EventC("event_c_1"), + new EventC("event_c_2"), + new EventC("event_c_3")); + } + + @Test + void shouldThrowExceptionWhenSendingEventWithNegativeTypeId() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + + // when/then: + Assertions.assertThatThrownBy(() -> eventBus.sendSync(new EventWithNegativeTypeId("event"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unexpected typeId"); + } + + @Test + void shouldThrowExceptionWhenUnregisteringConsumerWithNegativeTypeId() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + + // when/then: + Assertions.assertThatThrownBy( + () -> eventBus.unregisterConsumer(EventWithNegativeTypeId.TYPE_ID, event -> {})) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unexpected typeId"); + } + + @Test + void shouldIgnoreUnregisterForOutOfRangeTypeId() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); + eventBus.registerConsumer(EventA.TYPE_ID, receivedEventsA::add); + + // when: + eventBus.unregisterConsumer(EventWithOutOfRangeTypeId.TYPE_ID, event -> {}); + eventBus.sendSync(new EventA("event_a_1")); + + // then: + assertThat(receivedEventsA).containsExactly(new EventA("event_a_1")); + } + + @Test + void shouldNotFailWhenSendingEventWithOutOfRangeTypeId() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + + // when/then: + Assertions.assertThatCode(() -> eventBus.sendSync(new EventWithOutOfRangeTypeId("event"))) + .doesNotThrowAnyException(); + } + + @Test + void shouldUnregisterConsumer() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); + Consumer consumer = receivedEventsA::add; + eventBus.registerConsumer(EventA.TYPE_ID, consumer); + + // when: + eventBus.unregisterConsumer(EventA.TYPE_ID, consumer); + eventBus.sendSync(new EventA("event_a_1")); + + // then: + assertThat(receivedEventsA).isEmpty(); + } + + @Test + void shouldIgnoreDuplicateUnregister() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + MutableArray firstConsumerEvents = ArrayFactory.mutableArray(EventA.class); + MutableArray secondConsumerEvents = ArrayFactory.mutableArray(EventA.class); + Consumer firstConsumer = firstConsumerEvents::add; + Consumer secondConsumer = secondConsumerEvents::add; + eventBus.registerConsumer(EventA.TYPE_ID, firstConsumer); + eventBus.registerConsumer(EventA.TYPE_ID, secondConsumer); + + // when: + eventBus.unregisterConsumer(EventA.TYPE_ID, firstConsumer); + eventBus.unregisterConsumer(EventA.TYPE_ID, firstConsumer); + eventBus.sendSync(new EventA("event_a_1")); + + // then: + assertThat(firstConsumerEvents).isEmpty(); + assertThat(secondConsumerEvents).containsExactly(new EventA("event_a_1")); + } + + @Test + void shouldThrowExceptionForConflictingTypeIdOfSameEventType() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + TypeId conflictingTypeId = new CustomTypeId<>(EventA.class, 1001); + eventBus.registerConsumer(EventA.TYPE_ID, event -> {}); + + // when/then: + Assertions.assertThatThrownBy(() -> eventBus.registerConsumer(conflictingTypeId, event -> {})) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("already registered"); + } + + @Test + void shouldDeliverEventsAsync() throws Exception { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); + CountDownLatch latch = new CountDownLatch(1); + eventBus.registerConsumer(EventA.TYPE_ID, event -> { + receivedEventsA.add(event); + latch.countDown(); + }); + + // when: + eventBus.sendAsync(new EventA("event_a_1")); + + // then: + assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); + assertThat(receivedEventsA).containsExactly(new EventA("event_a_1")); + } + + @Test + void shouldDeliverEventsForSparseTypeId() { + // given: + var eventBus = EventBusFactory.eventBus(typeIdFactory); + MutableArray receivedEvents = ArrayFactory.mutableArray(EventWithOutOfRangeTypeId.class); + eventBus.registerConsumer(EventWithOutOfRangeTypeId.TYPE_ID, receivedEvents::add); + + // when: + eventBus.sendSync(new EventWithOutOfRangeTypeId("event")); + + // then: + assertThat(receivedEvents).containsExactly(new EventWithOutOfRangeTypeId("event")); + } +} diff --git a/settings.gradle b/settings.gradle index 1a97b9d7..b5affd2b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,4 +20,5 @@ include ':rlib-functions' include ':rlib-reusable' include ':rlib-reference' include ':rlib-concurrent' -include ':test-coverage' \ No newline at end of file +include ':rlib-eventbus' +include ':test-coverage' From 3a28bd2c93ff9f94c2d8c620cfb1ee6dc4856ec7 Mon Sep 17 00:00:00 2001 From: javasabr Date: Sat, 30 May 2026 19:01:09 +0200 Subject: [PATCH 2/5] finalize the first iteration of eventbus module --- README.md | 27 +++- build.gradle | 2 +- .../java/javasabr/rlib/eventbus/EventBus.java | 121 +++++++++++++++--- .../rlib/eventbus/EventBusFactory.java | 24 +++- .../rlib/eventbus/impl/DefaultEventBus.java | 12 +- .../eventbus/impl/DefaultTypeIdFactory.java | 2 +- .../rlib/eventbus/DefaultEventBusTest.java | 99 +++++++------- 7 files changed, 213 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 38114e66..3f019ce4 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ A modular Java utility library providing common utilities for classpath scanning | `rlib-collections` | Extended collection implementations | | `rlib-compiler` | Runtime Java source compilation API | | `rlib-concurrent` | Concurrency utilities and helpers | +| `rlib-eventbus` | Typed low-overhead event bus API | | `rlib-classpath` | Classpath scanning and class discovery | | `rlib-functions` | Functional interfaces and utilities | | `rlib-geometry` | Geometry utilities | @@ -45,7 +46,7 @@ repositories { } ext { - rlibVersion = "10.0.alpha16" + rlibVersion = "10.0.alpha17" } dependencies { @@ -53,6 +54,7 @@ dependencies { implementation "javasabr.rlib:rlib-collections:$rlibVersion" implementation "javasabr.rlib:rlib-compiler:$rlibVersion" implementation "javasabr.rlib:rlib-concurrent:$rlibVersion" + implementation "javasabr.rlib:rlib-eventbus:$rlibVersion" implementation "javasabr.rlib:rlib-geometry:$rlibVersion" implementation "javasabr.rlib:rlib-logger-api:$rlibVersion" implementation "javasabr.rlib:rlib-logger-slf4j:$rlibVersion" @@ -120,6 +122,29 @@ LoggerLevel.DEBUG.setEnabled(false); logger.setEnabled(LoggerLevel.DEBUG, true); ``` +### EventBus API + +Type-safe event publishing and subscription with low dispatch overhead: + +```java +interface AppEvents extends EventBus.TypeIdSet {} + +record UserCreatedEvent( + String userId, + EventBus.TypeId typeId +) implements EventBus.Event {} + +var typeIdFactory = EventBusFactory.createTypeIdFactory(AppEvents.class); +var eventBus = EventBusFactory.createEventBus(typeIdFactory); +var userCreatedTypeId = typeIdFactory.typeIdOf(UserCreatedEvent.class); + +eventBus.subscribe(userCreatedTypeId, event -> + System.out.println("User created: " + event.userId())); + +eventBus.send(new UserCreatedEvent("user-42", userCreatedTypeId)); +eventBus.sendInBackground(new UserCreatedEvent("user-43", userCreatedTypeId)); +``` + ### Mail Sender Send emails synchronously or asynchronously: diff --git a/build.gradle b/build.gradle index 6319b978..5fafa035 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,4 @@ -rootProject.version = "10.0.alpha16" +rootProject.version = "10.0.alpha17" group = 'javasabr.rlib' allprojects { diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java index 15dc6801..f59697a8 100644 --- a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBus.java @@ -2,32 +2,123 @@ import java.util.function.Consumer; +/** + * Provides a typed event bus for subscribing and publishing events by {@link TypeId}. + * Implementations are expected to support concurrent subscribe, unsubscribe, and send operations. + * + * @param the type of type-id set + * @since 10.0.0 + */ public interface EventBus { - - > void registerConsumer(TypeId typeId, Consumer consumer); - - > void unregisterConsumer(TypeId typeId, Consumer consumer); - - void sendSync(Event event); - - void sendAsync(Event event); - + + /** + * Subscribes a consumer to events identified by the provided type id. + * + * @param typeId the event type id + * @param consumer the consumer of events + * @param the type of event + * @since 10.0.0 + */ + > void subscribe(TypeId typeId, Consumer consumer); + + /** + * Unsubscribes a consumer from events identified by the provided type id. + * + * @param typeId the event type id + * @param consumer the consumer of events + * @param the type of event + * @since 10.0.0 + */ + > void unsubscribe(TypeId typeId, Consumer consumer); + + /** + * Sends an event to all subscribed consumers in the current thread. + * This method does not enqueue work and returns only after dispatch is finished. + * Consumer exceptions are propagated to the caller. + * + * @param event the event to send + * @since 10.0.0 + */ + void send(Event event); + + /** + * Schedules sending an event on the background executor. + * This method returns immediately after scheduling. + * Delivery order between different background sends is not guaranteed and depends on the executor policy. + * Consumer exceptions are handled on the background thread. + * + * @param event the event to send + * @since 10.0.0 + */ + void sendInBackground(Event event); + + /** + * Marks a type-id namespace used to isolate event buses by event family. + * + * @since 10.0.0 + */ interface TypeIdSet { } - + + /** + * Creates stable {@link TypeId} instances for event types within one type-id namespace. + * + * @param the type of type-id set + * @since 10.0.0 + */ interface TypeIdFactory { - - > TypeId createFor(Class eventType); + + /** + * Returns a type id for the specified event type. + * + * @param eventType the event class + * @param the type of event + * @return the type id of the event class + * @since 10.0.0 + */ + > TypeId typeIdOf(Class eventType); } - + + /** + * Represents an identity of one event type within a {@link TypeIdSet}. + * + * @param the type of type-id set + * @param the type of event + * @since 10.0.0 + */ interface TypeId> { - + + /** + * Returns the event class associated with this type id. + * + * @return the event class + * @since 10.0.0 + */ Class eventType(); + + /** + * Returns the numeric id used for fast lookup in event bus internals. + * + * @return the numeric event type id + * @since 10.0.0 + */ int id(); } - + + /** + * Represents an event published to an {@link EventBus}. + * + * @param the type of type-id set + * @since 10.0.0 + */ interface Event { + /** + * Returns the type id of this event. + * + * @return the type id of this event + * @since 10.0.0 + */ TypeId> typeId(); } } diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java index cb40e359..988af72b 100644 --- a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java @@ -5,13 +5,35 @@ import javasabr.rlib.eventbus.impl.DefaultEventBus; import javasabr.rlib.eventbus.impl.DefaultTypeIdFactory; +/** + * Provides factory methods for creating event bus API components. + * + * @since 10.0.0 + */ public class EventBusFactory { + /** + * Creates a type-id factory for the provided type-id namespace. + * + * @param typeIdSetType the type-id namespace class + * @param the type of type-id set + * @return a type-id factory for the namespace + * @since 10.0.0 + */ public static TypeIdFactory createTypeIdFactory(Class typeIdSetType) { return DefaultTypeIdFactory.INSTANCE.typedTypeIdFactory(typeIdSetType); } - public static EventBus eventBus(@SuppressWarnings("unused") TypeIdFactory typeIdFactory) { + /** + * Creates an event bus instance. + * The default implementation uses a shared background executor for {@link EventBus#sendInBackground(EventBus.Event)}. + * + * @param typeIdFactory the type-id factory associated with this bus API + * @param the type of type-id set + * @return a new event bus + * @since 10.0.0 + */ + public static EventBus createEventBus(@SuppressWarnings("unused") TypeIdFactory typeIdFactory) { return new DefaultEventBus<>(); } } diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java index 5112ff4d..60527ab2 100644 --- a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultEventBus.java @@ -31,13 +31,13 @@ public DefaultEventBus(Executor asyncExecutor) { } @Override - public > void registerConsumer(TypeId typeId, Consumer consumer) { + public > void subscribe(TypeId typeId, Consumer consumer) { registerTypeId(typeId); consumers.unsafeGet(typeId.id()).add(consumer); } @Override - public > void unregisterConsumer(TypeId typeId, Consumer consumer) { + public > void unsubscribe(TypeId typeId, Consumer consumer) { if (typeId.id() < 0) { throw new IllegalArgumentException("Unexpected typeId:[%s]".formatted(typeId)); } @@ -47,13 +47,13 @@ public > void unregisterConsumer(TypeId typeId, Consume } @Override - public void sendSync(Event event) { + public void send(Event event) { UnsafeMutableArray> eventConsumers; try { eventConsumers = consumers.unsafeGet(event.typeId().id()); } catch (ArrayIndexOutOfBoundsException e) { registerTypeId(event.typeId()); - sendSync(event); + send(event); return; } //noinspection unchecked we control it during registering @@ -63,8 +63,8 @@ public void sendSync(Event event) { } @Override - public void sendAsync(Event event) { - asyncExecutor.execute(() -> sendSync(event)); + public void sendInBackground(Event event) { + asyncExecutor.execute(() -> send(event)); } private synchronized void registerTypeId(TypeId> typeId) { diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java index dc13696c..be2e73d8 100644 --- a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/impl/DefaultTypeIdFactory.java @@ -34,7 +34,7 @@ private TypedTypeIdFactory() { } @Override - public synchronized > TypeId createFor(Class eventType) { + public synchronized > TypeId typeIdOf(Class eventType) { TypeIdImpl exist = knownTypes.get(eventType); if (exist != null) { //noinspection unchecked it's checked during creation diff --git a/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java b/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java index 660d84d1..717d5965 100644 --- a/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java +++ b/rlib-eventbus/src/test/java/javasabr/rlib/eventbus/DefaultEventBusTest.java @@ -1,6 +1,8 @@ package javasabr.rlib.eventbus; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; @@ -9,7 +11,6 @@ import javasabr.rlib.collections.array.MutableArray; import javasabr.rlib.eventbus.EventBus.TypeId; import javasabr.rlib.eventbus.EventBus.TypeIdFactory; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; public class DefaultEventBusTest { @@ -24,7 +25,7 @@ public static class Test2TypeIdSet implements EventBus.TypeIdSet { public record EventA(String value) implements EventBus.Event { - static final TypeId TYPE_ID = typeIdFactory.createFor(EventA.class); + static final TypeId TYPE_ID = typeIdFactory.typeIdOf(EventA.class); @Override public TypeId typeId() { @@ -34,7 +35,7 @@ public TypeId typeId() { public record EventB(String value) implements EventBus.Event { - static final TypeId TYPE_ID = typeIdFactory.createFor(EventB.class); + static final TypeId TYPE_ID = typeIdFactory.typeIdOf(EventB.class); @Override public TypeId typeId() { @@ -44,7 +45,7 @@ public TypeId typeId() { public record EventC(String value) implements EventBus.Event { - static final TypeId TYPE_ID = typeIdFactory.createFor(EventC.class); + static final TypeId TYPE_ID = typeIdFactory.typeIdOf(EventC.class); @Override public TypeId typeId() { @@ -54,7 +55,7 @@ public TypeId typeId() { public record EventD(String value) implements EventBus.Event { - static final TypeId TYPE_ID = typeIdFactory2.createFor(EventD.class); + static final TypeId TYPE_ID = typeIdFactory2.typeIdOf(EventD.class); @Override public TypeId typeId() { @@ -92,7 +93,7 @@ public TypeId typeId() { @Test void shouldCorrectlyDeliverEvents() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray receivedEventsA1 = ArrayFactory.mutableArray(EventA.class); MutableArray receivedEventsA2 = ArrayFactory.mutableArray(EventA.class); @@ -101,23 +102,23 @@ void shouldCorrectlyDeliverEvents() { MutableArray receivedEventsC2 = ArrayFactory.mutableArray(EventC.class); MutableArray receivedEventsC3 = ArrayFactory.mutableArray(EventC.class); - eventBus.registerConsumer(EventA.TYPE_ID, receivedEventsA1::add); - eventBus.registerConsumer(EventA.TYPE_ID, eventA -> { + eventBus.subscribe(EventA.TYPE_ID, receivedEventsA1::add); + eventBus.subscribe(EventA.TYPE_ID, eventA -> { receivedEventsA1.add(eventA); receivedEventsA2.add(eventA); }); - eventBus.registerConsumer(EventB.TYPE_ID, receivedEventsB1::add); - eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC1::add); - eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC2::add); - eventBus.registerConsumer(EventC.TYPE_ID, receivedEventsC3::add); + eventBus.subscribe(EventB.TYPE_ID, receivedEventsB1::add); + eventBus.subscribe(EventC.TYPE_ID, receivedEventsC1::add); + eventBus.subscribe(EventC.TYPE_ID, receivedEventsC2::add); + eventBus.subscribe(EventC.TYPE_ID, receivedEventsC3::add); // when: - eventBus.sendSync(new EventA("event_a_1")); - eventBus.sendSync(new EventA("event_a_2")); - eventBus.sendSync(new EventB("event_b_1")); - eventBus.sendSync(new EventC("event_c_1")); - eventBus.sendSync(new EventC("event_c_2")); - eventBus.sendSync(new EventC("event_c_3")); + eventBus.send(new EventA("event_a_1")); + eventBus.send(new EventA("event_a_2")); + eventBus.send(new EventB("event_b_1")); + eventBus.send(new EventC("event_c_1")); + eventBus.send(new EventC("event_c_2")); + eventBus.send(new EventC("event_c_3")); // then: assertThat(receivedEventsA1).containsExactly( @@ -147,10 +148,10 @@ void shouldCorrectlyDeliverEvents() { @Test void shouldThrowExceptionWhenSendingEventWithNegativeTypeId() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); // when/then: - Assertions.assertThatThrownBy(() -> eventBus.sendSync(new EventWithNegativeTypeId("event"))) + assertThatThrownBy(() -> eventBus.send(new EventWithNegativeTypeId("event"))) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unexpected typeId"); } @@ -158,11 +159,11 @@ void shouldThrowExceptionWhenSendingEventWithNegativeTypeId() { @Test void shouldThrowExceptionWhenUnregisteringConsumerWithNegativeTypeId() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); // when/then: - Assertions.assertThatThrownBy( - () -> eventBus.unregisterConsumer(EventWithNegativeTypeId.TYPE_ID, event -> {})) + assertThatThrownBy( + () -> eventBus.unsubscribe(EventWithNegativeTypeId.TYPE_ID, event -> {})) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Unexpected typeId"); } @@ -170,13 +171,13 @@ void shouldThrowExceptionWhenUnregisteringConsumerWithNegativeTypeId() { @Test void shouldIgnoreUnregisterForOutOfRangeTypeId() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); - eventBus.registerConsumer(EventA.TYPE_ID, receivedEventsA::add); + eventBus.subscribe(EventA.TYPE_ID, receivedEventsA::add); // when: - eventBus.unregisterConsumer(EventWithOutOfRangeTypeId.TYPE_ID, event -> {}); - eventBus.sendSync(new EventA("event_a_1")); + eventBus.unsubscribe(EventWithOutOfRangeTypeId.TYPE_ID, event -> {}); + eventBus.send(new EventA("event_a_1")); // then: assertThat(receivedEventsA).containsExactly(new EventA("event_a_1")); @@ -185,24 +186,24 @@ void shouldIgnoreUnregisterForOutOfRangeTypeId() { @Test void shouldNotFailWhenSendingEventWithOutOfRangeTypeId() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); // when/then: - Assertions.assertThatCode(() -> eventBus.sendSync(new EventWithOutOfRangeTypeId("event"))) + assertThatCode(() -> eventBus.send(new EventWithOutOfRangeTypeId("event"))) .doesNotThrowAnyException(); } @Test - void shouldUnregisterConsumer() { + void shouldUnsubscribe() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); Consumer consumer = receivedEventsA::add; - eventBus.registerConsumer(EventA.TYPE_ID, consumer); + eventBus.subscribe(EventA.TYPE_ID, consumer); // when: - eventBus.unregisterConsumer(EventA.TYPE_ID, consumer); - eventBus.sendSync(new EventA("event_a_1")); + eventBus.unsubscribe(EventA.TYPE_ID, consumer); + eventBus.send(new EventA("event_a_1")); // then: assertThat(receivedEventsA).isEmpty(); @@ -211,18 +212,18 @@ void shouldUnregisterConsumer() { @Test void shouldIgnoreDuplicateUnregister() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray firstConsumerEvents = ArrayFactory.mutableArray(EventA.class); MutableArray secondConsumerEvents = ArrayFactory.mutableArray(EventA.class); Consumer firstConsumer = firstConsumerEvents::add; Consumer secondConsumer = secondConsumerEvents::add; - eventBus.registerConsumer(EventA.TYPE_ID, firstConsumer); - eventBus.registerConsumer(EventA.TYPE_ID, secondConsumer); + eventBus.subscribe(EventA.TYPE_ID, firstConsumer); + eventBus.subscribe(EventA.TYPE_ID, secondConsumer); // when: - eventBus.unregisterConsumer(EventA.TYPE_ID, firstConsumer); - eventBus.unregisterConsumer(EventA.TYPE_ID, firstConsumer); - eventBus.sendSync(new EventA("event_a_1")); + eventBus.unsubscribe(EventA.TYPE_ID, firstConsumer); + eventBus.unsubscribe(EventA.TYPE_ID, firstConsumer); + eventBus.send(new EventA("event_a_1")); // then: assertThat(firstConsumerEvents).isEmpty(); @@ -232,12 +233,12 @@ void shouldIgnoreDuplicateUnregister() { @Test void shouldThrowExceptionForConflictingTypeIdOfSameEventType() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); TypeId conflictingTypeId = new CustomTypeId<>(EventA.class, 1001); - eventBus.registerConsumer(EventA.TYPE_ID, event -> {}); + eventBus.subscribe(EventA.TYPE_ID, event -> {}); // when/then: - Assertions.assertThatThrownBy(() -> eventBus.registerConsumer(conflictingTypeId, event -> {})) + assertThatThrownBy(() -> eventBus.subscribe(conflictingTypeId, event -> {})) .isInstanceOf(IllegalStateException.class) .hasMessageContaining("already registered"); } @@ -245,16 +246,16 @@ void shouldThrowExceptionForConflictingTypeIdOfSameEventType() { @Test void shouldDeliverEventsAsync() throws Exception { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray receivedEventsA = ArrayFactory.mutableArray(EventA.class); CountDownLatch latch = new CountDownLatch(1); - eventBus.registerConsumer(EventA.TYPE_ID, event -> { + eventBus.subscribe(EventA.TYPE_ID, event -> { receivedEventsA.add(event); latch.countDown(); }); // when: - eventBus.sendAsync(new EventA("event_a_1")); + eventBus.sendInBackground(new EventA("event_a_1")); // then: assertThat(latch.await(2, TimeUnit.SECONDS)).isTrue(); @@ -264,12 +265,12 @@ void shouldDeliverEventsAsync() throws Exception { @Test void shouldDeliverEventsForSparseTypeId() { // given: - var eventBus = EventBusFactory.eventBus(typeIdFactory); + var eventBus = EventBusFactory.createEventBus(typeIdFactory); MutableArray receivedEvents = ArrayFactory.mutableArray(EventWithOutOfRangeTypeId.class); - eventBus.registerConsumer(EventWithOutOfRangeTypeId.TYPE_ID, receivedEvents::add); + eventBus.subscribe(EventWithOutOfRangeTypeId.TYPE_ID, receivedEvents::add); // when: - eventBus.sendSync(new EventWithOutOfRangeTypeId("event")); + eventBus.send(new EventWithOutOfRangeTypeId("event")); // then: assertThat(receivedEvents).containsExactly(new EventWithOutOfRangeTypeId("event")); From f63d3ced34d0a529fca241f2a39a7f99cb707504 Mon Sep 17 00:00:00 2001 From: javasabr Date: Sat, 30 May 2026 19:08:24 +0200 Subject: [PATCH 3/5] resolve CR comments --- .../src/main/java/javasabr/rlib/eventbus/EventBusFactory.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java index 988af72b..12935e78 100644 --- a/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java +++ b/rlib-eventbus/src/main/java/javasabr/rlib/eventbus/EventBusFactory.java @@ -4,12 +4,14 @@ import javasabr.rlib.eventbus.EventBus.TypeIdSet; import javasabr.rlib.eventbus.impl.DefaultEventBus; import javasabr.rlib.eventbus.impl.DefaultTypeIdFactory; +import lombok.experimental.UtilityClass; /** * Provides factory methods for creating event bus API components. * * @since 10.0.0 */ +@UtilityClass public class EventBusFactory { /** From 190e987dfefea6e9051ffcb50ada1799985ffb06 Mon Sep 17 00:00:00 2001 From: javasabr Date: Sat, 30 May 2026 19:09:58 +0200 Subject: [PATCH 4/5] update the skill --- .github/copilot-instructions.md | 3 +++ .github/skills/generate-branch-summary.md | 29 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9493d6f2..dca7beb6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -116,6 +116,9 @@ Behavior rules for the coding agent - If changing code that touches Testcontainers or integration tests, ensure Docker is available when validating; otherwise skip integration tests locally and make this explicit in PR. - If any command from these instructions fails, only then search the repository for more details or updated config. Trust this document first. - When tests fail locally, reproduce failing tests selectively with `--tests` before making fixes. +- Do not report exact test counts unless verified from test result files (`build/test-results/**/TEST-*.xml`); Gradle console summaries may be misleading with caching or filtered runs. +- When adding or updating documentation examples, verify API names and signatures against current source code (especially after recent renames). +- If the user says changes are already committed, avoid unsolicited commit-oriented follow-ups and focus on the requested review/documentation/diff task. Reference (root file list, quick) - README.md diff --git a/.github/skills/generate-branch-summary.md b/.github/skills/generate-branch-summary.md index 88e8ca89..628a54f2 100644 --- a/.github/skills/generate-branch-summary.md +++ b/.github/skills/generate-branch-summary.md @@ -37,6 +37,10 @@ Group modifications by their nature: Create a section in `summary.md` with this format: ```markdown +## Overview + +Short 2-4 sentence summary of what the branch introduces and why it matters. + ## Changes Summary ### (e.g., "New Array API") @@ -53,6 +57,12 @@ Create a section in `summary.md` with this format: - What documentation was added - Why it matters (e.g., for future contributors) +### Event flow diagram (when relevant) +- Add a Mermaid `flowchart` for runtime flow when the branch introduces/changes flow-heavy APIs + +### Usage samples (when relevant) +- Add copy-pasteable API usage examples for new public APIs + ## Testing & Validation - `./gradlew :module-name:build` ✅ - `./gradlew clean build` (recommended before merge) @@ -81,9 +91,16 @@ Create a section in `summary.md` with this format: - Use bullet points for lists - Use bold (`**`) for emphasis on key terms - Use inline code (`` ` ` ``) for class/method names +- Always include `## Overview` before `## Changes Summary` - Do not add a `Branch:` section or heading in the summary - Do not add `Status`, `Commits`, or `Version` metadata lines in the summary +#### Mermaid Diagram Safety (when diagrams are included) +- Prefer quoted node labels, e.g. `A["Create bus"]` +- Avoid method-signature punctuation inside node text (`(`, `)`, `,`, generics); use plain words instead +- Use edge labels like `|send|` instead of `|send(event)|` +- Keep node text short and parser-safe; move details to bullets under the diagram + ### 6. Output Location Always write to `summary.md` in the repository root, replacing or updating the previous summary section. @@ -93,6 +110,11 @@ If `summary.md` already contains unrelated sections (e.g., documentation of othe ## Example Output ```markdown +## Overview + +This branch extends the collections API with safe internal resizing and dictionary optimizations. +It reduces overhead on empty collections and adds documentation to clarify the new contract. + ## Changes Summary ### 1. New Array API: `tryTrimTo(int)` @@ -151,3 +173,10 @@ Mention: 4. **Be concise** — summaries should be readable in 2-3 minutes 5. **Link to code** — mention file paths and method names so reviewers can navigate easily 6. **Quantify changes** — "7 methods optimized" is more informative than "several optimizations" + +## Final Checklist (before saving `summary.md`) +- `## Overview` exists and is concise +- `## Changes Summary` exists with clear categories +- If a diagram is included, Mermaid renders with parser-safe labels +- If a new public API is introduced, usage samples are included +- `## Testing & Validation` includes executed commands and recommended full build From d86946b88eeda6c16252d5c57911083360731239 Mon Sep 17 00:00:00 2001 From: javasabr Date: Sat, 30 May 2026 19:16:55 +0200 Subject: [PATCH 5/5] update readme and test coverage configuration --- README.md | 1 + test-coverage/build.gradle | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 3f019ce4..9fc3b317 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ dependencies { implementation "javasabr.rlib:rlib-plugin-system:$rlibVersion" implementation "javasabr.rlib:rlib-reference:$rlibVersion" implementation "javasabr.rlib:rlib-reusable:$rlibVersion" + implementation "javasabr.rlib:rlib-eventbus:$rlibVersion" implementation "javasabr.rlib:rlib-fx:$rlibVersion" implementation "javasabr.rlib:rlib-network:$rlibVersion" implementation "javasabr.rlib:rlib-mail:$rlibVersion" diff --git a/test-coverage/build.gradle b/test-coverage/build.gradle index dfeca12a..ac2ca37b 100644 --- a/test-coverage/build.gradle +++ b/test-coverage/build.gradle @@ -19,5 +19,6 @@ dependencies { jacocoAggregation projects.rlibPluginSystem jacocoAggregation projects.rlibReference jacocoAggregation projects.rlibReusable + jacocoAggregation projects.rlibEventbus jacocoAggregation projects.rlibTestcontainers }