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()