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