github.com/99designs/gqlgen@v0.17.45/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 // Handle deregistration of the channel here. Note the `defer` 125 defer close(ch) 126 127 for { 128 // In our example we'll send the current time every second. 129 time.Sleep(1 * time.Second) 130 fmt.Println("Tick") 131 132 // Prepare your object. 133 currentTime := time.Now() 134 t := &model.Time{ 135 UnixTime: int(currentTime.Unix()), 136 TimeStamp: currentTime.Format(time.RFC3339), 137 } 138 139 // The subscription may have got closed due to the client disconnecting. 140 // Hence we do send in a select block with a check for context cancellation. 141 // This avoids goroutine getting blocked forever or panicking, 142 select { 143 case <-ctx.Done(): // This runs when context gets cancelled. Subscription closes. 144 fmt.Println("Subscription Closed") 145 // Handle deregistration of the channel here. `close(ch)` 146 return // Remember to return to end the routine. 147 148 case ch <- t: // This is the actual send. 149 // Our message went through, do nothing 150 } 151 } 152 }() 153 154 // We return the channel and no error. 155 return ch, nil 156 } 157 ``` 158 159 ## Trying it out 160 161 To try out your new subscription visit your GraphQL playground. This is exposed on 162 `http://localhost:8080` by default. 163 164 Use the following query: 165 166 ```graphql 167 subscription { 168 currentTime { 169 unixTime 170 timeStamp 171 } 172 } 173 ``` 174 175 Run your query and you should see a response updating with the current timestamp every 176 second. To gracefully stop the connection click the `Execute query` button again. 177 178 179 ## Adding Server-Sent Events transport 180 You can use instead of WebSocket (or in addition) [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) 181 as transport for subscriptions. This can have advantages and disadvantages over transport via WebSocket and requires a 182 compatible client library, for instance [graphql-sse](https://github.com/enisdenjo/graphql-sse). The connection between 183 server and client should be HTTP/2+. The client must send the subscription request via POST with 184 the header `accept: text/event-stream` and `content-type: application/json` in order to be accepted by the SSE transport. 185 The underling protocol is documented at [distinct connections mode](https://github.com/enisdenjo/graphql-sse/blob/master/PROTOCOL.md). 186 187 Add the SSE transport as first of all other transports, as the order is important. For that reason, `New` instead of 188 `NewDefaultServer` will be used. 189 ```go 190 srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 191 srv.AddTransport(transport.SSE{}) // <---- This is the important 192 193 // default server 194 srv.AddTransport(transport.Options{}) 195 srv.AddTransport(transport.GET{}) 196 srv.AddTransport(transport.POST{}) 197 srv.AddTransport(transport.MultipartForm{}) 198 srv.SetQueryCache(lru.New(1000)) 199 srv.Use(extension.Introspection{}) 200 srv.Use(extension.AutomaticPersistedQuery{ 201 Cache: lru.New(100), 202 }) 203 ``` 204 205 The GraphQL playground does not support SSE yet. You can try out the subscription via curl: 206 ```bash 207 curl -N --request POST --url http://localhost:8080/query \ 208 --data '{"query":"subscription { currentTime { unixTime timeStamp } }"}' \ 209 -H "accept: text/event-stream" -H 'content-type: application/json' \ 210 --verbose 211 ``` 212 213 ## Full Files 214 215 Here are all files at the end of this tutorial. Only files changed from the end 216 of the quick start are listed. 217 218 ### main.go 219 220 ```go 221 package main 222 223 import ( 224 "log" 225 "net/http" 226 "os" 227 228 "github.com/99designs/gqlgen/graphql/handler" 229 "github.com/99designs/gqlgen/graphql/handler/transport" 230 "github.com/99designs/gqlgen/graphql/playground" 231 "github.com/example/test/graph" 232 "github.com/example/test/graph/generated" 233 ) 234 235 const defaultPort = "8080" 236 237 func main() { 238 port := os.Getenv("PORT") 239 if port == "" { 240 port = defaultPort 241 } 242 243 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 244 245 srv.AddTransport(&transport.Websocket{}) 246 247 http.Handle("/", playground.Handler("GraphQL playground", "/query")) 248 http.Handle("/query", srv) 249 250 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 251 log.Fatal(http.ListenAndServe(":"+port, nil)) 252 } 253 ``` 254 255 ### schema.graphqls 256 257 ```graphql 258 type Query { 259 placeholder: String 260 } 261 262 type Time { 263 unixTime: Int! 264 timeStamp: String! 265 } 266 267 type Subscription { 268 currentTime: Time! 269 } 270 ``` 271 272 ### schema.resolvers.go 273 274 ```go 275 package graph 276 277 // This file will be automatically regenerated based on the schema, any resolver implementations 278 // will be copied through when generating and any unknown code will be moved to the end. 279 280 import ( 281 "context" 282 "fmt" 283 "time" 284 285 "github.com/example/test/graph/generated" 286 "github.com/example/test/graph/model" 287 ) 288 289 // Placeholder is the resolver for the placeholder field. 290 func (r *queryResolver) Placeholder(ctx context.Context) (*string, error) { 291 str := "Hello World" 292 return &str, nil 293 } 294 295 // CurrentTime is the resolver for the currentTime field. 296 func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) { 297 ch := make(chan *model.Time) 298 299 go func() { 300 defer close(ch) 301 302 for { 303 time.Sleep(1 * time.Second) 304 fmt.Println("Tick") 305 306 currentTime := time.Now() 307 308 t := &model.Time{ 309 UnixTime: int(currentTime.Unix()), 310 TimeStamp: currentTime.Format(time.RFC3339), 311 } 312 313 select { 314 case <-ctx.Done(): 315 // Exit on cancellation 316 fmt.Println("Subscription closed.") 317 return 318 319 case ch <- t: 320 // Our message went through, do nothing 321 } 322 323 } 324 }() 325 return ch, nil 326 } 327 328 // Query returns generated.QueryResolver implementation. 329 func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 330 331 // Subscription returns generated.SubscriptionResolver implementation. 332 func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} } 333 334 type queryResolver struct{ *Resolver } 335 type subscriptionResolver struct{ *Resolver } 336 ```