hotwired / turbo

The speed of a single-page web application without having to write any JavaScript
https://turbo.hotwired.dev
MIT License
6.54k stars 415 forks source link

ActionCable connection not reestablished after authentication with Turbo #176

Open jonsgreen opened 3 years ago

jonsgreen commented 3 years ago

So I recognize that there are other issues similar or related to this one like https://github.com/hotwired/turbo/issues/45 and if this has been covered elsewhere I apologize.

I am noticing that if I have a channel identified by current_user then if I submit a Turbo enabled authentication form (e.g. with Devise) then I don't have access to the current user through the ActionCable channel. A full refresh of the page establishes connection immediately. This behavior is also fixed by adding a turbo-false data element to the form. A final option that sort of works is to do something like this (note that this is with an ApplicationCable::Connection identified by both session id and current_user):

class Users::SessionsController < Devise::SessionsController
  def create
     super do
        ActionCable.server.remote_connections.where(session_id: session.id, current_user: nil).disconnect
     end
end

The problem with this last solution is that it takes several seconds for ActionCable to detect the stale connection which can lead to confusing UX for the user. The initial connection on page load is quite snappy in comparison.

My conclusion is that when Turbo handles the redirect the connection to ActionCable does not get refreshed.

Maybe just disabling turbo in the form is good enough but I am curious if there is a way and whether it would make sense for Turbo to trigger ActionCable to reconnect in such cases.

jonsgreen commented 3 years ago

In case someone else bumps into this here is one of probably many solutions to deal with this problem if you are using CableReady

first you need multiple identifiers for your connection to deal with unauthenticated users:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user
    identified_by :session_id

    def connect
      self.current_user = env["warden"].user(:user)
      self.session_id = request.session.id
      reject_unauthorized_connection unless self.current_user || self.session_id
    end
  end
end

then you will want a SessionChannel for when users are not yet authenticated:

class SessionChannel < ApplicationCable::Channel
  def subscribed
    stream_for session_id
  end
end
import CableReady from 'cable_ready'
import consumer from './consumer'

consumer.subscriptions.create('SessionChannel', {
  received (data) {
    if (data.cableReady) CableReady.perform(data.operations)
  },

  connected () {
    document.addEventListener('reconnect', this.reconnect)
  },

  reconnect() {
    consumer.disconnect()
    consumer.connect()
  }

})

Then finally in your session_controller:

class Users::SessionsController < Devise::SessionsController

  # POST /resource/sign_in
   def create
     super do
       cable_ready[SessionChannel].dispatch_event(name: 'reconnect')
         .broadcast_to(request.session.id)
     end
   end

 end

I am sure there are ways to do this with just Turbo and Stimulus but perhaps this can be an inspiration.