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  ```