8000 feat: initialize signals environment (#21490) (#21518) · vaadin/flow@8ef07e5 · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 8ef07e5

Browse files
vaadin-botmcollovatimshabarov
authored
feat: initialize signals environment (#21490) (#21518)
* chore: add feature flag for Flow full-stack signals Closes #21474 * feat: initialize signals environment Closes #21475 * test using static mock * Update flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java --------- Co-authored-by: Marco Collovati <marco@vaadin.com> Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent 6544098 commit 8ef07e5

File tree

13 files changed

+580
-72
lines changed

13 files changed

+580
-72
lines changed

flow-server/pom.xml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<project xmlns="http://maven.apache.org/POM/4.0.0"
3-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
45
<modelVersion>4.0.0</modelVersion>
56
<parent>
67
<groupId>com.vaadin</groupId>
@@ -21,7 +22,11 @@
2122
<artifactId>flow-push</artifactId>
2223
<version>${project.version}</version>
2324
</dependency>
24-
25+
<dependency>
26+
<groupId>com.vaadin</groupId>
27+
<artifactId>signals</artifactId>
28+
<version>${project.version}</version>
29+
</dependency>
2530
<dependency>
2631
<groupId>com.vaadin.servletdetector</groupId>
2732
<artifactId>throw-if-servlet3</artifactId>
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* Copyright 2000-2025 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.experimental;
17+
18+
/**
19+
* Exception thrown when attempting to use a feature controlled by a feature
20+
* flag that is not enabled at runtime.
21+
* <p>
22+
* This exception is thrown when code attempts to use experimental functionality
23+
* that requires an explicit opt-in via the FeatureFlags system. To resolve this
24+
* exception, ensure the corresponding feature is enabled before using the
25+
* functionality.
26+
* <p>
27+
* For internal use only. May be renamed or removed in a future release.
28+
*/
29+
public class DisabledFeatureException extends RuntimeException {
30+
31+
/**
32+
* Constructs an exception for when an attempt is made to use a feature that
33+
* is disabled.
34+
*
35+
* @param feature
36+
* the disabled feature that was attempted to be used
37+
*/
38+
public DisabledFeatureException(Feature feature) {
39+
super("""
40+
'%s' is currently an experimental feature and needs to be \
41+
explicitly enabled. The feature can be enabled using Copilot, in the \
42+
experimental features tab, or by adding a \
43+
`src/main/resources/vaadin-featureflags.properties` file with the following content: \
44+
`com.vaadin.experimental.%s=true`"""
45+
.formatted(feature.getTitle(), feature.getId()));
46+
47+
}
48+
}

flow-server/src/main/java/com/vaadin/experimental/FeatureFlags.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ public class FeatureFlags implements Serializable {
8585
"Hilla Full-stack Signals", "fullstackSignals",
8686
"https://github.com/vaadin/hilla/discussions/1902", true, null);
8787

88+
public static final Feature FLOW_FULLSTACK_SIGNALS = new Feature(
89+
"Flow Full-stack Signals", "flowFullstackSignals",
90+
"https://github.com/vaadin/platform/issues/7373", true, null);
91+
8892
public static final Feature DASHBOARD_COMPONENT = new Feature(
8993
"Dashboard component (Pro)", "dashboardComponent",
9094
"https://github.com/vaadin/platform/issues/6626", true,
@@ -144,6 +148,7 @@ public FeatureFlags(Lookup lookup) {
144148
features.add(new Feature(FORM_FILLER_ADDON));
145149
features.add(new Feature(HILLA_I18N));
146150
features.add(new Feature(HILLA_FULLSTACK_SIGNALS));
151+
features.add(new Feature(FLOW_FULLSTACK_SIGNALS));
147152
features.add(new Feature(COPILOT_EXPERIMENTAL));
148153
features.add(new Feature(DASHBOARD_COMPONENT));
149154
features.add(new Feature(CARD_COMPONENT));

flow-server/src/main/java/com/vaadin/flow/server/VaadinService.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,19 @@
5151
import java.util.concurrent.atomic.AtomicInteger;
5252
import java.util.concurrent.locks.Lock;
5353
import java.util.concurrent.locks.ReentrantLock;
54+
import java.util.function.Supplier;
5455
import java.util.stream.Collectors;
5556
import java.util.stream.Stream;
5657
import java.util.stream.StreamSupport;
5758

59+
import com.fasterxml.jackson.databind.ObjectMapper;
5860
import com.fasterxml.jackson.databind.node.ObjectNode;
61+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
5962
import org.slf4j.Logger;
6063
import org.slf4j.LoggerFactory;
6164

65+
import com.vaadin.experimental.DisabledFeatureException;
66+
import com.vaadin.experimental.FeatureFlags;
6267
import com.vaadin.flow.component.UI;
6368
import com.vaadin.flow.di.DefaultInstantiator;
6469
import com.vaadin.flow.di.Instantiator;
@@ -95,6 +100,7 @@
95100
import com.vaadin.flow.shared.JsonConstants;
96101
import com.vaadin.flow.shared.Registration;
97102
import com.vaadin.flow.shared.communication.PushMode;
103+
import com.vaadin.signals.SignalEnvironment;
98104

99105
import static java.nio.charset.StandardCharsets.UTF_8;
100106

@@ -318,6 +324,8 @@ public void init() throws ServiceException {
318324
+ " providing a custom Executor instance.");
319325
}
320326

327+
initSignalsEnvironment();
328+
321329
DeploymentConfiguration configuration = getDeploymentConfiguration();
322330
if (!configuration.isProductionMode()) {
323331
Logger logger = getLogger();
@@ -344,6 +352,54 @@ public void init() throws ServiceException {
344352
initialized = true;
345353
}
346354

355+
private void initSignalsEnvironment() {
356+
Executor signalsExecutor;
357+
Supplier<Executor> flowDispatcherOverride;
358+
FeatureFlags featureFlags = FeatureFlags.get(getContext());
359+
if (featureFlags
360+
.isEnabled(FeatureFlags.FLOW_FULLSTACK_SIGNALS.getId())) {
361+
signalsExecutor = this.executor;
362+
flowDispatcherOverride = () -> {
363+
UI owner = UI.getCurrent();
364+
if (owner == null) {
365+
return null;
366+
}
367+
368+
return task -> {
369+
if (UI.getCurrent() == owner) {
370+
task.run();
371+
} else {
372+
try {
373+
SignalEnvironment.defaultDispatcher()
374+
.execute(() -> owner.access(task::run));
375+
} catch (Exception e) {
376+
// a task is submitted when executor is shut down,
377+
// ignore
378+
}
379+
}
380+
};
381+
};
382+
} else {
383+
signalsExecutor = task -> {
384+
throw new DisabledFeatureException(
385+
FeatureFlags.FLOW_FULLSTACK_SIGNALS);
386+
};
387+
flowDispatcherOverride = () -> {
388+
throw new DisabledFeatureException(
389+
FeatureFlags.FLOW_FULLSTACK_SIGNALS);
390+
};
391+
}
392+
if (!SignalEnvironment.tryInitialize(createDefaultObjectMapper(),
393+
signalsExecutor)) {
394+
getLogger().warn("Signals environment is already initialized. "
395+
+ "It is recommended to let Vaadin setup Signals environment to prevent unexpected behavior. "
396+
+ "Please, avoid calling SignalEnvironment.tryInitialize() in application code.");
397+
}
398+
Runnable unregister = SignalEnvironment
399+
.addDispatcherOverride(flowDispatcherOverride);
400+
addServiceDestroyListener(event -> unregister.run());
401+
}
402+
347403
private void addRouterUsageStatistics() {
348404
if (UsageStatistics.getEntries().anyMatch(
349405
e -> Constants.STATISTIC_ROUTING_CLIENT.equals(e.getName()))) {
@@ -620,6 +676,20 @@ public Executor getExecutor() {
620676
return executor;
621677
}
622678

679+
/**
680+
* Creates and configures a default instance of {@link ObjectMapper}. The
681+
* configured {@link ObjectMapper} includes the registration of the
682+
* {@link JavaTimeModule} to handle serialization and deserialization of
683+
* Java time API objects.
684+
*
685+
* @return the configured {@link ObjectMapper} instance
686+
*/
687+
protected ObjectMapper createDefaultObjectMapper() {
688+
ObjectMapper objectMapper = new ObjectMapper();
689+
objectMapper.registerModule(new JavaTimeModule());
690+
return objectMapper;
691+
}
692+
623693
/**
624694
* Gets the class loader to use for loading classes loaded by name, e.g.
625695
* custom UI classes. This is by default the class loader that was used to

flow-server/src/test/java/com/vaadin/flow/hotswap/HotswapperTest.java

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,15 @@ public class HotswapperTest {
8686

8787
Hotswapper hotswapper;
8888
Lookup lookup;
89-
private VaadinService service;
89+
private MockVaadinServletService service;
9090
private VaadinHotswapper flowHotswapper;
9191
private VaadinHotswapper hillaHotswapper;
9292
private BrowserLiveReload liveReload;
9393

9494
@Before
9595
public void setup() {
96-
lookup = Mockito.mock(Lookup.class);
9796
service = new MockVaadinServletService();
98-
service.getContext().setAttribute(Lookup.class, lookup);
97+
lookup = service.getLookup();
9998

10099
ApplicationConfiguration appConfig = Mockito
101100
.mock(ApplicationConfiguration.class);
@@ -1098,14 +1097,18 @@ public Registration addUIInitListener(UIInitListener listener) {
10981097
uiInitInstalled.set(true);
10991098
return super.addUIInitListener(listener);
11001099
}
1100+
1101+
@Override
1102+
protected void instrumentMockLookup(Lookup lookup) {
1103+
ApplicationConfiguration appConfig = Mockito
1104+
.mock(ApplicationConfiguration.class);
1105+
Mockito.when(appConfig.isProductionMode()).then(
1106+
i -> getDeploymentConfiguration().isProductionMode());
1107+
Mockito.when(
1108+
lookup.lookup(ApplicationConfigurationFactory.class))
1109+
.thenReturn(context -> appConfig);
1110+
}
11011111
};
1102-
ApplicationConfiguration appConfig = Mockito
1103-
.mock(ApplicationConfiguration.class);
1104-
Mockito.when(appConfig.isProductionMode()).then(i -> vaadinService
1105-
.getDeploymentConfiguration().isProductionMode());
1106-
Mockito.when(lookup.lookup(ApplicationConfigurationFactory.class))
1107-
.thenReturn(context -> appConfig);
1108-
vaadinService.getContext().setAttribute(Lookup.class, lookup);
11091112
Hotswapper.register(vaadinService);
11101113

11111114
Assert.assertTrue(
@@ -1135,21 +1138,27 @@ public void register_productionMode_trackingListenerNotInstalled() {
11351138
@Override
11361139
public Registration addSessionInitListener(
11371140
SessionInitListener listener) {
1138-
sessionInitInstalled.set(true);
1141+
if (listener instanceof Hotswapper) {
1142+
sessionInitInstalled.set(true);
1143+
}
11391144
return super.addSessionInitListener(listener);
11401145
}
11411146

11421147
@Override
11431148
public Registration addSessionDestroyListener(
11441149
SessionDestroyListener listener) {
1145-
sessionDestroyInstalled.set(true);
1150+
if (listener instanceof Hotswapper) {
1151+
sessionDestroyInstalled.set(true);
1152+
}
11461153
return super.addSessionDestroyListener(listener);
11471154
}
11481155

11491156
@Override
11501157
public Registration addServiceDestroyListener(
11511158
ServiceDestroyListener listener) {
1152-
serviceDestroyInstalled.set(true);
1159+
if (listener instanceof Hotswapper) {
1160+
serviceDestroyInstalled.set(true);
1161+
}
11531162
return super.addServiceDestroyListener(listener);
11541163
}
11551164
};

flow-server/src/test/java/com/vaadin/flow/server/MockVaadinServletService.java

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,23 @@
1515
*/
1616
package com.vaadin.flow.server;
1717

18+
import jakarta.servlet.ServletException;
19+
20+
import java.lang.reflect.Field;
1821
import java.util.Collections;
1922
import java.util.List;
23+
import java.util.concurrent.CopyOnWriteArrayList;
24+
import java.util.concurrent.atomic.AtomicReference;
2025

21-
import jakarta.servlet.ServletException;
26+
import org.mockito.Mockito;
2227

2328
import com.vaadin.flow.di.Instantiator;
29+
import com.vaadin.flow.di.Lookup;
2430
import com.vaadin.flow.function.DeploymentConfiguration;
31+
import com.vaadin.flow.router.DefaultRoutePathProvider;
32+
import com.vaadin.flow.router.RoutePathProvider;
2533
import com.vaadin.flow.router.Router;
34+
import com.vaadin.signals.SignalEnvironment;
2635
import com.vaadin.tests.util.MockDeploymentConfiguration;
2736

2837
/**
@@ -38,6 +47,8 @@ public class MockVaadinServletService extends VaadinServletService {
3847

3948
private DeploymentConfiguration configuration;
4049

50+
private Lookup lookup;
51+
4152
private static class MockVaadinServlet extends VaadinServlet {
4253

4354
private final DeploymentConfiguration configuration;
@@ -117,24 +128,58 @@ protected Instantiator createInstantiator() throws ServiceException {
117128
@Override
118129
public void init() {
119130
try {
131+
resetSignalEnvironment();
120132
MockVaadinServlet servlet = (MockVaadinServlet) getServlet();
121133
servlet.service = this;
122134
if (getServlet().getServletConfig() == null) {
123135
getServlet().init(new MockServletConfig());
124136
}
137+
if (lookup == null
138+
&& getContext().getAttribute(Lookup.class) == null) {
139+
lookup = Mockito.mock(Lookup.class);
140+
Mockito.when(lookup.lookup(RoutePathProvider.class))
141+
.thenReturn(new DefaultRoutePathProvider());
142+
instrumentMockLookup(lookup);
143+
getContext().setAttribute(Lookup.class, lookup);
144+
}
125145
super.init();
126146
} catch (ServiceException | ServletException e) {
127147
throw new RuntimeException(e);
128148
}
129149
}
130150

151+
protected void instrumentMockLookup(Lookup lookup) {
152+
// no-op
153+
}
154+
131155
public void setConfiguration(DeploymentConfiguration configuration) {
132156
this.configuration = configuration;
133157
}
134158

159+
public Lookup getLookup() {
160+
return lookup;
161+
}
162+
135163
@Override
136164
public DeploymentConfiguration getDeploymentConfiguration() {
137165
return configuration != null ? configuration
138166
: super.getDeploymentConfiguration();
139167
}
168+
169+
private void resetSignalEnvironment() {
170+
try {
171+
Field state = SignalEnvironment.class.getDeclaredField("state");
172+
state.setAccessible(true);
173+
((AtomicReference<?>) state.get(null)).set(null);
174+
175+
Field dispatcherOverrides = SignalEnvironment.class
176+
.getDeclaredField("dispatcherOverrides");
177+
dispatcherOverrides.setAccessible(true);
178+
((List<?>) dispatcherOverrides.get(null)).clear();
179+
180+
} catch (Exception e) {
181+
throw new AssertionError("Failed to reset Signal environment", e);
182+
}
183+
}
184+
140185
}

0 commit comments

Comments
 (0)
0