Skip to content

Commit cf82b10

Browse files
mshabarovclaude
andauthored
feat: Add custom equality checker support to ValueSignal (#23611)
* feat: Add custom equality checker support to ValueSignal Optimize ValueSignal to skip updates when the new value equals the current value, and allow customization of equality logic. Changes: - Add SerializableBiPredicate<T, T> field for custom equality checking - Add constructor accepting custom equality checker (defaults to Objects::equals) - Update set() to skip updates when values are equal per equality checker - Update update() to use equality checker instead of reference equality - Add comprehensive tests for default and custom equality checkers Fixes #23588 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Javadoc, replace if value differs, extra test * improve javadoc * fix test --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4e82a4a commit cf82b10

File tree

2 files changed

+359
-8
lines changed

2 files changed

+359
-8
lines changed

flow-server/src/main/java/com/vaadin/flow/signals/local/ValueSignal.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
import org.jspecify.annotations.Nullable;
2222

23+
import com.vaadin.flow.function.SerializableBiPredicate;
2324
import com.vaadin.flow.function.SerializableConsumer;
2425
import com.vaadin.flow.server.VaadinSession;
2526
import com.vaadin.flow.signals.Signal;
@@ -57,6 +58,7 @@ public class ValueSignal<T> extends AbstractLocalSignal<T> {
5758
private boolean modifyRunning = false;
5859
private transient boolean modifyUsed = false;
5960
private transient boolean usedWithoutSessionLock = false;
61+
private final SerializableBiPredicate<T, T> equalityChecker;
6062

6163
/**
6264
* Creates a new value signal with the given initial value.
@@ -65,7 +67,29 @@ public class ValueSignal<T> extends AbstractLocalSignal<T> {
6567
* the initial value, may be <code>null</code>
6668
*/
6769
public ValueSignal(@Nullable T initialValue) {
70+
this(initialValue, Objects::equals);
71+
}
72+
73+
/**
74+
* Creates a new value signal with the given initial value and a custom
75+
* equality checker.
76+
* <p>
77+
* The equality checker is used to determine if a new value is equal to the
78+
* current value. If the equality checker returns {@code true}, the value
79+
* update is skipped, and no change notification is triggered, i.e., no
80+
* dependent effect function is triggered.
81+
*
82+
* @param initialValue
83+
* the initial value, may be <code>null</code>
84+
* @param equalityChecker
85+
* the predicate used to compare values for equality, not
86+
* <code>null</code>
87+
*/
88+
public ValueSignal(@Nullable T initialValue,
89+
SerializableBiPredicate<T, T> equalityChecker) {
6890
super(initialValue);
91+
this.equalityChecker = Objects.requireNonNull(equalityChecker,
92+
"Equality checker must not be null");
6993
}
7094

7195
@Override
@@ -104,8 +128,12 @@ protected void checkPreconditions() {
104128
/**
105129
* Sets the value of this signal.
106130
* <p>
107-
* Setting a new value will trigger effect functions that have reads from
108-
* this signal.
131+
* If the new value is not equal to the current value, the value is set and
132+
* effect functions that have reads from this signal are triggered. If the
133+
* values are equal, no change is made and effect functions are not
134+
* triggered. The equality checker provided in the constructor is used to
135+
* compare the values and defaults to
136+
* {@link Objects#equals(Object, Object)}.
109137
*
110138
* @param value
111139
* the value to set
@@ -115,7 +143,9 @@ public void set(@Nullable T value) {
115143
try {
116144
checkPreconditions();
117145

118-
setSignalValue(value);
146+
if (!equalityChecker.test(value, getSignalValue())) {
147+
setSignalValue(value);
148+
}
119149
} finally {
120150
unlock();
121151
}
@@ -127,8 +157,12 @@ public void set(@Nullable T value) {
127157
* counterpart to
128158
* {@link java.util.concurrent.atomic.AtomicReference#compareAndSet(Object, Object)}.
129159
* <p>
130-
* Comparison between the expected value and the new value is performed
131-
* using {@link #equals(Object)}.
160+
* If the expected value matches and the new value differs from the old
161+
* value, the value is set and effect functions are triggered. If the
162+
* expected value matches but the new value equals the old value, no change
163+
* is made and effect functions are not triggered. The equality checker
164+
* provided in the constructor is used to compare the expected value with
165+
* the current value, and to compare the new value with the old value.
132166
*
133167
* @param expectedValue
134168
* the expected value
@@ -142,8 +176,10 @@ public boolean replace(@Nullable T expectedValue, @Nullable T newValue) {
142176
try {
143177
checkPreconditions();
144178

145-
if (Objects.equals(expectedValue, getSignalValue())) {
146-
setSignalValue(newValue);
179+
if (equalityChecker.test(expectedValue, getSignalValue())) {
180+
if (!equalityChecker.test(newValue, getSignalValue())) {
181+
setSignalValue(newValue);
182+
}
147183
return true;
148184
} else {
149185
return false;
@@ -164,6 +200,10 @@ public boolean replace(@Nullable T expectedValue, @Nullable T newValue) {
164200
* would occur after the original transaction has already been committed.
165201
* For this reason, the whole operation completely bypasses all transaction
166202
* handling.
203+
* <p>
204+
* If the new value is equal to the current value, no change is made and
205+
* effect functions are not triggered. The equality checker provided in the
206+
* constructor is used to compare the values.
167207
*
168208
* @param updater
169209
* the value update callback, not <code>null</code>
@@ -177,7 +217,7 @@ public boolean replace(@Nullable T expectedValue, @Nullable T newValue) {
177217

178218
T oldValue = getSignalValue();
179219
T newValue = updater.update(oldValue);
180-
if (newValue != oldValue) {
220+
if (!equalityChecker.test(newValue, oldValue)) {
181221
setSignalValue(newValue);
182222
}
183223

0 commit comments

Comments
 (0)