github.com/mstephano/gqlgen-schemagen@v0.0.0-20230113041936-dd2cd4ea46aa/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/mstephano/gqlgen-schemagen/graphql/handler" 43 "github.com/mstephano/gqlgen-schemagen/graphql/handler/transport" 44 "github.com/mstephano/gqlgen-schemagen/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/mstephano/gqlgen-schemagen 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 channel may have gotten closed due to the client disconnecting. 137 // To not have our Goroutine block or panic, we do the send in a select block. 138 // This will jump to the default case if the channel is closed. 139 select { 140 case ch <- t: // This is the actual send. 141 // Our message went through, do nothing 142 default: // This is run when our send does not work. 143 fmt.Println("Channel closed.") 144 // You can handle any deregistration of the channel here. 145 return // We'll just return ending the routine. 146 } 147 } 148 }() 149 150 // We return the channel and no error. 151 return ch, nil 152 } 153 ``` 154 155 ## Trying it out 156 157 To try out your new subscription visit your GraphQL playground. This is exposed on 158 `http://localhost:8080` by default. 159 160 Use the following query: 161 162 ```graphql 163 subscription { 164 currentTime { 165 unixTime 166 timeStamp 167 } 168 } 169 ``` 170 171 Run your query and you should see a response updating with the current timestamp every 172 second. To gracefully stop the connection click the `Execute query` button again. 173 174 ## Full Files 175 176 Here are all files at the end of this tutorial. Only files changed from the end 177 of the quick start are listed. 178 179 ### main.go 180 181 ```go 182 package main 183 184 import ( 185 "log" 186 "net/http" 187 "os" 188 189 "github.com/mstephano/gqlgen-schemagen/graphql/handler" 190 "github.com/mstephano/gqlgen-schemagen/graphql/handler/transport" 191 "github.com/mstephano/gqlgen-schemagen/graphql/playground" 192 "github.com/example/test/graph" 193 "github.com/example/test/graph/generated" 194 ) 195 196 const defaultPort = "8080" 197 198 func main() { 199 port := os.Getenv("PORT") 200 if port == "" { 201 port = defaultPort 202 } 203 204 srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}})) 205 206 srv.AddTransport(&transport.Websocket{}) 207 208 http.Handle("/", playground.Handler("GraphQL playground", "/query")) 209 http.Handle("/query", srv) 210 211 log.Printf("connect to http://localhost:%s/ for GraphQL playground", port) 212 log.Fatal(http.ListenAndServe(":"+port, nil)) 213 } 214 ``` 215 216 ### schema.graphqls 217 218 ```graphql 219 type Query { 220 placeholder: String 221 } 222 223 type Time { 224 unixTime: Int! 225 timeStamp: String! 226 } 227 228 type Subscription { 229 currentTime: Time! 230 } 231 ``` 232 233 ### schema.resolvers.go 234 235 ```go 236 package graph 237 238 // This file will be automatically regenerated based on the schema, any resolver implementations 239 // will be copied through when generating and any unknown code will be moved to the end. 240 241 import ( 242 "context" 243 "fmt" 244 "time" 245 246 "github.com/example/test/graph/generated" 247 "github.com/example/test/graph/model" 248 ) 249 250 // Placeholder is the resolver for the placeholder field. 251 func (r *queryResolver) Placeholder(ctx context.Context) (*string, error) { 252 str := "Hello World" 253 return &str, nil 254 } 255 256 // CurrentTime is the resolver for the currentTime field. 257 func (r *subscriptionResolver) CurrentTime(ctx context.Context) (<-chan *model.Time, error) { 258 ch := make(chan *model.Time) 259 260 go func() { 261 for { 262 time.Sleep(1 * time.Second) 263 fmt.Println("Tick") 264 265 currentTime := time.Now() 266 267 t := &model.Time{ 268 UnixTime: int(currentTime.Unix()), 269 TimeStamp: currentTime.Format(time.RFC3339), 270 } 271 272 select { 273 case ch <- t: 274 // Our message went through, do nothing 275 default: 276 fmt.Println("Channel closed.") 277 return 278 } 279 280 } 281 }() 282 return ch, nil 283 } 284 285 // Query returns generated.QueryResolver implementation. 286 func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } 287 288 // Subscription returns generated.SubscriptionResolver implementation. 289 func (r *Resolver) Subscription() generated.SubscriptionResolver { return &subscriptionResolver{r} } 290 291 type queryResolver struct{ *Resolver } 292 type subscriptionResolver struct{ *Resolver } 293 ```