Skip to content

Commit c59a6dc

Browse files
committed
WebSockets Next: clarify connector API docs/javadoc
- also add tests for programmatic lookup of connectors - related to #44465
1 parent 323ba29 commit c59a6dc

File tree

9 files changed

+271
-12
lines changed

9 files changed

+271
-12
lines changed

docs/src/main/asciidoc/websockets-next-reference.adoc

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ and pull requests should be submitted there:
44
https://github.com/quarkusio/quarkus/tree/main/docs/src/main/asciidoc
55
////
66
[id="websockets-next-reference-guide"]
7-
= WebSockets Next extension reference guide
7+
= WebSockets Next reference guide
88
:extension-status: preview
99
include::_attributes.adoc[]
1010
:numbered:
@@ -78,7 +78,7 @@ implementation("io.quarkus:quarkus-websockets-next")
7878

7979
== Endpoints
8080

81-
Both the server and client APIs allow you to define _endpoints_ that are used to consume and send messages.
81+
Both the <<server-api>> and <<client-api>> define _endpoints_ that are used to consume and send messages.
8282
The endpoints are implemented as CDI beans and support injection.
8383
Endpoints declare <<callback-methods,_callback methods_>> annotated with `@OnTextMessage`, `@OnBinaryMessage`, `@OnPong`, `@OnOpen`, `@OnClose` and `@OnError`.
8484
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
559559
However, in some situations it is preferable to process events concurrently, i.e. with no ordering guarantees but also with no concurrency limits.
560560
For this cases, the `InboundProcessingMode#CONCURRENT` should be used.
561561

562+
[[server-api]]
562563
== Server API
563564

