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

Occasional disconnect, no documented way to monitor or reconnect and handle missed stream actions #1261

Open
krschacht opened this issue May 21, 2024 · 7 comments

Comments

@krschacht
Copy link
Contributor

I can't figure out how to monitor for a websocket disconnection (presumably an event I can listen to) so I can notify the end user.

In my production app, my users occasionally end up in a situation where they miss a turbo stream update. I'm almost certain this is due to flaky internet connection so that the websocket gets dropped. I've seen it happen to me occasionally.

I've been digging through the turbo repo, turbo rails, and actioncable, and reviewing the public docs for all three and I can't find any mention of a disconnect event.

Do we need to simply add this to public docs? Or does one not exist and we need a PR to add one?

@krschacht
Copy link
Contributor Author

I can't even figure out the relationship between Turbo and ActionCable. I know Turbo uses ActionCable, and I had to edit cable.yml as part of my original configuration. But notably, when I view source for my app there is no @rails/actioncable and so when I try to follow this guide for some potential debugging ideas:

import * as ActionCable from '@rails/actioncable'

ActionCable.logger.enabled = true

From: https://guides.rubyonrails.org/action_cable_overview.html#client-side-logging

That importmap reference doesn't exist.

@krschacht
Copy link
Contributor Author

Poking around the internals I've managed to wire up a poll interval. In my application.js I simply changed the turbo-rails import and added a little code:

import { cable } from "@hotwired/turbo-rails"

window.isConnected = false
window.cable = cable
window.consumer = await cable.createConsumer()
setInterval(() => {
  if (consumer.connection.isOpen() != window.isConnected) {
    window.isConnected = consumer.connection.isOpen()
    console.log(`cable ${window.isConnected ? 'connected' : 'DISCONNECTED'}`)
  }
}, 500)
window.consumer.connection.open()

I don't quite understand what it's doing. I'm creating a new consumer, but this must be working over the existing cable connection. Clearly this isn't the consumer that Turbo is using, but I think my creating a new one which I have access to then I can check the status of it. I still haven't been able to find any event I can listen to, but I'm going to proceed with this hack for now.

@leonvogt
Copy link

Are you using ActionCable itself or something like the turbo_stream_from helper from the turbo-rails gem?
With the latter, you could monitor the connected attribute of the turbo-cable-stream-source element.
As soon as the connection gets lost, the connected attribute will get removed.

<turbo-cable-stream-source channel="Turbo::StreamsChannel" signed-stream-name="..." connected></turbo-cable-stream-source>

In my experience, the websocket connection gets regularly lost, even with a stable internet connection.
I have a stimulus controller which monitors the connected attributes and performs a turbo refresh -/ morph action, under certain conditions.

@krschacht
Copy link
Contributor Author

@leonvogt Yes, I'm using turbo_stream_from from the turbo-rails gem. That's an interesting solution, thanks for sharing! I never noticed that tag in the rendered HTML. I need to go looking for that. I suspect the polling code (that I shared above) accomplishes a very similar thing, but your approach is a bit more elegant.

Can you share more about what conditions you preform a turbo refresh / morph and how you trigger that with javascript? That's exactly what I need to implement next.

Here is my motivation, just for more context. My app needs to support users remaining connected for potentially days in a row. The browser may be backgrounded & re-foregrounded, the computer may even go to sleep and come out of sleep. And I want the websocket/stream to be smart enough to manage itself during all of this. Currently, it does not. When I leave my computer unattended for one night and come back the next day, the front-end interactions are no longer working. I have to do a hard refresh of the page to get things working again — that's what I'm trying to solve.

@leonvogt
Copy link

leonvogt commented May 22, 2024

Sure, my current Stimulus controller looks like this:

import { Controller } from "@hotwired/stimulus"

// Connects to data-controller="ws-status"
export default class extends Controller {
  static targets = ["streamElement", "onlineIndicator", "offlineIndicator"]
  static values = {
    autoStart: { type: Boolean, default: true },
    autoRefresh: { type: Boolean, default: true },
    interval: { type: Number, default: 1000 }
  }

  connect() {
    if (this.autoStartValue) {
      this.start()
    }
  }

  disconnect() {
    this.stop()
  }

  start() {
    if (!this.hasStreamElementTarget) return;
    if (!this.hasOnlineIndicatorTarget) return;

    this.interval = setInterval(() => {
      if (this.isOnline) {
        const wasOffline = this.onlineIndicatorTarget.classList.contains('d-none')

        this.onlineIndicatorTarget.classList.remove('d-none')
        this.offlineIndicatorTarget.classList.add('d-none')

        if (wasOffline && this.autoRefreshValue) {
          Turbo.session.refresh(location.href);
        }
      } else {
        this.onlineIndicatorTarget.classList.add('d-none')
        this.offlineIndicatorTarget.classList.remove('d-none')
      }
    }, this.intervalValue)
  }

  stop() {
    clearInterval(this.interval)
  }

  get isOnline() {
    return this.streamElementTargets.every(element => element.hasAttribute('connected'))
  }
}

And can be used like this:

<div class="online-status-indication" data-controller="ws-status">
  <%= turbo_stream_from "dashboard", Current.customer.root.id, data: { ws_status_target: "streamElement", turbo_permanent: true } %>
  <div data-ws-status-target="onlineIndicator">
    <small class="text-muted"><%= t("dashboard.status_online") %></small>
    <%= icon('fas', 'circle', class: 'text-success') %>
  </div>

  <div class="d-none" data-ws-status-target="offlineIndicator">
    <small class="text-muted"><%= t("dashboard.status_offline") %></small>
    <%= icon('fas', 'circle', class: 'text-danger') %>
    <%= link_to dashboard_path, class: "btn btn-outline-secondary btn-sm" do %>
      <%= icon('fas', 'sync', class: 'me-2') %>
      <%= t('dashboard.refresh') %>
    <% end %>
  </div>
</div>

Please note that this is a quite simple approach and I'm not sure if it fits your described use case.
I guess a cleaner solution would be to use a MutationObserver for monitoring the attribute, instead of the interval approach.

@krschacht
Copy link
Contributor Author

Hi @leonvogt I never thanked you for this sample code! It was helpful and I ended up incorporating bits of this. I thought I replied right away but just came back to this issue and I was remiss in doing so. I really appreciate you taking the time to share.

@krschacht krschacht changed the title Occasional disconnect, no documented way to monitor or reconnect Occasional disconnect, no documented way to monitor or reconnect and handle missed streams Aug 5, 2024
@krschacht krschacht changed the title Occasional disconnect, no documented way to monitor or reconnect and handle missed streams Occasional disconnect, no documented way to monitor or reconnect and handle missed stream actions Aug 5, 2024
@krschacht
Copy link
Contributor Author

krschacht commented Aug 5, 2024

This issue with monitoring and handling disconnect/reconnect is related to a bigger issue I'm struggling with: missed turbo stream actions. I've now run into situations where my front-end is missing a turbo stream action (1) right before the page finishes rendering and connecting to the stream channel, and (2) when the page disconnects and reconnects for some reason (e.g. browser suspending the tab because you backgrounded it and later made the tab active again) and a stream action is fired during this period.

In both cases, when the page finally reconnects, I think it needs some general way of "catching up" missed stream actions. In this other Rails Issue I've opened rails/rails#52420 I discuss one work-around for situation #1 and a small PR I tee'd up was merged in which will help in implementing a hacky solution, but situation #2 is even more common and I think it really speaks to something deeper.

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