Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,6 @@ public ApplicationConnection(
Console.debug(
"Vaadin application servlet version: " + servletVersion);
}

ConnectionIndicator.setState(ConnectionIndicator.LOADING);
}

/**
Expand Down Expand Up @@ -210,12 +208,10 @@ private native void publishJavascriptMethods(String applicationId,
var ur = ap.@ApplicationConnection::registry.@com.vaadin.client.Registry::getURIResolver()();
return ur.@com.vaadin.client.URIResolver::resolveVaadinUri(Ljava/lang/String;)(uriToResolve);
});

client.sendEventMessage = $entry(function(nodeId, eventType, eventData) {
var sc = ap.@ApplicationConnection::registry.@com.vaadin.client.Registry::getServerConnector()();
sc.@com.vaadin.client.communication.ServerConnector::sendEventMessage(ILjava/lang/String;Lelemental/json/JsonObject;)(nodeId,eventType,eventData);
});

client.initializing = false;
client.exportedWebComponents = exportedWebComponents;
$wnd.Vaadin.Flow.clients[applicationId] = client;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,48 @@ public static native void setProperty(String property, Object value)
$wnd.Vaadin.connectionIndicator[property] = value;
}
}-*/;

/**
* Notifies the client-side connection state indicator that a loading
* operation has started.
* <p>
* This method triggers the {@link ConnectionIndicator#LOADING} state
* transition on the client side.
*/
public static native void loadingStarted()
/*-{
if ($wnd.Vaadin.connectionState) {
$wnd.Vaadin.connectionState.loadingStarted();
}
}-*/;

/**
* Notifies the client-side connection state indicator that a loading
* operation has completed successfully.
* <p>
* When all requests finish, this method triggers the
* {@link ConnectionIndicator#CONNECTED} state transition on the client
* side.
*/
public static native void loadingFinished()
/*-{
if ($wnd.Vaadin.connectionState) {
$wnd.Vaadin.connectionState.loadingFinished();
}
}-*/;

/**
* Notifies the client-side connection state indicator that a loading
* operation has encountered an error or failed.
* <p>
* If no requests are remaining, triggers the
* {@link ConnectionIndicator#CONNECTION_LOST} state transition on the
* client side.
*/
public static native void loadingFailed()
/*-{
if ($wnd.Vaadin.connectionState) {
$wnd.Vaadin.connectionState.loadingFailed();
}
}-*/;
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.vaadin.client.communication.ConnectionStateHandler;
import com.vaadin.client.communication.DefaultConnectionStateHandler;
import com.vaadin.client.communication.Heartbeat;
import com.vaadin.client.communication.LoadingIndicatorStateHandler;
import com.vaadin.client.communication.MessageHandler;
import com.vaadin.client.communication.MessageSender;
import com.vaadin.client.communication.Poller;
Expand Down Expand Up @@ -87,6 +88,8 @@ public DefaultRegistry(ApplicationConnection connection,
set(PushConfiguration.class, new PushConfiguration(this));
set(ReconnectConfiguration.class, new ReconnectConfiguration(this));
set(Poller.class, new Poller(this));
set(LoadingIndicatorStateHandler.class,
new LoadingIndicatorStateHandler(this));
}

}
10 changes: 10 additions & 0 deletions flow-client/src/main/java/com/vaadin/client/Registry.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import com.vaadin.client.communication.ConnectionStateHandler;
import com.vaadin.client.communication.Heartbeat;
import com.vaadin.client.communication.LoadingIndicatorStateHandler;
import com.vaadin.client.communication.MessageHandler;
import com.vaadin.client.communication.MessageSender;
import com.vaadin.client.communication.Poller;
Expand Down Expand Up @@ -319,6 +320,15 @@ public Poller getPoller() {
return get(Poller.class);
}

/**
* Gets the {@link LoadingIndicatorStateHandler} singleton.
*
* @return the {@link LoadingIndicatorStateHandler} singleton instance
*/
public LoadingIndicatorStateHandler getLoadingIndicatorStateHandler() {
return get(LoadingIndicatorStateHandler.class);
}

