Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Possible to detect Action Cable disconnections when using turbo_stream_from? #674

Open
searls opened this issue Sep 3, 2024 · 2 comments

Comments

@searls
Copy link

searls commented Sep 3, 2024

I'm using the turbo_stream_from helper to subscribe to Action Cable on a page that receives Turbo Stream broadcasts. One major issue, though, is that I have no idea when broadcasts aren't being received. I'd (ideally) like to be notified when they are with a JavaScript event, but I'd even be happy with a way to introspect the Cable connection, either via the Turbo or the Action Cable JS libraries.

Is this possible, or would I have to roll my own cable subscription outside Turbo to be able to do this?

@seanpdoyle
Copy link
Contributor

At the moment, I think the only way to "observe" connection state is to create a MutationObserver that observes the [connected] attribute.

At least from the @hotwired/turbo-rails side, the <turbo-cable-stream-source> element will toggle the [connected] attribute when connection state changes.

Unfortunately, there isn't an equivalent attribute on @hotwired/turbo's built-in <turbo-stream-source>. On a related note, hotwired/turbo#1287 adds [connected] attribute togglging, but the scope of that PR is more broad than just that attribute. The changes supporting [connected] toggling should probably be teased out into their own PR (PRs welcome!) to maintain parity between what's built-in and what's extended by Turbo Rails.

Dispatching an event (either a turbo:-namespaced event, or an Action Cable namespaced event) could be effective. Adding support for an event could involve changes to Turbo's StreamObserver.{connectStreamSource,disconnectStreamSource} methods to dispatch events. Both the built-in <turbo-stream-source> element and the turbo-rails-provided <turbo-cable-stream-source> element invoke those methods.

Similarly, events that communicate Action Cable subscription state changes could be just as valuable (separate from Turbo). It might be worthwhile to modify Action Cable's Consumer class to do so.

@searls
Copy link
Author

searls commented Sep 3, 2024

I was able to rustle up a working solution for what I needed. It depends on the fact the turbo library bundled in turbo-rails exports a cable object from which you can get the consumer.

Here's a stimulus control that monkey-patches any existing subscriptions at connect() time, emitting events for any action that happens on any already-defined cables:

import { Controller } from '@hotwired/stimulus'
import { cable } from '@hotwired/turbo-rails'

export default class CableWatcher extends Controller {
  async connect () {
    this.consumer = await cable.getConsumer()
    this.subscriptionConnected = this.subscriptionConnected.bind(this)
    this.subscriptionDisconnected = this.subscriptionDisconnected.bind(this)
    this.subscriptionRejected = this.subscriptionRejected.bind(this)

    this.#patchSubscriptions()
  }

  subscriptionConnected (identifier) {
    this.dispatch('connected', { detail: { value: JSON.parse(identifier) } })
  }

  subscriptionDisconnected (identifier) {
    this.dispatch('disconnected', { detail: { value: JSON.parse(identifier) } })
  }

  subscriptionRejected (identifier) {
    this.dispatch('rejected', { detail: { value: JSON.parse(identifier) } })
  }

  // This will only patch subscriptions that are already connected,
  // but any <%= turbo_stream_from %> tags should already have mounted by our connect() time
  #patchSubscriptions () {
    const self = this

    this.consumer.subscriptions.subscriptions.forEach(subscription => {
      const originalConnected = subscription.connected
      const originalDisconnected = subscription.disconnected
      const originalRejected = subscription.rejected
      subscription.connected = function () {
        originalConnected.call(this)
        self.subscriptionConnected(this.identifier)
      }

      subscription.disconnected = function () {
        originalDisconnected.call(this)
        self.subscriptionDisconnected(this.identifier)
      }

      subscription.rejected = function () {
        originalRejected.call(this)
        self.subscriptionRejected(this.identifier)
      }
    })
  }
}

Then I can subscribe to it like this:

data-action="cable-watcher:connected->my-controller#cableConnected cable-watcher:disconnected->my-controller#cableDisconnected cable-watcher:rejected->my-controller#cableDisconnected"

And in those controller actions, I can get at the channel/subscriber ID:

  cableConnected (e) {
    if (e.detail?.value?.channel === 'Turbo::StreamsChannel') {
      console.log(e.detail.value.signed_stream_name)
    }
  }

  cableDisconnected (e) {
    if (e.detail?.value?.channel === 'Turbo::StreamsChannel') {
      console.log(e.detail.value.signed_stream_name)
    }
  }

I'm using this to set up an interval polling fallback whenever the cable disconnects until it reconnects.

It'd be super cool if the library could handle this kind of plumbing for users somewhere or other, simply because relying on broadcasts can lead to some really goofy state problems if it disconnects intermittently

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants