Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions eppo/src/androidTest/java/cloud/eppo/android/EppoClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,50 @@ public void testConfigurationChangeListener() throws ExecutionException, Interru
assertEquals(3, received.size());
}

@Test
public void testConfigurationChangeListenerOneShot()
throws ExecutionException, InterruptedException {
List<Configuration> received = new ArrayList<>();

// Set up a changing response from the "server"
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);

// Mock sync get to return empty
when(mockHttpClient.get(anyString())).thenReturn(EMPTY_CONFIG);

// Mock async get to return empty
CompletableFuture<byte[]> emptyResponse = CompletableFuture.completedFuture(EMPTY_CONFIG);
when(mockHttpClient.getAsync(anyString())).thenReturn(emptyResponse);

setBaseClientHttpClientOverrideField(mockHttpClient);

EppoClient.Builder clientBuilder =
new EppoClient.Builder(DUMMY_API_KEY, ApplicationProvider.getApplicationContext())
.forceReinitialize(true)
.onConfigurationChange(received::add, true) // oneShot = true
.isGracefulMode(false);

// Initialize and no exception should be thrown.
EppoClient eppoClient = clientBuilder.buildAndInitAsync().get();

verify(mockHttpClient, times(1)).getAsync(anyString());
assertEquals(1, received.size());

// Now, return the boolean flag config so that the config has changed.
when(mockHttpClient.get(anyString())).thenReturn(BOOL_FLAG_CONFIG);

// Trigger a reload of the client
eppoClient.loadConfiguration();

// Callback should NOT be invoked again because it was oneShot
assertEquals(1, received.size());

// Reload the client again to verify it's still not called
eppoClient.loadConfiguration();

assertEquals(1, received.size());
}

@Test
public void testPollingClient() throws ExecutionException, InterruptedException {
EppoHttpClient mockHttpClient = mock(EppoHttpClient.class);
Expand Down
45 changes: 44 additions & 1 deletion eppo/src/main/java/cloud/eppo/android/EppoClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,31 @@ public static class Builder {
private IAssignmentCache assignmentCache = new LRUAssignmentCache(100);
@Nullable private Consumer<Configuration> configChangeCallback;

/**
* Wrapper for one-shot configuration change callbacks that auto-unsubscribes after first
* invocation.
*/
private static class OneShotConfigCallback implements Consumer<Configuration> {
private final Consumer<Configuration> delegate;
private Runnable unsubscriber;

OneShotConfigCallback(Consumer<Configuration> delegate) {
this.delegate = delegate;
}

void setUnsubscriber(Runnable unsubscriber) {
this.unsubscriber = unsubscriber;
}

@Override
public void accept(Configuration config) {
if (unsubscriber != null) {
unsubscriber.run();
}
delegate.accept(config);
}
}

public Builder(@NonNull String apiKey, @NonNull Application application) {
this.application = application;
this.apiKey = apiKey;
Expand Down Expand Up @@ -261,6 +286,20 @@ public Builder onConfigurationChange(Consumer<Configuration> configChangeCallbac
return this;
}

/**
* Registers a callback for when a new configuration is applied to the `EppoClient` instance.
*
* @param configChangeCallback The callback to invoke when configuration changes
* @param oneShot If true, the callback will be automatically unsubscribed after the first
* invocation
*/
public Builder onConfigurationChange(
Consumer<Configuration> configChangeCallback, boolean oneShot) {
this.configChangeCallback =
oneShot ? new OneShotConfigCallback(configChangeCallback) : configChangeCallback;
return this;
}

public CompletableFuture<EppoClient> buildAndInitAsync() {
if (application == null) {
throw new MissingApplicationException();
Expand Down Expand Up @@ -311,7 +350,11 @@ public CompletableFuture<EppoClient> buildAndInitAsync() {
assignmentCache);

if (configChangeCallback != null) {
instance.onConfigurationChange(configChangeCallback);
Runnable unsubscriber = instance.onConfigurationChange(configChangeCallback);
// If this is a one-shot callback, set the unsubscriber so it can clean up after first call
if (configChangeCallback instanceof OneShotConfigCallback) {
((OneShotConfigCallback) configChangeCallback).setUnsubscriber(unsubscriber);
}
}

final CompletableFuture<EppoClient> ret = new CompletableFuture<>();
Expand Down
Loading