DualLensCore.java
package sprouts.impl;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import sprouts.Channel;
import sprouts.Pair;
import sprouts.Val;
import sprouts.Var;
import java.util.Arrays;
import java.util.List;
import java.util.function.BiFunction;
import java.util.function.Function;
/**
* A {@link LensCore} backed by two parent {@link Var} properties,
* a getter {@link BiFunction} and a setter {@link Function} returning a {@link Pair}.
* <p>
* Includes a re-entrancy guard ({@code _settingFromSelf}) that suppresses
* source-change callbacks while the dual lens is writing back to both parents
* during a {@code set()} call. This ensures that a single {@code set()} produces
* exactly one change event with the correct final value rather than intermediate
* transient states.
*
* @param <A> The item type of the first source property.
* @param <B> The item type of the second source property.
* @param <T> The combined item type of this lens property.
*/
final class DualLensCore<A extends @Nullable Object, B extends @Nullable Object, T extends @Nullable Object>
implements LensCore<T>
{
private static final Logger log = org.slf4j.LoggerFactory.getLogger(DualLensCore.class);
private final Var<A> _firstParent;
private final Var<B> _secondParent;
private final BiFunction<A, B, T> _getter;
private final Function<T, Pair<A, B>> _setter;
/**
* Guards against re-entrant change notifications while writing
* a new combined value back into both source properties.
* See the detailed explanation in the class-level Javadoc.
*/
private boolean _settingFromSelf = false;
DualLensCore(
Var<A> firstParent,
Var<B> secondParent,
BiFunction<A, B, T> getter,
Function<T, Pair<A, B>> setter
) {
_firstParent = firstParent;
_secondParent = secondParent;
_getter = getter;
_setter = setter;
}
@Override
public @Nullable T fetchFromSources(@Nullable T lastKnownItem) {
T fetchedValue = lastKnownItem;
try {
fetchedValue = _getter.apply(
Util.fakeNonNull(_firstParent.orElseNull()),
Util.fakeNonNull(_secondParent.orElseNull())
);
} catch ( Exception e ) {
Util.sneakyThrowExceptionIfFatal(e);
Util._logError(log,
"Failed to fetch item for dual property lens from source " +
"properties '{}' and '{}' using the current getter function.",
_firstParent.id().isEmpty() ? "?" : "'" + _firstParent.id() + "'",
_secondParent.id().isEmpty() ? "?" : "'" + _secondParent.id() + "'",
e
);
}
return fetchedValue;
}
@Override
public void writeToSources(Channel channel, @Nullable T newItem) {
Pair<A, B> pair;
try {
pair = _setter.apply(Util.fakeNonNull(newItem));
} catch ( Exception e ) {
Util.sneakyThrowExceptionIfFatal(e);
Util._logError(log,
"Dual property lens failed to split the combined value " +
"into the two source values using the setter function.", e
);
return;
}
_settingFromSelf = true;
try {
try {
_firstParent.set(channel, pair.first());
} catch ( Exception e ) {
Util.sneakyThrowExceptionIfFatal(e);
Util._logError(log,
"Dual property lens failed to assign the first split value " +
"into the first source property.", e
);
}
try {
_secondParent.set(channel, pair.second());
} catch ( Exception e ) {
Util.sneakyThrowExceptionIfFatal(e);
Util._logError(log,
"Dual property lens failed to assign the second split value " +
"into the second source property.", e
);
}
} finally {
_settingFromSelf = false;
}
}
@Override
public Iterable<? extends Val<?>> sources() {
return Arrays.asList(_firstParent, _secondParent);
}
@Override
public boolean shouldSuppressSourceCallback() {
return _settingFromSelf;
}
@Override
public String coreName() {
return "DualLens";
}
@Override
public LensCore<T> newInstance() {
return new DualLensCore<>(_firstParent, _secondParent, _getter, _setter);
}
}