Search code examples
javascriptgraphqlgraphql-jskoagraphql-subscriptions

How to use GraphQL subscription correctly?


I have a GraphQL powered app. The query and mutation parts work well. I try to add GraphQL subscription.

The server GraphQL subscription part code is inspired by the demo in the readme of apollographql/subscriptions-transport-ws.

Please also check the comments in the code for more details.

import Koa from 'koa';
import Router from 'koa-router';
import graphqlHTTP from 'koa-graphql';
import asyncify from 'callback-to-async-iterator';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import firebase from 'firebase-admin';
import { execute, subscribe } from 'graphql';
import { GraphQLObjectType, GraphQLString } from 'graphql';

const MeType = new GraphQLObjectType({
  name: 'Me',
  fields: () => ({
    name: { type: GraphQLString },
    // ...
  }),
});

const listenMe = async (callback) => {
  // Below the firebase API returns real-time data
  return firebase
    .database()
    .ref('/users/123')
    .on('value', (snapshot) => {
      // snapshot.val() returns an Object including name field.
      // Here I tested is correct, it always returns { name: 'Rose', ... }
      // when some other fields inside got updated in database.
      return callback(snapshot.val());
    });
};

const Subscription = new GraphQLObjectType({
  name: 'Subscription',
  fields: () => ({
    meChanged: {
      type: MeType,
      subscribe: () => asyncify(listenMe),
    },
  }),
});

const schema = new GraphQLSchema({
  query: Query,
  mutation: Mutation,
  subscription: Subscription,
});

const app = new Koa();
app
  .use(new Router()
    .post('/graphql', async (ctx) => {
      // ...

      await graphqlHTTP({
        schema,
        graphiql: true,
      })(ctx);
    })
    .routes());

const server = app.listen(3009);

SubscriptionServer.create(
  {
    schema,
    execute,
    subscribe,
  },
  {
    server,
    path: '/subscriptions',
  },
);

I am using Altair GraphQL Client to test since it supports GraphQL subscription.

enter image description here

As the screenshot shows, it does get new data every time when the data changes in database.

However, meChanged is null and it does not throw any error. Any idea? Thanks


Solution

  • Finally have a new library can do the work without full Apollo framework.

    https://github.com/enisdenjo/graphql-ws

    Here are the codes that I have succeed:

    Server (GraphQL Schema Definition Language)

    import { useServer } from 'graphql-ws/lib/use/ws';
    import WebSocket from 'ws';
    import { buildSchema } from 'graphql';
    
    const schema = buildSchema(`
      type Subscription {
        greeting: String
      }
    `);
    
    const roots = {
      subscription: {
        greeting: async function* sayHiIn5Languages() {
          for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
            yield { greeting: hi };
          }
        },
      },
    };
    
    const wsServer = new ws.Server({
      server, // Your HTTP server
      path: '/graphql',
    });
    useServer(
      {
        schema,
        execute,
        subscribe,
        roots,
      },
      wsServer
    );
    

    Server (GraphQL.js GraphQLSchema object way)

    import { execute, subscribe, GraphQLObjectType, GraphQLSchema, GraphQLString } from 'graphql';
    import { useServer } from 'graphql-ws/lib/use/ws';
    import WebSocket from 'ws';
    import { PubSub } from 'graphql-subscriptions';
    
    const pubsub = new PubSub();
    
    const subscription = new GraphQLObjectType({
      name: 'Subscription',
      fields: {
        greeting: {
          type: GraphQLString,
          resolve: (source) => {
            if (source instanceof Error) {
              throw source;
            }
            return source.greeting;
          },
          subscribe: () => {
            return pubsub.asyncIterator('greeting');
          },
        },
      },
    });
    
    const schema = new GraphQLSchema({
      query,
      mutation,
      subscription,
    });
    
    setInterval(() => {
      pubsub.publish('greeting', {
        greeting: 'Bonjour',
      });
    }, 1000);
    
    const wsServer = new ws.Server({
      server, // Your HTTP server
      path: '/graphql',
    });
    useServer(
      {
        schema,
        execute,
        subscribe,
        roots,
      },
      wsServer
    );
    

    Client

    import { createClient } from 'graphql-ws';
    
    const client = createClient({
      url: 'wss://localhost:5000/graphql',
    });
    
    client.subscribe(
      {
        query: 'subscription { greeting }',
      },
      {
        next: (data) => {
          console.log('data', data);
        },
        error: (error) => {
          console.error('error', error);
        },
        complete: () => {
          console.log('no more greetings');
        },
      }
    );
    

    DISCLOSE: I am not associated with the library.