From de913a2b2defd04f9ed82f11f988c245264b597c Mon Sep 17 00:00:00 2001
From: "totally-not-ai[bot]"
<290682512+totally-not-ai[bot]@users.noreply.github.com>
Date: Fri, 23 Oct 2026 15:35:23 +0000
Subject: [PATCH 1/4] feat: expose pending JavaScript invocations without
draining the queue
---
.../flow/component/internal/UIInternals.java | 19 +++++++++++++------
1 file changed, 13 insertions(+), 6 deletions(-)
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
index bbb761d7cc9..a5e9cee6465 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
@@ -993,14 +993,21 @@ private void checkInvocationQueueLock() {
}
/**
- * Gets the pending javascript invocations added with
- * {@link #addJavaScriptInvocation(PendingJavaScriptInvocation)} after last
- * {@link #dumpPendingJavaScriptInvocations()}.
+ * Gets the pending JavaScript invocations added with
+ * {@link #addJavaScriptInvocation(PendingJavaScriptInvocation)} after the
+ * last {@link #dumpPendingJavaScriptInvocations()}, leaving them in the
+ * queue.
+ *
+ * Unlike {@link #dumpPendingJavaScriptInvocations()}, this does not mark
+ * anything as sent, so it lets something that drives the client side
+ * without a browser read the queue, act on the calls it simulates and
+ * cancel them with {@link PendingJavaScriptInvocation#cancelExecution()},
+ * while every other invocation stays queued. Canceled invocations are not
+ * included.
*
- * @return the pending javascript invocations, never null
+ * @return the pending JavaScript invocations, never null
*/
- // Non-private for testing purposes
- Stream getPendingJavaScriptInvocations() {
+ public Stream getPendingJavaScriptInvocations() {
session.checkHasLock();
return pendingJsInvocations.stream()
.filter(invocation -> !invocation.isCanceled());
From d3b8e8191aa578010298e8c3926078a86e1246be Mon Sep 17 00:00:00 2001
From: "totally-not-ai[bot]"
<290682512+totally-not-ai[bot]@users.noreply.github.com>
Date: Fri, 23 Oct 2026 15:40:42 +0000
Subject: [PATCH 2/4] fix: return a snapshot of the pending JavaScript
invocations
---
.../flow/component/internal/UIInternals.java | 13 +++++++---
.../component/internal/UIInternalsTest.java | 25 +++++++++++++++++++
2 files changed, 35 insertions(+), 3 deletions(-)
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
index a5e9cee6465..6b9c9dd5b37 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
@@ -1002,15 +1002,22 @@ private void checkInvocationQueueLock() {
* anything as sent, so it lets something that drives the client side
* without a browser read the queue, act on the calls it simulates and
* cancel them with {@link PendingJavaScriptInvocation#cancelExecution()},
- * while every other invocation stays queued. Canceled invocations are not
- * included.
+ * while every other invocation stays queued. Canceling reports a failure to
+ * an error handler registered on the result of the call.
+ *
+ * The invocations owned by invisible nodes, which a dump retains instead of
+ * sending, are included; check
+ * {@link PendingJavaScriptInvocation#getOwner()} to tell them apart.
+ * Canceled invocations are not included. The stream is a snapshot, so the
+ * queue can be changed while it is consumed.
*
* @return the pending JavaScript invocations, never null
*/
public Stream getPendingJavaScriptInvocations() {
session.checkHasLock();
return pendingJsInvocations.stream()
- .filter(invocation -> !invocation.isCanceled());
+ .filter(invocation -> !invocation.isCanceled()).toList()
+ .stream();
}
/**
diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
index 9fa39806374..200e90cada5 100644
--- a/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
@@ -449,6 +449,31 @@ void dumpPendingJavaScriptInvocations_invocationCanceled_pendingListPurged() {
assertEquals(0, internals.getPendingJavaScriptInvocations().count());
}
+ @Test
+ void getPendingJavaScriptInvocations_oneCanceledWhileReading_othersStillDumped() {
+ StateNode node = new StateNode(ElementData.class);
+ internals.getStateTree().getRootNode()
+ .getFeature(ElementChildrenList.class).add(0, node);
+
+ PendingJavaScriptInvocation canceled = new PendingJavaScriptInvocation(
+ node, new UIInternals.JavaScriptInvocation("1"));
+ PendingJavaScriptInvocation kept = new PendingJavaScriptInvocation(node,
+ new UIInternals.JavaScriptInvocation("2"));
+ PendingJavaScriptInvocation queuedOnCancel = new PendingJavaScriptInvocation(
+ node, new UIInternals.JavaScriptInvocation("3"));
+ canceled.then(value -> {
+ }, error -> internals.addJavaScriptInvocation(queuedOnCancel));
+ internals.addJavaScriptInvocation(canceled);
+ internals.addJavaScriptInvocation(kept);
+
+ internals.getPendingJavaScriptInvocations()
+ .filter(invocation -> invocation == canceled)
+ .forEach(PendingJavaScriptInvocation::cancelExecution);
+
+ assertEquals(List.of(kept, queuedOnCancel),
+ internals.dumpPendingJavaScriptInvocations());
+ }
+
@Test
void executeJs_elementMovedBeforeDump_invocationStillSent() {
UI realUI = new UI();
From 9eec6c088c72563c5940aa2a0482028cddfa80d8 Mon Sep 17 00:00:00 2001
From: "totally-not-ai[bot]"
<290682512+totally-not-ai[bot]@users.noreply.github.com>
Date: Fri, 23 Oct 2026 16:04:18 +0000
Subject: [PATCH 3/4] refactor: use Stream.toList() for production dependency
warning
---
.../java/com/vaadin/flow/component/internal/UIInternals.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
index 6b9c9dd5b37..958bef9c3f3 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
@@ -1515,11 +1515,11 @@ private void warnForUnavailableBundledDependencies(
// absence is not a problem
jsDeps.addAll(dependencies.getJavaScripts().stream().filter(
js -> !FrontendDependencyUrlResolver.isRuntimeJavaScript(js))
- .map(JavaScript::value).collect(Collectors.toList()));
+ .map(JavaScript::value).toList());
jsDeps.addAll(dependencies.getJsModules().stream().map(JsModule::value)
.filter(src -> !FrontendDependencyUrlResolver
.isRuntimeDependencyUrl(src))
- .collect(Collectors.toList()));
+ .toList());
if (!jsDeps.isEmpty()) {
maybeWarnAboutDependencies(componentClass, jsDeps);
From ef966c0bf351e44f895d2f06b7554c8db45ea5bb Mon Sep 17 00:00:00 2001
From: "totally-not-ai[bot]"
<290682512+totally-not-ai[bot]@users.noreply.github.com>
Date: Sat, 24 Oct 2026 10:20:20 +0000
Subject: [PATCH 4/4] refactor: return pending JavaScript invocations as a list
---
.../flow/component/internal/UIInternals.java | 21 +++++++++++--------
.../flow/component/internal/PageTest.java | 2 +-
.../component/internal/UIInternalsTest.java | 13 ++++++------
3 files changed, 20 insertions(+), 16 deletions(-)
diff --git a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
index 958bef9c3f3..13e91082596 100644
--- a/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
+++ b/flow-server/src/main/java/com/vaadin/flow/component/internal/UIInternals.java
@@ -922,7 +922,7 @@ public List dumpPendingJavaScriptInvocations() {
return Collections.emptyList();
}
- Map> partition = getPendingJavaScriptInvocations()
+ Map> partition = streamPendingJavaScriptInvocations()
.collect(Collectors.partitioningBy(
invocation -> invocation.getOwner().isVisible()));
@@ -1008,16 +1008,19 @@ private void checkInvocationQueueLock() {
* The invocations owned by invisible nodes, which a dump retains instead of
* sending, are included; check
* {@link PendingJavaScriptInvocation#getOwner()} to tell them apart.
- * Canceled invocations are not included. The stream is a snapshot, so the
- * queue can be changed while it is consumed.
+ * Canceled invocations are not included. The list is an unmodifiable
+ * snapshot, so the queue can be changed while it is iterated.
*
* @return the pending JavaScript invocations, never null
*/
- public Stream getPendingJavaScriptInvocations() {
+ public List getPendingJavaScriptInvocations() {
+ return streamPendingJavaScriptInvocations().toList();
+ }
+
+ private Stream streamPendingJavaScriptInvocations() {
session.checkHasLock();
return pendingJsInvocations.stream()
- .filter(invocation -> !invocation.isCanceled()).toList()
- .stream();
+ .filter(invocation -> !invocation.isCanceled());
}
/**
@@ -1030,7 +1033,7 @@ public Stream getPendingJavaScriptInvocations() {
* @since 24.4
*/
public boolean containsPendingJavascript(String containsFilter) {
- return getPendingJavaScriptInvocations().anyMatch(js -> js
+ return streamPendingJavaScriptInvocations().anyMatch(js -> js
.getInvocation().getExpression().contains(containsFilter));
}
@@ -1055,7 +1058,7 @@ public boolean containsPendingJavascript(String containsFilter) {
*/
public boolean containsPendingJsCall(Class> definitionType,
String methodName) {
- return getPendingJavaScriptInvocations()
+ return streamPendingJavaScriptInvocations()
.map(js -> js.getInvocation().getJsCall())
.anyMatch(call -> call != null
&& call.definitionType() == definitionType
@@ -1828,7 +1831,7 @@ public Router getRouter() {
* false otherwise
*/
public boolean isDirty() {
- return getStateTree().isDirty() || getPendingJavaScriptInvocations()
+ return getStateTree().isDirty() || streamPendingJavaScriptInvocations()
.anyMatch(invocation -> invocation.getOwner().isVisible());
}
diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/PageTest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/PageTest.java
index 10255c09cd5..00adbfb8fc0 100644
--- a/flow-server/src/test/java/com/vaadin/flow/component/internal/PageTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/PageTest.java
@@ -120,7 +120,7 @@ void addDynamicImport_dynamicDependencyIsAvaialbleViaGetPendingSendToClient() {
}
private long countPendingInvocations() {
- return ui.getInternals().getPendingJavaScriptInvocations().count();
+ return ui.getInternals().getPendingJavaScriptInvocations().size();
}
}
diff --git a/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java b/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
index 200e90cada5..1a5ba96b1a2 100644
--- a/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
+++ b/flow-server/src/test/java/com/vaadin/flow/component/internal/UIInternalsTest.java
@@ -403,13 +403,13 @@ void dumpPendingJavaScriptInvocations_ownerDetached_onlyItsInvocationsDiscarded(
internals.dumpPendingJavaScriptInvocations();
- assertEquals(3, internals.getPendingJavaScriptInvocations().count(),
+ assertEquals(3, internals.getPendingJavaScriptInvocations().size(),
"Invocations of invisible components should be retained");
detachedNode.setParent(null);
assertEquals(List.of(otherInvocation),
- internals.getPendingJavaScriptInvocations().toList(),
+ internals.getPendingJavaScriptInvocations(),
"Only the invocations of the detached owner should be discarded");
assertEquals(List.of(otherNode),
List.copyOf(invocationOwners(internals)),
@@ -446,7 +446,7 @@ void dumpPendingJavaScriptInvocations_invocationCanceled_pendingListPurged() {
invocation.cancelExecution();
- assertEquals(0, internals.getPendingJavaScriptInvocations().count());
+ assertEquals(0, internals.getPendingJavaScriptInvocations().size());
}
@Test
@@ -466,7 +466,7 @@ void getPendingJavaScriptInvocations_oneCanceledWhileReading_othersStillDumped()
internals.addJavaScriptInvocation(canceled);
internals.addJavaScriptInvocation(kept);
- internals.getPendingJavaScriptInvocations()
+ internals.getPendingJavaScriptInvocations().stream()
.filter(invocation -> invocation == canceled)
.forEach(PendingJavaScriptInvocation::cancelExecution);
@@ -736,11 +736,12 @@ void setTitle_titleAndPendingJsInvocationSetsCorrectTitle() {
internals.setTitle("new title");
assertEquals("new title", internals.getTitle());
- assertEquals(1, internals.getPendingJavaScriptInvocations().count(),
+ assertEquals(1, internals.getPendingJavaScriptInvocations().size(),
"one pending JavaScript invocation should exist");
var pendingJavaScriptInvocation = internals
- .getPendingJavaScriptInvocations().findFirst().orElse(null);
+ .getPendingJavaScriptInvocations().stream().findFirst()
+ .orElse(null);
assertNotNull(pendingJavaScriptInvocation,
"pendingJavaScriptInvocation should not be null");
assertEquals("new title", pendingJavaScriptInvocation.getInvocation()