Skip to content

Commit cd5c3cd

Browse files
authored
feat: make signals Serializable (#23400)
Makes serializable local signals: ValueSignal and ListSignal. Serializable ComponentEffect and ComputedSignal. Adds unit test for basic case: serialize and deserialize session with one UI instance and ensure that ComponentEffect in a Component is still executed when ValueSignal's value is changed. Same unit tests for Element bindText, bindProperty, bindAttribute, bindEnabled, bindVisible, Signal.computed(), Signal.map. Part of #22843
1 parent 0a3bfaf commit cd5c3cd

33 files changed

+539
-83
lines changed

flow-server/src/main/java/com/vaadin/flow/component/ComponentEffect.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import com.vaadin.flow.dom.ElementEffect;
2929
import com.vaadin.flow.function.SerializableBiConsumer;
3030
import com.vaadin.flow.function.SerializableFunction;
31+
import com.vaadin.flow.function.SerializableRunnable;
3132
import com.vaadin.flow.shared.Registration;
3233
import com.vaadin.signals.Signal;
3334
import com.vaadin.signals.function.EffectAction;
@@ -47,10 +48,10 @@
4748
*/
4849
public final class ComponentEffect implements Serializable {
4950

50-
private transient ElementEffect elementEffect;
51+
private ElementEffect elementEffect;
5152

5253
private <C extends Component> ComponentEffect(C owner,
53-
Runnable effectFunction) {
54+
SerializableRunnable effectFunction) {
5455
this.elementEffect = new ElementEffect(owner.getElement(),
5556
effectFunction);
5657
}
@@ -83,7 +84,7 @@ private <C extends Component> ComponentEffect(C owner,
8384
* function
8485
*/
8586
public static <C extends Component> Registration effect(C owner,
86-
Runnable effectFunction) {
87+
SerializableRunnable effectFunction) {
8788
ComponentEffect effect = new ComponentEffect(owner, effectFunction);
8889
return effect::close;
8990
}

flow-server/src/main/java/com/vaadin/flow/dom/ElementEffect.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.vaadin.flow.component.UI;
2424
import com.vaadin.flow.component.UIDetachedException;
2525
import com.vaadin.flow.function.SerializableBiConsumer;
26+
import com.vaadin.flow.function.SerializableRunnable;
2627
import com.vaadin.flow.server.ErrorEvent;
2728
import com.vaadin.flow.shared.Registration;
2829
import com.vaadin.signals.Signal;
@@ -43,12 +44,12 @@
4344
* @since 25.0
4445
*/
4546
public final class ElementEffect implements Serializable {
46-
private transient final Runnable effectFunction;
47+
private final SerializableRunnable effectFunction;
4748
private boolean closed = false;
48-
private transient Effect effect = null;
49+
private Effect effect = null;
4950
private Registration detachRegistration;
5051

51-
public ElementEffect(Element owner, Runnable effectFunction) {
52+
public ElementEffect(Element owner, SerializableRunnable effectFunction) {
5253
Objects.requireNonNull(owner, "Owner element cannot be null");
5354
Objects.requireNonNull(effectFunction,
5455
"Effect function cannot be null");
@@ -99,7 +100,8 @@ public ElementEffect(Element owner, Runnable effectFunction) {
99100
* @return a {@link Registration} that can be used to remove the effect
100101
* function
101102
*/
102-
public static Registration effect(Element owner, Runnable effectFunction) {
103+
public static Registration effect(Element owner,
104+
SerializableRunnable effectFunction) {
103105
ElementEffect effect = new ElementEffect(owner, effectFunction);
104106
return effect::close;
105107
}

flow-server/src/test/java/com/vaadin/tests/server/component/FlowClassesSerializableTest.java

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,39 @@
1616
package com.vaadin.tests.server.component;
1717

1818
import java.io.OutputStream;
19+
import java.util.concurrent.locks.Lock;
20+
import java.util.concurrent.locks.ReentrantLock;
1921

2022
import org.junit.Assert;
2123
import org.junit.Test;
24+
import org.mockito.Mockito;
2225

2326
import com.vaadin.flow.component.Component;
2427
import com.vaadin.flow.component.HtmlComponent;
2528
import com.vaadin.flow.component.HtmlContainer;
2629
import com.vaadin.flow.component.UI;
2730
import com.vaadin.flow.dom.Element;
31+
import com.vaadin.flow.internal.CurrentInstance;
32+
import com.vaadin.flow.internal.nodefeature.ElementPropertyMap;
33+
import com.vaadin.flow.internal.nodefeature.PropertyChangeDeniedException;
2834
import com.vaadin.flow.server.Command;
35+
import com.vaadin.flow.server.MockVaadinServletService;
36+
import com.vaadin.flow.server.MockVaadinSession;
2937
import com.vaadin.flow.server.StreamReceiver;
3038
import com.vaadin.flow.server.StreamVariable;
39+
import com.vaadin.flow.server.VaadinService;
40+
import com.vaadin.flow.server.VaadinSession;
41+
import com.vaadin.flow.server.WrappedSession;
42+
import com.vaadin.flow.server.startup.ApplicationConfiguration;
3143
import com.vaadin.flow.testutil.ClassesSerializableTest;
44+
import com.vaadin.signals.local.ValueSignal;
45+
import com.vaadin.tests.util.MockUI;
3246

3347
import static org.junit.Assert.assertEquals;
48+
import static org.junit.Assert.assertNotNull;
3449
import static org.junit.Assert.assertNotSame;
3550
import static org.junit.Assert.assertTrue;
51+
import static org.junit.Assert.fail;
3652

3753
public class FlowClassesSerializableTest extends ClassesSerializableTest {
3854

@@ -78,6 +94,128 @@ public void streamResource() throws Throwable {
7894
}
7995
}
8096

97+
@Test
98+
public void componentEffectSerializable() {
99+
CurrentInstance.clearAll();
100+
var service = new MockVaadinServletService() {
101+
private final Lock lock = new ReentrantLock();
102+
{
103+
lock.lock();
104+
}
105+
106+
@Override
107+
protected Lock getSessionLock(WrappedSession wrappedSession) {
108+
return lock;
109+
}
110+
111+
@Override
112+
public void init() {
113+
super.init();
114+
115+
ApplicationConfiguration configuration = Mockito
116+
.mock(ApplicationConfiguration.class);
117+
Mockito.when(configuration.isProductionMode()).thenReturn(
118+
getDeploymentConfiguration().isProductionMode());
119+
Mockito.when(
120+
configuration.isDevModeSessionSerializationEnabled())
121+
.thenReturn(true);
122+
getContext().setAttribute(ApplicationConfiguration.class,
123+
configuration);
124+
}
125+
};
126+
VaadinService.setCurrent(service);
127+
var session = new MockVaadinSession(service);
128+
session.lock();
129+
session.refreshTransients(null, service);
130+
MockUI ui = new MockUI(session);
131+
ui.doInit(null, 42, "foo");
132+
session.addUI(ui);
133+
134+
ValueSignal<String> signal = new ValueSignal<>("initial");
135+
SerializedComponent component = new SerializedComponent(signal);
136+
ui.add(component);
137+
Assert.assertEquals(1, component.effectExecutionCounter);
138+
139+
// verify that signal works before serialization
140+
signal.value("changed");
141+
Assert.assertEquals(2, component.effectExecutionCounter);
142+
143+
SerializedComponent deserializedComponent;
144+
VaadinSession deserializedSession = null;
145+
session.unlock(); // serialization happens for unlocked session
146+
try {
147+
deserializedSession = serializeAndDeserialize(session);
148+
assertNotNull(deserializedSession);
149+
assertNotSame(deserializedSession, session);
150+
} catch (Throwable e) {
151+
fail("ComponentEffect should be serializable: " + e.getClass()
152+
+ ": " + e.getMessage());
153+
}
154+
deserializedSession.refreshTransients(null, service);
155+
deserializedSession.lock();
156+
157+
UI deserializedUi = deserializedSession.getUIs().iterator().next();
158+
deserializedComponent = deserializedUi.getChildren()
159+
.filter(SerializedComponent.class::isInstance)
160+
.map(SerializedComponent.class::cast).findFirst()
161+
.orElseThrow(() -> new AssertionError(
162+
"SerializedComponent has not been deserialized"));
163+
assertNotSame(deserializedComponent, component);
164+
165+
UI.setCurrent(deserializedUi);
166+
deserializedComponent.signal.value("changed after deserialization");
167+
Assert.assertEquals(3, deserializedComponent.effectExecutionCounter);
168+
deserializedComponent.signal.value("changed");
169+
Assert.assertEquals(4, deserializedComponent.effectExecutionCounter);
170+
171+
signal.value("changed in original signal");
172+
// original signal change should not affect deserialized component
173+
Assert.assertEquals(4, deserializedComponent.effectExecutionCounter);
174+
175+
// remove registration and verify that effect is not called anymore
176+
deserializedComponent.registration.remove();
177+
deserializedComponent.signal.value("foo");
178+
Assert.assertEquals(4, deserializedComponent.effectExecutionCounter);
179+
180+
// verify various bindX methods
181+
Assert.assertEquals("foo",
182+
deserializedComponent.getElement().getText());
183+
Assert.assertEquals("foo",
184+
deserializedComponent.getElement().getAttribute("attr"));
185+
Assert.assertEquals("foo",
186+
deserializedComponent.getElement().getProperty("prop"));
187+
Assert.assertEquals("foo!!!",
188+
deserializedComponent.getElement().getProperty("two-way-prop"));
189+
// verify that two-way-binding works
190+
emulateClientUpdate(deserializedComponent.getElement(), "two-way-prop",
191+
"bar!!!");
192+
Assert.assertEquals("bar!!!",
193+
deserializedComponent.getElement().getProperty("two-way-prop"));
194+
Assert.assertEquals("bar", deserializedComponent.signal.peek());
195+
196+
// verify mapped and computed signals with bindEnabled and bindVisible
197+
Assert.assertTrue(deserializedComponent.getElement().isEnabled());
198+
Assert.assertTrue(deserializedComponent.getElement().isVisible());
199+
deserializedComponent.signal.value(null);
200+
Assert.assertFalse(deserializedComponent.getElement().isEnabled());
201+
Assert.assertFalse(deserializedComponent.getElement().isVisible());
202+
203+
deserializedSession.unlock();
204+
VaadinService.setCurrent(null);
205+
}
206+
207+
private void emulateClientUpdate(Element element, String property,
208+
String value) {
209+
ElementPropertyMap childModel = ElementPropertyMap
210+
.getModel(element.getNode());
211+
try {
212+
childModel.deferredUpdateFromClient(property, value);
213+
} catch (PropertyChangeDeniedException e) {
214+
Assert.fail(
215+
"Failed to update property from client: " + e.getMessage());
216+
}
217+
}
218+
81219
private static class MyStreamVariable implements StreamVariable {
82220
@Override
83221
public OutputStream getOutputStream() {
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.tests.server.component;
17+
18+
import com.vaadin.flow.component.Component;
19+
import com.vaadin.flow.component.ComponentEffect;
20+
import com.vaadin.flow.component.Tag;
21+
import com.vaadin.flow.shared.Registration;
22+
import com.vaadin.signals.Signal;
23+
import com.vaadin.signals.local.ValueSignal;
24+
25+
/**
26+
* Serializable test component using ComponentEffect.
27+
*/
28+
@Tag("div")
29+
class SerializedComponent extends Component {
30+
int effectExecutionCounter = 0;
31+
ValueSignal<String> signal;
32+
Registration registration;
33+
34+
SerializedComponent(ValueSignal<String> signal) {
35+
this.signal = signal;
36+
37+
registration = ComponentEffect.effect(this, () -> {
38+
signal.value();
39+
effectExecutionCounter++;
40+
});
41+
42+
getElement().bindText(signal);
43+
getElement().bindAttribute("attr", signal);
44+
getElement().bindProperty("prop", signal);
45+
getElement().bindEnabled(
46+
signal.map(value -> value != null && !value.isEmpty()));
47+
getElement().bindVisible(Signal.computed(() -> signal.value() != null));
48+
49+
getElement().bindProperty("two-way-prop", signal.map(str -> str + "!!!",
50+
(str, value) -> value.replace("!!!", "")));
51+
52+
// sync property from the client
53+
getElement().addPropertyChangeListener("two-way-prop",
54+
"two-way-prop-changed", e -> {
55+
});
56+
}
57+
58+
}

signals/src/main/java/com/vaadin/signals/Id.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.signals;
1717

18+
import java.io.Serializable;
1819
import java.math.BigInteger;
1920
import java.util.Base64;
2021
import java.util.Base64.Encoder;
@@ -36,7 +37,7 @@
3637
* @param value
3738
* the id value as a 64-bit integer
3839
*/
39-
public record Id(long value) implements Comparable<Id> {
40+
public record Id(long value) implements Comparable<Id>, Serializable {
4041
/**
4142
* Default or initial id in various contexts. Always used for the root node
4243
* in a signal hierarchy. The zero id is frequently used and has a custom

signals/src/main/java/com/vaadin/signals/Node.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.signals;
1717

18+
import java.io.Serializable;
1819
import java.util.List;
1920
import java.util.Map;
2021
import java.util.Objects;
@@ -39,7 +40,7 @@
3940
@Type(value = Node.Alias.class, name = "a")
4041

4142
})
42-
public sealed interface Node {
43+
public sealed interface Node extends Serializable {
4344

4445
/**
4546
* An empty data node without parent, scope owner, value or children and the

signals/src/main/java/com/vaadin/signals/Signal.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.signals;
1717

18+
import java.io.Serializable;
1819
import java.util.Objects;
1920

2021
import com.vaadin.signals.function.CleanupCallback;
@@ -49,7 +50,7 @@
4950
* the signal value type
5051
*/
5152
@FunctionalInterface
52-
public interface Signal<T> {
53+
public interface Signal<T> extends Serializable {
5354
/**
5455
* Gets the current value of this signal. The value is read in a way that
5556
* takes the current transaction into account and in the case of clustering

signals/src/main/java/com/vaadin/signals/SignalCommand.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.signals;
1717

18+
import java.io.Serializable;
1819
import java.util.List;
1920
import java.util.Map;
2021

@@ -55,7 +56,7 @@
5556
@Type(value = SignalCommand.SnapshotCommand.class, name = "snapshot"),
5657

5758
})
58-
public sealed interface SignalCommand {
59+
public sealed interface SignalCommand extends Serializable {
5960
/**
6061
* A signal command that sets the value of a signal.
6162
*/

signals/src/main/java/com/vaadin/signals/function/CleanupCallback.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package com.vaadin.signals.function;
1717

18+
import java.io.Serializable;
19+
1820
import com.vaadin.signals.Signal;
1921

2022
/**
@@ -28,7 +30,7 @@
2830
* @see Signal#effect(EffectAction)
2931
*/
3032
@FunctionalInterface
31-
public interface CleanupCallback {
33+
public interface CleanupCallback extends Serializable {
3234
/**
3335
* Performs cleanup operations such as unregistering listeners or disposing
3436
* resources.

signals/src/main/java/com/vaadin/signals/function/CommandValidator.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.vaadin.signals.function;
1717

18+
import java.io.Serializable;
1819
import java.util.Objects;
1920

2021
import com.vaadin.signals.SignalCommand;
@@ -28,7 +29,7 @@
2829
* require multiple validation rules to pass.
2930
*/
3031
@FunctionalInterface
31-
public interface CommandValidator {
32+
public interface CommandValidator extends Serializable {
3233
/**
3334
* A validator that accepts all commands without restriction.
3435
*/

0 commit comments

Comments
 (0)