Are graphql subscriptions with redwood possible?

Hey :wave:

I am not able to make graphql subscriptions work with redwood.

I have added the Subscription type to the allowedType field of GraphQLHandlerOptions. Added the subscription to the graphql schema and implemented the subscription resolver according to the generated type - object with subscribe and resolve functions. However, when I tried to subscribe, I got the error message Subscription field must return Async Iterable. Received: undefined. After debugging a bit, I noticed that redwood doesn’t really take subscriptions into consideration when creating an executable schema. It expects every resolver to be a function (makeMergedSchema.ts:50) and doesn’t map services to Subscription type (makeMergedSchema.ts:173). The implication is that the subscription resolvers are ignored. When subscription is called and graphql tries to call subscribe on the related field, it is undefined.

The question is - what is the current state of graphql subscriptions in redwood? I could not find any resource other than Does RedwoodJS v1 support GraphQL Subscriptions or Live Queries? - #6 by jeffreytgilbert or Support for GraphQL Subscriptions using SSE - #4 by ramandhingra which doesn’t make it clearer.

Thank you! :slight_smile:

1 Like

Same here. If I am not mistaken, you need to add the following to graphql.ts
allowedOperations: [
OperationTypeNode.QUERY,
OperationTypeNode.MUTATION,
OperationTypeNode.SUBSCRIPTION,
]

Redwood has lots of ingredients ready for subscription, but it is a pity there was not time yet, to take the last steps. sdl works, service can be created with SubscriptionResolvers, but reaching it is does not seem to be possible.
Cells are not compatible with subscriptions. So one needs to add its own component:

export const SubscribeData = ({ id }: { id: string }) => {
  const { data, loading, error } = useSubscription(QUERY, { variables: { id } })
 
if(loading) return Loading...
if(error) return Something went wrong...
if(!data) return null
return <>{JSON.stringify(data)</>
}

(Why do we need invisible logic of cells, if code can be this short?)

However this one does not reach the service.
Reason probably is a missing link in configuration of Apollo client.
And following graphql-yoga custom-link-recipe (Subscriptions – GraphQL Yoga)
I implemented the following in App.tsx

<RedwoodApolloProvider
              useAuth={useAuth}
              graphQLClientConfig={{
                link: (rwLinks) => {
                  const sseLink = new SSELink({
                    uri: globalThis.RWJS_API_GRAPHQL_URL,
                    withCredentials: true,
                  })
                  const httpsLink = split(
                    ({ query, operationName }) => {
                      const definition = getOperationAST(query, operationName)

                      return (
                        definition?.kind === 'OperationDefinition' &&
                        definition.operation === 'subscription'
                      )
                    },
                    sseLink,
                    rwLinks[3]
                  )
                  return ApolloLink.from([rwLinks[0], rwLinks[1], rwLinks[2], httpsLink])
                },
              }}
            >

For completeness my SSELink.tsx adapted from Yoga docs.

import { ApolloLink, FetchResult, Observable, Operation } from '@apollo/client/core'
import { print } from 'graphql'

type SSELinkOptions = EventSourceInit & { uri: string }

export class SSELink extends ApolloLink {
  constructor(private options: SSELinkOptions) {
    super()
  }

  request(operation: Operation): Observable<FetchResult> {
    const url = new URL('https://localhost/') // temp url... not nice
    url.searchParams.append('query', print(operation.query))
    if (operation.operationName) {
      url.searchParams.append('operationName', operation.operationName)
    }
    if (operation.variables) {
      url.searchParams.append('variables', JSON.stringify(operation.variables))
    }
    if (operation.extensions) {
      url.searchParams.append('extensions', JSON.stringify(operation.extensions))
    }

    return new Observable((sink) => {
      const eventsource = new EventSource(
        url.toString().replace('https://localhost/', this.options.uri),
        this.options
      )
      eventsource.onmessage = function (event) {
        const data = JSON.parse(event.data)
        sink.next(data)
        if (eventsource.readyState === 2) {
          sink.complete()
        }
      }
      eventsource.onerror = function (error) {
        sink.error(error)
      }
      return () => eventsource.close()
    })
  }
}

Now this one sends a Get requerst to server. The server logs incoming request /graphql?query=subscription.... but it does not reach the service.

So I am also stuck with not being able to subscribe.

I need subscription to know when an expensive function finished calculating… :confused:

Hi. The Core Team knows subscriptions are a missing but valuable part to Redwood GraphQL server.

We’re currently engaging teams from the Redwood Startup Club how are looking to implement— and we hope to have some updates perhaps in a few weeks as they look into it.

5 Likes