1
1
/*
2
- * Copyright 2002-2023 the original author or authors.
2
+ * Copyright 2002-2024 the original author or authors.
3
3
*
4
4
* Licensed under the Apache License, Version 2.0 (the "License");
5
5
* you may not use this file except in compliance with the License.
44
44
import org .springframework .graphql .server .support .GraphQlWebSocketMessage ;
45
45
import org .springframework .http .HttpHeaders ;
46
46
import org .springframework .http .codec .CodecConfigurer ;
47
+ import org .springframework .lang .Nullable ;
47
48
import org .springframework .util .Assert ;
48
49
import org .springframework .util .CollectionUtils ;
49
50
import org .springframework .web .reactive .socket .CloseStatus ;
@@ -72,10 +73,13 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
72
73
73
74
private final WebSocketGraphQlInterceptor webSocketInterceptor ;
74
75
75
- private final WebSocketCodecDelegate webSocketCodecDelegate ;
76
+ private final WebSocketCodecDelegate codecDelegate ;
76
77
77
78
private final Duration initTimeoutDuration ;
78
79
80
+ @ Nullable
81
+ private final Duration keepAliveDuration ;
82
+
79
83
80
84
/**
81
85
* Create a new instance.
@@ -87,12 +91,30 @@ public class GraphQlWebSocketHandler implements WebSocketHandler {
87
91
public GraphQlWebSocketHandler (
88
92
WebGraphQlHandler graphQlHandler , CodecConfigurer codecConfigurer , Duration connectionInitTimeout ) {
89
93
94
+ this (graphQlHandler , codecConfigurer , connectionInitTimeout , null );
95
+ }
96
+
97
+ /**
98
+ * Create a new instance.
99
+ * @param graphQlHandler common handler for GraphQL over WebSocket requests
100
+ * @param codecConfigurer codec configurer for JSON encoding and decoding
101
+ * @param connectionInitTimeout how long to wait after the establishment of
102
+ * the WebSocket for the {@code "connection_ini"} message from the client.
103
+ * @param keepAliveDuration how frequently to send ping messages; if not
104
+ * set then ping messages are not sent.
105
+ * @since 1.3
106
+ */
107
+ public GraphQlWebSocketHandler (
108
+ WebGraphQlHandler graphQlHandler , CodecConfigurer codecConfigurer ,
109
+ Duration connectionInitTimeout , @ Nullable Duration keepAliveDuration ) {
110
+
90
111
Assert .notNull (graphQlHandler , "WebGraphQlHandler is required" );
91
112
92
113
this .graphQlHandler = graphQlHandler ;
93
114
this .webSocketInterceptor = this .graphQlHandler .getWebSocketInterceptor ();
94
- this .webSocketCodecDelegate = new WebSocketCodecDelegate (codecConfigurer );
115
+ this .codecDelegate = new WebSocketCodecDelegate (codecConfigurer );
95
116
this .initTimeoutDuration = connectionInitTimeout ;
117
+ this .keepAliveDuration = keepAliveDuration ;
96
118
}
97
119
98
120
@@ -137,7 +159,7 @@ public Mono<Void> handle(WebSocketSession session) {
137
159
.subscribe ();
138
160
139
161
return session .send (session .receive ().flatMap ((webSocketMessage ) -> {
140
- GraphQlWebSocketMessage message = this .webSocketCodecDelegate .decode (webSocketMessage );
162
+ GraphQlWebSocketMessage message = this .codecDelegate .decode (webSocketMessage );
141
163
String id = message .getId ();
142
164
Map <String , Object > payload = message .getPayload ();
143
165
switch (message .resolvedType ()) {
@@ -159,7 +181,7 @@ public Mono<Void> handle(WebSocketSession session) {
159
181
.doOnTerminate (() -> subscriptions .remove (id ));
160
182
}
161
183
case PING -> {
162
- return Flux .just (this .webSocketCodecDelegate .encode (session , GraphQlWebSocketMessage .pong (null )));
184
+ return Flux .just (this .codecDelegate .encode (session , GraphQlWebSocketMessage .pong (null )));
163
185
}
164
186
case COMPLETE -> {
165
187
if (id != null ) {
@@ -176,11 +198,16 @@ public Mono<Void> handle(WebSocketSession session) {
176
198
if (!connectionInitPayloadRef .compareAndSet (null , payload )) {
177
199
return GraphQlStatus .close (session , GraphQlStatus .TOO_MANY_INIT_REQUESTS_STATUS );
178
200
}
179
- return this .webSocketInterceptor .handleConnectionInitialization (sessionInfo , payload )
201
+ Flux < WebSocketMessage > flux = this .webSocketInterceptor .handleConnectionInitialization (sessionInfo , payload )
180
202
.defaultIfEmpty (Collections .emptyMap ())
181
- .map ((ackPayload ) -> this .webSocketCodecDelegate .encodeConnectionAck (session , ackPayload ))
182
- .flux ()
183
- .onErrorResume ((ex ) -> GraphQlStatus .close (session , GraphQlStatus .UNAUTHORIZED_STATUS ));
203
+ .map ((ackPayload ) -> this .codecDelegate .encodeConnectionAck (session , ackPayload ))
204
+ .flux ();
205
+ if (this .keepAliveDuration != null ) {
206
+ flux = flux .mergeWith (Flux .interval (this .keepAliveDuration , this .keepAliveDuration )
207
+ .filter ((aLong ) -> !this .codecDelegate .checkMessagesEncodedAndClear ())
208
+ .map ((aLong ) -> this .codecDelegate .encode (session , GraphQlWebSocketMessage .ping (null ))));
209
+ }
210
+ return flux .onErrorResume ((ex ) -> GraphQlStatus .close (session , GraphQlStatus .UNAUTHORIZED_STATUS ));
184
211
}
185
212
default -> {
186
213
return GraphQlStatus .close (session , GraphQlStatus .INVALID_MESSAGE_STATUS );
@@ -218,14 +245,14 @@ private Flux<WebSocketMessage> handleResponse(WebSocketSession session, String i
218
245
}
219
246
220
247
return responseFlux
221
- .map ((responseMap ) -> this .webSocketCodecDelegate .encodeNext (session , id , responseMap ))
222
- .concatWith (Mono .fromCallable (() -> this .webSocketCodecDelegate .encodeComplete (session , id )))
248
+ .map ((responseMap ) -> this .codecDelegate .encodeNext (session , id , responseMap ))
249
+ .concatWith (Mono .fromCallable (() -> this .codecDelegate .encodeComplete (session , id )))
223
250
.onErrorResume ((ex ) -> {
224
251
if (ex instanceof SubscriptionExistsException ) {
225
252
CloseStatus status = new CloseStatus (4409 , "Subscriber for " + id + " already exists" );
226
253
return GraphQlStatus .close (session , status );
227
254
}
228
- return Mono .fromCallable (() -> this .webSocketCodecDelegate .encodeError (session , id , ex ));
255
+ return Mono .fromCallable (() -> this .codecDelegate .encodeError (session , id , ex ));
229
256
});
230
257
}
231
258
0 commit comments