Meteor/React/Apollo/Graphql Subscriptions (using GooglePubSub)

Hi, I’ve been trying to incorporate subscriptions into the aforementioned stack (the React bit is irrelevant since this issue is regarding the back-end) but I’ve just been coming across many ways of incorporating subscriptions but I can’t seem to figure out how to integrate it into my already existing Meteor app.

import { ApolloServer, gql } from "apollo-server-express"
import { makeExecutableSchema } from "graphql-tools"
import { WebApp } from "meteor/webapp"
import { getUser } from "meteor/apollo"
import merge from "lodash/merge"

import SchemaA from "../../api/a/SchemaA.graphql"
import ResolverA from "../../api/a/resolvers"
import SchemaB from "../../api/b/SchemaB.graphql"
import ResolverB from "../../api/b/resolvers"
import SchemaC from "../../api/c/SchemaC.graphql"
import ResolverC from "../../api/c/resolvers"
import { GooglePubSub } from "@axelspringer/graphql-google-pubsub"

const pubsub = new GooglePubSub()

const typeDefs = [SchemaA, SchemaB, SchemaC]
const resolvers = merge(ResolverA, ResolverB, ResolverC)

const schema = makeExecutableSchema({ typeDefs, resolvers })

const server = new ApolloServer({
    schema,
    context: async ({ req }) => ({
        user: await getUser(req.headers.authorization),
        pubsub
    }),
    subscriptions : {
        path: '/subscriptions'
    }
})

server.applyMiddleware({
    app: WebApp.connectHandlers,
    path: "/graphql",
})

WebApp.connectHandlers.use("/graphql", (req, res) => {
    if (req.method === "GET") {
        res.end()
    }
})

I understand from the Apollo docs that I should use a PubSub class from another library and not the standard one they show in the docs, and I decided to try Redis.

I have tried declaring this new PubSub object and passing that in via the ApolloServer context but when I test out my subscription in Graphiql I get the following error (IP masked):

{
  "error": "Could not connect to websocket endpoint ws://XXX.XXX.XX.XX:3000/subscriptions. Please check if the endpoint url is correct."
}

And in my terminal this prints every second or two:

Error: connect ECONNREFUSED 127.0.0.1:6379
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) {
errno: 'ECONNREFUSED',
code: 'ECONNREFUSED',
syscall: 'connect',
address: '127.0.0.1',
port: 6379
}

So clearly I am missing some configuration step, but I’ve not been able to figure out why. Any help would be appreciated, thank you.

EDIT: I tried GooglePubSub instead and now the last error isn’t thrown anymore in my terminal, but the error in Graphiql persists.

I use swydo:ddp-apollo. I find it makes setup very easy.

import {setup} from 'meteor/swydo:ddp-apollo';
import {makeExecutableSchema} from "graphql-tools";

const typeDefs = [AllSchema];
const resolvers = [AllResolvers];

const schema = makeExecutableSchema({
    typeDefs,
    resolvers
});

setup({
    schema
});


import {PostgresPubSub} from 'graphql-postgres-subscriptions';
const pubsub = new PostgresPubSub({
    user: Meteor.settings.postgres[Meteor.isDevelopment ? 'development' : 'production'].dbuser,
    host: Meteor.settings.postgres[Meteor.isDevelopment ? 'development' : 'production'].host,
    database: Meteor.settings.postgres[Meteor.isDevelopment ? 'development' : 'production'].dbname,
    password: Meteor.settings.postgres[Meteor.isDevelopment ? 'development' : 'production'].dbpsd,
    port: Meteor.settings.postgres[Meteor.isDevelopment ? 'development' : 'production'].port,
});

