Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,9 @@ Changes log
the server adapter also accepts `graphql-transport-ws` WebSocket upgrades on its own path
(HTTP/1.1 only), with several operations per connection, a `connection_init` timeout
and the protocol's close codes; `org.restlet.ext.websocket` is an optional dependency,
loaded only then. Incremental delivery of `@defer` results over `multipart/mixed` is
loaded only then. `GraphqlClientAdapter.setSubscriptionTransport(WEBSOCKET)` multiplexes
subscriptions over one `graphql-transport-ws` connection sharing the borrowed client's
event loop. Incremental delivery of `@defer` results over `multipart/mixed` is
available on both sides when enabled (experimental, off by default): the client
returns graphql-java's `IncrementalExecutionResult`, whose item publisher reads the
delayed results. Issue #1581.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,12 @@ private enum State {

private volatile boolean incrementalDeliveryEnabled;

private volatile GraphqlTransport subscriptionTransport = GraphqlTransport.SSE;

private volatile Map<String, Object> connectionInitPayload;

private volatile GraphqlSubscriptionTransport webSocketTransport;

private final Set<GraphqlSsePublisher.EventStreamSubscription> subscriptions =
ConcurrentHashMap.newKeySet();

Expand Down Expand Up @@ -282,6 +288,50 @@ public void setIncrementalDeliveryEnabled(boolean incrementalDeliveryEnabled) {
this.incrementalDeliveryEnabled = incrementalDeliveryEnabled;
}

/**
* Returns the transport of subscriptions.
*
* @return The transport of subscriptions.
*/
public GraphqlTransport getSubscriptionTransport() {
return subscriptionTransport;
}

/**
* Sets the transport of subscriptions. Defaults to {@link GraphqlTransport#SSE}, with one
* request per subscription. With {@link GraphqlTransport#WEBSOCKET}, subscriptions are
* multiplexed over one {@code graphql-transport-ws} connection, opened on the first demand at
* the endpoint URI with the "ws" or "wss" scheme. It shares the event loop of the borrowed
* client, which must then be a started Netty HTTP client, and requires the {@code
* org.restlet.ext.websocket} module. The request customizer adds handshake headers.
*
* @param subscriptionTransport The transport of subscriptions.
*/
public void setSubscriptionTransport(GraphqlTransport subscriptionTransport) {
checkNew();
this.subscriptionTransport = Objects.requireNonNull(subscriptionTransport);
}

/**
* Returns the payload of the {@code connection_init} message of WebSocket connections.
*
* @return The payload, or null.
*/
public Map<String, Object> getConnectionInitPayload() {
return connectionInitPayload;
}

/**
* Sets the payload of the {@code connection_init} message of WebSocket connections, for example
* an authentication token.
*
* @param connectionInitPayload The payload, or null.
*/
public void setConnectionInitPayload(Map<String, Object> connectionInitPayload) {
checkNew();
this.connectionInitPayload = connectionInitPayload;
}

/**
* Validates and freezes the configuration.
*
Expand All @@ -299,6 +349,14 @@ public synchronized void start() {
if (parserOptions == null) {
parserOptions = ParserOptions.getDefaultOperationParserOptions();
}
if (subscriptionTransport == GraphqlTransport.WEBSOCKET) {
try {
// Only loads the optional WebSocket extension when selected.
webSocketTransport = new GraphqlWebSocketTransport(this);
} catch (IOException e) {
throw new IllegalStateException("Unable to start the WebSocket client", e);
}
}
state = State.STARTED;
}

Expand All @@ -312,6 +370,10 @@ public synchronized void stop() {
List.copyOf(subscriptions)) {
subscription.stop();
}
GraphqlSubscriptionTransport transport = webSocketTransport;
if (transport != null) {
transport.stop();
}
}

/**
Expand Down Expand Up @@ -388,8 +450,7 @@ public Publisher<ExecutionResult> subscribe(
ExecutionResult invalid = validate(document);
return invalid != null
? single(invalid)
: new GraphqlSsePublisher(
this, AstPrinter.printAst(document), operationName, variables);
: publisher(AstPrinter.printAst(document), operationName, variables);
}

/**
Expand Down Expand Up @@ -423,7 +484,15 @@ public Publisher<ExecutionResult> subscribe(
return single(invalid);
}
}
return new GraphqlSsePublisher(this, document, operationName, variables);
return publisher(document, operationName, variables);
}

private Publisher<ExecutionResult> publisher(
String document, String operationName, Map<String, Object> variables) {
GraphqlSubscriptionTransport transport = webSocketTransport;
return transport != null
? transport.subscribe(document, operationName, variables)
: new GraphqlSsePublisher(this, document, operationName, variables);
}

/** A publisher of one result, for documents that fail local validation. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Copyright 2005-2026 Qlik
*<p>
* The content of this file is subject to the terms of the Apache 2.0 open
* source license available at https://www.opensource.org/licenses/apache-2.0
*<p>
* Restlet is a registered trademark of QlikTech International AB.
*/
package org.restlet.ext.graphql;

import graphql.ExecutionResult;
import java.util.Map;
import org.reactivestreams.Publisher;

/**
* A subscription transport of {@link GraphqlClientAdapter} other than SSE. The implementation
* depends on the optional WebSocket extension: the adapter only refers to it through this
* interface, so that the extension is loaded only when {@link GraphqlTransport#WEBSOCKET} is
* selected.
*/
interface GraphqlSubscriptionTransport {

/**
* Subscribes to an operation.
*
* @param document The document.
* @param operationName The name of the operation to execute, or null.
* @param variables The variables, or null.
* @return A cold publisher of the results.
*/
Publisher<ExecutionResult> subscribe(
String document, String operationName, Map<String, Object> variables);

/** Fails the open subscriptions and closes the connection. */
void stop();
}
Loading
Loading