question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

How to get the `operationName` inside `subscriptionExchange`?

See original GitHub issue

I’m running a GraphQL API with graphql-ruby and needed to get subscriptions work with Action Cable, so I’ve created this SubscriptionForwarder, based on the official ActionCableLink implementation:

import * as ActionCable from '@rails/actioncable'
import { SubscriptionOperation } from '@urql/core/dist/types/exchanges/subscription'
import Observable from 'zen-observable-ts'

class ActionCableExchange {
  cable: ActionCable.Cable
  channelName: string
  actionName: string
  connectionParams: object

  constructor(options: { cable: ActionCable.Cable; channelName?: string; actionName?: string; connectionParams?: object }) {
    this.cable = options.cable
    this.channelName = options.channelName || 'GraphqlChannel'
    this.actionName = options.actionName || 'execute'
    this.connectionParams = options.connectionParams || {}
  }

  request(operation: SubscriptionOperation) {
    return new Observable(observer => {
      var channelId = Math.round(Date.now() + Math.random() * 100000).toString(16)
      var actionName = this.actionName
      var subscription = this.cable.subscriptions.create(
        Object.assign(
          {},
          {
            channel: this.channelName,
            channelId: channelId,
          },
          this.connectionParams,
        ),
        {
          connected: function() {
            this.perform(actionName, {
              query: operation.query ? operation.query : null,
              variables: operation.variables,
              // TODO: add this to support persisted queries. But we need to get the operationName at first
              // operationId: (operation as { operationId?: string }).operationId,
              // operationName: operation.operationName,
            })
          },
          received: function(payload) {
            if (payload.result.data || payload.result.errors) {
              observer.next(payload.result)
            }

            if (!payload.more) {
              observer.complete()
            }
          },
        },
      )

      // Make the ActionCable subscription behave like an Apollo subscription
      return Object.assign(subscription, { closed: false })
    })
  }
}

const cable = ActionCable.createConsumer('ws://localhost:3000/cable')
const actionCableClient = new ActionCableExchange({ cable })

const defaultExchanges = [
  dedupExchange,
  cacheExchange,
  errorExchange,
  fetchExchange,
  subscriptionExchange({
    forwardSubscription: operation => {
      return actionCableClient.request(operation)
    },
  }),
]

I’d like to add this to the graphql-ruby repository and then add some documentation about the usage with ActionCable to urql. But I need to figure out how to get the operationName of the query in my actionCableClient at first, since graphql-ruby needs this to support persisted operations. I took a look at the source of the subscriptionExchange, but I think that it’s not possible to get the operationName at the moment, isn’t it?

Also I was thinking about the possibility to use the operationName to invalidate the cache (little bit off topic, but I don’t want to open a second issue). Sometimes it would be helpful to invalidate certain queries, since I don’t want to invalidate all of my viewer queries if I just could invalidate GetViewerIssues for example. Any thoughts about this? 😊

Last but not least: thank you so much for urql. I’ve switched from Apollo and everything feels way more “under control” now 😄

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:6 (2 by maintainers)

github_iconTop GitHub Comments

6reactions
gopetercommented, Sep 23, 2020

More or less, I’ve omitted the operationId for now and don’t use persisted queries, but the subscription works:

import * as ActionCable from '@rails/actioncable'
import { Exchange, createClient, dedupExchange, fetchExchange, subscriptionExchange } from 'urql'
import { Cache, cacheExchange as createCacheExchange } from '@urql/exchange-graphcache'
import { SubscriptionOperation } from '@urql/core/dist/types/exchanges/subscription'
import { devtoolsExchange } from '@urql/devtools'
import Observable from 'zen-observable-ts'
import { DocumentNode, Kind, parse } from 'graphql'

// based on https://github.com/rmosolgo/graphql-ruby/blob/master/javascript_client/src/subscriptions/ActionCableLink.ts
class ActionCableExchange {
  cable: ActionCable.Cable
  channelName: string
  actionName: string
  connectionParams: object

  constructor(options: { cable: ActionCable.Cable; channelName?: string; actionName?: string; connectionParams?: object }) {
    this.cable = options.cable
    this.channelName = options.channelName || 'GraphqlChannel'
    this.actionName = options.actionName || 'execute'
    this.connectionParams = options.connectionParams || {}
  }

  getOperationName(query: DocumentNode): string | undefined {
    for (let i = 0, l = query.definitions.length; i < l; i++) {
      const node = query.definitions[i]
      if (node.kind === Kind.OPERATION_DEFINITION && node.name) {
        return node.name.value
      }
    }
  }

  request(operation: SubscriptionOperation) {
    return new Observable((observer) => {
      const channelId = Math.round(Date.now() + Math.random() * 100000).toString(16)
      const actionName = this.actionName
      const operationName = this.getOperationName(parse(operation.query))
      const subscription = this.cable.subscriptions.create(
        Object.assign(
          {},
          {
            channel: this.channelName,
            channelId: channelId,
          },
          this.connectionParams,
        ),
        {
          connected: function () {
            this.perform(actionName, {
              query: operation.query ? operation.query : null,
              variables: operation.variables,
              // TODO: add this to support persisted queries. But we need to get the operationName at first
              // operationId: (operation as { operationId?: string }).operationId,
              operationName: operationName,
            })
          },
          received: function (payload) {
            if (payload.result.data || payload.result.errors) {
              observer.next(payload.result)
            }

            if (!payload.more) {
              observer.complete()
            }
          },
        },
      )

      // Make the ActionCable subscription behave like an Apollo subscription
      return Object.assign(subscription, { closed: false })
    })
  }
}

const actionCableClient = new ActionCableExchange({ cable })

const defaultExchanges: Exchange[] = [
  devtoolsExchange,
  dedupExchange,
  cacheExchange,
  errorExchange,
  fetchExchange,
  subscriptionExchange({
    forwardSubscription: (operation) => {
      return actionCableClient.request(operation)
    },
  }),
]
0reactions
jatbonecommented, Sep 23, 2020

Hi @gopeter, were you able to figure this out? It’s possible to share it please? I have no idea how to make subscriptions work with rubys action cable. Thanks

Read more comments on GitHub >

github_iconTop Results From Across the Web

How to get the `operationName` inside `subscriptionExchange`?
I'm running a GraphQL API with graphql-ruby and needed to get subscriptions work with Action Cable, so I've created this ...
Read more >
@urql/core | urql Documentation
Read more about how to use this API on the "Subscriptions" page. client.reexecuteOperation. This method is commonly used in Exchanges to reexecute an...
Read more >
Subscriptions – GraphQL Yoga
A GraphQL subscription initiates an event stream, where each event in the stream is pushed from the server to the client that issued...
Read more >
View Raw - UNPKG
toBeCalledTimes(5); // we say "Hi" in 5 languages })(); ``` ## Recipes <details ... return subscriptionsClient.subscribe( { operationName: operation.name, ...
Read more >
graphql-ws - npm
In Explorer Settings, click "Edit" for "Connection Settings" and select graphql-ws under "Implementation". Client usage with GraphiQL.
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found