pubsub.subscribe("error", err => {
    console.log(`pubsub error: ${err.message}`);
});

		// in a resolver that mutates something you want to publish to a subscription:
		pubsub.publish(APPT_ADDED_CHANNEL, {
			APPTSubscription: appt,
			args: args,
			context: context
		});
		
		// the subscription resolver 
        Subscription: {
            APPTSubscription: {
                subscribe: withFilter(
                    () => pubsub.asyncIterator(APPT_ADDED_CHANNEL),
                    (payload, args) => {
                        let result = false;
                        if (typeof (payload) === 'undefined')
                            return result;

						//get the userId of the client -- I think it iterates through all the subscribed clients -- 
                        const userid = payload.context.userId;
                        let APPTSubscription = payload.APPTSubscription;
                        const user0id = APPTSubscription.originatingUserId;
                        const user1id = APPTSubscription.apptWithUserId;

						//check to confirm that this info should be published to this client
                        result = ((user0id === userid) || (user1id === userid));
                        return result;
                    }
                )
            }
            ,
		

2 Likes

Thank you very much for your reply. I have looked into this and have spent a few hours on it so far but haven’t gotten it quite working yet.

On my server I have:

const typeDefs = [AllSchemas]
const resolvers = merge(AllResolvers)

const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
})

const server = new ApolloServer({
    schema,
    context: async ({ req }) => ({
        user: await getUser(req.headers.authorization),
    }),
    subscriptions : {
        path: '/subscriptions'
    }
})

server.applyMiddleware({
    app: WebApp.connectHandlers,
    path: "/graphql",
})

WebApp.connectHandlers.use("/graphql", (req, res) => {
    if (req.method === "GET") {
        res.end()
    }
})

I can’t seem to setup the server as per recommended in the docs for swydo:ddp-apollo. I tried:

const context = async ({req}) => ({
    user: await getUser(req.headers.authorization)
})
setup({
    schema,
    context
})

But then in my application I am automatically logged out and can’t log back in, I suspect there is some header error being thrown as I get an ApolloError: Uncaught (in promise) Error: Unexpected token < in JSON at position 0.

On my client side I have:

const httpLink = new HttpLink({
    uri: "http://XXX.XXX.XX.XX:3000/graphql"
})

const authLink = new ApolloLink((operation, forward) => {
    const token = Accounts._storedLoginToken()
    operation.setContext(() => ({
        headers: {
            authorization: token
        }
    }))
    return forward(operation)
})

const subscriptionLink = new DDPSubscriptionLink()

const cache = new InMemoryCache()

const link = split(
  isSubscription,
  subscriptionLink,
  httpLink
)

const client = new ApolloClient({
    link: from([authLink, link]),
    cache,
})

const ApolloApp = () => (
    <BrowserRouter>
        <ApolloProvider client={client}>
            <App />
        </ApolloProvider>
    </BrowserRouter>
)

Meteor.startup(() => {
    render(<ApolloApp />, document.getElementById("app"))
})

And this works fine without any issue. I declared a simple schema and subscription:

type Message {
    _id: String!
    no: Int!
    message: String!
}
type Subscription {
    submsg: [Message]!
}

My resolver is as follows. Here I use PubSub from graphql-subscriptions as I want to get this working then I will change the import to some other production-ready pubsub library.

import { PubSub } from 'graphql-subscriptions'
const pubsub = new PubSub()
pubsub.subscribe("error", err => {
    console.log(`pubsub error: ${err.message}`);
})
...
Mutation: {
    postmsg(obj,args,context) {
        Messages.insert({no:1, message: args.message})
        pubsub.publish("SOMETHING_CHANGED", {message:args.message})
        return args.message
    }
}
Subscription: {
    submsg: {
        subscribe: (parent, args, context) => {
            return pubsub.asyncIterator("SOMETHING_CHANGED")
        }
    }
}

What am I missing here? I don’t know if my subscription is working as I can’t test it at all, I still get the same error in Graphiql that it cannot connect to ws://XXX.XXX.XX.XX:3000/subscriptions. I have a React component which is using the useSubcription hook but the data and error fields are both undefined and loading is perpetually true.

