github.com/geneva/gqlgen@v0.17.7-0.20230801155730-7b9317164836/docs/content/recipes/subscriptions.md (about) 1 --- 2 title: "Subscriptions" 3 description: Subscriptions allow for streaming real-time events to your clients. This is how to do that with gqlgen. 4 linkTitle: "Subscriptions" 5 menu: { main: { parent: 'recipes' } } 6 --- 7 8 GraphQL Subscriptions allow you to stream events to your clients in real-time. 9 This is easy to do in gqlgen and this recipe will show you how to setup a quick example. 10 11 ## Preparation 12 13 This recipe starts with the empty project after the quick start steps were followed. 14 Although the steps are the same in an existing, more complex projects you will need 15 to be careful to configure routing correctly. 16 17 In this recipe you will learn how to 18 19 1. add WebSocket Transport to your server 20 2. add the `Subscription` type to your schema 21 3. implement a real-time resolver. 22 23 ## Adding WebSocket Transport 24 25 To send real-time data to clients, your GraphQL server needs to have an open connection 26 with the client. This is done using WebSockets. 27 28 To add the WebSocket transport change your `main.go` by calling `AddTransport(&transport.Websocket{})` 29 on your query handler. 30 31 **If you are using an external router, remember to send *ALL* `/query`-requests to your handler!** 32 **Not just POST requests!** 33 34 ```go 35 package main 36 37 import ( 38 "log" 39 "net/http" 40 "os" 41 42 "github.com/99designs/gqlgen/graphql/handler" 43 "github.com/99designs/gqlgen/graphql/handler/transport" 44 "github.com/99designs/gqlgen/graphql/playground" 45 "github.com/example/test/graph" 46 "github.com/example/test/graph/generated" 47 ) 48 49 const defaultPort = "8080" 50 51 func main() { 52 port := os.Getenv("PORT") 53 if port == "" { 54 port = defaultPort 55 } 56 57 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 58 59 srv.AddTransport(&transport.Websocket{}) // <---- This is the important part! 60 61 http.Handle("/", playground.Handler("GraphQL playground", "/query")) 62 http.Handle("/query", srv) 63 64 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 65 log.Fatal(http.ListenAndServe(":"+port, nil)) 66 } 67 ``` 68 69 ## Adding Subscriptions to your Schema 70 71 Next you'll have to define the subscriptions in your schema in the `Subscription` top-level type. 72 73 ```graphql 74 """ 75 Make sure you have at least something in your `Query` type. 76 If you don't have a query the playground will be unable 77 to introspect your schema! 78 """ 79 type Query { 80 placeholder: String 81 } 82 83 """ 84 `Time` is a simple type only containing the current time as 85 a unix epoch timestamp and a string timestamp. 86 """ 87 type Time { 88 unixTime: Int! 89 timeStamp: String! 90 } 91 92 """ 93 `Subscription` is where all the subscriptions your clients can 94 request. You can use Schema Directives like normal to restrict 95 access. 96 """ 97 type Subscription { 98 """ 99 `currentTime` will return a stream of `Time` objects. 100 """ 101 currentTime: Time! 102 } 103 ``` 104 105 ## Implementing your Resolver 106 107 After regenerating your code with `go run github.com/99designs/gqlgen generate` you'll find a 108 new resolver for your subscription. It will look like any other resolver, except it expects 109 a `<-chan *model.Time` (or whatever your type is). This is a 110 [channel](https://go.dev/tour/concurrency/2). Channels in Go are used to send objects to a 111 single receiver. 112 113 The resolver for our example `currentTime` subscription looks as follows: 114 115 ```go 116 // CurrentTime is the resolver for the currentTime field. 117 func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) { 118 // First you'll need to `make()` your channel. Use your type here! 119 ch := make(chan *model.Time) 120 121 // You can (and probably should) handle your channels in a central place outside of `schema.resolvers.go`. 122 // For this example we'll simply use a Goroutine with a simple loop. 123 go func() { 124 for { 125 // In our example we'll send the current time every second. 126 time.Sleep(1 * time.Second) 127 fmt.Println("Tick") 128 129 // Prepare your object. 130 currentTime := time.Now() 131 t := &model.Time{ 132 UnixTime: int(currentTime.Unix()), 133 TimeStamp: currentTime.Format(time.RFC3339), 134 } 135 136 // The subscription may have got closed due to the client disconnecting. 137 // Hence we do send in a select block with a check for context cancellation. 138 // This avoids goroutine getting blocked forever or panicking, 139 select { 140 case <-ctx.Done(): // This runs when context gets cancelled. Subscription closes. 141 fmt.Println("Subscription Closed") 142 // Handle deregistration of the channel here. `close(ch)` 143 return // Remember to return to end the routine. 144 145 case ch <- t: // This is the actual send. 146 // Our message went through, do nothing 147 } 148 } 149 }() 150 151 // We return the channel and no error. 152 return ch, nil 153 } 154 ``` 155 156 ## Trying it out 157 158 To try out your new subscription visit your GraphQL playground. This is exposed on 159 `http://localhost:8080` by default. 160 161 Use the following query: 162 163 ```graphql 164 subscription { 165 currentTime { 166 unixTime 167 timeStamp 168 } 169 } 170 ``` 171 172 Run your query and you should see a response updating with the current timestamp every 173 second. To gracefully stop the connection click the `Execute query` button again. 174 175 176 ## Adding Server-Sent Events transport 177 You can use instead of WebSocket (or in addition) [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) 178 as transport for subscriptions. This can have advantages and disadvantages over transport via WebSocket and requires a 179 compatible client library, for instance [graphql-sse](https://github.com/enisdenjo/graphql-sse). The connection between 180 server and client should be HTTP/2+. The client must send the subscription request via POST with 181 the header `accept: text/event-stream` and `content-type: application/json` in order to be accepted by the SSE transport. 182 The underling protocol is documented at [distinct connections mode](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md). 183 184 Add the SSE transport as first of all other transports, as the order is important. For that reason, `New` instead of 185 `NewDefaultServer` will be used. 186 ```go 187 srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 188 srv.AddTransport(transport.SSE{}) // <---- This is the important 189 190 // default server 191 srv.AddTransport(transport.Options{}) 192 srv.AddTransport(transport.GET{}) 193 srv.AddTransport(transport.POST{}) 194 srv.AddTransport(transport.MultipartForm{}) 195 srv.SetQueryCache(lru.New(1000)) 196 srv.Use(extension.Introspection{}) 197 srv.Use(extension.AutomaticPersistedQuery{ 198 Cache: lru.New(100), 199 }) 200 ``` 201 202 The GraphQL playground does not support SSE yet. You can try out the subscription via curl: 203 ```bash 204 curl -N --request POST --url http://localhost:8080/query \ 205 --data '{"query":"subscription { currentTime { unixTime timeStamp } }"}' \ 206 -H "accept: text/event-stream" -H 'content-type: application/json' \ 207 --verbose 208 ``` 209 210 ## Full Files 211 212 Here are all files at the end of this tutorial. Only files changed from the end 213 of the quick start are listed. 214 215 ### main.go 216 217 ```go 218 package main 219 220 import ( 221 "log" 222 "net/http" 223 "os" 224 225 "github.com/99designs/gqlgen/graphql/handler" 226 "github.com/99designs/gqlgen/graphql/handler/transport" 227 "github.com/99designs/gqlgen/graphql/playground" 228 "github.com/example/test/graph" 229 "github.com/example/test/graph/generated" 230 ) 231 232 const defaultPort = "8080" 233 234 func main() { 235 port := os.Getenv("PORT") 236 if port == "" { 237 port = defaultPort 238 } 239 240 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 241 242 srv.AddTransport(&transport.Websocket{}) 243 244 http.Handle("/", playground.Handler("GraphQL playground", "/query")) 245 http.Handle("/query", srv) 246 247 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 248 log.Fatal(http.ListenAndServe(":"+port, nil)) 249 } 250 ``` 251 252 ### schema.graphqls 253 254 ```graphql 255 type Query { 256 placeholder: String 257 } 258 259 type Time { 260 unixTime: Int! 261 timeStamp: String! 262 } 263 264 type Subscription { 265 currentTime: Time! 266 } 267 ``` 268 269 ### schema.resolvers.go 270 271 ```go 272 package graph 273 274 // This file will be automatically regenerated based on the schema, any resolver implementations 275 // will be copied through when generating and any unknown code will be moved to the end. 276 277 import ( 278 "context" 279 "fmt" 280 "time" 281 282 "github.com/example/test/graph/generated" 283 "github.com/example/test/graph/model" 284 ) 285 286 // Placeholder is the resolver for the placeholder field. 287 func (r *queryResolver) Placeholder(ctx context.Context) (*string, error) { 288 str := "Hello World" 289 return &str, nil 290 } 291 292 // CurrentTime is the resolver for the currentTime field. 293 func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) { 294 ch := make(chan *model.Time) 295 296 go func() { 297 for { 298 time.Sleep(1 * time.Second) 299 fmt.Println("Tick") 300 301 currentTime := time.Now() 302 303 t := &model.Time{ 304 UnixTime: int(currentTime.Unix()), 305 TimeStamp: currentTime.Format(time.RFC3339), 306 } 307 308 select { 309 case <-ctx.Done(): 310 // Exit on cancellation 311 fmt.Println("Subscription closed.") 312 return 313 314 case ch <- t: 315 // Our message went through, do nothing 316 } 317 318 } 319 }() 320 return ch, nil 321 } 322 323 // Query returns generated.QueryResolver implementation. 324 func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 325 326 // Subscription returns generated.SubscriptionResolver implementation. 327 func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} } 328 329 type queryResolver struct{ *Resolver } 330 type subscriptionResolver struct{ *Resolver } 331 ```