123456789_123456789_123456789_123456789_123456789_

Class: ActionCable::Channel::Base

Relationships & Source Files
Super Chains via Extension / Inclusion / Inheritance
Class Chain:
Instance Chain:
Inherits: Object
Defined in: actioncable/lib/action_cable/channel/base.rb

Overview

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.

::ActionCable::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.

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.

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 < ApplicationCable::Channel
  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.

Action processing

Unlike subclasses of ::ActionController::Base, channels do not follow a RESTful constraint form for their actions. Instead, Action Cable operates through a remote-procedure call model. You can declare any public method on the channel (optionally taking a data argument), and this method is automatically exposed as callable to the client.

Example:

class AppearanceChannel < ApplicationCable::Channel
  def subscribed
    @connection_token = generate_connection_token
  end

  def unsubscribed
    current_user.disappear @connection_token
  end

  def appear(data)
    current_user.appear @connection_token, on: data['appearing_on']
  end

  def away
    current_user.away @connection_token
  end

  private
    def generate_connection_token
      SecureRandom.hex(36)
    end
end

In this example, the subscribed and unsubscribed methods are not callable methods, as they were already declared in Base, but #appear and #away are. #generate_connection_token is also not callable, since it’s a private method. You’ll see that appear accepts a data parameter, which it then uses as part of its model call. #away does not, since it’s simply a trigger action.

Also note that in this example, current_user 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.

Rejecting subscription requests

A channel can reject a subscription request in the #subscribed callback by invoking the #reject method:

class ChatChannel < ApplicationCable::Channel
  def subscribed
    @room = Chat::Room[params[:room_number]]
    reject unless current_user.can_access?(@room)
  end
end

In this example, the subscription will be rejected if the current_user does not have access to the chat room. On the client-side, the Channel#rejected callback will get invoked when the server rejects the subscription request.

Constant Summary

::ActiveSupport::Callbacks - Included

CALLBACK_FILTER_TYPES

PeriodicTimers - Attributes & Methods

::ActiveSupport::Rescuable - Attributes & Methods

Class Method Summary

::ActiveSupport::DescendantsTracker - self

Instance Attribute Summary

Instance Method Summary

::ActiveSupport::Rescuable - Included

#rescue_with_handler

Delegates to the class method, but uses the instance as the subject for rescue_from handlers (method calls, instance_exec blocks).

#handler_for_rescue

Internal handler lookup.

Broadcasting - Included

Naming - Included

Streams - Included

#stop_all_streams

Unsubscribes all streams associated with this channel from the pubsub queue.

#stop_stream_for

Unsubscribes streams for the model.

#stop_stream_from

Unsubscribes streams from the named broadcasting.

#stream_for

Start streaming the pubsub queue for the model in this channel.

#stream_from

Start streaming from the named broadcasting pubsub queue.

#stream_or_reject_for

Calls stream_for with the given model if it’s present to start streaming, otherwise rejects the subscription.

#default_stream_handler

May be overridden to change the default stream handling behavior which decodes JSON and transmits to the client.

#identity_handler, #stream_decoder,
#stream_handler

May be overridden to add instrumentation, logging, specialized error handling, or other forms of handler decoration.

#stream_transmitter, #streams,
#worker_pool_stream_handler

Always wrap the outermost handler to invoke the user handler on the worker pool rather than blocking the event loop.

PeriodicTimers - Included

::ActiveSupport::Callbacks - Included

#run_callbacks

Runs the callbacks for the given event.

#halted_callback_hook

A hook invoked every time a before callback is halted.

Constructor Details

.new(connection, identifier, params = {}) ⇒ Base

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 155

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

  # When a channel is streaming via pubsub, we want to delay the confirmation
  # transmission until pubsub subscription is confirmed.
  #
  # The counter starts at 1 because it's awaiting a call to #subscribe_to_channel
  @defer_subscription_confirmation_counter = Concurrent::AtomicFixnum.new(1)

  @reject_subscription = nil
  @subscription_confirmation_sent = nil

  delegate_connection_identifiers
end

Class Attribute Details

.periodic_timers (rw)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/periodic_timers.rb', line 11

class_attribute :periodic_timers, instance_reader: false, default: []

.periodic_timers?Boolean (rw)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/periodic_timers.rb', line 11

class_attribute :periodic_timers, instance_reader: false, default: []

.rescue_handlers (rw)

[ GitHub ]

  
# File 'activesupport/lib/active_support/rescuable.rb', line 15

class_attribute :rescue_handlers, default: []

.rescue_handlers?Boolean (rw)

[ GitHub ]

  
# File 'activesupport/lib/active_support/rescuable.rb', line 15

class_attribute :rescue_handlers, default: []

Class Method Details

.action_methods

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

  • Set - A set of all methods that should be considered actions.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 128

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

.clear_action_methods! (private)

action_methods are cached and there is sometimes need to refresh them. .clear_action_methods! allows you to do that, so next time you run action_methods, they will be recalculated.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 144

def clear_action_methods! # :doc:
  @action_methods = nil
end

.method_added(name) (private)

Refresh the cached action_methods when a new action_method is added.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 149

def method_added(name) # :doc:
  super
  clear_action_methods!
end

Instance Attribute Details

#connection (readonly)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 117

attr_reader :params, :connection, :identifier

#defer_subscription_confirmation?Boolean (readonly, private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 248

