base.rb 11.1 KB
Newer Older
1
require 'action_cable/channel/log_subscriber'
2 3
require 'set'

P
Pratik Naik 已提交
4 5
module ActionCable
  module Channel
6
    # The channel provides the basic structure of grouping behavior into logical units when communicating over the WebSocket connection.
7 8 9 10 11 12
    # 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
13
    # as is normally the case with a controller instance that gets thrown away after every request.
14 15 16
    #
    # Long-lived channels (and connections) also mean you're responsible for ensuring that the data is fresh. If you hold a reference to a user
    # record, but the name is changed while that reference is held, you may be sending stale data if you don't take precautions to avoid it.
17 18 19 20
    #
    # 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:
    #
21
    #   class ChatChannel < ApplicationCable::Channel
22 23 24 25 26 27 28 29 30 31 32 33
    #     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.
    #
34 35
    # == Action processing
    #
36 37
    # Unlike subclasses of ActionController::Base, channels do not follow a RESTful
    # constraint form for their actions. Instead, Action Cable operates through a
38 39 40
    # remote-procedure call model. You can declare any public method on the
    # channel (optionally taking a <tt>data</tt> argument), and this method is
    # automatically exposed as callable to the client.
41 42 43 44 45 46 47
    #
    # Example:
    #
    #   class AppearanceChannel < ApplicationCable::Channel
    #     def subscribed
    #       @connection_token = generate_connection_token
    #     end
48
    #
49 50 51
    #     def unsubscribed
    #       current_user.disappear @connection_token
    #     end
52
    #
53 54 55
    #     def appear(data)
    #       current_user.appear @connection_token, on: data['appearing_on']
    #     end
56
    #
57 58 59
    #     def away
    #       current_user.away @connection_token
    #     end
60
    #
61 62 63 64 65 66
    #     private
    #       def generate_connection_token
    #         SecureRandom.hex(36)
    #       end
    #   end
    #
67
    # In this example, the subscribed and unsubscribed methods are not callable methods, as they
68 69
    # were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
    # and <tt>#away</tt> are. <tt>#generate_connection_token</tt> is also not
70
    # callable, since it's a private method. You'll see that appear accepts a data
71 72
    # parameter, which it then uses as part of its model call. <tt>#away</tt>
    # does not, since it's simply a trigger action.
73
    #
74 75 76 77
    # Also note that in this example, <tt>current_user</tt> is available because
    # it was marked as an identifying attribute on the connection. All such
    # identifiers will automatically create a delegation method of the same name
    # on the channel instance.
78 79 80
    #
    # == Rejecting subscription requests
    #
81 82
    # A channel can reject a subscription request in the #subscribed callback by
    # invoking the #reject method:
83 84 85 86
    #
    #   class ChatChannel < ApplicationCable::Channel
    #     def subscribed
    #       @room = Chat::Room[params[:room_number]]
87
    #       reject unless current_user.can_access?(@room)
88 89 90
    #     end
    #   end
    #
91 92 93 94
    # In this example, the subscription will be rejected if the
    # <tt>current_user</tt> does not have access to the chat room. On the
    # client-side, the <tt>Channel#rejected</tt> callback will get invoked when
    # the server rejects the subscription request.
P
Pratik Naik 已提交
95 96
    class Base
      include Callbacks
97
      include PeriodicTimers
98
      include Streams
99 100
      include Naming
      include Broadcasting
P
Pratik Naik 已提交
101

P
Pratik Naik 已提交
102
      attr_reader :params, :connection, :identifier
103
      delegate :logger, to: :connection
P
Pratik Naik 已提交
104

105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128
      class << self
        # A list of method names that should be considered actions. This
        # includes all public instance methods on a channel, less
        # any internal methods (defined on Base), adding back in
        # any methods that are internal, but still exist on the class
        # itself.
        #
        # ==== Returns
        # * <tt>Set</tt> - A set of all methods that should be considered actions.
        def action_methods
          @action_methods ||= begin
            # All public instance methods of this class, including ancestors
            methods = (public_instance_methods(true) -
              # Except for public instance methods of Base and its ancestors
              ActionCable::Channel::Base.public_instance_methods(true) +
              # Be sure to include shadowed public instance methods of this class
              public_instance_methods(false)).uniq.map(&:to_s)
            methods.to_set
          end
        end

        protected
          # action_methods are cached and there is sometimes need to refresh
          # them. ::clear_action_methods! allows you to do that, so next time
129
          # you run action_methods, they will be recalculated.
130 131 132 133 134 135 136 137 138 139 140
          def clear_action_methods!
            @action_methods = nil
          end

          # Refresh the cached action_methods when a new action_method is added.
          def method_added(name)
            super
            clear_action_methods!
          end
      end