const { data, loading, error} = useSubscription(gql`
        subscription getSub {
            submsg {
                _id
                no
                message
            }
        }
    `)

You may have some old setup code that is conflicting with swydo setup code. For example I still see:

const server = new ApolloServer({
    schema,
    context: async ({ req }) => ({
        user: await getUser(req.headers.authorization),
    }),
    subscriptions : {
        path: '/subscriptions'
    }
})

I believe I provided all the setup code required in its entirety. So just drop out all your old setup code and replace it with the setup code I provided. Please let me know what happens with that.

1 Like

P.S. Here’s my React router code:

import React from 'react'
import {ApolloClient} from "@apollo/client";
import {InMemoryCache} from "apollo-cache-inmemory";

import {DDPLink} from 'meteor/swydo:ddp-apollo';

const client = new ApolloClient({
    link: new DDPLink(),
    cache: new InMemoryCache()
});

import {ApolloProvider} from '@apollo/client'

function App(props) {
    return (
        <React.Fragment>
                <ApolloProvider client={ApolloClient}>
                    <Router>

1 Like

Here’s sample useSubscription code:


function isDuplicateObject(newObject, existingObjects) {
    if (!newObject) {
        return true;
    }
    if (!existingObjects) {
        return false;
    }

    let result = newObject.id !== null && existingObjects.some(anObject => newObject.id === anObject.id);
    return result;
}

[.....]

    const useIncomingApptsData = useSubscription(
        APPT_SUBSCRIPTION_QUERY,
        {
            variables: {"originatingUserId": Meteor.userId()},
            skip: !Meteor.userId(),
            onSubscriptionData: ({subscriptionData: {data}, loading}) => {
                if (loading) return null;

                if (!data.APPTSubscription) return null;
                const newAPPT = data.APPTSubscription;

                // don't double add the message
                if (isDuplicateObject(newAPPT, Appts.current)) {
                    return null;
                }

                Appts.current.push(newAPPT);
            }
        },
    )
1 Like

Thank you for the examples. I did try it before but ran into some errors, but I’ve tried it again and done some further debugging. Now my server file only contains this:

const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
})

setup({
    schema
})

All my Graphql operations do not work. When I inspect the network tab, the response is the raw HTML of my app. localhost:3000/graphql doesn’t open up Graphiql anymore. I’m still trying to debug and will update this if I can figure out how to make sure Graphiql still runs and /graphql is still a valid endpoint. I’ve read through some issues and it seems support was added in this PR but I just tried importing createHttpServer from the package but it’s no longer in it so I wonder if it was removed or something.

Either way - do you need to separately initialize a Graphql server or something of that sort? It seems odd that there is no substitution for applyMiddleware and connectHandlers, and that the package has it built in but clearly it isn’t working, is there some other configuration needed?

Also, shouldn’t <ApolloProvider client={ApolloClient}> be <ApolloProvider client={client}>?

Correct. I actually import the code that creates the client from another file like this:

import ApolloClient from '../../startup/client/apollo_client';

1 Like

Could you please post your client code?

Yes, I posted it in the third post. Here:

const httpLink = new HttpLink({
    uri: "http://XXX.XXX.XX.XX:3000/graphql"
})

const authLink = new ApolloLink((operation, forward) => {
    const token = Accounts._storedLoginToken()
    operation.setContext(() => ({
        headers: {
            authorization: token
        }
    }))
    return forward(operation)
})

const subscriptionLink = new DDPSubscriptionLink()

const cache = new InMemoryCache()

const link = split(
  isSubscription,
  subscriptionLink,
  httpLink
)

const client = new ApolloClient({
    link: from([authLink, link]),
    cache,
})

const ApolloApp = () => (
    <BrowserRouter>
        <ApolloProvider client={client}>
            <App />
        </ApolloProvider>
    </BrowserRouter>
)

