diff --git a/actioncable/lib/action_cable/channel/base.rb b/actioncable/lib/action_cable/channel/base.rb index 10809618cc7..7e2f82a797f 100644 --- a/actioncable/lib/action_cable/channel/base.rb +++ b/actioncable/lib/action_cable/channel/base.rb @@ -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 data 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 #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. + # def unsubscribed + # current_user.disappear @connection_token + # end # - # 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. + # 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 - # 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. + # 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 - # * Set - 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)}" diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb index 1ae5d2f0583..1a22e1fb307 100644 --- a/actioncable/lib/action_cable/channel/broadcasting.rb +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -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 model 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 model 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 diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb index 7aee88b11da..5df7bc16943 100644 --- a/actioncable/lib/action_cable/channel/callbacks.rb +++ b/actioncable/lib/action_cable/channel/callbacks.rb @@ -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) diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index 0f2849962f4..9a17fc514bd 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -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 Channel 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 diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index 830b3efa3c8..2c5a5746265 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -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 every: - # 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 # diff --git a/actioncable/lib/action_cable/channel/streams.rb b/actioncable/lib/action_cable/channel/streams.rb index f9d96329795..7ab3ab20d7a 100644 --- a/actioncable/lib/action_cable/channel/streams.rb +++ b/actioncable/lib/action_cable/channel/streams.rb @@ -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, comments_for_45 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 comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE. + # 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 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. + # 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 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. + # 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 coder: ActiveSupport::JSON to decode messages as JSON before passing to the callback. - # Defaults to coder: nil 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 broadcasting. + # 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 model. + # 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 model 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 diff --git a/actioncable/lib/action_cable/channel/test_case.rb b/actioncable/lib/action_cable/channel/test_case.rb index 5ac792f7ccb..d654503b8c9 100644 --- a/actioncable/lib/action_cable/channel/test_case.rb +++ b/actioncable/lib/action_cable/channel/test_case.rb @@ -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: # - # 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. + # **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)) diff --git a/actioncable/lib/action_cable/connection/authorization.rb b/actioncable/lib/action_cable/connection/authorization.rb index ca7c1cf51e7..de996e30517 100644 --- a/actioncable/lib/action_cable/connection/authorization.rb +++ b/actioncable/lib/action_cable/connection/authorization.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/base.rb b/actioncable/lib/action_cable/connection/base.rb index b85daa7d5c5..5ac3bc646b1 100644 --- a/actioncable/lib/action_cable/connection/base.rb +++ b/actioncable/lib/action_cable/connection/base.rb @@ -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 identifier, started_at, subscriptions, and request_id. - # 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 } diff --git a/actioncable/lib/action_cable/connection/callbacks.rb b/actioncable/lib/action_cable/connection/callbacks.rb index a4549dff9a4..85a27c6f9f5 100644 --- a/actioncable/lib/action_cable/connection/callbacks.rb +++ b/actioncable/lib/action_cable/connection/callbacks.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/client_socket.rb b/actioncable/lib/action_cable/connection/client_socket.rb index 4b1964c4ae7..9d8be5e92fe 100644 --- a/actioncable/lib/action_cable/connection/client_socket.rb +++ b/actioncable/lib/action_cable/connection/client_socket.rb @@ -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 } diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index 519f64ba9c0..b358406b754 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -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}") } diff --git a/actioncable/lib/action_cable/connection/internal_channel.rb b/actioncable/lib/action_cable/connection/internal_channel.rb index 9bcfd5cd32a..23933e8660b 100644 --- a/actioncable/lib/action_cable/connection/internal_channel.rb +++ b/actioncable/lib/action_cable/connection/internal_channel.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/message_buffer.rb b/actioncable/lib/action_cable/connection/message_buffer.rb index 965841b67e7..35813930244 100644 --- a/actioncable/lib/action_cable/connection/message_buffer.rb +++ b/actioncable/lib/action_cable/connection/message_buffer.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb index 82eb3a36e2b..01c2d114ec1 100644 --- a/actioncable/lib/action_cable/connection/stream.rb +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Connection #-- diff --git a/actioncable/lib/action_cable/connection/stream_event_loop.rb b/actioncable/lib/action_cable/connection/stream_event_loop.rb index 8fe4718d9a9..f7d44673511 100644 --- a/actioncable/lib/action_cable/connection/stream_event_loop.rb +++ b/actioncable/lib/action_cable/connection/stream_event_loop.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/subscriptions.rb b/actioncable/lib/action_cable/connection/subscriptions.rb index 366e4fd7249..a9b1bca7cbc 100644 --- a/actioncable/lib/action_cable/connection/subscriptions.rb +++ b/actioncable/lib/action_cable/connection/subscriptions.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb index d10778a34f7..b7d97afb09e 100644 --- a/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb +++ b/actioncable/lib/action_cable/connection/tagged_logger_proxy.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/test_case.rb b/actioncable/lib/action_cable/connection/test_case.rb index ee08ce8a3ca..231e61fc03a 100644 --- a/actioncable/lib/action_cable/connection/test_case.rb +++ b/actioncable/lib/action_cable/connection/test_case.rb @@ -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 diff --git a/actioncable/lib/action_cable/connection/web_socket.rb b/actioncable/lib/action_cable/connection/web_socket.rb index c9186f3b791..15fff156d55 100644 --- a/actioncable/lib/action_cable/connection/web_socket.rb +++ b/actioncable/lib/action_cable/connection/web_socket.rb @@ -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: diff --git a/actioncable/lib/action_cable/deprecator.rb b/actioncable/lib/action_cable/deprecator.rb index 15162ff234c..b2e74e8ee8e 100644 --- a/actioncable/lib/action_cable/deprecator.rb +++ b/actioncable/lib/action_cable/deprecator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable def self.deprecator # :nodoc: @deprecator ||= ActiveSupport::Deprecation.new diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index 249c8da56e8..d8a92d28b65 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -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 diff --git a/actioncable/lib/action_cable/gem_version.rb b/actioncable/lib/action_cable/gem_version.rb index f1e674f4da7..81936ff7db4 100644 --- a/actioncable/lib/action_cable/gem_version.rb +++ b/actioncable/lib/action_cable/gem_version.rb @@ -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 diff --git a/actioncable/lib/action_cable/helpers/action_cable_helper.rb b/actioncable/lib/action_cable/helpers/action_cable_helper.rb index 26cddad2b6b..93d21a983ce 100644 --- a/actioncable/lib/action_cable/helpers/action_cable_helper.rb +++ b/actioncable/lib/action_cable/helpers/action_cable_helper.rb @@ -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: # - #
- # <%= action_cable_meta_tag %> - # <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %> - # + # + # <%= action_cable_meta_tag %> + # <%= javascript_include_tag 'application', 'data-turbo-track' => 'reload' %> + # # - # 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: - # => + # config.action_cable.mount_path = "/cable123" + # <%= action_cable_meta_tag %> would render: + # => # - # config.action_cable.url = "ws://actioncable.com" - # <%= action_cable_meta_tag %> would render: - # => + # config.action_cable.url = "ws://actioncable.com" + # <%= action_cable_meta_tag %> would render: + # => # def action_cable_meta_tag tag "meta", name: "action-cable-url", content: ( diff --git a/actioncable/lib/action_cable/remote_connections.rb b/actioncable/lib/action_cable/remote_connections.rb index baee1a815e0..06694acdc25 100644 --- a/actioncable/lib/action_cable/remote_connections.rb +++ b/actioncable/lib/action_cable/remote_connections.rb @@ -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 - # User.find(1), 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 ActionCable.server.remote_connections.where(*). - # 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 diff --git a/actioncable/lib/action_cable/server/base.rb b/actioncable/lib/action_cable/server/base.rb index f71baa5572e..cd150700e23 100644 --- a/actioncable/lib/action_cable/server/base.rb +++ b/actioncable/lib/action_cable/server/base.rb @@ -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 config.action_cable.worker_pool_size. + # 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 diff --git a/actioncable/lib/action_cable/server/broadcasting.rb b/actioncable/lib/action_cable/server/broadcasting.rb index 42685e16daa..34ef50992b2 100644 --- a/actioncable/lib/action_cable/server/broadcasting.rb +++ b/actioncable/lib/action_cable/server/broadcasting.rb @@ -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 broadcasting. 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 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. + # 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 diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index a63b8dcde8b..6210bfcf47b 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -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 diff --git a/actioncable/lib/action_cable/server/connections.rb b/actioncable/lib/action_cable/server/connections.rb index 5ab2de26aa0..e51933b1774 100644 --- a/actioncable/lib/action_cable/server/connections.rb +++ b/actioncable/lib/action_cable/server/connections.rb @@ -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 diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index 686598f4b4a..ac5538b71c9 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -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 diff --git a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb index 63f20fa34dd..2512c500f42 100644 --- a/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb +++ b/actioncable/lib/action_cable/server/worker/active_record_connection_management.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module Server class Worker diff --git a/actioncable/lib/action_cable/subscription_adapter/async.rb b/actioncable/lib/action_cable/subscription_adapter/async.rb index 1647cbfc20a..f78edb9ae7c 100644 --- a/actioncable/lib/action_cable/subscription_adapter/async.rb +++ b/actioncable/lib/action_cable/subscription_adapter/async.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class Async < Inline # :nodoc: diff --git a/actioncable/lib/action_cable/subscription_adapter/base.rb b/actioncable/lib/action_cable/subscription_adapter/base.rb index 4d3d6ee913f..7de9fc2a888 100644 --- a/actioncable/lib/action_cable/subscription_adapter/base.rb +++ b/actioncable/lib/action_cable/subscription_adapter/base.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class Base diff --git a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb index df0aa040f5b..2c3e88f307e 100644 --- a/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb +++ b/actioncable/lib/action_cable/subscription_adapter/channel_prefix.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter module ChannelPrefix # :nodoc: diff --git a/actioncable/lib/action_cable/subscription_adapter/inline.rb b/actioncable/lib/action_cable/subscription_adapter/inline.rb index d2c85c1c8d5..88559fb39c6 100644 --- a/actioncable/lib/action_cable/subscription_adapter/inline.rb +++ b/actioncable/lib/action_cable/subscription_adapter/inline.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class Inline < Base # :nodoc: diff --git a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb index b99a0a96617..2a90e4842e9 100644 --- a/actioncable/lib/action_cable/subscription_adapter/postgresql.rb +++ b/actioncable/lib/action_cable/subscription_adapter/postgresql.rb @@ -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 diff --git a/actioncable/lib/action_cable/subscription_adapter/redis.rb b/actioncable/lib/action_cable/subscription_adapter/redis.rb index d8d36cad219..da58e8652ce 100644 --- a/actioncable/lib/action_cable/subscription_adapter/redis.rb +++ b/actioncable/lib/action_cable/subscription_adapter/redis.rb @@ -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 diff --git a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb index 01cdc2dfa18..d541681d3d4 100644 --- a/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb +++ b/actioncable/lib/action_cable/subscription_adapter/subscriber_map.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module ActionCable module SubscriptionAdapter class SubscriberMap diff --git a/actioncable/lib/action_cable/subscription_adapter/test.rb b/actioncable/lib/action_cable/subscription_adapter/test.rb index d4b9eaa088f..d09018aabba 100644 --- a/actioncable/lib/action_cable/subscription_adapter/test.rb +++ b/actioncable/lib/action_cable/subscription_adapter/test.rb @@ -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 diff --git a/actioncable/lib/action_cable/test_case.rb b/actioncable/lib/action_cable/test_case.rb index d153259bf63..b56f2ead6c3 100644 --- a/actioncable/lib/action_cable/test_case.rb +++ b/actioncable/lib/action_cable/test_case.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + require "active_support/test_case" module ActionCable diff --git a/actioncable/lib/action_cable/test_helper.rb b/actioncable/lib/action_cable/test_helper.rb index 33141826274..35a916704dd 100644 --- a/actioncable/lib/action_cable/test_helper.rb +++ b/actioncable/lib/action_cable/test_helper.rb @@ -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)) diff --git a/actioncable/lib/action_cable/version.rb b/actioncable/lib/action_cable/version.rb index 8fa38e45200..14423f9a6a1 100644 --- a/actioncable/lib/action_cable/version.rb +++ b/actioncable/lib/action_cable/version.rb @@ -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 diff --git a/actioncable/lib/rails/generators/channel/channel_generator.rb b/actioncable/lib/rails/generators/channel/channel_generator.rb index 9f48f15b2ae..c128dd6c8ca 100644 --- a/actioncable/lib/rails/generators/channel/channel_generator.rb +++ b/actioncable/lib/rails/generators/channel/channel_generator.rb @@ -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 diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb index d6726972830..2a175311ed5 100644 --- a/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/channel.rb @@ -1,3 +1,5 @@ +# :markup: markdown + module ApplicationCable class Channel < ActionCable::Channel::Base end diff --git a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb index 0ff5442f476..61c8ba30708 100644 --- a/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb +++ b/actioncable/lib/rails/generators/channel/templates/application_cable/connection.rb @@ -1,3 +1,5 @@ +# :markup: markdown + module ApplicationCable class Connection < ActionCable::Connection::Base end diff --git a/actioncable/lib/rails/generators/test_unit/channel_generator.rb b/actioncable/lib/rails/generators/test_unit/channel_generator.rb index 7d13a12f0a3..6054a3be77a 100644 --- a/actioncable/lib/rails/generators/test_unit/channel_generator.rb +++ b/actioncable/lib/rails/generators/test_unit/channel_generator.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +# :markup: markdown + module TestUnit module Generators class ChannelGenerator < ::Rails::Generators::NamedBase