141
      def initialize(connection, identifier, params = {})
P
Pratik Naik 已提交
142
        @connection = connection
143 144
        @identifier = identifier
        @params     = params
P
Pratik Naik 已提交
145

146 147
        # When a channel is streaming via pubsub, we want to delay the confirmation
        # transmission until pubsub subscription is confirmed.
148 149
        @defer_subscription_confirmation = false

A
Arun Agrawal 已提交
150 151 152
        @reject_subscription = nil
        @subscription_confirmation_sent = nil

153
        delegate_connection_identifiers
154
        subscribe_to_channel
P
Pratik Naik 已提交
155 156
      end

D
David Heinemeier Hansson 已提交
157 158 159
      # 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).
160
      def perform_action(data)
161 162 163
        action = extract_action(data)

        if processable_action?(action)
164 165 166 167
          payload = { channel_class: self.class.name, action: action, data: data }
          ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
            dispatch_action(action, data)
          end
168
        else
169
          logger.error "Unable to process #{action_signature(action, data)}"
170
        end
P
Pratik Naik 已提交
171 172
      end

173
      # Called by the cable connection when it's cut, so the channel has a chance to cleanup with callbacks.
174
      # This method is not intended to be called directly by the user. Instead, overwrite the #unsubscribed callback.
175
      def unsubscribe_from_channel # :nodoc:
D
Diego Ballona 已提交
176 177 178
        run_callbacks :unsubscribe do
          unsubscribed
        end
D
David Heinemeier Hansson 已提交
179 180
      end

P
Pratik Naik 已提交
181 182

      protected
D
David Heinemeier Hansson 已提交
183 184
        # 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.
185
        def subscribed
P
Pratik Naik 已提交
186 187 188
          # Override in subclasses
        end

D
David Heinemeier Hansson 已提交
189
        # Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
190
        # users as offline or the like.
191
        def unsubscribed
192 193
          # Override in subclasses
        end
194 195

        # Transmit a hash of data to the subscriber. The hash will automatically be wrapped in a JSON envelope with
D
David Heinemeier Hansson 已提交
196
        # the proper channel identifier marked as the recipient.
197
        def transmit(data, via: nil)
198 199 200 201
          payload = { channel_class: self.class.name, data: data, via: via }
          ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
            connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, message: data)
          end
P
Pratik Naik 已提交
202 203
        end

204 205 206 207 208 209 210 211
        def defer_subscription_confirmation!
          @defer_subscription_confirmation = true
        end

        def defer_subscription_confirmation?
          @defer_subscription_confirmation
        end

212 213 214 215
        def subscription_confirmation_sent?
          @subscription_confirmation_sent
        end

216
        def reject
217 218 219 220 221 222 223
          @reject_subscription = true
        end

        def subscription_rejected?
          @reject_subscription
        end

224
      private
225 226 227 228 229 230 231 232
        def delegate_connection_identifiers
          connection.identifiers.each do |identifier|
            define_singleton_method(identifier) do
              connection.send(identifier)
            end
          end
        end

233
        def subscribe_to_channel
D
Diego Ballona 已提交
234 235 236
          run_callbacks :subscribe do
            subscribed
          end
237 238 239 240 241 242

          if subscription_rejected?
            reject_subscription
          else
            transmit_subscription_confirmation unless defer_subscription_confirmation?
          end
243 244
        end

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

249
        def processable_action?(action)
250
          self.class.action_methods.include?(action.to_s)
251 252
        end

253 254
        def dispatch_action(action, data)
          logger.info action_signature(action, data)
255

256 257 258 259 260 261 262
          if method(action).arity == 1
            public_send action, data
          else
            public_send action
          end
        end

D
David Heinemeier Hansson 已提交
263
        def action_signature(action, data)
264
          "#{self.class.name}##{action}".tap do |signature|
265
            if (arguments = data.except('action')).any?
D
David Heinemeier Hansson 已提交
266
              signature << "(#{arguments.inspect})"
267 268 269 270
            end
          end
        end

271
        def transmit_subscription_confirmation
272
          unless subscription_confirmation_sent?
273 274 275 276
            ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name) do
              connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation])
              @subscription_confirmation_sent = true
            end
277
          end
278 279
        end

280 281 282 283 284 285
        def reject_subscription
          connection.subscriptions.remove_subscription self
          transmit_subscription_rejection
        end

        def transmit_subscription_rejection
286 287 288
          ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name) do
            connection.transmit ActiveSupport::JSON.encode(identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection])
          end
289
        end
P
Pratik Naik 已提交
290 291
    end
  end
J
Javan Makhmali 已提交
292
end