564565
=== HTTP server configuration
@@ -900,14 +901,15 @@ public class CustomTenantResolver implements TenantResolver {
900901
----
901902
For more information on Hibernate multitenancy, refer to the https://quarkus.io/guides/hibernate-orm#multitenancy[hibernate documentation].
902903

904+
[[client-api]]
903905
== Client API
904906

905907
[[client-connectors]]
906908
=== Client connectors
907909

908-
The `io.quarkus.websockets.next.WebSocketConnector<CLIENT>` is used to configure and create new connections for client endpoints.
909-
A CDI bean that implements this interface is provided and can be injected in other beans.
910-
The actual type argument is used to determine the client endpoint.
910+
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.
911+
Quarkus provides a CDI bean with bean type `io.quarkus.websockets.next.WebSocketConnector<CLIENT>` and default qualifer that can be injected in other beans.
912+
The actual type argument of an injection point is used to determine the client endpoint.
911913
The type is validated during build - if it does not represent a client endpoint the build fails.
912914

913915
Let’s consider the following client endpoint:
@@ -955,6 +957,31 @@ public class MyBean {
955957

956958
NOTE: If an application attempts to inject a connector for a missing endpoint, an error is thrown.
957959

960+
Connectors are not thread-safe and should not be used concurrently.
961+
Connectors should also not be reused.
962+
If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`:
963+
964+
[source, java]
965+
----
966+
import jakarta.enterprise.inject.Instance;
967+
968+
@Singleton
969+
public class MyBean {
970+
971+
@Inject
972+
Instance<WebSocketConnector<MyEndpoint>> connector;
973+
974+
void connect() {
975+
var connection1 = connector.get().baseUri(uri)
976+
.addHeader("Foo", "alpha")
977+
.connectAndAwait();
978+
var connection2 = connector.get().baseUri(uri)
979+
.addHeader("Foo", "bravo")
980+
.connectAndAwait();
981+
}
982+
}
983+
----
984+
958985
==== Basic connector
959986

960987
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
9911018
However, unlike others low-level WebSocket clients, it is still a CDI bean and can be injected in other beans.
9921019
It also provides a way to configure the execution model of the callbacks, ensuring optimal integration with the rest of Quarkus.
9931020

1021+
Connectors are not thread-safe and should not be used concurrently.
1022+
Connectors should also not be reused.
1023+
If you need to create multiple connections in a row you'll need to obtain a new connetor instance programmatically using `Instance#get()`:
1024+
1025+
[source, java]
1026+
----
1027+
import jakarta.enterprise.inject.Instance;
1028+
1029+
@Singleton
1030+
public class MyBean {
1031+
1032+
@Inject
1033+
Instance<BasicWebSocketConnector> connector;
1034+
1035+
void connect() {
1036+
var connection1 = connector.get().baseUri(uri)
1037+
.addHeader("Foo", "alpha")
1038+
.connectAndAwait();
1039+
var connection2 = connector.get().baseUri(uri)
1040+
.addHeader("Foo", "bravo")
1041+
.connectAndAwait();
1042+
}
1043+
}
1044+
----
1045+
9941046
[[ws-client-connection]]
9951047
=== WebSocket client connection
9961048

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package io.quarkus.websockets.next.test.client.programmatic;
2+
3+
import static org.junit.jupiter.api.Assertions.assertTrue;
4+
5+
import java.net.URI;
6+
import java.util.List;
7+
import java.util.concurrent.CopyOnWriteArrayList;
8+
import java.util.concurrent.CountDownLatch;
9+
import java.util.concurrent.TimeUnit;
10+
11+
import jakarta.enterprise.inject.Instance;
12+
import jakarta.inject.Inject;
13+
14+
import org.junit.jupiter.api.Test;
15+
import org.junit.jupiter.api.extension.RegisterExtension;
16+
17+
import io.quarkus.test.QuarkusUnitTest;
18+
import io.quarkus.test.common.http.TestHTTPResource;
19+
import io.quarkus.websockets.next.HandshakeRequest;
20+
import io.quarkus.websockets.next.OnClose;
21+
import io.quarkus.websockets.next.OnOpen;
22+
import io.quarkus.websockets.next.OnTextMessage;
23+
import io.quarkus.websockets.next.WebSocket;
24+
import io.quarkus.websockets.next.WebSocketClient;
25+
import io.quarkus.websockets.next.WebSocketClientConnection;
26+
import io.quarkus.websockets.next.WebSocketConnector;
27+
28+
public class ClientEndpointProgrammaticTest {
29+
30+
@RegisterExtension
31+
public static final QuarkusUnitTest test = new QuarkusUnitTest()
32+
.withApplicationRoot(root -> {
33+
root.addClasses(ServerEndpoint.class, ClientEndpoint.class);
34+
});
35+
36+
@Inject
37+
Instance<WebSocketConnector<ClientEndpoint>> connector;
38+
39+
@TestHTTPResource("/")
40+
URI uri;
41+
42+
@Test
43+
void testClient() throws InterruptedException {
44+
WebSocketClientConnection connection1 = connector
45+
.get()
46+
.baseUri(uri)
47+
.addHeader("Foo", "Lu")
48+
.connectAndAwait();
49+
connection1.sendTextAndAwait("Hi!");
50+
51+
WebSocketClientConnection connection2 = connector
52+
.get()
53+
.baseUri(uri)
54+
.addHeader("Foo", "Ma")
55+
.connectAndAwait();
56+
connection2.sendTextAndAwait("Hi!");
57+
58+
assertTrue(ClientEndpoint.MESSAGE_LATCH.await(5, TimeUnit.SECONDS));
59+
assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hello Lu!"));
60+
assertTrue(ClientEndpoint.MESSAGES.contains("Lu:Hi!"));
61+
assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hello Ma!"));
62+
assertTrue(ClientEndpoint.MESSAGES.contains("Ma:Hi!"), ClientEndpoint.MESSAGES.toString());
63+
64+
connection1.closeAndAwait();
65+
connection2.closeAndAwait();
66+
assertTrue(ClientEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
67+
assertTrue(ServerEndpoint.CLOSED_LATCH.await(5, TimeUnit.SECONDS));
68+
}
69+
70+
@WebSocket(path = "/endpoint")
71+
public static class ServerEndpoint {
72+
73+
static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2);
74+
75+
@OnOpen
76+
String open(HandshakeRequest handshakeRequest) {
77+
return "Hello " + handshakeRequest.header("Foo") + "!";
78+
}
79+
80+
@OnTextMessage
81+
String echo(String message) {
82+
return message;
83+
}
84+
85+
@OnClose
86+
void close() {
87+
CLOSED_LATCH.countDown();
88+
}
89+
90+
}
91+
92+
@WebSocketClient(path = "/endpoint")
93+
public static class ClientEndpoint {
94+
95+
static final CountDownLatch MESSAGE_LATCH = new CountDownLatch(4);
96+
97+
static final List<String> MESSAGES = new CopyOnWriteArrayList<>();
98+
99+
static final CountDownLatch CLOSED_LATCH = new CountDownLatch(2);
100+
101+
@OnTextMessage
102+
void onMessage(String message, HandshakeRequest handshakeRequest) {
103+
MESSAGES.add(handshakeRequest.header("Foo") + ":" + message);
104+
MESSAGE_LATCH.countDown();
105+
}
106+
107+
@OnClose
108+
void close() {
109+
CLOSED_LATCH.countDown();
110+
}
111+
112+
}
113+
114+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.quarkus.websockets.next.test.client.programmatic;
2+
3+
import static org.junit.jupiter.api.Assertions.fail;
4+
5+
import jakarta.enterprise.inject.Instance;
6+
import jakarta.inject.Inject;
7+
import jakarta.inject.Singleton;
8+
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.RegisterExtension;
11+
12+
import io.quarkus.arc.Unremovable;
13+
import io.quarkus.test.QuarkusUnitTest;
14+
import io.quarkus.websockets.next.WebSocketClientException;
15+
import io.quarkus.websockets.next.WebSocketConnector;
16+
17+
public class InvalidConnectorProgrammaticInjectionPointTest {
18+
19+
@RegisterExtension
20+
public static final QuarkusUnitTest test = new QuarkusUnitTest()
21+
.withApplicationRoot(root -> {
22+
root.addClasses(Service.class);
23+
})
24+
.setExpectedException(WebSocketClientException.class, true);
25+
26+
@Test
27+
void testInvalidInjectionPoint() {
28+
fail();
29+
}
30+
31+
@Unremovable
32+
@Singleton
33+
public static class Service {
34+
35+
@Inject
36+
Instance<WebSocketConnector<String>> invalid;
37+
38+
}
39+
40+
}

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/BasicWebSocketConnector.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,40 @@
55
import java.util.function.BiConsumer;
66
import java.util.function.Consumer;
77

8+
import jakarta.enterprise.inject.Default;
9+
import jakarta.enterprise.inject.Instance;
10+
811
import io.quarkus.arc.Arc;
912
import io.smallrye.common.annotation.CheckReturnValue;
1013
import io.smallrye.common.annotation.Experimental;
1114
import io.smallrye.mutiny.Uni;
1215
import io.vertx.core.buffer.Buffer;
1316

1417
/**
15-
* This basic connector can be used to configure and open new client connections. Unlike with {@link WebSocketConnector} a
16-
* client endpoint class is not needed.
18+
* A basic connector can be used to configure and open a new client connection. Unlike with {@link WebSocketConnector} a
19+
* client endpoint is not used to consume and send messages.
20+
* <p>
21+
* Quarkus provides a CDI bean with bean type {@code BasicWebSocketConnector} and qualifier {@link Default}.
1722
* <p>
1823
* This construct is not thread-safe and should not be used concurrently.
24+
* <p>
25+
* Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor
26+
* instance programmatically using {@link Instance#get()}:
27+
* <code><pre>
28+
* import jakarta.enterprise.inject.Instance;
29+
*
30+
* &#64;Inject
31+
* Instance&#60;BasicWebSocketConnector&#62; connector;
32+
*
33+
* void connect() {
34+
* var connection1 = connector.get().baseUri(uri)
35+
* .addHeader("Foo", "alpha")
36+
* .connectAndAwait();
37+
* var connection2 = connector.get().baseUri(uri)
38+
* .addHeader("Foo", "bravo")
39+
* .connectAndAwait();
40+
* }
41+
* </pre></code>
1942
*
2043
* @see WebSocketClientConnection
2144
*/

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenClientConnections.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import java.util.Optional;
55
import java.util.stream.Stream;
66

7+
import jakarta.enterprise.inject.Default;
8+
79
import io.smallrye.common.annotation.Experimental;
810

911
/**
1012
* Provides convenient access to all open client connections.
1113
* <p>
12-
* Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface.
14+
* Quarkus provides a CDI bean with bean type {@link OpenClientConnections} and qualifier {@link Default}.
1315
*/
1416
@Experimental("This API is experimental and may change in the future")
1517
public interface OpenClientConnections extends Iterable<WebSocketClientConnection> {

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/OpenConnections.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import java.util.Optional;
55
import java.util.stream.Stream;
66

7+
import jakarta.enterprise.inject.Default;
8+
79
import io.smallrye.common.annotation.Experimental;
810

911
/**
1012
* Provides convenient access to all open connections.
1113
* <p>
12-
* Quarkus provides a built-in CDI bean with the {@link jakarta.inject.Singleton} scope that implements this interface.
14+
* Quarkus provides a CDI bean with bean type {@link OpenConnections} and qualifier {@link Default}.
1315
*/
1416
@Experimental("This API is experimental and may change in the future")
1517
public interface OpenConnections extends Iterable<WebSocketConnection> {

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketClientConnection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
/**
66
* This interface represents a client connection to a WebSocket endpoint.
77
* <p>
8-
* Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocketClient}
8+
* Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocketClient}
99
* endpoint and used to interact with the connected server.
1010
*/
1111
@Experimental("This API is experimental and may change in the future")

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnection.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
/**
99
* This interface represents a connection from a client to a specific {@link WebSocket} endpoint on the server.
1010
* <p>
11-
* Quarkus provides a built-in CDI bean that implements this interface and can be injected in a {@link WebSocket}
11+
* Quarkus provides a CDI bean that implements this interface and can be injected in a {@link WebSocket}
1212
* endpoint and used to interact with the connected client, or all clients connected to the endpoint respectively
1313
* (broadcasting).
1414
* <p>

extensions/websockets-next/runtime/src/main/java/io/quarkus/websockets/next/WebSocketConnector.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,40 @@
33
import java.net.URI;
44
import java.net.URLEncoder;
55

6+
import jakarta.enterprise.inject.Default;
7+
import jakarta.enterprise.inject.Instance;
8+
69
import io.smallrye.common.annotation.CheckReturnValue;
710
import io.smallrye.common.annotation.Experimental;
811
import io.smallrye.mutiny.Uni;
912

1013
/**
11-
* This connector can be used to configure and open new client connections using a client endpoint class.
14+
* A connector can be used to configure and open a new client connection backed by a client endpoint that is used to
15+
* consume and send messages.
16+
* <p>
17+
* Quarkus provides a CDI bean with bean type {@code WebSocketConnector<CLIENT>} and qualifier {@link Default}. The actual type
18+
* argument of an injection point is used to determine the client endpoint. The type is validated during build
19+
* and if it does not represent a client endpoint then the build fails.
1220
* <p>
1321
* This construct is not thread-safe and should not be used concurrently.
22+
* <p>
23+
* Connectors should not be reused. If you need to create multiple connections in a row you'll need to obtain a new connetor
24+
* instance programmatically using {@link Instance#get()}:
25+
* <code><pre>
26+
* import jakarta.enterprise.inject.Instance;
27+
*
28+
* &#64;Inject
29+
* Instance&#60;WebSocketConnector&#60;MyEndpoint&#62;&#62; connector;
30+
*
31+
* void connect() {
32+
* var connection1 = connector.get().baseUri(uri)
33+
* .addHeader("Foo", "alpha")
34+
* .connectAndAwait();
35+
* var connection2 = connector.get().baseUri(uri)
36+
* .addHeader("Foo", "bravo")
37+
* .connectAndAwait();
38+
* }
39+
* </pre></code>
1440
*
1541
* @param <CLIENT> The client endpoint class
1642
* @see WebSocketClient

0 commit comments

Comments
 (0)