/**
* Deletes and recreates resettable instances of registry singletons.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,16 @@ private void resolveTemporaryError(Type type) {
scheduledReconnect.cancel();
scheduledReconnect = null;
}
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);
if (Type.HEARTBEAT.equals(type)) {
// Heartbeat never has loading indication, it is safe to assume
// that no other requests are in progress and set the `CONNECTED`
// state directly.
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);
} else {
// Let the loading indicator state handler check and remove
// the prior loading state indication if necessary.
registry.getLoadingIndicatorStateHandler().stopLoading();
}

Console.debug("Re-established connection to server");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2000-2026 Vaadin Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.vaadin.client.communication;

import com.google.gwt.core.client.Scheduler;

import com.vaadin.client.ConnectionIndicator;
import com.vaadin.client.Registry;
import com.vaadin.client.flow.collection.JsCollections;
import com.vaadin.client.flow.collection.JsSet;
import com.vaadin.flow.shared.JsonConstants;

/**
* Manages the state of loading indicator based on active RPC requests, event
* types, and lifecycle events.
* <p>
* This class ensures appropriate visual feedback (e.g., loading bar) is shown
* or hidden according to the current network conditions and request status. It
* is responsible for muting the loading indication when RPC requests are
* triggered by high-frequency UI events (mousemove and such) to avoid excessive
* visual noise in these cases.
*/
public class LoadingIndicatorStateHandler {
private final Registry registry;

private boolean loading = false;

private boolean showLoading = false;

// High-frequency events, whose related RPC requests are not expected
// to trigger loading indication.
private static final JsSet<String> SILENT_EVENT_TYPES = JsCollections.set();
{
JsCollections.array("keydown", "keypress", "keyup", "mousemove",
"pointermove", "pointerrawupdate", "touchmove", "beforeinput",
"input", "scroll", "wheel", "drag", "dragover")
.forEach(SILENT_EVENT_TYPES::add);
}

/**
* Creates a new instance connected to the given registry.
*
* @param registry
* the global registry
*/
public LoadingIndicatorStateHandler(Registry registry) {
this.registry = registry;
}

/**
* Updates the connection state to {@link ConnectionIndicator#LOADING} when
* a non-silent request starts.
*/
public void startLoading() {
if (!showLoading) {
// The next request is muted, do not show loading.
return;
}

update();
}

/**
* Updates the connection state to {@link ConnectionIndicator#CONNECTED}
* when active requests finish.
*/
public void stopLoading() {
if (registry.getRequestResponseTracker().hasActiveRequest()) {
// Some request is in progress, skip the current stop.
return;
}

// Reset the loading state
showLoading = false;

// Debounce the update to avoid hiding loading when a follow-up
// request is started or scheduled right away.
Scheduler.get().scheduleDeferred(this::update);
}

/**
* Processes an RPC message to determine if a loading indicator should be
* displayed.
*
* @param rpcType
* the type of RPC request being processed
* @param eventType
* for event RPC requests, the name of the event, otherwise
* {@code null}
*/
public void processMessage(String rpcType, String eventType) {
// Require at least one non-silent message to indicate loading for
// the next request.
boolean silent = JsonConstants.RPC_EVENT_TYPE.equals(rpcType)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be RPC_TYPE_EVENT? The constants have the same "event" value, but RPC_EVENT_TYPE seems incorrect here.

&& eventType != null && SILENT_EVENT_TYPES.has(eventType);
if (!silent) {
showLoading = true;
}
}

/**
* Applies the loading state change after a dirty check.
*/
private void update() {
if (showLoading == loading) {
return;
}

loading = showLoading;
// Setting the loading state directly using
// `ConnectionIndicator.setState()` interferes with other loading
// parties
// (Flow router, Hilla requests), therefore `.loadingStarted()` /
// `.loadingFinished()` are preferred.
if (loading) {
ConnectionIndicator.loadingStarted();
} else {
ConnectionIndicator.loadingFinished();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ private void endRequestIfResponse(ValueMap json) {
// End the request if the received message was a
// response, not sent asynchronously
registry.getRequestResponseTracker().endRequest();
registry.getLoadingIndicatorStateHandler().stopLoading();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.Timer;

import com.vaadin.client.ConnectionIndicator;
import com.vaadin.client.Console;
import com.vaadin.client.Registry;
import com.vaadin.flow.shared.ApplicationConstants;
Expand Down Expand Up @@ -137,7 +136,6 @@ private void doSendInvocationsToServer() {
+ pushPendingMessage.toJson());
JsonObject payload = pushPendingMessage;
pushPendingMessage = null;
registry.getRequestResponseTracker().startRequest();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why the call to startRequest() can be removed?
Same in the send method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having multiple calls to startRequest() on different branches of message sending code path is needlessly complex and confusing.

The sendPayload(final JsonObject payload) actually already takes care of calling startRequest() itself just before actually sending any message, regardless of the transport (XHR or push). This makes it an ideal single place candidate to keep for me.

In this instance, the sendPayload(payload) invoked below also call startRequest() if that was not done already, therefore making the call here is probably unnecessary.

In the send() case, one of the branches on the code path ends up calling startRequest() and then queueing the message for later instead of actually sending it with sendPayload(payload), which seems just inaccurate to me. Again, in that case, the queued message will get sent eventually through the sendPayload(payload) method, and during that time the startRequest() will be called by sendPayload(payload) itself.

Now that the code path is cleaned up a bit, it would rather make sense to me to remove if guarding against the “another request active” exception around startRequest() in sendPayload(payload), which feels like something added earlier to aid with multiple and sometimes missed startRequest() calls in various code path branches. Would that make sense?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably it can be removed, but I have some concerns about the "reconnect" path.

sendPayload(payload);
return;
} else if (hasQueuedMessages()) {
Expand All @@ -156,7 +154,6 @@ private void doSendInvocationsToServer() {
return;
}

boolean showLoadingIndicator = serverRpcQueue.showLoadingIndicator();
JsonArray reqJson = serverRpcQueue.toJson();
serverRpcQueue.clear();

Expand All @@ -177,9 +174,7 @@ private void doSendInvocationsToServer() {
resetTimer();
extraJson.put(ApplicationConstants.RESYNCHRONIZE_ID, true);
}
if (showLoadingIndicator) {
ConnectionIndicator.setState(ConnectionIndicator.LOADING);
}
registry.getLoadingIndicatorStateHandler().startLoading();
send(reqJson, extraJson);
}

Expand All @@ -193,7 +188,6 @@ private void doSendInvocationsToServer() {
*/
protected void send(final JsonArray reqInvocations,
final JsonObject extraJson) {
registry.getRequestResponseTracker().startRequest();
send(preparePayload(reqInvocations, extraJson));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
import com.google.web.bindery.event.shared.EventBus;
import com.google.web.bindery.event.shared.HandlerRegistration;

import com.vaadin.client.ConnectionIndicator;
import com.vaadin.client.Registry;
import com.vaadin.client.communication.MessageSender.ResynchronizationState;
import com.vaadin.client.gwt.com.google.web.bindery.event.shared.SimpleEventBus;
Expand Down Expand Up @@ -120,13 +119,6 @@ public void endRequest() {
registry.getMessageSender().sendInvocationsToServer();
}

// Always reset loading indicator when request ends.
// Client-side component will handle timing for each request
// independently.
// This ensures rapid successive requests get individual timing instead
// of accumulating time across requests.
ConnectionIndicator.setState(ConnectionIndicator.CONNECTED);

fireEvent(new ResponseHandlingEndedEvent());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,9 @@ public void sendReturnChannelMessage(int stateNodeId, int channelId,
}

private void sendMessage(JsonObject message) {
registry.getLoadingIndicatorStateHandler().processMessage(
message.getString(JsonConstants.RPC_TYPE),
message.getString(JsonConstants.RPC_EVENT_TYPE));
ServerRpcQueue rpcQueue = registry.getServerRpcQueue();
rpcQueue.add(message);
rpcQueue.flush();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,40 @@
* from being run as a regular JVM unit test.
*/
public abstract class ClientEngineTestBase extends GWTTestCase {
protected static native void createDummyConnectionState()
/*-{
if (!$wnd.Vaadin) {
$wnd.Vaadin = {};
}
if (!$wnd.Vaadin.connectionState) {
$wnd.Vaadin.connectionState = {
state: 'connected',
requestCount: 0,
setState: function(state) {
this.state = state;
},
loadingStarted: function() {
this.state = 'loading';
this.requestCount++;
},
loadingFinished: function() {
if (this.requestCount == 0) { return; }
this.requestCount--;
if (this.requestCount == 0) { this.state = 'connected'; }
},
loadingFailed: function() {
if (this.requestCount == 0) { return; }
this.requestCount--;
if (this.requestCount == 0) { this.state = 'connection-lost'; }
}
};
} else {
// reset to initial state
$wnd.Vaadin.connectionState.setState('connected');
$wnd.Vaadin.connectionState.requestCount = 0;
}
}-*/;

@Override
protected void gwtSetUp() throws Exception {
installPolyfills();
Expand Down
Loading
Loading