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
PeriodicTimers - Attributes & Methods
::ActiveSupport::Rescuable - Attributes & Methods
Class Method Summary
-
.action_methods
A list of method names that should be considered actions.
- .new(connection, identifier, params = {}) ⇒ Base constructor
-
.clear_action_methods!
private
action_methods are cached and there is sometimes need to refresh them.
- .internal_methods private
-
.method_added(name)
private
Refresh the cached action_methods when a new action_method is added.
::ActiveSupport::DescendantsTracker - self
Instance Attribute Summary
- #connection readonly
- #identifier readonly
- #logger readonly
- #params readonly
- #defer_subscription_confirmation? ⇒ Boolean readonly private
- #subscription_confirmation_sent? ⇒ Boolean readonly private
- #subscription_rejected? ⇒ Boolean readonly private
- #unsubscribed? ⇒ Boolean readonly Internal use only
Streams - Included
Callbacks - Included
Instance Method Summary
-
#perform_action(data)
Extract the action name from the passed data and process it via the channel.
-
#subscribe_to_channel
This method is called after subscription has been added to the connection and confirms or rejects the subscription.
- #action_signature(action, data) private
- #defer_subscription_confirmation! private
- #delegate_connection_identifiers private
- #dispatch_action(action, data) private
- #ensure_confirmation_sent private
- #extract_action(data) private
- #parameter_filter private
- #processable_action?(action) ⇒ Boolean private
- #reject private
- #reject_subscription private
-
#subscribed
private
Called once a consumer has become a subscriber of the channel.
-
#transmit(data, via: nil)
private
Transmit a hash of data to the subscriber.
- #transmit_subscription_confirmation private
- #transmit_subscription_rejection private
-
#unsubscribed
readonly
private
Called once a consumer has cut its cable connection.
-
#unsubscribe_from_channel
Internal use only
Called by the cable connection when it’s cut, so the channel has a chance to cleanup with callbacks.
::ActiveSupport::Rescuable - Included
| #rescue_with_handler | Delegates to the class method, but uses the instance as the subject for rescue_from handlers (method calls, |
| #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 |
| #stop_stream_from | Unsubscribes streams from the named |
| #stream_for | Start streaming the pubsub queue for the |
| #stream_from | Start streaming from the named |
| #stream_or_reject_for | Calls stream_for with the given |
| #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
# File 'actioncable/lib/action_cable/channel/base.rb', line 163
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 @unsubscribed = false 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.
# 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) - # Except the internal methods internal_methods).uniq methods.map!(&:name) 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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 148
def clear_action_methods! # :doc: @action_methods = nil end
.internal_methods (private)
[ GitHub ]# File 'actioncable/lib/action_cable/channel/base.rb', line 158
def internal_methods super end
.method_added(name) (private)
Refresh the cached action_methods when a new action_method is added.
# File 'actioncable/lib/action_cable/channel/base.rb', line 153
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 262
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 266
def subscription_confirmation_sent? # :doc: @subscription_confirmation_sent end
#subscription_rejected? ⇒ Boolean (readonly, private)
[ GitHub ]
# File 'actioncable/lib/action_cable/channel/base.rb', line 274
def subscription_rejected? # :doc: @reject_subscription end
#unsubscribed? ⇒ Boolean (readonly)
# File 'actioncable/lib/action_cable/channel/base.rb', line 218
def unsubscribed? # :nodoc: @unsubscribed end
Instance Method Details
#action_signature(action, data) (private)
[ GitHub ]# File 'actioncable/lib/action_cable/channel/base.rb', line 306
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 258
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 278
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 294
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 252
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 286
def extract_action(data) (data["action"].presence || :receive).to_sym end
#parameter_filter (private)
[ GitHub ]# File 'actioncable/lib/action_cable/channel/base.rb', line 317
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).
# File 'actioncable/lib/action_cable/channel/base.rb', line 184
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)
# File 'actioncable/lib/action_cable/channel/base.rb', line 290
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 270
def reject # :doc: @reject_subscription = true end
#reject_subscription (private)
[ GitHub ]# File 'actioncable/lib/action_cable/channel/base.rb', line 332
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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 199
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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 226
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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 239
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 321
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[:][:confirmation] @subscription_confirmation_sent = true end end end
#transmit_subscription_rejection (private)
[ GitHub ]# File 'actioncable/lib/action_cable/channel/base.rb', line 337
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[:][:rejection] end end
#unsubscribe_from_channel
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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 211
def unsubscribe_from_channel # :nodoc: @unsubscribed = true run_callbacks :unsubscribe do unsubscribed end end
#unsubscribed (readonly, 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.
# File 'actioncable/lib/action_cable/channel/base.rb', line 232
def unsubscribed # :doc: # Override in subclasses end