diff --git a/api/src/main/java/io/split/android/client/SplitClient.java b/api/src/main/java/io/split/android/client/SplitClient.java index 51f02dcaa..007303091 100644 --- a/api/src/main/java/io/split/android/client/SplitClient.java +++ b/api/src/main/java/io/split/android/client/SplitClient.java @@ -186,6 +186,8 @@ public interface SplitClient extends AttributesManager { * This method provides type-safe callbacks for SDK_READY, SDK_UPDATE, and SDK_READY_FROM_CACHE events. * Override the methods you need in the listener. *
+ * Multiple listeners can be registered. Each listener will be invoked once per event. + *
* Example usage: *
{@code
* client.addEventListener(new SdkEventListener() {
@@ -210,9 +212,9 @@ public interface SplitClient extends AttributesManager {
* });
* }
*
- * @param listener the event listener to register
+ * @param listener the event listener to register. Must not be null.
*/
- void addEventListener(SdkEventListener listener);
+ void addEventListener(@NonNull SdkEventListener listener);
/**
* Enqueue a new event to be sent to Split data collection services.
diff --git a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
index ccb1eea8b..24d3283f5 100644
--- a/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
+++ b/main/src/androidTest/java/tests/integration/events/SdkEventsIntegrationTest.java
@@ -977,33 +977,39 @@ public void onUpdate(SplitClient client, SdkUpdateMetadata metadata) {
* Given a SplitClient with an EventsManager and a handler H registered for sdkUpdate
* And sdkReady has already been emitted
* When the client is destroyed
- * And an internal "splitsUpdated" event is notified for that client
- * Then no external events are emitted
- * And handler H is never invoked
+ * And an internal "splitsUpdated" event is notified via SSE
+ * Then handler H is never invoked (handlers were cleared on destroy)
* When registering a new handler H2 for sdkUpdate after destroy
* Then the registration is a no-op
- * And H2 is never invoked
+ * And H2 is never invoked even when another update is pushed
*/
@Test
public void destroyingClientStopsEventsAndClearsHandlers() throws Exception {
- // Given: sdkReady has already been emitted
- TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1"));
+ // Given: sdkReady has already been emitted (with streaming support)
+ TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1"));
AtomicInteger handler1Count = new AtomicInteger(0);
AtomicInteger handler2Count = new AtomicInteger(0);
- // Given: a handler H registered for sdkUpdate
- registerUpdateHandler(fixture.client, handler1Count, null);
+ // Given: a handler H registered for sdkUpdate before destroy
+ fixture.client.addEventListener(createOnUpdateListener(handler1Count, null, null));
// When: the client is destroyed
fixture.client.destroy();
- // When: registering a new handler H2 for sdkUpdate after destroy
- registerUpdateHandler(fixture.client, handler2Count, null);
+ fixture.pushSplitUpdate("3000", "2000");
- // Then: handlers are not invoked (client is destroyed)
+ // Handler H is never invoked (handlers were cleared on destroy)
Thread.sleep(1000);
assertEquals("Handler H1 should not be invoked after destroy", 0, handler1Count.get());
+
+ // When: registering a new handler H2 for sdkUpdate after destroy
+ fixture.client.addEventListener(createOnUpdateListener(handler2Count, null, null));
+
+ fixture.pushSplitUpdate("4000", "3000");
+
+ Thread.sleep(1000);
+ assertEquals("Handler H1 should still be 0", 0, handler1Count.get());
assertEquals("Handler H2 should not be invoked after destroy", 0, handler2Count.get());
fixture.destroy();
@@ -1497,6 +1503,188 @@ public void onPostExecution(SplitClient c) {
return receivedMetadataList;
}
+
+
+ /**
+ * Scenario: Multiple listeners with onUpdate are both invoked
+ *
+ * Given sdkReady has already been emitted
+ * And two different SdkEventListener instances (L1 and L2) with onUpdate handlers are registered
+ * When a split update notification arrives via SSE
+ * Then SDK_UPDATE is emitted once
+ * And both L1.onUpdate and L2.onUpdate are invoked exactly once each
+ */
+ @Test
+ public void multipleListenersWithOnUpdateBothInvoked() throws Exception {
+ TestClientFixture fixture = createStreamingClientAndWaitForReady(new Key("key_1"));
+
+ AtomicInteger listener1Count = new AtomicInteger(0);
+ AtomicInteger listener2Count = new AtomicInteger(0);
+ AtomicReference
+ * Given the SDK is starting
+ * And two different SdkEventListener instances (L1 and L2) with onReady handlers are registered
+ * When SDK_READY fires
+ * Then both L1.onReady and L2.onReady are invoked exactly once each
+ * And both receive SdkReadyMetadata
+ */
+ @Test
+ public void multipleListenersWithOnReadyBothInvoked() throws Exception {
+ populateDatabaseWithCacheData(System.currentTimeMillis());
+ SplitFactory factory = buildFactory(buildConfig());
+ SplitClient client = factory.client(new Key("key_1"));
+
+ AtomicInteger listener1Count = new AtomicInteger(0);
+ AtomicInteger listener2Count = new AtomicInteger(0);
+ AtomicReference
+ * Given the SDK is starting
+ * And a SdkEventListener L1 with onReady handler is registered
+ * And a SdkEventListener L2 with onUpdate handler is registered
+ * When SDK_READY fires
+ * Then L1.onReady is invoked
+ * And L2.onUpdate is NOT invoked (wrong event type)
+ * When an SDK_UPDATE notification arrives via SSE
+ * Then L2.onUpdate is invoked
+ * And L1.onReady is NOT invoked again (already fired once for SDK_READY)
+ */
+ @Test
+ public void listenersWithDifferentCallbacksInvokedOnCorrectEventType() throws Exception {
+ TestClientFixture fixture = createStreamingClient(new Key("key_1"));
+
+ AtomicInteger onReadyCount = new AtomicInteger(0);
+ AtomicInteger onUpdateCount = new AtomicInteger(0);
+ CountDownLatch readyLatch = new CountDownLatch(1);
+ CountDownLatch updateLatch = new CountDownLatch(1);
+
+ fixture.client.addEventListener(createOnReadyListener(onReadyCount, null, readyLatch));
+ fixture.client.addEventListener(createOnUpdateListener(onUpdateCount, null, updateLatch));
+
+ assertTrue("SDK_READY should fire", readyLatch.await(10, TimeUnit.SECONDS));
+ assertEquals("onReady should be invoked exactly once", 1, onReadyCount.get());
+ assertEquals("onUpdate should NOT be invoked on SDK_READY", 0, onUpdateCount.get());
+
+ fixture.waitForSseConnection();
+ fixture.pushSplitUpdate();
+
+ assertTrue("SDK_UPDATE should fire", updateLatch.await(10, TimeUnit.SECONDS));
+ assertEquals("onUpdate should be invoked exactly once", 1, onUpdateCount.get());
+ assertEquals("onReady should still be 1 (not invoked again)", 1, onReadyCount.get());
+
+ fixture.destroy();
+ }
+
+ /**
+ * Scenario: Multiple listeners with both onReady and onUpdate in same listener
+ *
+ * Given the SDK is starting
+ * And two SdkEventListener instances (L1 and L2) each with both onReady and onUpdate handlers
+ * When SDK_READY fires
+ * Then both L1.onReady and L2.onReady are invoked exactly once each
+ * And neither L1.onUpdate nor L2.onUpdate are invoked
+ * When an SDK_UPDATE notification arrives via SSE
+ * Then both L1.onUpdate and L2.onUpdate are invoked exactly once each
+ */
+ @Test
+ public void multipleListenersWithBothReadyAndUpdateHandlers() throws Exception {
+ TestClientFixture fixture = createStreamingClient(new Key("key_1"));
+
+ AtomicInteger listener1ReadyCount = new AtomicInteger(0);
+ AtomicInteger listener1UpdateCount = new AtomicInteger(0);
+ AtomicInteger listener2ReadyCount = new AtomicInteger(0);
+ AtomicInteger listener2UpdateCount = new AtomicInteger(0);
+ CountDownLatch readyLatch = new CountDownLatch(2);
+ CountDownLatch updateLatch = new CountDownLatch(2);
+
+ fixture.client.addEventListener(createDualListener(listener1ReadyCount, readyLatch, listener1UpdateCount, updateLatch));
+ fixture.client.addEventListener(createDualListener(listener2ReadyCount, readyLatch, listener2UpdateCount, updateLatch));
+
+ assertTrue("Both onReady handlers should be invoked", readyLatch.await(10, TimeUnit.SECONDS));
+ assertEquals("Listener 1 onReady should be invoked once", 1, listener1ReadyCount.get());
+ assertEquals("Listener 2 onReady should be invoked once", 1, listener2ReadyCount.get());
+ assertEquals("Listener 1 onUpdate should NOT be invoked on SDK_READY", 0, listener1UpdateCount.get());
+ assertEquals("Listener 2 onUpdate should NOT be invoked on SDK_READY", 0, listener2UpdateCount.get());
+
+ fixture.waitForSseConnection();
+ fixture.pushSplitUpdate();
+
+ assertTrue("Both onUpdate handlers should be invoked", updateLatch.await(10, TimeUnit.SECONDS));
+ assertEquals("Listener 1 onUpdate should be invoked once", 1, listener1UpdateCount.get());
+ assertEquals("Listener 2 onUpdate should be invoked once", 1, listener2UpdateCount.get());
+ assertEquals("Listener 1 onReady should still be 1", 1, listener1ReadyCount.get());
+ assertEquals("Listener 2 onReady should still be 1", 1, listener2ReadyCount.get());
+
+ fixture.destroy();
+ }
+
+ /**
+ * Scenario: Multiple listeners with onReady replay to late subscribers
+ *
+ * Given SDK_READY has already been emitted
+ * And a SdkEventListener L1 with onReady was registered before SDK_READY and was invoked
+ * When a new SdkEventListener L2 with onReady is registered after SDK_READY has fired
+ * Then L2.onReady is invoked (replay)
+ * And L1.onReady is NOT invoked again
+ */
+ @Test
+ public void multipleListenersWithOnReadyReplayToLateSubscribers() throws Exception {
+ TestClientFixture fixture = createClientAndWaitForReady(new Key("key_1"));
+
+ AtomicInteger listener1Count = new AtomicInteger(0);
+ AtomicInteger listener2Count = new AtomicInteger(0);
+ CountDownLatch listener1Latch = new CountDownLatch(1);
+ CountDownLatch listener2Latch = new CountDownLatch(1);
+
+ fixture.client.addEventListener(createOnReadyListener(listener1Count, null, listener1Latch));
+ assertTrue("Listener 1 should receive replay", listener1Latch.await(5, TimeUnit.SECONDS));
+ assertEquals("Listener 1 should be invoked once (replay)", 1, listener1Count.get());
+
+ fixture.client.addEventListener(createOnReadyListener(listener2Count, null, listener2Latch));
+ assertTrue("Listener 2 should receive replay", listener2Latch.await(5, TimeUnit.SECONDS));
+ assertEquals("Listener 2 should be invoked once (replay)", 1, listener2Count.get());
+
+ Thread.sleep(500);
+ assertEquals("Listener 1 should still be 1 (not invoked again)", 1, listener1Count.get());
+
+ fixture.destroy();
+ }
+
/**
* Creates a client and waits for SDK_READY to fire.
* Returns a TestClientFixture containing the factory, client, and ready latch.
@@ -1690,6 +1878,58 @@ public void onPostExecution(SplitClient client) {
});
}
+ /**
+ * Creates a SdkEventListener that counts onReady invocations and captures metadata.
+ */
+ private SdkEventListener createOnReadyListener(AtomicInteger count,
+ AtomicReference