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