Skip to content

Commit d88a521

Browse files
vaadin-botplatoshamcollovatimshabarov
authored
fix(flow-client): loading state muting based on trigger events (#24230) (CP: 25.1) (#24335)
This PR cherry-picks changes from the original PR #24230 to branch 25.1. --- #### Original PR description > Fixes #24075 > > This change reverts the eager removal of loading state introduced by #23229, as it causes the indication to disappear during ongoing loading. As a replacement, it re-introduces debouncing tracking of active requests, and adds event-based silencing of the loading indication to avoid flashing the indicator for high-frequency UI interactions. > > In addition, instead of setting loading state using `ConnectionState.setState()` directly, the proper connection state methods (`loadingStarted()`, `loadingFinished()`) are used to avoid interference with loading state for requests from other sources outside Flow client. > Co-authored-by: Anton Platonov <anton@vaadin.com> Co-authored-by: Marco Collovati <marco@vaadin.com> Co-authored-by: Mikhail Shabarov <61410877+mshabarov@users.noreply.github.com>
1 parent de7942d commit d88a521

21 files changed

Lines changed: 754 additions & 34 deletions

flow-client/src/main/java/com/vaadin/client/ApplicationConnection.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,6 @@ public ApplicationConnection(
9090
Console.debug(
9191
"Vaadin application servlet version: " + servletVersion);
9292
}
93-
94-
ConnectionIndicator.setState(ConnectionIndicator.LOADING);
9593
}
9694

9795
/**
@@ -210,12 +208,10 @@ private native void publishJavascriptMethods(String applicationId,
210208
var ur = ap.@ApplicationConnection::registry.@com.vaadin.client.Registry::getURIResolver()();
211209
return ur.@com.vaadin.client.URIResolver::resolveVaadinUri(Ljava/lang/String;)(uriToResolve);
212210
});
213-
214211
client.sendEventMessage = $entry(function(nodeId, eventType, eventData) {
215212
var sc = ap.@ApplicationConnection::registry.@com.vaadin.client.Registry::getServerConnector()();
216213
sc.@com.vaadin.client.communication.ServerConnector::sendEventMessage(ILjava/lang/String;Lelemental/json/JsonObject;)(nodeId,eventType,eventData);
217214
});
218-
219215
client.initializing = false;
220216
client.exportedWebComponents = exportedWebComponents;
221217
$wnd.Vaadin.Flow.clients[applicationId] = client;

flow-client/src/main/java/com/vaadin/client/ConnectionIndicator.java

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,4 +100,48 @@ public static native void setProperty(String property, Object value)
100100
$wnd.Vaadin.connectionIndicator[property] = value;
101101
}
102102
}-*/;
103+
104+
/**
105+
* Notifies the client-side connection state indicator that a loading
106+
* operation has started.
107+
* <p>
108+
* This method triggers the {@link ConnectionIndicator#LOADING} state
109+
* transition on the client side.
110+
*/
111+
public static native void loadingStarted()
112+
/*-{
113+
if ($wnd.Vaadin.connectionState) {
114+
$wnd.Vaadin.connectionState.loadingStarted();
115+
}
116+
}-*/;
117+
118+
/**
119+
* Notifies the client-side connection state indicator that a loading
120+
* operation has completed successfully.
121+
* <p>
122+
* When all requests finish, this method triggers the
123+
* {@link ConnectionIndicator#CONNECTED} state transition on the client
124+
* side.
125+
*/
126+
public static native void loadingFinished()
127+
/*-{
128+
if ($wnd.Vaadin.connectionState) {
129+
$wnd.Vaadin.connectionState.loadingFinished();
130+
}
131+
}-*/;
132+
133+
/**
134+
* Notifies the client-side connection state indicator that a loading
135+
* operation has encountered an error or failed.
136+
* <p>
137+
* If no requests are remaining, triggers the
138+
* {@link ConnectionIndicator#CONNECTION_LOST} state transition on the
139+
* client side.
140+
*/
141+
public static native void loadingFailed()
142+
/*-{
143+
if ($wnd.Vaadin.connectionState) {
144+
$wnd.Vaadin.connectionState.loadingFailed();
145+
}
146+
}-*/;
103147
}

flow-client/src/main/java/com/vaadin/client/DefaultRegistry.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.vaadin.client.communication.ConnectionStateHandler;
2121
import com.vaadin.client.communication.DefaultConnectionStateHandler;
2222
import com.vaadin.client.communication.Heartbeat;
23+
import com.vaadin.client.communication.LoadingIndicatorStateHandler;
2324
import com.vaadin.client.communication.MessageHandler;
2425
import com.vaadin.client.communication.MessageSender;
2526
import com.vaadin.client.communication.Poller;
@@ -87,6 +88,8 @@ public DefaultRegistry(ApplicationConnection connection,
8788
set(PushConfiguration.class, new PushConfiguration(this));
8889
set(ReconnectConfiguration.class, new ReconnectConfiguration(this));
8990
set(Poller.class, new Poller(this));
91+
set(LoadingIndicatorStateHandler.class,
92+
new LoadingIndicatorStateHandler(this));
9093
}
9194

