diff --git a/docs/src/main/asciidoc/websockets-next-reference.adoc b/docs/src/main/asciidoc/websockets-next-reference.adoc index e90a96fe2284c..23d223caef1ca 100644 --- a/docs/src/main/asciidoc/websockets-next-reference.adoc +++ b/docs/src/main/asciidoc/websockets-next-reference.adoc @@ -4,7 +4,7 @@ and pull requests should be submitted there: https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc //// [id="websockets-next-reference-guide"] -= WebSockets Next extension reference guide += WebSockets Next reference guide :extension-status: preview include::_attributes.adoc[] :numbered: @@ -78,7 +78,7 @@ implementation("io.quarkus:quarkus-websockets-next") == Endpoints -Both the server and client APIs allow you to define _endpoints_ that are used to consume and send messages. +Both the <> and <> define _endpoints_ that are used to consume and send messages. The endpoints are implemented as CDI beans and support injection. Endpoints declare <> annotated with `@OnTextMessage`, `@OnBinaryMessage`, `@OnPong`, `@OnOpen`, `@OnClose` and `@OnError`. These methods are used to handle various WebSocket events. @@ -559,6 +559,7 @@ This means that if an endpoint receives events `A` and `B` (in this particular o However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits. For this cases, the `InboundProcessingMode#CONCURRENT` should be used. +[[server-api]] == Server API === HTTP server configuration @@ -900,14 +901,15 @@ public class CustomTenantResolver implements TenantResolver { ---- For more information on Hibernate multitenancy, refer to the https://quarkus.io/guides/hibernate-orm#multitenancy[hibernate documentation]. +[[client-api]] == Client API [[client-connectors]] === Client connectors -The `io.quarkus.websockets.next.WebSocketConnector` is used to configure and create new connections for client endpoints. -A CDI bean that implements this interface is provided and can be injected in other beans. -The actual type argument is used to determine the client endpoint. +A connector can be used to configure and open a new client connection backed by a client endpoint that is used to consume and send messages. +Quarkus provides a CDI bean with bean type `io.quarkus.websockets.next.WebSocketConnector` and default qualifer that can be injected in other beans. +The actual type argument of an injection point is used to determine the client endpoint. The type is validated during build - if it does not represent a client endpoint the build fails. Let’s consider the following client endpoint: @@ -955,6 +957,31 @@ public class MyBean { NOTE: If an application attempts to inject a connector for a missing endpoint, an error is thrown. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance> connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + ==== Basic connector In the case where the application developer does not need the combination of the client endpoint and the connector, a _basic connector_ can be used. @@ -991,6 +1018,31 @@ The basic connector is closer to a low-level API and is reserved for advanced us However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans. It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus. +Connectors are not thread-safe and should not be used concurrently. +Connectors should also not be reused. +If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`: + +[source, java] +---- +import jakarta.enterprise.inject.Instance; + +@Singleton +public class MyBean { + + @Inject + Instance connector; + + void connect() { + var connection1 = connector.get().baseUri(uri) + .addHeader("Foo", "alpha") + .connectAndAwait(); + var connection2 = connector.get().baseUri(uri) + .addHeader("Foo", "bravo") + .connectAndAwait(); + } +} +---- + [[ws-client-connection]] === WebSocket client connection diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java new file mode 100644 index 0000000000000..b885c5c82f0ff --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/ClientEndpointProgrammaticTest.java @@ -0,0 +1,114 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.websockets.next.HandshakeRequest; +import io.quarkus.websockets.next.OnClose; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import io.quarkus.websockets.next.WebSocketClient; +import io.quarkus.websockets.next.WebSocketClientConnection; +import io.quarkus.websockets.next.WebSocketConnector; + +public class ClientEndpointProgrammaticTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(ServerEndpoint.class, ClientEndpoint.class); + }); + + @Inject + Instance> connector; + + @TestHTTPResource("/") + URI uri; + + @Test + void testClient() throws InterruptedException { + WebSocketClientConnection connection1 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Lu") + .connectAndAwait(); + connection1.sendTextAndAwait("Hi!"); + + WebSocketClientConnection connection2 = connector + .get() + .baseUri(uri) + .addHeader("Foo", "Ma") + .connectAndAwait(); + connection2.sendTextAndAwait("Hi!"); + + assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hello Lu!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hi!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hello Ma!")); + assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hi!"), ClientEndpoint.MESSAGES.toString()); + + connection1.closeAndAwait(); + connection2.closeAndAwait(); + assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS)); + } + + @WebSocket(path = "/endpoint") + public static class ServerEndpoint { + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnOpen + String open(HandshakeRequest handshakeRequest) { + return "Hello " + handshakeRequest.header("Foo") + "!"; + } + + @OnTextMessage + String echo(String message) { + return message; + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + + @WebSocketClient(path = "/endpoint") + public static class ClientEndpoint { + + static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(4); + + static final List MESSAGES = new CopyOnWriteArrayList<>(); + + static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2); + + @OnTextMessage + void onMessage(String message, HandshakeRequest handshakeRequest) { + MESSAGES.add(handshakeRequest.header("Foo") + ":" + message); + MESSAGE_LATCH.countDown(); + } + + @OnClose + void close() { + CLOSED_LATCH.countDown(); + } + + } + +} diff --git a/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java new file mode 100644 index 0000000000000..80a4a39226b82 --- /dev/null +++ b/extensions/websockets-next/deployment/src/test/java/io/quarkus/websockets/next/test/client/programmatic/InvalidConnectorProgrammaticInjectionPointTest.java @@ -0,0 +1,40 @@ +package io.quarkus.websockets.next.test.client.programmatic; + +import static org.junit.jupiter.api.Assertions.fail; + +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.websockets.next.WebSocketClientException; +import io.quarkus.websockets.next.WebSocketConnector; + +public class InvalidConnectorProgrammaticInjectionPointTest { + + @RegisterExtension + public static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot(root -> { + root.addClasses(Service.class); + }) + .setExpectedException(WebSocketClientException.class, true); + + @Test + void testInvalidInjectionPoint() { + fail(); + } + + @Unremovable + @Singleton + public static class Service { + + @Inject + Instance> invalid; + + } + +} diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java index 98ab5ac1596e2..f261b513dce2a 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java @@ -5,6 +5,9 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.quarkus.arc.Arc; import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; @@ -12,10 +15,30 @@ import io.vertx.core.buffer.Buffer; /** - * This basic connector can be used to configure and open new client connections. Unlike with {@link WebSocketConnector} a - * client endpoint class is not needed. + * A basic connector can be used to configure and open a new client connection. Unlike with {@link WebSocketConnector} a + * client endpoint is not used to consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code BasicWebSocketConnector} and qualifier {@link Default}. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<BasicWebSocketConnector> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @see WebSocketClientConnection */ diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java index e4270cc8b54ae..0f99844f49ecd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open client connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenClientConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenClientConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java index c8a5c797289c7..6f5f59c2cdbf0 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java @@ -4,12 +4,14 @@ import java.util.Optional; import java.util.stream.Stream; +import jakarta.enterprise.inject.Default; + import io.smallrye.common.annotation.Experimental; /** * Provides convenient access to all open connections. *

- * Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface. + * Quarkus provides a CDI bean with bean type {@link OpenConnections} and qualifier {@link Default}. */ @Experimental("This API is experimental and may change in the future") public interface OpenConnections extends Iterable { diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java index e33f95bea1e54..d6c11f3f5bccd 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java @@ -5,7 +5,7 @@ /** * This interface represents a client connection to a WebSocket endpoint. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocketClient} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocketClient} * endpoint and used to interact with the connected server. */ @Experimental("This API is experimental and may change in the future") diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java index c5deaa339b216..ea82ba5942b4d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java @@ -8,7 +8,7 @@ /** * This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server. *

- * Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocket} + * Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocket} * endpoint and used to interact with the connected client, or all clients connected to the endpoint respectively * (broadcasting). *

diff --git a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java index 06f91ddf3e919..9a170e4f1431d 100644 --- a/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java +++ b/extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java @@ -3,14 +3,40 @@ import java.net.URI; import java.net.URLEncoder; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; + import io.smallrye.common.annotation.CheckReturnValue; import io.smallrye.common.annotation.Experimental; import io.smallrye.mutiny.Uni; /** - * This connector can be used to configure and open new client connections using a client endpoint class. + * A connector can be used to configure and open a new client connection backed by a client endpoint that is used to + * consume and send messages. + *

+ * Quarkus provides a CDI bean with bean type {@code WebSocketConnector} and qualifier {@link Default}. The actual type + * argument of an injection point is used to determine the client endpoint. The type is validated during build + * and if it does not represent a client endpoint then the build fails. *

* This construct is not thread-safe and should not be used concurrently. + *

+ * Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor + * instance programmatically using {@link Instance#get()}: + *

+ * import jakarta.enterprise.inject.Instance;
+ *
+ * @Inject
+ * Instance<WebSocketConnector<MyEndpoint>> connector;
+ *
+ * void connect() {
+ *      var connection1 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "alpha")
+ *                  .connectAndAwait();
+ *      var connection2 = connector.get().baseUri(uri)
+ *                  .addHeader("Foo", "bravo")
+ *                  .connectAndAwait();
+ * }
+ * 
* * @param The client endpoint class * @see WebSocketClient