mirror of https://github.com/rails/rails
Merge pull request #51035 from rails/rm-docs-actioncable
Transform actioncable documentation to Markdown
This commit is contained in:
commit
a9573eaa6b
|
@ -1,102 +1,112 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "set"
|
||||
require "active_support/rescuable"
|
||||
require "active_support/parameter_filter"
|
||||
|
||||
module ActionCable
|
||||
module Channel
|
||||
# = Action Cable \Channel \Base
|
||||
# # Action Cable Channel Base
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
# 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.
|
||||
# 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:
|
||||
# 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]]
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# @room = Chat::Room[params[:room_number]]
|
||||
# end
|
||||
#
|
||||
# def speak(data)
|
||||
# @room.speak data, user: current_user
|
||||
# end
|
||||
# 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.
|
||||
#
|
||||
# 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
|
||||
# ## 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 <tt>data</tt> argument), and this method is
|
||||
# automatically exposed as callable to the client.
|
||||
# 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)
|
||||
# class AppearanceChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# @connection_token = generate_connection_token
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# In this example, the subscribed and unsubscribed methods are not callable methods, as they
|
||||
# were already declared in ActionCable::Channel::Base, but <tt>#appear</tt>
|
||||
# and <tt>#away</tt> are. <tt>#generate_connection_token</tt> 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. <tt>#away</tt>
|
||||
# does not, since it's simply a trigger action.
|
||||
# def unsubscribed
|
||||
# current_user.disappear @connection_token
|
||||
# end
|
||||
#
|
||||
# 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.
|
||||
# def appear(data)
|
||||
# current_user.appear @connection_token, on: data['appearing_on']
|
||||
# end
|
||||
#
|
||||
# == Rejecting subscription requests
|
||||
# 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 ActionCable::Channel::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)
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# @room = Chat::Room[params[:room_number]]
|
||||
# reject unless current_user.can_access?(@room)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# 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.
|
||||
# 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.
|
||||
class Base
|
||||
include Callbacks
|
||||
include PeriodicTimers
|
||||
|
@ -109,14 +119,13 @@ module ActionCable
|
|||
delegate :logger, to: :connection
|
||||
|
||||
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.
|
||||
# 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.
|
||||
# #### Returns
|
||||
# * `Set` - 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
|
||||
|
@ -130,9 +139,9 @@ module ActionCable
|
|||
end
|
||||
|
||||
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.
|
||||
# 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.
|
||||
def clear_action_methods! # :doc:
|
||||
@action_methods = nil
|
||||
end
|
||||
|
@ -161,9 +170,9 @@ module ActionCable
|
|||
delegate_connection_identifiers
|
||||
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).
|
||||
# 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 perform_action(data)
|
||||
action = extract_action(data)
|
||||
|
||||
|
@ -177,8 +186,8 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# This method is called after subscription has been added to the connection
|
||||
# and confirms or rejects the subscription.
|
||||
# This method is called after subscription has been added to the connection and
|
||||
# confirms or rejects the subscription.
|
||||
def subscribe_to_channel
|
||||
run_callbacks :subscribe do
|
||||
subscribed
|
||||
|
@ -188,8 +197,9 @@ module ActionCable
|
|||
ensure_confirmation_sent
|
||||
end
|
||||
|
||||
# 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.
|
||||
# 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.
|
||||
def unsubscribe_from_channel # :nodoc:
|
||||
run_callbacks :unsubscribe do
|
||||
unsubscribed
|
||||
|
@ -197,20 +207,22 @@ module ActionCable
|
|||
end
|
||||
|
||||
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.
|
||||
# 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.
|
||||
def subscribed # :doc:
|
||||
# Override in subclasses
|
||||
end
|
||||
|
||||
# Called once a consumer has cut its cable connection. Can be used for cleaning up connections or marking
|
||||
# users as offline or the like.
|
||||
# Called once a consumer has cut its cable connection. Can be used for cleaning
|
||||
# up connections or marking users as offline or the like.
|
||||
def unsubscribed # :doc:
|
||||
# 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.
|
||||
# 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) # :doc:
|
||||
logger.debug do
|
||||
status = "#{self.class.name} transmitting #{data.inspect.truncate(300)}"
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/core_ext/object/to_param"
|
||||
|
||||
module ActionCable
|
||||
|
@ -8,17 +10,17 @@ module ActionCable
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel.
|
||||
# Broadcast a hash to a unique broadcasting for this `model` in this channel.
|
||||
def broadcast_to(model, message)
|
||||
ActionCable.server.broadcast(broadcasting_for(model), message)
|
||||
end
|
||||
|
||||
# Returns a unique broadcasting identifier for this <tt>model</tt> in this channel:
|
||||
# Returns a unique broadcasting identifier for this `model` in this channel:
|
||||
#
|
||||
# CommentsChannel.broadcasting_for("all") # => "comments:all"
|
||||
# CommentsChannel.broadcasting_for("all") # => "comments:all"
|
||||
#
|
||||
# You can pass any object as a target (e.g. Active Record model), and it
|
||||
# would be serialized into a string under the hood.
|
||||
# You can pass any object as a target (e.g. Active Record model), and it would
|
||||
# be serialized into a string under the hood.
|
||||
def broadcasting_for(model)
|
||||
serialize_broadcasting([ channel_name, model ])
|
||||
end
|
||||
|
|
|
@ -1,36 +1,39 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/callbacks"
|
||||
|
||||
module ActionCable
|
||||
module Channel
|
||||
# = Action Cable \Channel \Callbacks
|
||||
# # Action Cable Channel Callbacks
|
||||
#
|
||||
# Action Cable Channel provides callback hooks that are invoked during the
|
||||
# life cycle of a channel:
|
||||
# Action Cable Channel provides callback hooks that are invoked during the life
|
||||
# cycle of a channel:
|
||||
#
|
||||
# * {before_subscribe}[rdoc-ref:ClassMethods#before_subscribe]
|
||||
# * {after_subscribe}[rdoc-ref:ClassMethods#after_subscribe] (aliased as
|
||||
# {on_subscribe}[rdoc-ref:ClassMethods#on_subscribe])
|
||||
# * {before_unsubscribe}[rdoc-ref:ClassMethods#before_unsubscribe]
|
||||
# * {after_unsubscribe}[rdoc-ref:ClassMethods#after_unsubscribe] (aliased as
|
||||
# {on_unsubscribe}[rdoc-ref:ClassMethods#on_unsubscribe])
|
||||
# * [before_subscribe](rdoc-ref:ClassMethods#before_subscribe)
|
||||
# * [after_subscribe](rdoc-ref:ClassMethods#after_subscribe) (aliased as
|
||||
# [on_subscribe](rdoc-ref:ClassMethods#on_subscribe))
|
||||
# * [before_unsubscribe](rdoc-ref:ClassMethods#before_unsubscribe)
|
||||
# * [after_unsubscribe](rdoc-ref:ClassMethods#after_unsubscribe) (aliased as
|
||||
# [on_unsubscribe](rdoc-ref:ClassMethods#on_unsubscribe))
|
||||
#
|
||||
# ==== Example
|
||||
#
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# after_subscribe :send_welcome_message, unless: :subscription_rejected?
|
||||
# after_subscribe :track_subscription
|
||||
# #### Example
|
||||
#
|
||||
# private
|
||||
# def send_welcome_message
|
||||
# broadcast_to(...)
|
||||
# end
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# after_subscribe :send_welcome_message, unless: :subscription_rejected?
|
||||
# after_subscribe :track_subscription
|
||||
#
|
||||
# def track_subscription
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
# private
|
||||
# def send_welcome_message
|
||||
# broadcast_to(...)
|
||||
# end
|
||||
#
|
||||
# def track_subscription
|
||||
# # ...
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module Callbacks
|
||||
extend ActiveSupport::Concern
|
||||
|
@ -46,14 +49,13 @@ module ActionCable
|
|||
set_callback(:subscribe, :before, *methods, &block)
|
||||
end
|
||||
|
||||
# This callback will be triggered after the Base#subscribed method is
|
||||
# called, even if the subscription was rejected with the Base#reject
|
||||
# method.
|
||||
# This callback will be triggered after the Base#subscribed method is called,
|
||||
# even if the subscription was rejected with the Base#reject method.
|
||||
#
|
||||
# To trigger the callback only on successful subscriptions, use the
|
||||
# Base#subscription_rejected? method:
|
||||
#
|
||||
# after_subscribe :my_method, unless: :subscription_rejected?
|
||||
# after_subscribe :my_method, unless: :subscription_rejected?
|
||||
#
|
||||
def after_subscribe(*methods, &block)
|
||||
set_callback(:subscribe, :after, *methods, &block)
|
||||
|
|
|
@ -1,18 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Channel
|
||||
module Naming
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# Returns the name of the channel, underscored, without the <tt>Channel</tt> ending.
|
||||
# If the channel is in a namespace, then the namespaces are represented by single
|
||||
# Returns the name of the channel, underscored, without the `Channel` ending. If
|
||||
# the channel is in a namespace, then the namespaces are represented by single
|
||||
# colon separators in the channel name.
|
||||
#
|
||||
# ChatChannel.channel_name # => 'chat'
|
||||
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
|
||||
# FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
|
||||
# ChatChannel.channel_name # => 'chat'
|
||||
# Chats::AppearancesChannel.channel_name # => 'chats:appearances'
|
||||
# FooChats::BarAppearancesChannel.channel_name # => 'foo_chats:bar_appearances'
|
||||
def channel_name
|
||||
@channel_name ||= name.delete_suffix("Channel").gsub("::", ":").underscore
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Channel
|
||||
module PeriodicTimers
|
||||
|
@ -13,14 +15,12 @@ module ActionCable
|
|||
end
|
||||
|
||||
module ClassMethods
|
||||
# Periodically performs a task on the channel, like updating an online
|
||||
# user counter, polling a backend for new status messages, sending
|
||||
# regular "heartbeat" messages, or doing some internal work and giving
|
||||
# progress updates.
|
||||
# Periodically performs a task on the channel, like updating an online user
|
||||
# counter, polling a backend for new status messages, sending regular
|
||||
# "heartbeat" messages, or doing some internal work and giving progress updates.
|
||||
#
|
||||
# Pass a method name or lambda argument or provide a block to call.
|
||||
# Specify the calling period in seconds using the <tt>every:</tt>
|
||||
# keyword argument.
|
||||
# Pass a method name or lambda argument or provide a block to call. Specify the
|
||||
# calling period in seconds using the `every:` keyword argument.
|
||||
#
|
||||
# periodically :transmit_progress, every: 5.seconds
|
||||
#
|
||||
|
|
|
@ -1,67 +1,77 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Channel
|
||||
# = Action Cable \Channel \Streams
|
||||
# # Action Cable Channel Streams
|
||||
#
|
||||
# Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pubsub queue where any data
|
||||
# placed into it is automatically sent to the clients that are connected at that time. It's purely an online queue, though. If you're not
|
||||
# streaming a broadcasting at the very moment it sends out an update, you will not get that update, even if you connect after it has been sent.
|
||||
# Streams allow channels to route broadcastings to the subscriber. A
|
||||
# broadcasting is, as discussed elsewhere, a pubsub queue where any data placed
|
||||
# into it is automatically sent to the clients that are connected at that time.
|
||||
# It's purely an online queue, though. If you're not streaming a broadcasting at
|
||||
# the very moment it sends out an update, you will not get that update, even if
|
||||
# you connect after it has been sent.
|
||||
#
|
||||
# Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between
|
||||
# the two parties (the broadcaster and the channel subscriber). Here's an example of a channel that allows subscribers to get all new
|
||||
# comments on a given page:
|
||||
# Most commonly, the streamed broadcast is sent straight to the subscriber on
|
||||
# the client-side. The channel just acts as a connector between the two parties
|
||||
# (the broadcaster and the channel subscriber). Here's an example of a channel
|
||||
# that allows subscribers to get all new comments on a given page:
|
||||
#
|
||||
# class CommentsChannel < ApplicationCable::Channel
|
||||
# def follow(data)
|
||||
# stream_from "comments_for_#{data['recording_id']}"
|
||||
# class CommentsChannel < ApplicationCable::Channel
|
||||
# def follow(data)
|
||||
# stream_from "comments_for_#{data['recording_id']}"
|
||||
# end
|
||||
#
|
||||
# def unfollow
|
||||
# stop_all_streams
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def unfollow
|
||||
# stop_all_streams
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Based on the above example, the subscribers of this channel will get whatever data is put into the,
|
||||
# let's say, <tt>comments_for_45</tt> broadcasting as soon as it's put there.
|
||||
# Based on the above example, the subscribers of this channel will get whatever
|
||||
# data is put into the, let's say, `comments_for_45` broadcasting as soon as
|
||||
# it's put there.
|
||||
#
|
||||
# An example broadcasting for this channel looks like so:
|
||||
#
|
||||
# ActionCable.server.broadcast "comments_for_45", { author: 'DHH', content: 'Rails is just swell' }
|
||||
# ActionCable.server.broadcast "comments_for_45", { author: 'DHH', content: 'Rails is just swell' }
|
||||
#
|
||||
# If you have a stream that is related to a model, then the broadcasting used can be generated from the model and channel.
|
||||
# The following example would subscribe to a broadcasting like <tt>comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE</tt>.
|
||||
# If you have a stream that is related to a model, then the broadcasting used
|
||||
# can be generated from the model and channel. The following example would
|
||||
# subscribe to a broadcasting like `comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE`.
|
||||
#
|
||||
# class CommentsChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# post = Post.find(params[:id])
|
||||
# stream_for post
|
||||
# class CommentsChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# post = Post.find(params[:id])
|
||||
# stream_for post
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can then broadcast to this channel using:
|
||||
#
|
||||
# CommentsChannel.broadcast_to(@post, @comment)
|
||||
# CommentsChannel.broadcast_to(@post, @comment)
|
||||
#
|
||||
# If you don't just want to parlay the broadcast unfiltered to the subscriber, you can also supply a callback that lets you alter what is sent out.
|
||||
# The below example shows how you can use this to provide performance introspection in the process:
|
||||
# If you don't just want to parlay the broadcast unfiltered to the subscriber,
|
||||
# you can also supply a callback that lets you alter what is sent out. The below
|
||||
# example shows how you can use this to provide performance introspection in the
|
||||
# process:
|
||||
#
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# @room = Chat::Room[params[:room_number]]
|
||||
# class ChatChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# @room = Chat::Room[params[:room_number]]
|
||||
#
|
||||
# stream_for @room, coder: ActiveSupport::JSON do |message|
|
||||
# if message['originated_at'].present?
|
||||
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
|
||||
# stream_for @room, coder: ActiveSupport::JSON do |message|
|
||||
# if message['originated_at'].present?
|
||||
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2)
|
||||
#
|
||||
# ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
|
||||
# logger.info "Message took #{elapsed_time}s to arrive"
|
||||
# ActiveSupport::Notifications.instrument :performance, measurement: 'Chat.message_delay', value: elapsed_time, action: :timing
|
||||
# logger.info "Message took #{elapsed_time}s to arrive"
|
||||
# end
|
||||
#
|
||||
# transmit message
|
||||
# end
|
||||
#
|
||||
# transmit message
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can stop streaming from all broadcasts by calling #stop_all_streams.
|
||||
module Streams
|
||||
|
@ -71,18 +81,20 @@ module ActionCable
|
|||
on_unsubscribe :stop_all_streams
|
||||
end
|
||||
|
||||
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used
|
||||
# instead of the default of just transmitting the updates straight to the subscriber.
|
||||
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
|
||||
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
|
||||
# Start streaming from the named `broadcasting` pubsub queue. Optionally, you
|
||||
# can pass a `callback` that'll be used instead of the default of just
|
||||
# transmitting the updates straight to the subscriber. Pass `coder:
|
||||
# ActiveSupport::JSON` to decode messages as JSON before passing to the
|
||||
# callback. Defaults to `coder: nil` which does no decoding, passes raw
|
||||
# messages.
|
||||
def stream_from(broadcasting, callback = nil, coder: nil, &block)
|
||||
broadcasting = String(broadcasting)
|
||||
|
||||
# Don't send the confirmation until pubsub#subscribe is successful
|
||||
defer_subscription_confirmation!
|
||||
|
||||
# Build a stream handler by wrapping the user-provided callback with
|
||||
# a decoder or defaulting to a JSON-decoding retransmitter.
|
||||
# Build a stream handler by wrapping the user-provided callback with a decoder
|
||||
# or defaulting to a JSON-decoding retransmitter.
|
||||
handler = worker_pool_stream_handler(broadcasting, callback || block, coder: coder)
|
||||
streams[broadcasting] = handler
|
||||
|
||||
|
@ -94,17 +106,18 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a
|
||||
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight
|
||||
# to the subscriber.
|
||||
# Start streaming the pubsub queue for the `model` in this channel. Optionally,
|
||||
# you can pass a `callback` that'll be used instead of the default of just
|
||||
# transmitting the updates straight to the subscriber.
|
||||
#
|
||||
# Pass <tt>coder: ActiveSupport::JSON</tt> to decode messages as JSON before passing to the callback.
|
||||
# Defaults to <tt>coder: nil</tt> which does no decoding, passes raw messages.
|
||||
# Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to
|
||||
# the callback. Defaults to `coder: nil` which does no decoding, passes raw
|
||||
# messages.
|
||||
def stream_for(model, callback = nil, coder: nil, &block)
|
||||
stream_from(broadcasting_for(model), callback || block, coder: coder)
|
||||
end
|
||||
|
||||
# Unsubscribes streams from the named <tt>broadcasting</tt>.
|
||||
# Unsubscribes streams from the named `broadcasting`.
|
||||
def stop_stream_from(broadcasting)
|
||||
callback = streams.delete(broadcasting)
|
||||
if callback
|
||||
|
@ -113,7 +126,7 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# Unsubscribes streams for the <tt>model</tt>.
|
||||
# Unsubscribes streams for the `model`.
|
||||
def stop_stream_for(model)
|
||||
stop_stream_from(broadcasting_for(model))
|
||||
end
|
||||
|
@ -126,7 +139,7 @@ module ActionCable
|
|||
end.clear
|
||||
end
|
||||
|
||||
# Calls stream_for with the given <tt>model</tt> if it's present to start streaming,
|
||||
# Calls stream_for with the given `model` if it's present to start streaming,
|
||||
# otherwise rejects the subscription.
|
||||
def stream_or_reject_for(model)
|
||||
if model
|
||||
|
@ -143,8 +156,8 @@ module ActionCable
|
|||
@_streams ||= {}
|
||||
end
|
||||
|
||||
# Always wrap the outermost handler to invoke the user handler on the
|
||||
# worker pool rather than blocking the event loop.
|
||||
# Always wrap the outermost handler to invoke the user handler on the worker
|
||||
# pool rather than blocking the event loop.
|
||||
def worker_pool_stream_handler(broadcasting, user_handler, coder: nil)
|
||||
handler = stream_handler(broadcasting, user_handler, coder: coder)
|
||||
|
||||
|
@ -153,8 +166,8 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# May be overridden to add instrumentation, logging, specialized error
|
||||
# handling, or other forms of handler decoration.
|
||||
# May be overridden to add instrumentation, logging, specialized error handling,
|
||||
# or other forms of handler decoration.
|
||||
#
|
||||
# TODO: Tests demonstrating this.
|
||||
def stream_handler(broadcasting, user_handler, coder: nil)
|
||||
|
@ -165,14 +178,14 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# May be overridden to change the default stream handling behavior
|
||||
# which decodes JSON and transmits to the client.
|
||||
# May be overridden to change the default stream handling behavior which decodes
|
||||
# JSON and transmits to the client.
|
||||
#
|
||||
# TODO: Tests demonstrating this.
|
||||
#
|
||||
# TODO: Room for optimization. Update transmit API to be coder-aware
|
||||
# so we can no-op when pubsub and connection are both JSON-encoded.
|
||||
# Then we can skip decode+encode if we're just proxying messages.
|
||||
# TODO: Room for optimization. Update transmit API to be coder-aware so we can
|
||||
# no-op when pubsub and connection are both JSON-encoded. Then we can skip
|
||||
# decode+encode if we're just proxying messages.
|
||||
def default_stream_handler(broadcasting, coder:)
|
||||
coder ||= ActiveSupport::JSON
|
||||
stream_transmitter stream_decoder(coder: coder), broadcasting: broadcasting
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support"
|
||||
require "active_support/test_case"
|
||||
require "active_support/core_ext/hash/indifferent_access"
|
||||
|
@ -15,11 +17,10 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# = Action Cable \Channel Stub
|
||||
# # Action Cable Channel Stub
|
||||
#
|
||||
# Stub +stream_from+ to track streams for the channel.
|
||||
# Add public aliases for +subscription_confirmation_sent?+ and
|
||||
# +subscription_rejected?+.
|
||||
# Stub `stream_from` to track streams for the channel. Add public aliases for
|
||||
# `subscription_confirmation_sent?` and `subscription_rejected?`.
|
||||
module ChannelStub
|
||||
def confirmed?
|
||||
subscription_confirmation_sent?
|
||||
|
@ -86,103 +87,104 @@ module ActionCable
|
|||
|
||||
# Superclass for Action Cable channel functional tests.
|
||||
#
|
||||
# == Basic example
|
||||
# ## Basic example
|
||||
#
|
||||
# Functional tests are written as follows:
|
||||
# 1. First, one uses the +subscribe+ method to simulate subscription creation.
|
||||
# 2. Then, one asserts whether the current state is as expected. "State" can be anything:
|
||||
# transmitted messages, subscribed streams, etc.
|
||||
# 1. First, one uses the `subscribe` method to simulate subscription creation.
|
||||
# 2. Then, one asserts whether the current state is as expected. "State" can be
|
||||
# anything: transmitted messages, subscribed streams, etc.
|
||||
#
|
||||
#
|
||||
# For example:
|
||||
#
|
||||
# class ChatChannelTest < ActionCable::Channel::TestCase
|
||||
# def test_subscribed_with_room_number
|
||||
# # Simulate a subscription creation
|
||||
# subscribe room_number: 1
|
||||
# class ChatChannelTest < ActionCable::Channel::TestCase
|
||||
# def test_subscribed_with_room_number
|
||||
# # Simulate a subscription creation
|
||||
# subscribe room_number: 1
|
||||
#
|
||||
# # Asserts that the subscription was successfully created
|
||||
# assert subscription.confirmed?
|
||||
# # Asserts that the subscription was successfully created
|
||||
# assert subscription.confirmed?
|
||||
#
|
||||
# # Asserts that the channel subscribes connection to a stream
|
||||
# assert_has_stream "chat_1"
|
||||
# # Asserts that the channel subscribes connection to a stream
|
||||
# assert_has_stream "chat_1"
|
||||
#
|
||||
# # Asserts that the channel subscribes connection to a specific
|
||||
# # stream created for a model
|
||||
# assert_has_stream_for Room.find(1)
|
||||
# # Asserts that the channel subscribes connection to a specific
|
||||
# # stream created for a model
|
||||
# assert_has_stream_for Room.find(1)
|
||||
# end
|
||||
#
|
||||
# def test_does_not_stream_with_incorrect_room_number
|
||||
# subscribe room_number: -1
|
||||
#
|
||||
# # Asserts that not streams was started
|
||||
# assert_no_streams
|
||||
# end
|
||||
#
|
||||
# def test_does_not_subscribe_without_room_number
|
||||
# subscribe
|
||||
#
|
||||
# # Asserts that the subscription was rejected
|
||||
# assert subscription.rejected?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_does_not_stream_with_incorrect_room_number
|
||||
# subscribe room_number: -1
|
||||
#
|
||||
# # Asserts that not streams was started
|
||||
# assert_no_streams
|
||||
# end
|
||||
#
|
||||
# def test_does_not_subscribe_without_room_number
|
||||
# subscribe
|
||||
#
|
||||
# # Asserts that the subscription was rejected
|
||||
# assert subscription.rejected?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# You can also perform actions:
|
||||
# def test_perform_speak
|
||||
# subscribe room_number: 1
|
||||
# def test_perform_speak
|
||||
# subscribe room_number: 1
|
||||
#
|
||||
# perform :speak, message: "Hello, Rails!"
|
||||
# perform :speak, message: "Hello, Rails!"
|
||||
#
|
||||
# assert_equal "Hello, Rails!", transmissions.last["text"]
|
||||
# end
|
||||
# assert_equal "Hello, Rails!", transmissions.last["text"]
|
||||
# end
|
||||
#
|
||||
# == Special methods
|
||||
# ## Special methods
|
||||
#
|
||||
# ActionCable::Channel::TestCase will also automatically provide the following instance
|
||||
# methods for use in the tests:
|
||||
# ActionCable::Channel::TestCase will also automatically provide the following
|
||||
# instance methods for use in the tests:
|
||||
#
|
||||
# <b>connection</b>::
|
||||
# An ActionCable::Channel::ConnectionStub, representing the current HTTP connection.
|
||||
# <b>subscription</b>::
|
||||
# An instance of the current channel, created when you call +subscribe+.
|
||||
# <b>transmissions</b>::
|
||||
# A list of all messages that have been transmitted into the channel.
|
||||
# **connection**
|
||||
# : An ActionCable::Channel::ConnectionStub, representing the current HTTP
|
||||
# connection.
|
||||
# **subscription**
|
||||
# : An instance of the current channel, created when you call `subscribe`.
|
||||
# **transmissions**
|
||||
# : A list of all messages that have been transmitted into the channel.
|
||||
#
|
||||
#
|
||||
# == Channel is automatically inferred
|
||||
# ## Channel is automatically inferred
|
||||
#
|
||||
# ActionCable::Channel::TestCase will automatically infer the channel under test
|
||||
# from the test class name. If the channel cannot be inferred from the test
|
||||
# class name, you can explicitly set it with +tests+.
|
||||
# class name, you can explicitly set it with `tests`.
|
||||
#
|
||||
# class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
|
||||
# tests SpecialChannel
|
||||
# end
|
||||
# class SpecialEdgeCaseChannelTest < ActionCable::Channel::TestCase
|
||||
# tests SpecialChannel
|
||||
# end
|
||||
#
|
||||
# == Specifying connection identifiers
|
||||
# ## Specifying connection identifiers
|
||||
#
|
||||
# You need to set up your connection manually to provide values for the identifiers.
|
||||
# To do this just use:
|
||||
# You need to set up your connection manually to provide values for the
|
||||
# identifiers. To do this just use:
|
||||
#
|
||||
# stub_connection(user: users(:john))
|
||||
# stub_connection(user: users(:john))
|
||||
#
|
||||
# == Testing broadcasting
|
||||
# ## Testing broadcasting
|
||||
#
|
||||
# ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions (e.g.
|
||||
# +assert_broadcasts+) to handle broadcasting to models:
|
||||
# ActionCable::Channel::TestCase enhances ActionCable::TestHelper assertions
|
||||
# (e.g. `assert_broadcasts`) to handle broadcasting to models:
|
||||
#
|
||||
# # in your channel
|
||||
# def speak(data)
|
||||
# broadcast_to room, text: data["message"]
|
||||
# end
|
||||
#
|
||||
# # in your channel
|
||||
# def speak(data)
|
||||
# broadcast_to room, text: data["message"]
|
||||
# end
|
||||
# def test_speak
|
||||
# subscribe room_id: rooms(:chat).id
|
||||
#
|
||||
# def test_speak
|
||||
# subscribe room_id: rooms(:chat).id
|
||||
#
|
||||
# assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do
|
||||
# perform :speak, message: "Hello, Rails!"
|
||||
# end
|
||||
# end
|
||||
# assert_broadcast_on(rooms(:chat), text: "Hello, Rails!") do
|
||||
# perform :speak, message: "Hello, Rails!"
|
||||
# end
|
||||
# end
|
||||
class TestCase < ActiveSupport::TestCase
|
||||
module Behavior
|
||||
extend ActiveSupport::Concern
|
||||
|
@ -231,16 +233,17 @@ module ActionCable
|
|||
|
||||
# Set up test connection with the specified identifiers:
|
||||
#
|
||||
# class ApplicationCable < ActionCable::Connection::Base
|
||||
# identified_by :user, :token
|
||||
# end
|
||||
# class ApplicationCable < ActionCable::Connection::Base
|
||||
# identified_by :user, :token
|
||||
# end
|
||||
#
|
||||
# stub_connection(user: users[:john], token: 'my-secret-token')
|
||||
# stub_connection(user: users[:john], token: 'my-secret-token')
|
||||
def stub_connection(identifiers = {})
|
||||
@connection = ConnectionStub.new(identifiers)
|
||||
end
|
||||
|
||||
# Subscribe to the channel under test. Optionally pass subscription parameters as a Hash.
|
||||
# Subscribe to the channel under test. Optionally pass subscription parameters
|
||||
# as a Hash.
|
||||
def subscribe(params = {})
|
||||
@connection ||= stub_connection
|
||||
@subscription = self.class.channel_class.new(connection, CHANNEL_IDENTIFIER, params.with_indifferent_access)
|
||||
|
@ -269,8 +272,7 @@ module ActionCable
|
|||
connection.transmissions.filter_map { |data| data["message"] }
|
||||
end
|
||||
|
||||
# Enhance TestHelper assertions to handle non-String
|
||||
# broadcastings
|
||||
# Enhance TestHelper assertions to handle non-String broadcastings
|
||||
def assert_broadcasts(stream_or_object, *args)
|
||||
super(broadcasting_for(stream_or_object), *args)
|
||||
end
|
||||
|
@ -281,10 +283,10 @@ module ActionCable
|
|||
|
||||
# Asserts that no streams have been started.
|
||||
#
|
||||
# def test_assert_no_started_stream
|
||||
# subscribe
|
||||
# assert_no_streams
|
||||
# end
|
||||
# def test_assert_no_started_stream
|
||||
# subscribe
|
||||
# assert_no_streams
|
||||
# end
|
||||
#
|
||||
def assert_no_streams
|
||||
assert subscription.streams.empty?, "No streams started was expected, but #{subscription.streams.count} found"
|
||||
|
@ -292,10 +294,10 @@ module ActionCable
|
|||
|
||||
# Asserts that the specified stream has been started.
|
||||
#
|
||||
# def test_assert_started_stream
|
||||
# subscribe
|
||||
# assert_has_stream 'messages'
|
||||
# end
|
||||
# def test_assert_started_stream
|
||||
# subscribe
|
||||
# assert_has_stream 'messages'
|
||||
# end
|
||||
#
|
||||
def assert_has_stream(stream)
|
||||
assert subscription.streams.include?(stream), "Stream #{stream} has not been started"
|
||||
|
@ -303,10 +305,10 @@ module ActionCable
|
|||
|
||||
# Asserts that the specified stream for a model has started.
|
||||
#
|
||||
# def test_assert_started_stream_for
|
||||
# subscribe id: 42
|
||||
# assert_has_stream_for User.find(42)
|
||||
# end
|
||||
# def test_assert_started_stream_for
|
||||
# subscribe id: 42
|
||||
# assert_has_stream_for User.find(42)
|
||||
# end
|
||||
#
|
||||
def assert_has_stream_for(object)
|
||||
assert_has_stream(broadcasting_for(object))
|
||||
|
@ -314,10 +316,10 @@ module ActionCable
|
|||
|
||||
# Asserts that the specified stream has not been started.
|
||||
#
|
||||
# def test_assert_no_started_stream
|
||||
# subscribe
|
||||
# assert_has_no_stream 'messages'
|
||||
# end
|
||||
# def test_assert_no_started_stream
|
||||
# subscribe
|
||||
# assert_has_no_stream 'messages'
|
||||
# end
|
||||
#
|
||||
def assert_has_no_stream(stream)
|
||||
assert subscription.streams.exclude?(stream), "Stream #{stream} has been started"
|
||||
|
@ -325,10 +327,10 @@ module ActionCable
|
|||
|
||||
# Asserts that the specified stream for a model has not started.
|
||||
#
|
||||
# def test_assert_no_started_stream_for
|
||||
# subscribe id: 41
|
||||
# assert_has_no_stream_for User.find(42)
|
||||
# end
|
||||
# def test_assert_no_started_stream_for
|
||||
# subscribe id: 41
|
||||
# assert_has_no_stream_for User.find(42)
|
||||
# end
|
||||
#
|
||||
def assert_has_no_stream_for(object)
|
||||
assert_has_no_stream(broadcasting_for(object))
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
module Authorization
|
||||
class UnauthorizedError < StandardError; end
|
||||
|
||||
# Closes the WebSocket connection if it is open and returns an "unauthorized" reason.
|
||||
# Closes the WebSocket connection if it is open and returns an "unauthorized"
|
||||
# reason.
|
||||
def reject_unauthorized_connection
|
||||
logger.error "An unauthorized connection attempt was rejected"
|
||||
raise UnauthorizedError
|
||||
|
|
|
@ -1,48 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "action_dispatch"
|
||||
require "active_support/rescuable"
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \Connection \Base
|
||||
# # Action Cable Connection Base
|
||||
#
|
||||
# For every WebSocket connection the Action Cable server accepts, a Connection object will be instantiated. This instance becomes the parent
|
||||
# of all of the channel subscriptions that are created from there on. Incoming messages are then routed to these channel subscriptions
|
||||
# based on an identifier sent by the Action Cable consumer. The Connection itself does not deal with any specific application logic beyond
|
||||
# authentication and authorization.
|
||||
# For every WebSocket connection the Action Cable server accepts, a Connection
|
||||
# object will be instantiated. This instance becomes the parent of all of the
|
||||
# channel subscriptions that are created from there on. Incoming messages are
|
||||
# then routed to these channel subscriptions based on an identifier sent by the
|
||||
# Action Cable consumer. The Connection itself does not deal with any specific
|
||||
# application logic beyond authentication and authorization.
|
||||
#
|
||||
# Here's a basic example:
|
||||
#
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :current_user
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :current_user
|
||||
#
|
||||
# def connect
|
||||
# self.current_user = find_verified_user
|
||||
# logger.add_tags current_user.name
|
||||
# end
|
||||
#
|
||||
# def disconnect
|
||||
# # Any cleanup work needed when the cable connection is cut.
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def find_verified_user
|
||||
# User.find_by_identity(cookies.encrypted[:identity_id]) ||
|
||||
# reject_unauthorized_connection
|
||||
# def connect
|
||||
# self.current_user = find_verified_user
|
||||
# logger.add_tags current_user.name
|
||||
# end
|
||||
#
|
||||
# def disconnect
|
||||
# # Any cleanup work needed when the cable connection is cut.
|
||||
# end
|
||||
#
|
||||
# private
|
||||
# def find_verified_user
|
||||
# User.find_by_identity(cookies.encrypted[:identity_id]) ||
|
||||
# reject_unauthorized_connection
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# First, we declare that this connection can be identified by its current_user. This allows us to later be able to find all connections
|
||||
# established for that current_user (and potentially disconnect them). You can declare as many
|
||||
# identification indexes as you like. Declaring an identification means that an attr_accessor is automatically set for that key.
|
||||
# First, we declare that this connection can be identified by its current_user.
|
||||
# This allows us to later be able to find all connections established for that
|
||||
# current_user (and potentially disconnect them). You can declare as many
|
||||
# identification indexes as you like. Declaring an identification means that an
|
||||
# attr_accessor is automatically set for that key.
|
||||
#
|
||||
# Second, we rely on the fact that the WebSocket connection is established with the cookies from the domain being sent along. This makes
|
||||
# it easy to use signed cookies that were set when logging in via a web interface to authorize the WebSocket connection.
|
||||
# Second, we rely on the fact that the WebSocket connection is established with
|
||||
# the cookies from the domain being sent along. This makes it easy to use signed
|
||||
# cookies that were set when logging in via a web interface to authorize the
|
||||
# WebSocket connection.
|
||||
#
|
||||
# Finally, we add a tag to the connection-specific logger with the name of the current user to easily distinguish their messages in the log.
|
||||
# Finally, we add a tag to the connection-specific logger with the name of the
|
||||
# current user to easily distinguish their messages in the log.
|
||||
#
|
||||
# Pretty simple, eh?
|
||||
class Base
|
||||
|
@ -69,8 +78,10 @@ module ActionCable
|
|||
@started_at = Time.now
|
||||
end
|
||||
|
||||
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user.
|
||||
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks.
|
||||
# Called by the server when a new WebSocket connection is established. This
|
||||
# configures the callbacks intended for overwriting by the user. This method
|
||||
# should not be called directly -- instead rely upon on the #connect (and
|
||||
# #disconnect) callbacks.
|
||||
def process # :nodoc:
|
||||
logger.info started_request_message
|
||||
|
||||
|
@ -115,13 +126,15 @@ module ActionCable
|
|||
websocket.close
|
||||
end
|
||||
|
||||
# Invoke a method on the connection asynchronously through the pool of thread workers.
|
||||
# Invoke a method on the connection asynchronously through the pool of thread
|
||||
# workers.
|
||||
def send_async(method, *arguments)
|
||||
worker_pool.async_invoke(self, method, *arguments)
|
||||
end
|
||||
|
||||
# Return a basic hash of statistics for the connection keyed with <tt>identifier</tt>, <tt>started_at</tt>, <tt>subscriptions</tt>, and <tt>request_id</tt>.
|
||||
# This can be returned by a health check against the connection.
|
||||
# Return a basic hash of statistics for the connection keyed with `identifier`,
|
||||
# `started_at`, `subscriptions`, and `request_id`. This can be returned by a
|
||||
# health check against the connection.
|
||||
def statistics
|
||||
{
|
||||
identifier: connection_identifier,
|
||||
|
@ -160,7 +173,8 @@ module ActionCable
|
|||
attr_reader :websocket
|
||||
attr_reader :message_buffer
|
||||
|
||||
# The request that initiated the WebSocket connection is available here. This gives access to the environment, cookies, etc.
|
||||
# The request that initiated the WebSocket connection is available here. This
|
||||
# gives access to the environment, cookies, etc.
|
||||
def request # :doc:
|
||||
@request ||= begin
|
||||
environment = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
|
||||
|
@ -168,7 +182,8 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# The cookies of the request that initiated the WebSocket connection. Useful for performing authorization checks.
|
||||
# The cookies of the request that initiated the WebSocket connection. Useful for
|
||||
# performing authorization checks.
|
||||
def cookies # :doc:
|
||||
request.cookie_jar
|
||||
end
|
||||
|
@ -205,9 +220,8 @@ module ActionCable
|
|||
end
|
||||
|
||||
def send_welcome_message
|
||||
# Send welcome message to the internal connection monitor channel.
|
||||
# This ensures the connection monitor state is reset after a successful
|
||||
# websocket connection.
|
||||
# Send welcome message to the internal connection monitor channel. This ensures
|
||||
# the connection monitor state is reset after a successful websocket connection.
|
||||
transmit type: ActionCable::INTERNAL[:message_types][:welcome]
|
||||
end
|
||||
|
||||
|
@ -238,7 +252,8 @@ module ActionCable
|
|||
[ 404, { Rack::CONTENT_TYPE => "text/plain; charset=utf-8" }, [ "Page not found" ] ]
|
||||
end
|
||||
|
||||
# Tags are declared in the server but computed in the connection. This allows us per-connection tailored tags.
|
||||
# Tags are declared in the server but computed in the connection. This allows us
|
||||
# per-connection tailored tags.
|
||||
def new_tagged_logger
|
||||
TaggedLoggerProxy.new server.logger,
|
||||
tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize }
|
||||
|
|
|
@ -1,33 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/callbacks"
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \Connection \Callbacks
|
||||
# # Action Cable Connection Callbacks
|
||||
#
|
||||
# The {before_command}[rdoc-ref:ClassMethods#before_command],
|
||||
# {after_command}[rdoc-ref:ClassMethods#after_command], and
|
||||
# {around_command}[rdoc-ref:ClassMethods#around_command] callbacks are
|
||||
# invoked when sending commands to the client, such as when subscribing,
|
||||
# unsubscribing, or performing an action.
|
||||
# The [before_command](rdoc-ref:ClassMethods#before_command),
|
||||
# [after_command](rdoc-ref:ClassMethods#after_command), and
|
||||
# [around_command](rdoc-ref:ClassMethods#around_command) callbacks are invoked
|
||||
# when sending commands to the client, such as when subscribing, unsubscribing,
|
||||
# or performing an action.
|
||||
#
|
||||
# ==== Example
|
||||
# #### Example
|
||||
#
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :user
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :user
|
||||
#
|
||||
# around_command :set_current_account
|
||||
# around_command :set_current_account
|
||||
#
|
||||
# private
|
||||
# private
|
||||
#
|
||||
# def set_current_account
|
||||
# # Now all channels could use Current.account
|
||||
# Current.set(account: user.account) { yield }
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
# def set_current_account
|
||||
# # Now all channels could use Current.account
|
||||
# Current.set(account: user.account) { yield }
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
module Callbacks
|
||||
extend ActiveSupport::Concern
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "websocket/driver"
|
||||
|
||||
module ActionCable
|
||||
|
@ -43,7 +45,7 @@ module ActionCable
|
|||
|
||||
@ready_state = CONNECTING
|
||||
|
||||
# The driver calls +env+, +url+, and +write+
|
||||
# The driver calls `env`, `url`, and `write`
|
||||
@driver = ::WebSocket::Driver.rack(self, protocols: protocols)
|
||||
|
||||
@driver.on(:open) { |e| open }
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "set"
|
||||
|
||||
module ActionCable
|
||||
|
@ -12,18 +14,20 @@ module ActionCable
|
|||
end
|
||||
|
||||
module ClassMethods
|
||||
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
||||
# Common identifiers are current_user and current_account, but could be anything, really.
|
||||
# Mark a key as being a connection identifier index that can then be used to
|
||||
# find the specific connection again later. Common identifiers are current_user
|
||||
# and current_account, but could be anything, really.
|
||||
#
|
||||
# Note that anything marked as an identifier will automatically create a delegate by the same name on any
|
||||
# channel instances created off the connection.
|
||||
# Note that anything marked as an identifier will automatically create a
|
||||
# delegate by the same name on any channel instances created off the connection.
|
||||
def identified_by(*identifiers)
|
||||
Array(identifiers).each { |identifier| attr_accessor identifier }
|
||||
self.identifiers += identifiers
|
||||
end
|
||||
end
|
||||
|
||||
# Return a single connection identifier that combines the value of all the registered identifiers into a single gid.
|
||||
# Return a single connection identifier that combines the value of all the
|
||||
# registered identifiers into a single gid.
|
||||
def connection_identifier
|
||||
unless defined? @connection_identifier
|
||||
@connection_identifier = connection_gid identifiers.filter_map { |id| instance_variable_get("@#{id}") }
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \InternalChannel
|
||||
# # Action Cable InternalChannel
|
||||
#
|
||||
# Makes it possible for the RemoteConnection to disconnect a specific connection.
|
||||
# Makes it possible for the RemoteConnection to disconnect a specific
|
||||
# connection.
|
||||
module InternalChannel
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized, and is ready to receive them.
|
||||
# Allows us to buffer messages received from the WebSocket before the Connection
|
||||
# has been fully initialized, and is ready to receive them.
|
||||
class MessageBuffer # :nodoc:
|
||||
def initialize(connection)
|
||||
@connection = connection
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
#--
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "nio"
|
||||
|
||||
module ActionCable
|
||||
|
@ -116,9 +118,8 @@ module ActionCable
|
|||
stream.receive incoming
|
||||
end
|
||||
rescue
|
||||
# We expect one of EOFError or Errno::ECONNRESET in
|
||||
# normal operation (when the client goes away). But if
|
||||
# anything else goes wrong, this is still the best way
|
||||
# We expect one of EOFError or Errno::ECONNRESET in normal operation (when the
|
||||
# client goes away). But if anything else goes wrong, this is still the best way
|
||||
# to handle it.
|
||||
begin
|
||||
stream.close
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/core_ext/hash/indifferent_access"
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \Connection \Subscriptions
|
||||
# # Action Cable Connection Subscriptions
|
||||
#
|
||||
# Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
|
||||
# the connection to the proper channel.
|
||||
# Collection class for all the channel subscriptions established on a given
|
||||
# connection. Responsible for routing incoming commands that arrive on the
|
||||
# connection to the proper channel.
|
||||
class Subscriptions # :nodoc:
|
||||
def initialize(connection)
|
||||
@connection = connection
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \Connection \TaggedLoggerProxy
|
||||
# # Action Cable Connection TaggedLoggerProxy
|
||||
#
|
||||
# Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
|
||||
# ActiveSupport::TaggedLogging enhanced Rails.logger, as that logger will reset the tags between requests.
|
||||
# The connection is long-lived, so it needs its own set of tags for its independent duration.
|
||||
# Allows the use of per-connection tags against the server logger. This wouldn't
|
||||
# work using the traditional ActiveSupport::TaggedLogging enhanced Rails.logger,
|
||||
# as that logger will reset the tags between requests. The connection is
|
||||
# long-lived, so it needs its own set of tags for its independent duration.
|
||||
class TaggedLoggerProxy
|
||||
attr_reader :tags
|
||||
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support"
|
||||
require "active_support/test_case"
|
||||
require "active_support/core_ext/hash/indifferent_access"
|
||||
|
@ -18,18 +20,19 @@ module ActionCable
|
|||
end
|
||||
|
||||
module Assertions
|
||||
# Asserts that the connection is rejected (via +reject_unauthorized_connection+).
|
||||
# Asserts that the connection is rejected (via
|
||||
# `reject_unauthorized_connection`).
|
||||
#
|
||||
# # Asserts that connection without user_id fails
|
||||
# assert_reject_connection { connect params: { user_id: '' } }
|
||||
# # Asserts that connection without user_id fails
|
||||
# assert_reject_connection { connect params: { user_id: '' } }
|
||||
def assert_reject_connection(&block)
|
||||
assert_raises(Authorization::UnauthorizedError, "Expected to reject connection but no rejection was made", &block)
|
||||
end
|
||||
end
|
||||
|
||||
# We don't want to use the whole "encryption stack" for connection
|
||||
# unit-tests, but we want to make sure that users test against the correct types
|
||||
# of cookies (i.e. signed or encrypted or plain)
|
||||
# We don't want to use the whole "encryption stack" for connection unit-tests,
|
||||
# but we want to make sure that users test against the correct types of cookies
|
||||
# (i.e. signed or encrypted or plain)
|
||||
class TestCookieJar < ActiveSupport::HashWithIndifferentAccess
|
||||
def signed
|
||||
self[:signed] ||= {}.with_indifferent_access
|
||||
|
@ -56,77 +59,77 @@ module ActionCable
|
|||
end
|
||||
end
|
||||
|
||||
# = Action Cable \Connection \TestCase
|
||||
# # Action Cable Connection TestCase
|
||||
#
|
||||
# Unit test Action Cable connections.
|
||||
#
|
||||
# Useful to check whether a connection's +identified_by+ gets assigned properly
|
||||
# Useful to check whether a connection's `identified_by` gets assigned properly
|
||||
# and that any improper connection requests are rejected.
|
||||
#
|
||||
# == Basic example
|
||||
# ## Basic example
|
||||
#
|
||||
# Unit tests are written as follows:
|
||||
#
|
||||
# 1. Simulate a connection attempt by calling +connect+.
|
||||
# 2. Assert state, e.g. identifiers, has been assigned.
|
||||
# 1. Simulate a connection attempt by calling `connect`.
|
||||
# 2. Assert state, e.g. identifiers, has been assigned.
|
||||
#
|
||||
#
|
||||
# class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
|
||||
# def test_connects_with_proper_cookie
|
||||
# # Simulate the connection request with a cookie.
|
||||
# cookies["user_id"] = users(:john).id
|
||||
# class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
|
||||
# def test_connects_with_proper_cookie
|
||||
# # Simulate the connection request with a cookie.
|
||||
# cookies["user_id"] = users(:john).id
|
||||
#
|
||||
# connect
|
||||
# connect
|
||||
#
|
||||
# # Assert the connection identifier matches the fixture.
|
||||
# assert_equal users(:john).id, connection.user.id
|
||||
# # Assert the connection identifier matches the fixture.
|
||||
# assert_equal users(:john).id, connection.user.id
|
||||
# end
|
||||
#
|
||||
# def test_rejects_connection_without_proper_cookie
|
||||
# assert_reject_connection { connect }
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def test_rejects_connection_without_proper_cookie
|
||||
# assert_reject_connection { connect }
|
||||
# `connect` accepts additional information about the HTTP request with the
|
||||
# `params`, `headers`, `session`, and Rack `env` options.
|
||||
#
|
||||
# def test_connect_with_headers_and_query_string
|
||||
# connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
|
||||
#
|
||||
# assert_equal "1", connection.user.id
|
||||
# assert_equal "secret-my", connection.token
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# +connect+ accepts additional information about the HTTP request with the
|
||||
# +params+, +headers+, +session+, and Rack +env+ options.
|
||||
# def test_connect_with_params
|
||||
# connect params: { user_id: 1 }
|
||||
#
|
||||
# def test_connect_with_headers_and_query_string
|
||||
# connect params: { user_id: 1 }, headers: { "X-API-TOKEN" => "secret-my" }
|
||||
#
|
||||
# assert_equal "1", connection.user.id
|
||||
# assert_equal "secret-my", connection.token
|
||||
# end
|
||||
#
|
||||
# def test_connect_with_params
|
||||
# connect params: { user_id: 1 }
|
||||
#
|
||||
# assert_equal "1", connection.user.id
|
||||
# end
|
||||
# assert_equal "1", connection.user.id
|
||||
# end
|
||||
#
|
||||
# You can also set up the correct cookies before the connection request:
|
||||
#
|
||||
# def test_connect_with_cookies
|
||||
# # Plain cookies:
|
||||
# cookies["user_id"] = 1
|
||||
# def test_connect_with_cookies
|
||||
# # Plain cookies:
|
||||
# cookies["user_id"] = 1
|
||||
#
|
||||
# # Or signed/encrypted:
|
||||
# # cookies.signed["user_id"] = 1
|
||||
# # cookies.encrypted["user_id"] = 1
|
||||
# # Or signed/encrypted:
|
||||
# # cookies.signed["user_id"] = 1
|
||||
# # cookies.encrypted["user_id"] = 1
|
||||
#
|
||||
# connect
|
||||
# connect
|
||||
#
|
||||
# assert_equal "1", connection.user_id
|
||||
# end
|
||||
# assert_equal "1", connection.user_id
|
||||
# end
|
||||
#
|
||||
# == \Connection is automatically inferred
|
||||
# ## Connection is automatically inferred
|
||||
#
|
||||
# ActionCable::Connection::TestCase will automatically infer the connection under test
|
||||
# from the test class name. If the channel cannot be inferred from the test
|
||||
# class name, you can explicitly set it with +tests+.
|
||||
# ActionCable::Connection::TestCase will automatically infer the connection
|
||||
# under test from the test class name. If the channel cannot be inferred from
|
||||
# the test class name, you can explicitly set it with `tests`.
|
||||
#
|
||||
# class ConnectionTest < ActionCable::Connection::TestCase
|
||||
# tests ApplicationCable::Connection
|
||||
# end
|
||||
# class ConnectionTest < ActionCable::Connection::TestCase
|
||||
# tests ApplicationCable::Connection
|
||||
# end
|
||||
#
|
||||
class TestCase < ActiveSupport::TestCase
|
||||
module Behavior
|
||||
|
@ -178,10 +181,10 @@ module ActionCable
|
|||
#
|
||||
# Accepts request path as the first argument and the following request options:
|
||||
#
|
||||
# - params – URL parameters (Hash)
|
||||
# - headers – request headers (Hash)
|
||||
# - session – session data (Hash)
|
||||
# - env – additional Rack env configuration (Hash)
|
||||
# * params – URL parameters (Hash)
|
||||
# * headers – request headers (Hash)
|
||||
# * session – session data (Hash)
|
||||
# * env – additional Rack env configuration (Hash)
|
||||
def connect(path = ActionCable.server.config.mount_path, **request_params)
|
||||
path ||= DEFAULT_PATH
|
||||
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "websocket/driver"
|
||||
|
||||
module ActionCable
|
||||
module Connection
|
||||
# = Action Cable \Connection \WebSocket
|
||||
# # Action Cable Connection WebSocket
|
||||
#
|
||||
# Wrap the real socket to minimize the externally-presented API
|
||||
class WebSocket # :nodoc:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
def self.deprecator # :nodoc:
|
||||
@deprecator ||= ActiveSupport::Deprecation.new
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "rails"
|
||||
require "action_cable"
|
||||
require "active_support/core_ext/hash/indifferent_access"
|
||||
|
@ -72,9 +74,9 @@ module ActionCable
|
|||
ActiveSupport.on_load(:action_cable) do
|
||||
ActionCable::Server::Worker.set_callback :work, :around, prepend: true do |_, inner|
|
||||
app.executor.wrap(source: "application.action_cable") do
|
||||
# If we took a while to get the lock, we may have been halted
|
||||
# in the meantime. As we haven't started doing any real work
|
||||
# yet, we should pretend that we never made it off the queue.
|
||||
# If we took a while to get the lock, we may have been halted in the meantime.
|
||||
# As we haven't started doing any real work yet, we should pretend that we never
|
||||
# made it off the queue.
|
||||
unless stopping?
|
||||
inner.call
|
||||
end
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
# Returns the currently loaded version of Action Cable as a +Gem::Version+.
|
||||
# Returns the currently loaded version of Action Cable as a `Gem::Version`.
|
||||
def self.gem_version
|
||||
Gem::Version.new VERSION::STRING
|
||||
end
|
||||
|
|
|
@ -1,35 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Helpers
|
||||
module ActionCableHelper
|
||||
# Returns an "action-cable-url" meta tag with the value of the URL specified in your
|
||||
# configuration. Ensure this is above your JavaScript tag:
|
||||
# Returns an "action-cable-url" meta tag with the value of the URL specified in
|
||||
# your configuration. Ensure this is above your JavaScript tag:
|
||||
#
|
||||
# <head>
|
||||
# <%= action_cable_meta_tag %>
|
||||
# <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
|
||||
# </head>
|
||||
# <head>
|
||||
# <%= action_cable_meta_tag %>
|
||||
# <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %>
|
||||
# </head>
|
||||
#
|
||||
# This is then used by Action Cable to determine the URL of your WebSocket server.
|
||||
# Your JavaScript can then connect to the server without needing to specify the
|
||||
# URL directly:
|
||||
# This is then used by Action Cable to determine the URL of your WebSocket
|
||||
# server. Your JavaScript can then connect to the server without needing to
|
||||
# specify the URL directly:
|
||||
#
|
||||
# import Cable from "@rails/actioncable"
|
||||
# window.Cable = Cable
|
||||
# window.App = {}
|
||||
# App.cable = Cable.createConsumer()
|
||||
# import Cable from "@rails/actioncable"
|
||||
# window.Cable = Cable
|
||||
# window.App = {}
|
||||
# App.cable = Cable.createConsumer()
|
||||
#
|
||||
# Make sure to specify the correct server location in each of your environment
|
||||
# config files:
|
||||
#
|
||||
# config.action_cable.mount_path = "/cable123"
|
||||
# <%= action_cable_meta_tag %> would render:
|
||||
# => <meta name="action-cable-url" content="/cable123" />
|
||||
# config.action_cable.mount_path = "/cable123"
|
||||
# <%= action_cable_meta_tag %> would render:
|
||||
# => <meta name="action-cable-url" content="/cable123" />
|
||||
#
|
||||
# config.action_cable.url = "ws://actioncable.com"
|
||||
# <%= action_cable_meta_tag %> would render:
|
||||
# => <meta name="action-cable-url" content="ws://actioncable.com" />
|
||||
# config.action_cable.url = "ws://actioncable.com"
|
||||
# <%= action_cable_meta_tag %> would render:
|
||||
# => <meta name="action-cable-url" content="ws://actioncable.com" />
|
||||
#
|
||||
def action_cable_meta_tag
|
||||
tag "meta", name: "action-cable-url", content: (
|
||||
|
|
|
@ -1,31 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/core_ext/module/redefine_method"
|
||||
|
||||
module ActionCable
|
||||
# = Action Cable Remote Connections
|
||||
# # Action Cable Remote Connections
|
||||
#
|
||||
# If you need to disconnect a given connection, you can go through the
|
||||
# RemoteConnections. You can find the connections you're looking for by
|
||||
# searching for the identifier declared on the connection. For example:
|
||||
#
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :current_user
|
||||
# ....
|
||||
# module ApplicationCable
|
||||
# class Connection < ActionCable::Connection::Base
|
||||
# identified_by :current_user
|
||||
# ....
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
|
||||
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
|
||||
#
|
||||
# This will disconnect all the connections established for
|
||||
# <tt>User.find(1)</tt>, across all servers running on all machines, because
|
||||
# it uses the internal channel that all of these servers are subscribed to.
|
||||
# This will disconnect all the connections established for `User.find(1)`,
|
||||
# across all servers running on all machines, because it uses the internal
|
||||
# channel that all of these servers are subscribed to.
|
||||
#
|
||||
# By default, server sends a "disconnect" message with "reconnect" flag set to true.
|
||||
# You can override it by specifying the +reconnect+ option:
|
||||
# By default, server sends a "disconnect" message with "reconnect" flag set to
|
||||
# true. You can override it by specifying the `reconnect` option:
|
||||
#
|
||||
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
|
||||
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect(reconnect: false)
|
||||
class RemoteConnections
|
||||
attr_reader :server
|
||||
|
||||
|
@ -38,10 +40,11 @@ module ActionCable
|
|||
end
|
||||
|
||||
private
|
||||
# = Action Cable Remote \Connection
|
||||
# # Action Cable Remote Connection
|
||||
#
|
||||
# Represents a single remote connection found via <tt>ActionCable.server.remote_connections.where(*)</tt>.
|
||||
# Exists solely for the purpose of calling #disconnect on that connection.
|
||||
# Represents a single remote connection found via
|
||||
# `ActionCable.server.remote_connections.where(*)`. Exists solely for the
|
||||
# purpose of calling #disconnect on that connection.
|
||||
class RemoteConnection
|
||||
class InvalidIdentifiersError < StandardError; end
|
||||
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "monitor"
|
||||
|
||||
module ActionCable
|
||||
module Server
|
||||
# = Action Cable \Server \Base
|
||||
# # Action Cable Server Base
|
||||
#
|
||||
# A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the Rack process that starts the Action Cable server, but
|
||||
# is also used by the user to reach the RemoteConnections object, which is used for finding and disconnecting connections across all servers.
|
||||
# A singleton ActionCable::Server instance is available via ActionCable.server.
|
||||
# It's used by the Rack process that starts the Action Cable server, but is also
|
||||
# used by the user to reach the RemoteConnections object, which is used for
|
||||
# finding and disconnecting connections across all servers.
|
||||
#
|
||||
# Also, this is the server instance used for broadcasting. See Broadcasting for more information.
|
||||
# Also, this is the server instance used for broadcasting. See Broadcasting for
|
||||
# more information.
|
||||
class Base
|
||||
include ActionCable::Server::Broadcasting
|
||||
include ActionCable::Server::Connections
|
||||
|
@ -36,7 +41,8 @@ module ActionCable
|
|||
config.connection_class.call.new(self, env).process
|
||||
end
|
||||
|
||||
# Disconnect all the connections identified by +identifiers+ on this server or any others via RemoteConnections.
|
||||
# Disconnect all the connections identified by `identifiers` on this server or
|
||||
# any others via RemoteConnections.
|
||||
def disconnect(identifiers)
|
||||
remote_connections.where(identifiers).disconnect
|
||||
end
|
||||
|
@ -66,17 +72,22 @@ module ActionCable
|
|||
@event_loop || @mutex.synchronize { @event_loop ||= ActionCable::Connection::StreamEventLoop.new }
|
||||
end
|
||||
|
||||
# The worker pool is where we run connection callbacks and channel actions. We do as little as possible on the server's main thread.
|
||||
# The worker pool is an executor service that's backed by a pool of threads working from a task queue. The thread pool size maxes out
|
||||
# at 4 worker threads by default. Tune the size yourself with <tt>config.action_cable.worker_pool_size</tt>.
|
||||
# The worker pool is where we run connection callbacks and channel actions. We
|
||||
# do as little as possible on the server's main thread. The worker pool is an
|
||||
# executor service that's backed by a pool of threads working from a task queue.
|
||||
# The thread pool size maxes out at 4 worker threads by default. Tune the size
|
||||
# yourself with `config.action_cable.worker_pool_size`.
|
||||
#
|
||||
# Using Active Record, Redis, etc within your channel actions means you'll get a separate connection from each thread in the worker pool.
|
||||
# Plan your deployment accordingly: 5 servers each running 5 Puma workers each running an 8-thread worker pool means at least 200 database
|
||||
# connections.
|
||||
# Using Active Record, Redis, etc within your channel actions means you'll get a
|
||||
# separate connection from each thread in the worker pool. Plan your deployment
|
||||
# accordingly: 5 servers each running 5 Puma workers each running an 8-thread
|
||||
# worker pool means at least 200 database connections.
|
||||
#
|
||||
# Also, ensure that your database connection pool size is as least as large as your worker pool size. Otherwise, workers may oversubscribe
|
||||
# the database connection pool and block while they wait for other workers to release their connections. Use a smaller worker pool or a larger
|
||||
# database connection pool instead.
|
||||
# Also, ensure that your database connection pool size is as least as large as
|
||||
# your worker pool size. Otherwise, workers may oversubscribe the database
|
||||
# connection pool and block while they wait for other workers to release their
|
||||
# connections. Use a smaller worker pool or a larger database connection pool
|
||||
# instead.
|
||||
def worker_pool
|
||||
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
|
||||
end
|
||||
|
@ -86,7 +97,8 @@ module ActionCable
|
|||
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
|
||||
end
|
||||
|
||||
# All of the identifiers applied to the connection class associated with this server.
|
||||
# All of the identifiers applied to the connection class associated with this
|
||||
# server.
|
||||
def connection_identifiers
|
||||
config.connection_class.call.identifiers
|
||||
end
|
||||
|
|
|
@ -1,34 +1,40 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Server
|
||||
# = Action Cable \Server \Broadcasting
|
||||
# # Action Cable Server Broadcasting
|
||||
#
|
||||
# Broadcasting is how other parts of your application can send messages to a channel's subscribers. As explained in Channel, most of the time, these
|
||||
# broadcastings are streamed directly to the clients subscribed to the named broadcasting. Let's explain with a full-stack example:
|
||||
# Broadcasting is how other parts of your application can send messages to a
|
||||
# channel's subscribers. As explained in Channel, most of the time, these
|
||||
# broadcastings are streamed directly to the clients subscribed to the named
|
||||
# broadcasting. Let's explain with a full-stack example:
|
||||
#
|
||||
# class WebNotificationsChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# stream_from "web_notifications_#{current_user.id}"
|
||||
# class WebNotificationsChannel < ApplicationCable::Channel
|
||||
# def subscribed
|
||||
# stream_from "web_notifications_#{current_user.id}"
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# # Somewhere in your app this is called, perhaps from a NewCommentJob:
|
||||
# ActionCable.server.broadcast \
|
||||
# "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
|
||||
# # Somewhere in your app this is called, perhaps from a NewCommentJob:
|
||||
# ActionCable.server.broadcast \
|
||||
# "web_notifications_1", { title: "New things!", body: "All that's fit for print" }
|
||||
#
|
||||
# # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
|
||||
# App.cable.subscriptions.create "WebNotificationsChannel",
|
||||
# received: (data) ->
|
||||
# new Notification data['title'], body: data['body']
|
||||
# # Client-side CoffeeScript, which assumes you've already requested the right to send web notifications:
|
||||
# App.cable.subscriptions.create "WebNotificationsChannel",
|
||||
# received: (data) ->
|
||||
# new Notification data['title'], body: data['body']
|
||||
module Broadcasting
|
||||
# Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
|
||||
# Broadcast a hash directly to a named `broadcasting`. This will later be JSON
|
||||
# encoded.
|
||||
def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
|
||||
broadcaster_for(broadcasting, coder: coder).broadcast(message)
|
||||
end
|
||||
|
||||
# Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
|
||||
# may need multiple spots to transmit to a specific broadcasting over and over.
|
||||
# Returns a broadcaster for a named `broadcasting` that can be reused. Useful
|
||||
# when you have an object that may need multiple spots to transmit to a specific
|
||||
# broadcasting over and over.
|
||||
def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
|
||||
Broadcaster.new(self, String(broadcasting), coder: coder)
|
||||
end
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "rack"
|
||||
|
||||
module ActionCable
|
||||
module Server
|
||||
# = Action Cable \Server \Configuration
|
||||
# # Action Cable Server Configuration
|
||||
#
|
||||
# An instance of this configuration object is available via ActionCable.server.config, which allows you to tweak Action Cable configuration
|
||||
# in a \Rails config initializer.
|
||||
# An instance of this configuration object is available via
|
||||
# ActionCable.server.config, which allows you to tweak Action Cable
|
||||
# configuration in a Rails config initializer.
|
||||
class Configuration
|
||||
attr_accessor :logger, :log_tags
|
||||
attr_accessor :connection_class, :worker_pool_size
|
||||
|
@ -31,28 +34,28 @@ module ActionCable
|
|||
}
|
||||
end
|
||||
|
||||
# Returns constant of subscription adapter specified in config/cable.yml.
|
||||
# If the adapter cannot be found, this will default to the Redis adapter.
|
||||
# Also makes sure proper dependencies are required.
|
||||
# Returns constant of subscription adapter specified in config/cable.yml. If the
|
||||
# adapter cannot be found, this will default to the Redis adapter. Also makes
|
||||
# sure proper dependencies are required.
|
||||
def pubsub_adapter
|
||||
adapter = (cable.fetch("adapter") { "redis" })
|
||||
|
||||
# Require the adapter itself and give useful feedback about
|
||||
# 1. Missing adapter gems and
|
||||
# 2. Adapter gems' missing dependencies.
|
||||
# 1. Missing adapter gems and
|
||||
# 2. Adapter gems' missing dependencies.
|
||||
path_to_adapter = "action_cable/subscription_adapter/#{adapter}"
|
||||
begin
|
||||
require path_to_adapter
|
||||
rescue LoadError => e
|
||||
# We couldn't require the adapter itself. Raise an exception that
|
||||
# points out config typos and missing gems.
|
||||
# We couldn't require the adapter itself. Raise an exception that points out
|
||||
# config typos and missing gems.
|
||||
if e.path == path_to_adapter
|
||||
# We can assume that a non-builtin adapter was specified, so it's
|
||||
# either misspelled or missing from Gemfile.
|
||||
# We can assume that a non-builtin adapter was specified, so it's either
|
||||
# misspelled or missing from Gemfile.
|
||||
raise e.class, "Could not load the '#{adapter}' Action Cable pubsub adapter. Ensure that the adapter is spelled correctly in config/cable.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
|
||||
|
||||
# Bubbled up from the adapter require. Prefix the exception message
|
||||
# with some guidance about how to address it and reraise.
|
||||
# Bubbled up from the adapter require. Prefix the exception message with some
|
||||
# guidance about how to address it and reraise.
|
||||
else
|
||||
raise e.class, "Error loading the '#{adapter}' Action Cable pubsub adapter. Missing a gem it depends on? #{e.message}", e.backtrace
|
||||
end
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Server
|
||||
# = Action Cable \Server \Connections
|
||||
# # Action Cable Server Connections
|
||||
#
|
||||
# Collection class for all the connections that have been established on this specific server. Remember, usually you'll run many Action Cable servers, so
|
||||
# you can't use this collection as a full list of all of the connections established against your application. Instead, use RemoteConnections for that.
|
||||
# Collection class for all the connections that have been established on this
|
||||
# specific server. Remember, usually you'll run many Action Cable servers, so
|
||||
# you can't use this collection as a full list of all of the connections
|
||||
# established against your application. Instead, use RemoteConnections for that.
|
||||
module Connections # :nodoc:
|
||||
BEAT_INTERVAL = 3
|
||||
|
||||
|
@ -21,8 +25,10 @@ module ActionCable
|
|||
connections.delete connection
|
||||
end
|
||||
|
||||
# WebSocket connection implementations differ on when they'll mark a connection as stale. We basically never want a connection to go stale, as you
|
||||
# then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
|
||||
# WebSocket connection implementations differ on when they'll mark a connection
|
||||
# as stale. We basically never want a connection to go stale, as you then can't
|
||||
# rely on being able to communicate with the connection. To solve this, a 3
|
||||
# second heartbeat runs on all connections. If the beat fails, we automatically
|
||||
# disconnect.
|
||||
def setup_heartbeat_timer
|
||||
@heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/callbacks"
|
||||
require "active_support/core_ext/module/attribute_accessors_per_thread"
|
||||
require "concurrent"
|
||||
|
@ -25,8 +27,8 @@ module ActionCable
|
|||
)
|
||||
end
|
||||
|
||||
# Stop processing work: any work that has not already started
|
||||
# running will be discarded from the queue
|
||||
# Stop processing work: any work that has not already started running will be
|
||||
# discarded from the queue
|
||||
def halt
|
||||
@executor.shutdown
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module Server
|
||||
class Worker
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
class Async < Inline # :nodoc:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
class Base
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
module ChannelPrefix # :nodoc:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
class Inline < Base # :nodoc:
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
gem "pg", "~> 1.1"
|
||||
require "pg"
|
||||
require "openssl"
|
||||
|
@ -34,8 +36,8 @@ module ActionCable
|
|||
|
||||
def with_subscriptions_connection(&block) # :nodoc:
|
||||
ar_conn = ActiveRecord::Base.connection_pool.checkout.tap do |conn|
|
||||
# Action Cable is taking ownership over this database connection, and
|
||||
# will perform the necessary cleanup tasks
|
||||
# Action Cable is taking ownership over this database connection, and will
|
||||
# perform the necessary cleanup tasks
|
||||
ActiveRecord::Base.connection_pool.remove(conn)
|
||||
end
|
||||
pg_conn = ar_conn.raw_connection
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
gem "redis", ">= 4", "< 6"
|
||||
require "redis"
|
||||
|
||||
|
@ -10,8 +12,9 @@ module ActionCable
|
|||
class Redis < Base # :nodoc:
|
||||
prepend ChannelPrefix
|
||||
|
||||
# Overwrite this factory method for Redis connections if you want to use a different Redis library than the redis gem.
|
||||
# This is needed, for example, when using Makara proxies for distributed Redis.
|
||||
# Overwrite this factory method for Redis connections if you want to use a
|
||||
# different Redis library than the redis gem. This is needed, for example, when
|
||||
# using Makara proxies for distributed Redis.
|
||||
cattr_accessor :redis_connector, default: ->(config) do
|
||||
::Redis.new(config.except(:adapter, :channel_prefix))
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
class SubscriberMap
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
module SubscriptionAdapter
|
||||
# == \Test adapter for Action Cable
|
||||
# ## Test adapter for Action Cable
|
||||
#
|
||||
# The test adapter should be used only in testing. Along with
|
||||
# ActionCable::TestHelper it makes a great tool to test your \Rails application.
|
||||
# ActionCable::TestHelper it makes a great tool to test your Rails application.
|
||||
#
|
||||
# To use the test adapter set +adapter+ value to +test+ in your +config/cable.yml+ file.
|
||||
# To use the test adapter set `adapter` value to `test` in your
|
||||
# `config/cable.yml` file.
|
||||
#
|
||||
# NOTE: +Test+ adapter extends the +ActionCable::SubscriptionAdapter::Async+ adapter,
|
||||
# so it could be used in system tests too.
|
||||
# NOTE: `Test` adapter extends the `ActionCable::SubscriptionAdapter::Async`
|
||||
# adapter, so it could be used in system tests too.
|
||||
class Test < Async
|
||||
def broadcast(channel, payload)
|
||||
broadcasts(channel) << payload
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require "active_support/test_case"
|
||||
|
||||
module ActionCable
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module ActionCable
|
||||
# Provides helper methods for testing Action Cable broadcasting
|
||||
module TestHelper
|
||||
|
@ -18,33 +20,34 @@ module ActionCable
|
|||
ActionCable.server.instance_variable_set(:@pubsub, @old_pubsub_adapter)
|
||||
end
|
||||
|
||||
# Asserts that the number of broadcasted messages to the stream matches the given number.
|
||||
# Asserts that the number of broadcasted messages to the stream matches the
|
||||
# given number.
|
||||
#
|
||||
# def test_broadcasts
|
||||
# assert_broadcasts 'messages', 0
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
||||
# assert_broadcasts 'messages', 1
|
||||
# ActionCable.server.broadcast 'messages', { text: 'world' }
|
||||
# assert_broadcasts 'messages', 2
|
||||
# end
|
||||
#
|
||||
# If a block is passed, that block should cause the specified number of
|
||||
# messages to be broadcasted. It returns the messages that were broadcasted.
|
||||
#
|
||||
# def test_broadcasts_again
|
||||
# message = assert_broadcasts('messages', 1) do
|
||||
# def test_broadcasts
|
||||
# assert_broadcasts 'messages', 0
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
||||
# assert_broadcasts 'messages', 1
|
||||
# ActionCable.server.broadcast 'messages', { text: 'world' }
|
||||
# assert_broadcasts 'messages', 2
|
||||
# end
|
||||
# assert_equal({ text: 'hello' }, message)
|
||||
#
|
||||
# messages = assert_broadcasts('messages', 2) do
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
||||
# If a block is passed, that block should cause the specified number of messages
|
||||
# to be broadcasted. It returns the messages that were broadcasted.
|
||||
#
|
||||
# def test_broadcasts_again
|
||||
# message = assert_broadcasts('messages', 1) do
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hello' }
|
||||
# end
|
||||
# assert_equal({ text: 'hello' }, message)
|
||||
#
|
||||
# messages = assert_broadcasts('messages', 2) do
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
||||
# end
|
||||
# assert_equal 2, messages.length
|
||||
# assert_equal({ text: 'hi' }, messages.first)
|
||||
# assert_equal({ text: 'how are you?' }, messages.last)
|
||||
# end
|
||||
# assert_equal 2, messages.length
|
||||
# assert_equal({ text: 'hi' }, messages.first)
|
||||
# assert_equal({ text: 'how are you?' }, messages.last)
|
||||
# end
|
||||
#
|
||||
def assert_broadcasts(stream, number, &block)
|
||||
if block_given?
|
||||
|
@ -60,23 +63,23 @@ module ActionCable
|
|||
|
||||
# Asserts that no messages have been sent to the stream.
|
||||
#
|
||||
# def test_no_broadcasts
|
||||
# assert_no_broadcasts 'messages'
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# assert_broadcasts 'messages', 1
|
||||
# end
|
||||
# def test_no_broadcasts
|
||||
# assert_no_broadcasts 'messages'
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# assert_broadcasts 'messages', 1
|
||||
# end
|
||||
#
|
||||
# If a block is passed, that block should not cause any message to be sent.
|
||||
#
|
||||
# def test_broadcasts_again
|
||||
# assert_no_broadcasts 'messages' do
|
||||
# # No job messages should be sent from this block
|
||||
# def test_broadcasts_again
|
||||
# assert_no_broadcasts 'messages' do
|
||||
# # No job messages should be sent from this block
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Note: This assertion is simply a shortcut for:
|
||||
#
|
||||
# assert_broadcasts 'messages', 0, &block
|
||||
# assert_broadcasts 'messages', 0, &block
|
||||
#
|
||||
def assert_no_broadcasts(stream, &block)
|
||||
assert_broadcasts stream, 0, &block
|
||||
|
@ -84,15 +87,15 @@ module ActionCable
|
|||
|
||||
# Returns the messages that are broadcasted in the block.
|
||||
#
|
||||
# def test_broadcasts
|
||||
# messages = capture_broadcasts('messages') do
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
||||
# def test_broadcasts
|
||||
# messages = capture_broadcasts('messages') do
|
||||
# ActionCable.server.broadcast 'messages', { text: 'hi' }
|
||||
# ActionCable.server.broadcast 'messages', { text: 'how are you?' }
|
||||
# end
|
||||
# assert_equal 2, messages.length
|
||||
# assert_equal({ text: 'hi' }, messages.first)
|
||||
# assert_equal({ text: 'how are you?' }, messages.last)
|
||||
# end
|
||||
# assert_equal 2, messages.length
|
||||
# assert_equal({ text: 'hi' }, messages.first)
|
||||
# assert_equal({ text: 'how are you?' }, messages.last)
|
||||
# end
|
||||
#
|
||||
def capture_broadcasts(stream, &block)
|
||||
new_broadcasts_from(broadcasts(stream), stream, "capture_broadcasts", &block).map { |m| ActiveSupport::JSON.decode(m) }
|
||||
|
@ -100,23 +103,23 @@ module ActionCable
|
|||
|
||||
# Asserts that the specified message has been sent to the stream.
|
||||
#
|
||||
# def test_assert_transmitted_message
|
||||
# ActionCable.server.broadcast 'messages', text: 'hello'
|
||||
# assert_broadcast_on('messages', text: 'hello')
|
||||
# end
|
||||
#
|
||||
# If a block is passed, that block should cause a message with the specified data to be sent.
|
||||
#
|
||||
# def test_assert_broadcast_on_again
|
||||
# assert_broadcast_on('messages', text: 'hello') do
|
||||
# def test_assert_transmitted_message
|
||||
# ActionCable.server.broadcast 'messages', text: 'hello'
|
||||
# assert_broadcast_on('messages', text: 'hello')
|
||||
# end
|
||||
#
|
||||
# If a block is passed, that block should cause a message with the specified
|
||||
# data to be sent.
|
||||
#
|
||||
# def test_assert_broadcast_on_again
|
||||
# assert_broadcast_on('messages', text: 'hello') do
|
||||
# ActionCable.server.broadcast 'messages', text: 'hello'
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
def assert_broadcast_on(stream, data, &block)
|
||||
# Encode to JSON and back–we want to use this value to compare
|
||||
# with decoded JSON.
|
||||
# Comparing JSON strings doesn't work due to the order if the keys.
|
||||
# Encode to JSON and back–we want to use this value to compare with decoded
|
||||
# JSON. Comparing JSON strings doesn't work due to the order if the keys.
|
||||
serialized_msg =
|
||||
ActiveSupport::JSON.decode(ActiveSupport::JSON.encode(data))
|
||||
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
require_relative "gem_version"
|
||||
|
||||
module ActionCable
|
||||
# Returns the currently loaded version of Action Cable as a +Gem::Version+.
|
||||
# Returns the currently loaded version of Action Cable as a `Gem::Version`.
|
||||
def self.version
|
||||
gem_version
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
class ChannelGenerator < NamedBase
|
||||
|
@ -103,8 +105,8 @@ pin_all_from "app/javascript/channels", under: "channels"
|
|||
end
|
||||
|
||||
def using_bun?
|
||||
# Cannot assume bun.lockb has been generated yet so we look for
|
||||
# a file known to be generated by the jsbundling-rails gem
|
||||
# Cannot assume bun.lockb has been generated yet so we look for a file known to
|
||||
# be generated by the jsbundling-rails gem
|
||||
@using_bun ||= using_js_runtime? && root.join("bun.config.js").exist?
|
||||
end
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# :markup: markdown
|
||||
|
||||
module ApplicationCable
|
||||
class Channel < ActionCable::Channel::Base
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# :markup: markdown
|
||||
|
||||
module ApplicationCable
|
||||
class Connection < ActionCable::Connection::Base
|
||||
end
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# :markup: markdown
|
||||
|
||||
module TestUnit
|
||||
module Generators
|
||||
class ChannelGenerator < ::Rails::Generators::NamedBase
|
||||
|
|
Loading…
Reference in New Issue