9295
}

flow-client/src/main/java/com/vaadin/client/Registry.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
import com.vaadin.client.communication.ConnectionStateHandler;
2121
import com.vaadin.client.communication.Heartbeat;
22+
import com.vaadin.client.communication.LoadingIndicatorStateHandler;
2223
import com.vaadin.client.communication.MessageHandler;
2324
import com.vaadin.client.communication.MessageSender;
2425
import com.vaadin.client.communication.Poller;
@@ -319,6 +320,15 @@ public Poller getPoller() {
319320
return get(Poller.class);
320321
}
321322

323+
/**
324+
* Gets the {@link LoadingIndicatorStateHandler} singleton.
325+
*
326+
* @return the {@link LoadingIndicatorStateHandler} singleton instance
327+
*/
328+
public LoadingIndicatorStateHandler getLoadingIndicatorStateHandler() {
329+
return get(LoadingIndicatorStateHandler.class);
330+
}
331+
322332
/**
323333
* Deletes and recreates resettable instances of registry singletons.
324334
*/

flow-client/src/main/java/com/vaadin/client/communication/DefaultConnectionStateHandler.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,16 @@ private void resolveTemporaryError(Type type) {
460460
scheduledReconnect.cancel();
461461
scheduledReconnect = null;
462462
}
463-
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);
463+
if (Type.HEARTBEAT.equals(type)) {
464+
// Heartbeat never has loading indication, it is safe to assume
465+
// that no other requests are in progress and set the `CONNECTED`
466+
// state directly.
467+
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);
468+
} else {
469+
// Let the loading indicator state handler check and remove
470+
// the prior loading state indication if necessary.
471+
registry.getLoadingIndicatorStateHandler().stopLoading();
472+
}
464473

465474
Console.debug("Re-established connection to server");
466475
}
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
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.client.communication;
17+
18+
import com.google.gwt.core.client.Scheduler;
19+
20+
import com.vaadin.client.ConnectionIndicator;
21+
import com.vaadin.client.Registry;
22+
import com.vaadin.client.flow.collection.JsCollections;
23+
import com.vaadin.client.flow.collection.JsSet;
24+
import com.vaadin.flow.shared.JsonConstants;
25+
26+
/**
27+
* Manages the state of loading indicator based on active RPC requests, event
28+
* types, and lifecycle events.
29+
* <p>
30+
* This class ensures appropriate visual feedback (e.g., loading bar) is shown
31+
* or hidden according to the current network conditions and request status. It
32+
* is responsible for muting the loading indication when RPC requests are
33+
* triggered by high-frequency UI events (mousemove and such) to avoid excessive
34+
* visual noise in these cases.
35+
*/
36+
public class LoadingIndicatorStateHandler {
37+
private final Registry registry;
38+
39+
private boolean loading = false;
40+
41+
private boolean showLoading = false;
42+
43+
// High-frequency events, whose related RPC requests are not expected
44+
// to trigger loading indication.
45+
private static final JsSet<String> SILENT_EVENT_TYPES = JsCollections.set();
46+
{
47+
JsCollections.array("keydown", "keypress", "keyup", "mousemove",
48+
"pointermove", "pointerrawupdate", "touchmove", "beforeinput",
49+
"input", "scroll", "wheel", "drag", "dragover")
50+
.forEach(SILENT_EVENT_TYPES::add);
51+
}
52+
53+
/**
54+
* Creates a new instance connected to the given registry.
55+
*
56+
* @param registry
57+
* the global registry
58+
*/
59+
public LoadingIndicatorStateHandler(Registry registry) {
60+
this.registry = registry;
61+
}
62+
63+
/**
64+
* Updates the connection state to {@link ConnectionIndicator#LOADING} when
65+
* a non-silent request starts.
66+
*/
67+
public void startLoading() {
68+
if (!showLoading) {
69+
// The next request is muted, do not show loading.
70+
return;
71+
}
72+
73+
update();
74+
}
75+
76+
/**
77+
* Updates the connection state to {@link ConnectionIndicator#CONNECTED}
78+
* when active requests finish.
79+
*/
80+
public void stopLoading() {
81+
if (registry.getRequestResponseTracker().hasActiveRequest()) {
82+
// Some request is in progress, skip the current stop.
83+
return;
84+
}
85+
86+
// Reset the loading state
87+
showLoading = false;
88+
89+
// Debounce the update to avoid hiding loading when a follow-up
90+
// request is started or scheduled right away.
91+
Scheduler.get().scheduleDeferred(this::update);
92+
}
93+
94+
/**
95+
* Processes an RPC message to determine if a loading indicator should be
96+
* displayed.
97+
*
98+
* @param rpcType
99+
* the type of RPC request being processed
100+
* @param eventType
101+
* for event RPC requests, the name of the event, otherwise
102+
* {@code null}
103+
*/
104+
public void processMessage(String rpcType, String eventType) {
105+
// Require at least one non-silent message to indicate loading for
106+
// the next request.
107+
boolean silent = JsonConstants.RPC_TYPE_EVENT.equals(rpcType)
108+
&& eventType != null && SILENT_EVENT_TYPES.has(eventType);
109+
if (!silent) {
110+
showLoading = true;
111+
}
112+
}
113+
114+
/**
115+
* Applies the loading state change after a dirty check.
116+
*/
117+
private void update() {
118+
if (showLoading == loading) {
119+
return;
120+
}
121+
122+
loading = showLoading;
123+
// Setting the loading state directly using
124+
// `ConnectionIndicator.setState()` interferes with other loading
125+
// parties
126+
// (Flow router, Hilla requests), therefore `.loadingStarted()` /
127+
// `.loadingFinished()` are preferred.
128+
if (loading) {
129+
ConnectionIndicator.loadingStarted();
130+
} else {
131+
ConnectionIndicator.loadingFinished();
132+
}
133+
}
134+
}