Meteor.startup(() => {
    render(<ApolloApp />, document.getElementById("app"))
})

This is the client code. Even if I remove the authLink, I still can’t run any Graphql operations. In your implementation you can access Graphiql at /graphql?

Can you please replace your client code with the code I posted above, replacing client with ApolloClient as noted by @ajaypillay? Please include your import statements as well.

I’m going into a meeting now… will check back later.

So now my server and client are the same as the examples you supplied. Server:

import { setup } from 'meteor/swydo:ddp-apollo'
import { makeExecutableSchema } from "graphql-tools"
const schema = makeExecutableSchema({
    typeDefs,
    resolvers,
})
setup({
    schema
})

Client:

import { ApolloClient } from "@apollo/client"
import { InMemoryCache } from "apollo-cache-inmemory"
import { ApolloProvider } from "@apollo/react-hooks"
import { DDPLink } from "meteor/swydo:ddp-apollo"

const client = new ApolloClient({
    link: new DDPLink(),
    cache: new InMemoryCache()
})

const ApolloApp = () => (
    <BrowserRouter>
        <ApolloProvider client={client}>
            <App />
        </ApolloProvider>
    </BrowserRouter>
)

Meteor.startup(() => {
    render(<ApolloApp />, document.getElementById("app"))
})

However, localhost:3000/graphql still does not open up Graphiql. I can only import ApolloProvider from @apollo/react-hooks or react-apollo. If I import it from @apollo/client, I get the following error:

Uncaught Invariant Violation: Could not find "client" in the context or passed in as an option. Wrap the root component in an <ApolloProvider>, or pass an ApolloClient instance in via options.

swydo-apollo does not support graphiql. You can get the same graphiql functions via a chrome extension – I think it may be the Apollo extension.

What happens when your app runs a graphQL query?

Oh I see. The issue is that with the browser extension, when I click on “Graphiql” it’s just stuck at a blank screen.

Right now in my main App I have the following:

const App = (props) => {

    if (props.data.loading) return (<Loading />)
    
    if (props.data.error) {
        console.log(props.data.error)
        return null
    }
    
    console.log(props.data.userID)
    if (!props.data.userID) return (<Welcome />)

    return <SomeComponent />
}

const userQuery = gql`
    query {
        userID
    }
`
export default graphql(userQuery)(withApollo(App))

Now this code works perfectly fine with my old client/server code but breaks with the new code. In the Network panel, I see that there is a websocket connection open and the data was received:
image
When I console.log(props.data.userID), it is undefined. How should I update this in the main app?

To confirm, is this a react component? It’s named App so I want to confirm this is just a component.

Yes it is a React component. @vikr00001

Here’s how I do it. I send the ApolloClient to the component from the Router in case it’s needed:

import React, {useState} from "react";
import {useQuery} from "@apollo/client";
import MY_GRAPHQL_QUERY from "myGraphQLQueryLibrary";

function MakeAppt(props) {
    const {client} = props;
    
   const {data, error, loading, refetch} = useQuery(MY_GRAPHQL_QUERY, {
        variables: {"a_var": myVar},
        skip: false,
    });

    if (loading || error) return (null);

    if (data) {
		// do something with data
    }
    

    return (
        <>
			
        </>
    );
}

export default MakeAppt;

Do you send the client into every single component? Is there any reason not to use the withApollo HOC? I was under the impression that this HOC allows us to avoid the need to pass client into every component.

I tried your method out and it seems to work for now, thank you. But I keep getting this error:

Uncaught Invariant Violation: Could not find "client" in the context of ApolloConsumer. Wrap the root component in an <ApolloProvider>

My codebase was already rather large before subscriptions and if I can’t use the withApollo HOC for all my components, I’m going to have to rewrite every single component as most of them use withApollo. Initially I had this error for App because I had export default withApollo(App) but once I removed withApollo() it was fine.

Here’s a StackOverflow post addressing the error you are seeing.