def defer_subscription_confirmation? # :doc:
  @defer_subscription_confirmation_counter.value > 0
end

#identifier (readonly)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 117

attr_reader :params, :connection, :identifier

#logger (readonly)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 118

delegate :logger, to: :connection

#params (readonly)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 117

attr_reader :params, :connection, :identifier

#rescue_handlers (rw)

[ GitHub ]

  
# File 'activesupport/lib/active_support/rescuable.rb', line 15

class_attribute :rescue_handlers, default: []

#rescue_handlers?Boolean (rw)

[ GitHub ]

  
# File 'activesupport/lib/active_support/rescuable.rb', line 15

class_attribute :rescue_handlers, default: []

#subscription_confirmation_sent?Boolean (readonly, private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 252

def subscription_confirmation_sent? # :doc:
  @subscription_confirmation_sent
end

#subscription_rejected?Boolean (readonly, private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 260

def subscription_rejected? # :doc:
  @reject_subscription
end

Instance Method Details

#action_signature(action, data) (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 292

def action_signature(action, data)
  (+"#{self.class.name}##{action}").tap do |signature|
    arguments = data.except("action")

    if arguments.any?
      arguments = parameter_filter.filter(arguments)
      signature << "(#{arguments.inspect})"
    end
  end
end

#defer_subscription_confirmation! (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 244

def defer_subscription_confirmation! # :doc:
  @defer_subscription_confirmation_counter.increment
end

#delegate_connection_identifiers (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 264

def delegate_connection_identifiers
  connection.identifiers.each do |identifier|
    define_singleton_method(identifier) do
      connection.send(identifier)
    end
  end
end

#dispatch_action(action, data) (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 280

def dispatch_action(action, data)
  logger.debug action_signature(action, data)

  if method(action).arity == 1
    public_send action, data
  else
    public_send action
  end
rescue Exception => exception
  rescue_with_handler(exception) || raise
end

#ensure_confirmation_sent (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 238

def ensure_confirmation_sent # :doc:
  return if subscription_rejected?
  @defer_subscription_confirmation_counter.decrement
  transmit_subscription_confirmation unless defer_subscription_confirmation?
end

#extract_action(data) (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 272

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

#parameter_filter (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 303

def parameter_filter
  @parameter_filter ||= ActiveSupport::ParameterFilter.new(connection.config.filter_parameters)
end

#perform_action(data)

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).

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 175

def perform_action(data)
  action = extract_action(data)

  if processable_action?(action)
    payload = { channel_class: self.class.name, action: action, data: data }
    ActiveSupport::Notifications.instrument("perform_action.action_cable", payload) do
      dispatch_action(action, data)
    end
  else
    logger.error "Unable to process #{action_signature(action, data)}"
  end
end

#processable_action?(action) ⇒ Boolean (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 276

def processable_action?(action)
  self.class.action_methods.include?(action.to_s) unless subscription_rejected?
end

#reject (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 256

def reject # :doc:
  @reject_subscription = true
end

#reject_subscription (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 318

def reject_subscription
  connection.subscriptions.remove_subscription self
  transmit_subscription_rejection
end

#subscribe_to_channel

This method is called after subscription has been added to the connection and confirms or rejects the subscription.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 190

def subscribe_to_channel
  run_callbacks :subscribe do
    subscribed
  end

  reject_subscription if subscription_rejected?
  ensure_confirmation_sent
end

#subscribed (private)

Called once a consumer has become a subscriber of the channel. Usually the place to set up any streams you want this channel to be sending to the subscriber.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 212

def subscribed # :doc:
  # Override in subclasses
end

#transmit(data, via: nil) (private)

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.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 225

def transmit(data, via: nil) # :doc:
  logger.debug do
    status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
    status += " (via #{via})" if via
    status
  end

  payload = { channel_class: self.class.name, data: data, via: via }
  ActiveSupport::Notifications.instrument("transmit.action_cable", payload) do
    connection.transmit identifier: @identifier, message: data
  end
end

#transmit_subscription_confirmation (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 307

def transmit_subscription_confirmation
  unless subscription_confirmation_sent?
    logger.debug "#{self.class.name} is transmitting the subscription confirmation"

    ActiveSupport::Notifications.instrument("transmit_subscription_confirmation.action_cable", channel_class: self.class.name, identifier: @identifier) do
      connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:confirmation]
      @subscription_confirmation_sent = true
    end
  end
end

#transmit_subscription_rejection (private)

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 323

def transmit_subscription_rejection
  logger.debug "#{self.class.name} is transmitting the subscription rejection"

  ActiveSupport::Notifications.instrument("transmit_subscription_rejection.action_cable", channel_class: self.class.name, identifier: @identifier) do
    connection.transmit identifier: @identifier, type: ActionCable::INTERNAL[:message_types][:rejection]
  end
end

#unsubscribe_from_channel

This method is for internal use only.

Called by the cable connection when it’s cut, so the channel has a chance to cleanup with callbacks. This method is not intended to be called directly by the user. Instead, override the #unsubscribed callback.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 202

def unsubscribe_from_channel # :nodoc:
  run_callbacks :unsubscribe do
    unsubscribed
  end
end

#unsubscribed (private)

Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking users as offline or the like.

[ GitHub ]

  
# File 'actioncable/lib/action_cable/channel/base.rb', line 218

def unsubscribed # :doc:
  # Override in subclasses
end