Skip to content

Commit b8c177b

Browse files
authored
Added websockets support for ktor (#44)
* Added websockets support for ktor * update docs * add changelog entry
1 parent c49871c commit b8c177b

File tree

10 files changed

+75
-23
lines changed

10 files changed

+75
-23
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* Add `Ftv1Instrumentation` and `ApolloReportingInstrumentation` for respectively federated and monograph operation and fields insights.
44
* Publish to GCS
5+
* Add `apolloSubscriptionModule` for subscription support with Ktor.
56

67
# Version 0.1.0
78
_2024-10-23_

Writerside/topics/ktor.md

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
> See [sample-ktor](https://github.com/apollographql/apollo-kotlin-execution/tree/main/sample-ktor) for a project using the Ktor integration.
55
6+
## Add apollo-execution-ktor to your project
7+
68
To use the Ktor integration, add `apollo-execution-ktor` to your dependencies and a Ktor engine:
79

810
```kotlin
@@ -15,28 +17,23 @@ dependencies {
1517
}
1618
```
1719

18-
`apollo-execution-ktor` provides an `apolloModule(ExecutableSchema)` function that adds a `/graphql` route to your application:
19-
20-
```kotlin
21-
embeddedServer(Netty, port = 8080) {
22-
// /graphql route
23-
apolloModule(ServiceExecutableSchemaBuilder().build())
24-
}.start(wait = true)
25-
```
26-
27-
> `ServiceExecutableSchemaBuilder` is generated by the KSP processor. See ["Getting started"](getting-started.md) for more details.
20+
`apollo-execution-ktor` provides 3 modules:
2821

29-
You can also opt in the Apollo Sandbox route by using `apolloSandboxModule()`
22+
- `apolloModule(ExecutableSchema)` adds the main `/graphql` route for queries/mutations.
23+
- `apolloSubscriptionModule(ExecutableSchema)` adds the `/subscription` route for subscriptions.
24+
- `apolloSandboxModule(ExecutableSchema)` adds the `/sandbox/index.html` for the online IDE
3025

3126
```kotlin
3227
embeddedServer(Netty, port = 8080) {
28+
val executableSchema = ServiceExecutableSchemaBuilder().build()
29+
// /graphql route
30+
apolloModule(executableSchema)
31+
// /subscription route
32+
apolloSubscriptionModule(executableSchema)
3333
// /sandbox/index.html route
3434
apolloSandboxModule()
35-
}
35+
}.start(wait = true)
3636
```
3737

38-
`apolloSandboxModule()` adds a `sandbox/index.html` route to your application.
39-
40-
Open [`http://localhost:8080/sandbox/index.html`](http://localhost:8080/sandbox/index.html) and try out your API in the [Apollo sandbox](https://www.apollographql.com/docs/graphos/explorer/sandbox/)
38+
> `ServiceExecutableSchemaBuilder` is generated by the KSP processor. See ["Getting started"](getting-started.md) for more details.
4139
42-
[![Apollo Sandbox](sandbox.png)](http://localhost:8080/sandbox/index.html)

apollo-execution-ktor/api/apollo-execution-ktor.api

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ public final class com/apollographql/execution/ktor/MainKt {
33
public static synthetic fun apolloModule$default (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
44
public static final fun apolloSandboxModule (Lio/ktor/server/application/Application;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
55
public static synthetic fun apolloSandboxModule$default (Lio/ktor/server/application/Application;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
6+
public static final fun apolloSubscriptionModule (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V
7+
public static synthetic fun apolloSubscriptionModule$default (Lio/ktor/server/application/Application;Lcom/apollographql/execution/ExecutableSchema;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
68
public static final fun parseAsGraphQLRequest (Lio/ktor/server/request/ApplicationRequest;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
79
public static final fun respondGraphQL (Lio/ktor/server/application/ApplicationCall;Lcom/apollographql/execution/ExecutableSchema;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
810
public static synthetic fun respondGraphQL$default (Lio/ktor/server/application/ApplicationCall;Lcom/apollographql/execution/ExecutableSchema;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function2;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;

apollo-execution-ktor/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ kotlin {
1616
implementation(libs.atomicfu)
1717
api(libs.coroutines)
1818
api(libs.ktor.server.core)
19+
implementation(libs.ktor.server.websockets)
1920
}
2021
}
2122
getByName("jvmTest") {

apollo-execution-ktor/src/commonMain/kotlin/com/apollographql/execution/ktor/main.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,24 @@ package com.apollographql.execution.ktor
22

33
import com.apollographql.apollo.api.ExecutionContext
44
import com.apollographql.execution.*
5+
import com.apollographql.execution.websocket.ConnectionInitAck
6+
import com.apollographql.execution.websocket.SubscriptionWebSocketHandler
7+
import com.apollographql.execution.websocket.WebSocketBinaryMessage
8+
import com.apollographql.execution.websocket.WebSocketHandler
9+
import com.apollographql.execution.websocket.WebSocketTextMessage
510
import io.ktor.http.*
611
import io.ktor.http.content.*
712
import io.ktor.server.application.*
813
import io.ktor.server.request.*
914
import io.ktor.server.response.*
1015
import io.ktor.server.routing.*
1116
import io.ktor.server.util.*
17+
import io.ktor.server.websocket.WebSockets
18+
import io.ktor.server.websocket.webSocket
1219
import io.ktor.utils.io.*
20+
import io.ktor.websocket.Frame
21+
import io.ktor.websocket.readText
22+
import kotlinx.coroutines.coroutineScope
1323
import okio.Buffer
1424

1525
suspend fun ApplicationCall.respondGraphQL(
@@ -86,6 +96,42 @@ fun Application.apolloModule(
8696
}
8797
}
8898

99+
fun Application.apolloSubscriptionModule(
100+
executableSchema: ExecutableSchema,
101+
path: String = "/subscription",
102+
executionContext: (ApplicationRequest) -> ExecutionContext = { ExecutionContext.Empty }
103+
) {
104+
install(WebSockets)
105+
106+
routing {
107+
webSocket(path) {
108+
coroutineScope {
109+
val handler = SubscriptionWebSocketHandler(
110+
executableSchema = executableSchema,
111+
scope = this,
112+
executionContext = executionContext(call.request),
113+
sendMessage = {
114+
when (it) {
115+
is WebSocketBinaryMessage -> send(Frame.Binary(true, it.data))
116+
is WebSocketTextMessage -> send(Frame.Text(it.data))
117+
}
118+
},
119+
connectionInitHandler = {
120+
ConnectionInitAck
121+
}
122+
)
123+
124+
for (frame in incoming) {
125+
if (frame !is Frame.Text) {
126+
continue
127+
}
128+
handler.handleMessage(WebSocketTextMessage(frame.readText()))
129+
}
130+
}
131+
}
132+
}
133+
}
134+
89135
fun Application.apolloSandboxModule(
90136
title: String = "API sandbox",
91137
sandboxPath: String = "/sandbox",

apollo-execution-runtime/api/apollo-execution-runtime.api

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -279,8 +279,8 @@ public abstract interface class com/apollographql/execution/websocket/Connection
279279
}
280280

281281
public final class com/apollographql/execution/websocket/SubscriptionWebSocketHandler : com/apollographql/execution/websocket/WebSocketHandler {
282-
public fun <init> (Lcom/apollographql/execution/ExecutableSchema;Lkotlinx/coroutines/CoroutineScope;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;)V
283-
public synthetic fun <init> (Lcom/apollographql/execution/ExecutableSchema;Lkotlinx/coroutines/CoroutineScope;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
282+
public fun <init> (Lcom/apollographql/execution/ExecutableSchema;Lkotlinx/coroutines/CoroutineScope;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;)V
283+
public synthetic fun <init> (Lcom/apollographql/execution/ExecutableSchema;Lkotlinx/coroutines/CoroutineScope;Lcom/apollographql/apollo/api/ExecutionContext;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
284284
public final fun close ()V
285285
public fun handleMessage (Lcom/apollographql/execution/websocket/WebSocketMessage;)V
286286
}

apollo-execution-runtime/api/apollo-execution-runtime.klib.api

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class com.apollographql.execution.websocket/ConnectionInitError : com.apol
4444
final fun <get-payload>(): com.apollographql.apollo.api/Optional<kotlin/Any?> // com.apollographql.execution.websocket/ConnectionInitError.payload.<get-payload>|<get-payload>(){}[0]
4545
}
4646
final class com.apollographql.execution.websocket/SubscriptionWebSocketHandler : com.apollographql.execution.websocket/WebSocketHandler { // com.apollographql.execution.websocket/SubscriptionWebSocketHandler|null[0]
47-
constructor <init>(com.apollographql.execution/ExecutableSchema, kotlinx.coroutines/CoroutineScope, com.apollographql.apollo.api/ExecutionContext, kotlin/Function1<com.apollographql.execution.websocket/WebSocketMessage, kotlin/Unit>, kotlin.coroutines/SuspendFunction1<kotlin/Any?, com.apollographql.execution.websocket/ConnectionInitResult> = ...) // com.apollographql.execution.websocket/SubscriptionWebSocketHandler.<init>|<init>(com.apollographql.execution.ExecutableSchema;kotlinx.coroutines.CoroutineScope;com.apollographql.apollo.api.ExecutionContext;kotlin.Function1<com.apollographql.execution.websocket.WebSocketMessage,kotlin.Unit>;kotlin.coroutines.SuspendFunction1<kotlin.Any?,com.apollographql.execution.websocket.ConnectionInitResult>){}[0]
47+
constructor <init>(com.apollographql.execution/ExecutableSchema, kotlinx.coroutines/CoroutineScope, com.apollographql.apollo.api/ExecutionContext, kotlin.coroutines/SuspendFunction1<com.apollographql.execution.websocket/WebSocketMessage, kotlin/Unit>, kotlin.coroutines/SuspendFunction1<kotlin/Any?, com.apollographql.execution.websocket/ConnectionInitResult> = ...) // com.apollographql.execution.websocket/SubscriptionWebSocketHandler.<init>|<init>(com.apollographql.execution.ExecutableSchema;kotlinx.coroutines.CoroutineScope;com.apollographql.apollo.api.ExecutionContext;kotlin.coroutines.SuspendFunction1<com.apollographql.execution.websocket.WebSocketMessage,kotlin.Unit>;kotlin.coroutines.SuspendFunction1<kotlin.Any?,com.apollographql.execution.websocket.ConnectionInitResult>){}[0]
4848
final fun close() // com.apollographql.execution.websocket/SubscriptionWebSocketHandler.close|close(){}[0]
4949
final fun handleMessage(com.apollographql.execution.websocket/WebSocketMessage) // com.apollographql.execution.websocket/SubscriptionWebSocketHandler.handleMessage|handleMessage(com.apollographql.execution.websocket.WebSocketMessage){}[0]
5050
}

apollo-execution-runtime/src/commonMain/kotlin/com/apollographql/execution/internal/OperationContext.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ internal class OperationContext(
7676
"query" -> queryRoot?.resolveRoot()
7777
"mutation" -> mutationRoot?.resolveRoot()
7878
"subscription" -> {
79-
return graphqlErrorResponse("Use executeSubscription() to execute subscriptions")
79+
return graphqlErrorResponse("Use subscribe() to execute subscriptions")
8080
}
8181

8282
else -> {

apollo-execution-runtime/src/commonMain/kotlin/com/apollographql/execution/websocket/SubscriptionWebSocketHandler.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class SubscriptionWebSocketHandler(
3232
private val executableSchema: ExecutableSchema,
3333
private val scope: CoroutineScope,
3434
private val executionContext: ExecutionContext,
35-
private val sendMessage: (WebSocketMessage) -> Unit,
35+
private val sendMessage: suspend (WebSocketMessage) -> Unit,
3636
private val connectionInitHandler: ConnectionInitHandler = { ConnectionInitAck },
3737
) : WebSocketHandler {
3838
private val lock = reentrantLock()
@@ -67,7 +67,9 @@ class SubscriptionWebSocketHandler(
6767
activeSubscriptions.containsKey(clientMessage.id)
6868
}
6969
if (isActive) {
70-
sendMessage(SubscriptionWebsocketError(id = clientMessage.id, error = Error.Builder("Subscription ${clientMessage.id} is already active").build()).toWsMessage())
70+
scope.launch {
71+
sendMessage(SubscriptionWebsocketError(id = clientMessage.id, error = Error.Builder("Subscription ${clientMessage.id} is already active").build()).toWsMessage())
72+
}
7173
return
7274
}
7375

@@ -107,7 +109,9 @@ class SubscriptionWebSocketHandler(
107109
}
108110

109111
is SubscriptionWebsocketClientMessageParseError -> {
110-
sendMessage(SubscriptionWebsocketError(null, Error.Builder("Cannot handle message (${clientMessage.message})").build()).toWsMessage())
112+
scope.launch {
113+
sendMessage(SubscriptionWebsocketError(null, Error.Builder("Cannot handle message (${clientMessage.message})").build()).toWsMessage())
114+
}
111115
}
112116
}
113117
}

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ ktor-server-netty = { group = "io.ktor", name = "ktor-server-netty", version.ref
2323
ktor-server-core = { group = "io.ktor", name = "ktor-server-core", version.ref ="ktor" }
2424
ktor-server-cors = { group = "io.ktor", name = "ktor-server-cors", version.ref ="ktor" }
2525
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
26+
ktor-server-websockets = { group = "io.ktor", name = "ktor-server-websockets", version.ref ="ktor" }
2627
slf4j-simple = "org.slf4j:slf4j-simple:2.0.13"
2728
slf4j-nop = "org.slf4j:slf4j-nop:2.0.13"
2829
ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" }

0 commit comments

Comments
 (0)