-
Notifications
You must be signed in to change notification settings - Fork 5
Support for GraphQL subscriptions
In the articles "Overview"
and "Abstract types", we looked in detail at the execution of GraphQL queries
in the generated DSL. Execution of GraphQL mutations in the generated DSL is exactly the same as the execution of
GraphQL queries (we just need to use context mutation function instead of query function). It remains to deal with
GraphQL subscriptions. Let's define a GraphQL schema:
type Subscription {
filmCreated: Film!
}
type Film {
title: String!
}According to this schema, we can subscribe to new films:
subscription {
filmCreated {
title
}
}To receive JSON messages that look like this:
{
"data": {
"filmCreated": {
"title": "First"
}
}
}Let's take a look at the context interface generated by the schema:
public interface ExampleContext {
public suspend fun query(__projection: QueryProjection.() -> Unit): Query
public suspend fun mutation(__projection: MutationProjection.() -> Unit): Mutation
public fun subscription(__projection: SubscriptionProjection.() -> Unit): ExampleSubscriber<Subscription>
}
public fun interface ExampleSubscriber<T> {
public suspend fun subscribe(block: suspend ExampleReceiver<T>.() -> Unit): Unit
}
@ExampleDSL
public fun interface ExampleReceiver<out T> {
public suspend fun receive(): T
}The semantics of the subscription function is different from the semantics of the query and mutation functions.
While the query and mutation functions take a projection argument to build a query and return the result of the
query execution, the subscription function takes a projection but returns a ExampleSubscriber interface. The
"subscriber" interface allows us to create a long-lived session to listen for incoming messages. The session lifetime is
the same as the execution time of the subscribe function in the ExampleSubscriber interface. When we enter
the subscribe function, a session is created, and when we exit it, the session is destroyed.
The subscribe function is executed in scope of ExampleReceiver interface, and we can call the receive function to
receive the next message. Messages are usually listened to in an infinite loop with a call to the receive function
inside the loop. Let's try this:
fun main() = runBlocking {
val context: ExampleContext = exampleContextOf(createMyAdapter())
context.subscription {
filmCreated {
title()
}
}.subscribe {
// Subscription session created
for (i in 1..3) { // Listening to the first 3 messages
val message: Subscription = receive() // receive the next message
println("Film created: ${message.filmCreated.title}")
}
}
// Subscription session destroyed
}There are no surprises here - projections, entities and data transfer objects are the same as we used to see them in queries.
Projection:
@ExampleDSL
public interface SubscriptionProjection {
public fun filmCreated(__projection: FilmProjection.() -> Unit): Unit
}
@ExampleDSL
public interface FilmProjection {
public fun title(): Unit
}Entity:
public interface Subscription : ExampleContext {
public val filmCreated: Film
}
public interface Film : ExampleContext {
public val title: String
}DTO (without Jackson's annotations):
public data class SubscriptionDto(
public val filmCreated: FilmDto? = null
)
public data class FilmDto(
public val title: String? = null
)