hotwired / turbo-rails

Use Turbo in your Ruby on Rails app
https://turbo.hotwired.dev
MIT License
2.08k stars 322 forks source link

Turbo Stream broken after following a turbo link away and visiting it again #173

Open arusa opened 3 years ago

arusa commented 3 years ago

My whole rails website is using turbo for navigation. Subscribing to a turbo-stream using turbo_stream_from and then using broadcasts_to to broadcast to the stream works fine on every page even when navigating with turbo (without page reloads).

But when I visit a page again, after leaving it, the turbo stream on this page does not work anymore.

After leaving the page, safari shows me that an unsubscribe message was sent through the websocket and when I enter the page again it shows me a new subscribe message followed by a confirm_subscription message, instantly followed by an unsubscribe message and another subscribe message that doesn't get a confirmation anymore.

I don't understand why this only happens at the second visit of any page that subscribes to a turbo-stream.

I have attached a screenshot of this message history:

Screenshot 2021-05-11 at 07 58 37

edit:

I am running:

psagan commented 2 years ago

Hi, I encountered this issue today and confirm it exists. Maybe it's related to cable mode in cable.yml, because locally it works properly (no issue) when async mode and one puma instance. Issue exists on production where I am using Redis for cable and puma with two workers

cable.yml

development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>

puma.rb

...
workers ENV.fetch("WEB_CONCURRENCY") { 2 }
psagan commented 2 years ago

I think it's not related to puma workers - I decreased WEB_CONCURRENCY to 1 and the issue still exists (randomly) sometimes it works, sometimes not.

rabbitbike commented 2 years ago

For me the issue seems to be the same, with ActionCable not confirming connection. I checked the dev tool and the stream stops working after not receiving "confirm_subscription." https://github.com/rails/rails/pull/41581

I tried using data-turbo-permanent in various places but that doesn't seem to help.

nicowenterodt commented 5 months ago

I also having this issue:

When I have a turbo_stream_from tag in place to connect my client to a stream and the client then navigates forth and back (using turbo-drive) to other pages where this tag is also in present, I see multiple unsubscribes and subscribes in my websocket monitor which occasionally leads to an unsubscribe from a stream which should be still connected.

When "hard-reloading" the page the websocket subscription to that stream is established as expected. So this only happens when a client navigates through the page.

This leads to the websocket not receiving messages anymore because the stream has been unsubscribed which leads to:

Turbo::StreamsChannel stopped streaming from ....

Occasionally throwing:

Could not execute command from ({"command"=>"unsubscribe" ....})
[RuntimeError - Unable to find subscription with identifier: ... ]

Imho there should be no unsubscribe at all when turbo-drive checks there is the same stream present again on the page the client navigates to.

I'm still trying to understand, however this seems related. Feels a bit like a race-condition.

Update: Still not found a solution -> however I currently deactivate turbo-drive for targets where I need a stable websocket connection with data-turbo="false" forcing the page to be fully loaded establishing a fresh websocket connection.

kyrylo commented 5 months ago

I use a Stimulus controller to connect to my ActionCable channel in the connect() hook.

I unsubscribe() in the disconnect() hook.

When I load my initial page, then navigate away, and then press the link to go back to the original view (not via the back button, but via a link click, hence, it's not a restoration visit), my Stimulus controller executes these functions at once:

A restoration visit works as it should.

Versions:

Simplified Stimulus controller:

import { Controller } from "@hotwired/stimulus";
import consumer from "../channels/consumer";

export default class extends Controller {
  connect() {
    console.log("connect");

    this.channel = consumer.subscriptions.create("MyChannel", {
      connected: (...args) => {},
      disconnected: (...args) => {},
      received: (...args) => {},
    });
  }

  disconnect() {
    console.log("disconnect");
    this.channel.unsubscribe();
  }
}