flow-client/src/main/java/com/vaadin/client/communication/MessageHandler.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,7 @@ private void endRequestIfResponse(ValueMap json) {
592592
// End the request if the received message was a
593593
// response, not sent asynchronously
594594
registry.getRequestResponseTracker().endRequest();
595+
registry.getLoadingIndicatorStateHandler().stopLoading();
595596
}
596597
}
597598

flow-client/src/main/java/com/vaadin/client/communication/MessageSender.java

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import com.google.gwt.core.client.GWT;
2222
import com.google.gwt.user.client.Timer;
2323

24-
import com.vaadin.client.ConnectionIndicator;
2524
import com.vaadin.client.Console;
2625
import com.vaadin.client.Registry;
2726
import com.vaadin.flow.shared.ApplicationConstants;
@@ -137,7 +136,6 @@ private void doSendInvocationsToServer() {
137136
+ pushPendingMessage.toJson());
138137
JsonObject payload = pushPendingMessage;
139138
pushPendingMessage = null;
140-
registry.getRequestResponseTracker().startRequest();
141139
sendPayload(payload);
142140
return;
143141
} else if (hasQueuedMessages()) {
@@ -156,7 +154,6 @@ private void doSendInvocationsToServer() {
156154
return;
157155
}
158156

159-
boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator();
160157
JsonArray reqJson = serverRpcQueue.toJson();
161158
serverRpcQueue.clear();
162159

@@ -177,9 +174,7 @@ private void doSendInvocationsToServer() {
177174
resetTimer();
178175
extraJson.put(ApplicationConstants.RESYNCHRONIZE_ID, true);
179176
}
180-
if (showLoadingIndicator) {
181-
ConnectionIndicator.setState(ConnectionIndicator.LOADING);
182-
}
177+
registry.getLoadingIndicatorStateHandler().startLoading();
183178
send(reqJson, extraJson);
184179
}
185180

@@ -193,7 +188,6 @@ private void doSendInvocationsToServer() {
193188
*/
194189
protected void send(final JsonArray reqInvocations,
195190
final JsonObject extraJson) {
196-
registry.getRequestResponseTracker().startRequest();
197191
send(preparePayload(reqInvocations, extraJson));
198192
}
199193

flow-client/src/main/java/com/vaadin/client/communication/RequestResponseTracker.java

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import com.google.web.bindery.event.shared.EventBus;
2020
import com.google.web.bindery.event.shared.HandlerRegistration;
2121

22-
import com.vaadin.client.ConnectionIndicator;
2322
import com.vaadin.client.Registry;
2423
import com.vaadin.client.communication.MessageSender.ResynchronizationState;
2524
import com.vaadin.client.gwt.com.google.web.bindery.event.shared.SimpleEventBus;
@@ -120,13 +119,6 @@ public void endRequest() {
120119
registry.getMessageSender().sendInvocationsToServer();
121120
}
122121

123-
// Always reset loading indicator when request ends.
124-
// Client-side component will handle timing for each request
125-
// independently.
126-
// This ensures rapid successive requests get individual timing instead
127-
// of accumulating time across requests.
128-
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);
129-
130122
fireEvent(new ResponseHandlingEndedEvent());
131123
}
132124

flow-client/src/main/java/com/vaadin/client/communication/ServerConnector.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,9 @@ public void sendReturnChannelMessage(int stateNodeId, int channelId,
244244
}
245245

246246
private void sendMessage(JsonObject message) {
247+
registry.getLoadingIndicatorStateHandler().processMessage(
248+
message.getString(JsonConstants.RPC_TYPE),
249+
message.getString(JsonConstants.RPC_EVENT_TYPE));
247250
ServerRpcQueue rpcQueue = registry.getServerRpcQueue();
248251
rpcQueue.add(message);
249252
rpcQueue.flush();

0 commit comments

Comments
 (0)