aboutsummaryrefslogblamecommitdiffstats
path: root/lib/action_cable/channel/base.rb
blob: ee22b29f4e603a0e256e7ad27ef28d51db4bd3f8 (plain) (tree)
1
2

                  

























                                                                                                                                                      

                       
                            
                     
 

                                  
 
                                      
                                       
 
                                                         
                                

                                
 
                            

         


                                                                                                                     
                              
                      
                                       
 
                                        
                                                      
                                    
              
                                                                              



                      

         
                                  
                                 
                                                  

         

               





                                
                                                             

           

                                                                                                               
                      


                                  

                                                                                                                 
                        


                                  


                                                                                                                    
                                    
                        
                                                                                                              
                                                                                   


                        

           




                         
 
             




                                                   



                                                      
                                       


                                                             

                                                        
                                                       
                                                   




               



                                                                              


                                                                                
       
     
   
module ActionCable
  module Channel
    # The channel provides the basic structure of grouping behavior into logical units when communicating over the websocket connection.
    # You can think of a channel like a form of controller, but one that's capable of pushing content to the subscriber in addition to simply
    # responding to the subscriber's direct requests.
    #
    # Channel instances are long-lived. A channel object will be instantiated when the cable consumer becomes a subscriber, and then
    # lives until the consumer disconnects. This may be seconds, minutes, hours, or even days. That means you have to take special care
    # not to do anything silly in a channel that would balloon its memory footprint or whatever. The references are forever, so they won't be released
    # as is normally the case with a controller instance that gets thrown away after every request.
    #
    # The upside of long-lived channel instances is that you can use instance variables to keep reference to objects that future subscriber requests
    # can interact with. Here's a quick example:
    #
    #   class ChatChannel < ApplicationChannel
    #     def subscribed
    #       @room = Chat::Room[params[:room_number]]
    #     end
    #
    #     def speak(data)
    #       @room.speak data, user: current_user
    #     end
    #   end
    #
    # The #speak action simply uses the Chat::Room object that was created when the channel was first subscribed to by the consumer when that
    # subscriber wants to say something in the room.
    #
    # 
    class Base
      include Callbacks
      include PeriodicTimers
      include Streams

      on_subscribe   :subscribed
      on_unsubscribe :unsubscribed

      attr_reader :params, :connection
      delegate :logger, to: :connection

      def initialize(connection, identifier, params = {})
        @connection = connection
        @identifier = identifier
        @params     = params

        subscribe_to_channel
      end

      # Extract the action name from the passed data and process it via the channel. The process will ensure
      # that the action requested is a public method on the channel declared by the user (so not one of the callbacks
      # like #subscribed).
      def process_action(data)
        if authorized?
          action = extract_action(data)

          if processable_action?(action)
            logger.info action_signature(action, data)
            public_send action, data
          else
            logger.error "Unable to process #{action_signature(action, data)}"
          end
        else
          unauthorized
        end
      end

      def unsubscribe_from_channel
        run_unsubscribe_callbacks
        logger.info "#{channel_name} unsubscribed"
      end


      protected
        # Override in subclasses
        def authorized?
          true
        end

        def unauthorized
          logger.error "#{channel_name}: Unauthorized access"
        end

        # Called once a consumer has become a subscriber of the channel. Usually the place to setup any streams
        # you want this channel to be sending to the subscriber.
        def subscribed
          # Override in subclasses
        end

        # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
        # people as offline or the like.
        def unsubscribed
          # Override in subclasses
        end

        
        # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with 
        # the proper channel identifier marked as the recipient.
        def transmit(data, via: nil)
          if authorized?
            logger.info "#{channel_name} transmitting #{data.inspect}".tap { |m| m << " (via #{via})" if via }
            connection.transmit({ identifier: @identifier, message: data }.to_json)
          else
            unauthorized
          end
        end


        def channel_name
          self.class.name
        end


      private
        def subscribe_to_channel
          logger.info "#{channel_name} subscribing"
          run_subscribe_callbacks
        end

        def extract_action(data)
          (data['action'].presence || :receive).to_sym
        end

        def processable_action?(action)
          self.class.instance_methods(false).include?(action)
        end

        def action_signature(action, data)
          "#{channel_name}##{action}".tap do |signature|
            if (arguments = data.except('action')).any?
              signature << "(#{arguments.inspect})"
            end
          end
        end


        def run_subscribe_callbacks
          self.class.on_subscribe_callbacks.each { |callback| send(callback) }
        end

        def run_unsubscribe_callbacks
          self.class.on_unsubscribe_callbacks.each { |callback| send(callback) }
        end
    end
  end
end