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..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())); @@ -993,14 +993,31 @@ 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. 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 list is an unmodifiable + * snapshot, so the queue can be changed while it is iterated. * - * @return the pending javascript invocations, never null + * @return the pending JavaScript invocations, never null */ - // Non-private for testing purposes - Stream getPendingJavaScriptInvocations() { + public List getPendingJavaScriptInvocations() { + return streamPendingJavaScriptInvocations().toList(); + } + + private Stream streamPendingJavaScriptInvocations() { session.checkHasLock(); return pendingJsInvocations.stream() .filter(invocation -> !invocation.isCanceled()); @@ -1016,7 +1033,7 @@ Stream getPendingJavaScriptInvocations() { * @since 24.4 */ public boolean containsPendingJavascript(String containsFilter) { - return getPendingJavaScriptInvocations().anyMatch(js -> js + return streamPendingJavaScriptInvocations().anyMatch(js -> js .getInvocation().getExpression().contains(containsFilter)); } @@ -1041,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 @@ -1501,11 +1518,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); @@ -1814,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 9fa39806374..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,32 @@ void dumpPendingJavaScriptInvocations_invocationCanceled_pendingListPurged() { invocation.cancelExecution(); - assertEquals(0, internals.getPendingJavaScriptInvocations().count()); + assertEquals(0, internals.getPendingJavaScriptInvocations().size()); + } + + @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().stream() + .filter(invocation -> invocation == canceled) + .forEach(PendingJavaScriptInvocation::cancelExecution); + + assertEquals(List.of(kept, queuedOnCancel), + internals.dumpPendingJavaScriptInvocations()); } @Test @@ -711,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()