2020
2121import org .jspecify .annotations .Nullable ;
2222
23+ import com .vaadin .flow .function .SerializableBiPredicate ;
2324import com .vaadin .flow .function .SerializableConsumer ;
2425import com .vaadin .flow .server .VaadinSession ;
2526import 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