dotansimha / graphql-yoga

🧘 Rewrite of a fully-featured GraphQL Server with focus on easy setup, performance & great developer experience. The core of Yoga implements WHATWG Fetch API and can run/deploy on any JS environment.
https://the-guild.dev/graphql/yoga-server
MIT License
8.11k stars 558 forks source link

[integrations/fastify] Add documentation to enable SSE subscriptions #3276

Open EmrysMyrddin opened 1 month ago

EmrysMyrddin commented 1 month ago

Discussed in https://github.com/dotansimha/graphql-yoga/discussions/3273

Originally posted by **santino** May 15, 2024 Hello folks, as mentioned by the title, I am looking to setup subscriptions in my Yoga implementation. I am not using Yoga as a full server, but instead I am integrating it with my `fastify` instance. The `useGraphQLSSE` plugin only seems to work if Yoga is executed as a server. Do you have any example for writing up a plugin that only handles subscription requests received on my single `/graphql` endpoint?
santino commented 1 month ago

I ended up building this plugin to handle subscriptions in my Fastify setup

import { EOL } from 'os'

export const useSubscription = () => {
  return {
    async onSubscribe () {
      return {
        async onSubscribeResult ({
          result,
          args: {
            contextValue: { req, reply }
          }
        }) {
          req.socket.on('end', () => {
            result.return()
            reply.hijack() // tell Fastify to skip the automatic invocation of reply.send()
          })
          req.socket.on('close', () => {
            reply.log.info(
              {
                res: reply,
                responseTime: reply.elapsedTime
              },
              'request completed'
            )
            reply.raw.end() // tell the server that all of the response headers and body have been sent
          })

          for await (const data of result) {
            reply.raw.write(
              `event: next${EOL}data: ${JSON.stringify(data)}${EOL}${EOL}`
            )
          }
        }
      }
    }
  }
}

Hopefully this is useful to other devs as well.

ardatan commented 1 month ago

GraphQL Yoga doesn't need an extra plugin for SSE except single connection mode. You can see Fastify tests below that use the same path for subscriptions. Maybe you can help us reproducing your issue here; https://github.com/dotansimha/graphql-yoga/blob/main/examples/fastify/__integration-tests__/fastify.spec.ts GraphQL SSE plugin only handles single connection mode of GraphQL SSE protocol, and you can configure the path of that listens the subscriptions; https://github.com/dotansimha/graphql-yoga/blob/main/packages/plugins/graphql-sse/src/index.ts#L17

I'd not use that kind of plugin which bypasses entire Yoga plugin hooks, and it might cause an unexpected behavior.

santino commented 1 month ago

You are absolutely right. Thanks for your comment. I looked at the test and decided I had to make my implementation work in the correct way.

The issue I had initially was related to how I was setting an initial value for my subscription. I was doing a pubSub.subscribe and then used a process.nextTick to create the first event for my subscription topic. It was a convoluted solution with some drawbacks (had to make sure the "artificial" event would be issued only to the new subscriber), but it worked.

After migrating to Yoga the process.nextTick was somehow issued before the subscriber was actually subscribed to the pubSub. Not sure why, but this caused my "artificial" event to not be delivered. Because of this I wrongly assumed that this setup wasn't working.

Now I spent more time into this and figured all this out. So I removed my custom useSubscription plugin entirely and switched to a much better solution to deliver my initial data to the subscription with this:

Repeater.merge([
  initialData,
  pubSub.subscribe(topic)
])

I think I would probably add something to the documentation that documents simple subscriptions without any need for the SSE plugin. Maybe make it more explicit. In any case, this issue can be closed.

EmrysMyrddin commented 1 month ago

Thank you for your feedback!

It seems our documentation still need some update on this ? If it's the case, I will let this issue open to not forget about it :-)