diff options
-rw-r--r-- | .travis.yml | 19 | ||||
-rw-r--r-- | Gemfile.lock | 2 | ||||
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | actioncable.gemspec | 5 | ||||
-rw-r--r-- | lib/action_cable.rb | 3 | ||||
-rw-r--r-- | lib/action_cable/channel.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/channel/base.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/channel/broadcasting.rb | 29 | ||||
-rw-r--r-- | lib/action_cable/channel/naming.rb | 22 | ||||
-rw-r--r-- | lib/action_cable/channel/streams.rb | 41 | ||||
-rw-r--r-- | lib/action_cable/connection/base.rb | 12 | ||||
-rw-r--r-- | lib/action_cable/connection/heartbeat.rb | 6 | ||||
-rw-r--r-- | lib/action_cable/connection/identification.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/connection/subscriptions.rb | 4 | ||||
-rw-r--r-- | lib/action_cable/remote_connections.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/server/base.rb | 8 | ||||
-rw-r--r-- | lib/action_cable/server/broadcasting.rb | 2 | ||||
-rw-r--r-- | lib/action_cable/version.rb | 3 | ||||
-rw-r--r-- | test/channel/broadcasting_test.rb | 29 | ||||
-rw-r--r-- | test/channel/naming_test.rb | 10 | ||||
-rw-r--r-- | test/channel/stream_test.rb | 14 | ||||
-rw-r--r-- | test/connection/subscriptions_test.rb | 2 | ||||
-rw-r--r-- | test/stubs/room.rb | 4 |
23 files changed, 187 insertions, 37 deletions
diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000000..99a95ae240 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +sudo: false +cache: bundler +rvm: + - 2.2 + - ruby-head +matrix: + fast_finish: true +notifications: + email: false + irc: + on_success: change + on_failure: always + channels: + - "irc.freenode.org#rails-contrib" + campfire: + on_success: change + on_failure: always + rooms: + - secure: "EZmqsgjEQbWouCx6xL/30jslug7xcq+Dl09twDGjBs369GB5LiUm17/I7d6H1YQFY0Vu2LpiQ/zs+6ihlBjslRV/2RYM3AgAA9OOC3pn7uENFVTXaECi/io1wjvlbMNrf1YJSc3aUyiWKykRsdZnZSFszkDs4DMnZG1s/Oxf1JTYEGNWW3WcOFfYkzcS7NWlOW9OBf4RuzjtLYF05IO4t4FZI1aTWrNV3NNMZ+tqmiQHHNrQE/CzQE3ujqFiea2vVZ7PwvmjVWJgC29UZqS7HcNuq6cCMtMZZuubCZmyT85GjJ/SKTShxFqfV1oCpY3y6kyWcTAQsUoLtPEX0OxLeX+CgWNIJK0rY5+5/v5pZP1uwRsMfLerfp2a9g4fAnlcAKaZjalOc39rOkJl8FdvLQtqFIGWxpjWdJbMrCt3SrnnOccpDqDWpAL798LVBONcOuor71rEeNj1dZ6fCoHTKhLVy6UVm9eUI8zt1APM0xzHgTBI1KBVZi0ikqPcaW604rrNUSk8g/AFQk0pIKyDzV9qYMJD2wnr42cyPKg0gfk1tc9KRCNeH+My1HdZS6Zogpjkc3plAzJQ1DAPY0EBWUlEKghpkyCunjpxN3cw390iKgZUN52phtmGMRkyNnwI8+ELnT4I+Jata1mFyWiETM85q8Rqx+FeA0W/BBsEAp8="
\ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 0299c50f9f..5548531abe 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - actioncable (0.1.0) + actioncable (0.0.3) actionpack (>= 4.2.0) activesupport (>= 4.2.0) celluloid (~> 0.16.0) @@ -1,4 +1,5 @@ # Action Cable – Integrated websockets for Rails +[![Build Status](https://travis-ci.org/rails/actioncable.svg)](https://travis-ci.org/rails/actioncable) Action Cable seamlessly integrates websockets with the rest of your Rails application. It allows for real-time features to be written in Ruby in the same style diff --git a/actioncable.gemspec b/actioncable.gemspec index e3aaa21fe7..02350186db 100644 --- a/actioncable.gemspec +++ b/actioncable.gemspec @@ -1,6 +1,9 @@ +$:.push File.expand_path("../lib", __FILE__) +require 'action_cable/version' + Gem::Specification.new do |s| s.name = 'actioncable' - s.version = '0.1.0' + s.version = ActionCable::VERSION s.summary = 'Websockets framework for Rails.' s.description = 'Structure many real-time application concerns into channels over a single websockets connection.' s.license = 'MIT' diff --git a/lib/action_cable.rb b/lib/action_cable.rb index 968adafc25..13c5c77578 100644 --- a/lib/action_cable.rb +++ b/lib/action_cable.rb @@ -16,10 +16,9 @@ require 'em-hiredis' require 'redis' require 'action_cable/engine' if defined?(Rails) +require 'action_cable/version' module ActionCable - VERSION = '0.0.3' - autoload :Server, 'action_cable/server' autoload :Connection, 'action_cable/connection' autoload :Channel, 'action_cable/channel' diff --git a/lib/action_cable/channel.rb b/lib/action_cable/channel.rb index 9e4d3d3f93..3b973ba0a7 100644 --- a/lib/action_cable/channel.rb +++ b/lib/action_cable/channel.rb @@ -1,7 +1,9 @@ module ActionCable module Channel autoload :Base, 'action_cable/channel/base' + autoload :Broadcasting, 'action_cable/channel/broadcasting' autoload :Callbacks, 'action_cable/channel/callbacks' + autoload :Naming, 'action_cable/channel/naming' autoload :PeriodicTimers, 'action_cable/channel/periodic_timers' autoload :Streams, 'action_cable/channel/streams' end diff --git a/lib/action_cable/channel/base.rb b/lib/action_cable/channel/base.rb index d6bd98d180..2f1b4a187d 100644 --- a/lib/action_cable/channel/base.rb +++ b/lib/action_cable/channel/base.rb @@ -68,6 +68,8 @@ module ActionCable include Callbacks include PeriodicTimers include Streams + include Naming + include Broadcasting on_subscribe :subscribed on_unsubscribe :unsubscribed diff --git a/lib/action_cable/channel/broadcasting.rb b/lib/action_cable/channel/broadcasting.rb new file mode 100644 index 0000000000..afc23d7d1a --- /dev/null +++ b/lib/action_cable/channel/broadcasting.rb @@ -0,0 +1,29 @@ +require 'active_support/core_ext/object/to_param' + +module ActionCable + module Channel + module Broadcasting + extend ActiveSupport::Concern + + delegate :broadcasting_for, to: :class + + class_methods do + # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel. + def broadcast_to(model, message) + ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message) + end + + def broadcasting_for(model) #:nodoc: + case + when model.is_a?(Array) + model.map { |m| broadcasting_for(m) }.join(':') + when model.respond_to?(:to_gid_param) + model.to_gid_param + else + model.to_param + end + end + end + end + end +end diff --git a/lib/action_cable/channel/naming.rb b/lib/action_cable/channel/naming.rb new file mode 100644 index 0000000000..4c9d53b15a --- /dev/null +++ b/lib/action_cable/channel/naming.rb @@ -0,0 +1,22 @@ +module ActionCable + module Channel + module Naming + extend ActiveSupport::Concern + + class_methods do + # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending. + # If the channel is in a namespace, then the namespaces are represented by single + # colon separators in the channel name. + # + # ChatChannel.channel_name # => 'chat' + # Chats::AppearancesChannel.channel_name # => 'chats:appearances' + def channel_name + @channel_name ||= name.sub(/Channel$/, '').gsub('::',':').underscore + end + end + + # Delegates to the class' <tt>channel_name</tt> + delegate :channel_name, to: :class + end + end +end diff --git a/lib/action_cable/channel/streams.rb b/lib/action_cable/channel/streams.rb index 6a3dc76c1d..a37194b884 100644 --- a/lib/action_cable/channel/streams.rb +++ b/lib/action_cable/channel/streams.rb @@ -1,7 +1,7 @@ module ActionCable module Channel - # Streams allow channels to route broadcastings to the subscriber. A broadcasting is an discussed elsewhere a pub/sub queue where any data - # put 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 + # Streams allow channels to route broadcastings to the subscriber. A broadcasting is, as discussed elsewhere, a pub/sub queue where any data + # put 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'll not get that update when connecting later. # # Most commonly, the streamed broadcast is sent straight to the subscriber on the client-side. The channel just acts as a connector between @@ -12,7 +12,7 @@ module ActionCable # def follow(data) # stream_from "comments_for_#{data['recording_id']}" # end - # + # # def unfollow # stop_all_streams # end @@ -23,23 +23,37 @@ module ActionCable # # ActionCable.server.broadcast "comments_for_45", author: 'DHH', content: 'Rails is just swell' # - # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can supply a callback that let's you alter what goes out. + # 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 + # end + # end + # + # You can then broadcast to this channel using: + # + # CommentsChannel.broadcast_to(@post, @comment) + # + # If you don't just want to parlay the broadcast unfiltered to the subscriber, you can supply a callback that lets you alter what goes out. # Example below 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]] - # - # stream_from @room.channel, -> (message) do - # message = ActiveSupport::JSON.decode(m) - # + # + # stream_for @room, -> (encoded_message) do + # message = ActiveSupport::JSON.decode(encoded_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" # end - # + # # transmit message # end # end @@ -63,6 +77,13 @@ module ActionCable logger.info "#{self.class.name} is streaming from #{broadcasting}" end + # Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a + # <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight + # to the subscriber. + def stream_for(model, callback = nil) + stream_from(broadcasting_for([ channel_name, model ]), callback) + end + def stop_all_streams streams.each do |broadcasting, callback| pubsub.unsubscribe_proc broadcasting, callback diff --git a/lib/action_cable/connection/base.rb b/lib/action_cable/connection/base.rb index 5671dc1f88..08a75156a3 100644 --- a/lib/action_cable/connection/base.rb +++ b/lib/action_cable/connection/base.rb @@ -12,7 +12,7 @@ module ActionCable # 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 @@ -21,7 +21,7 @@ module ActionCable # def disconnect # # Any cleanup work needed when the cable connection is cut. # end - # + # # protected # def find_verified_user # if current_user = User.find_by_identity cookies.signed[:identity_id] @@ -37,7 +37,7 @@ module ActionCable # established for that current_user (and potentially disconnect them if the user was removed from an account). You can declare as many # identification indexes as you like. Declaring an identification means that a attr_accessor is automatically set for that key. # - # Second, we rely on the fact that the websocket connection is established with the cookies from that domain being sent along. This makes + # 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 name of the current user to easily distinguish their messages in the log. @@ -75,14 +75,14 @@ module ActionCable websocket.on(:open) { |event| send_async :on_open } websocket.on(:message) { |event| on_message event.data } websocket.on(:close) { |event| send_async :on_close } - + respond_to_successful_request else respond_to_invalid_request end end - # Data received over the cable is handled by this method. It's expected that everything inbound is encoded with JSON. + # Data received over the cable is handled by this method. It's expected that everything inbound is JSON encoded. # The data is routed to the proper channel that the connection has subscribed to. def receive(data_in_json) if websocket.alive? @@ -177,7 +177,7 @@ module ActionCable # 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, + TaggedLoggerProxy.new server.logger, tags: server.config.log_tags.map { |tag| tag.respond_to?(:call) ? tag.call(request) : tag.to_s.camelize } end diff --git a/lib/action_cable/connection/heartbeat.rb b/lib/action_cable/connection/heartbeat.rb index e0f4a97f53..2918938ba5 100644 --- a/lib/action_cable/connection/heartbeat.rb +++ b/lib/action_cable/connection/heartbeat.rb @@ -5,7 +5,7 @@ module ActionCable # disconnect. class Heartbeat BEAT_INTERVAL = 3 - + def initialize(connection) @connection = connection end @@ -21,10 +21,10 @@ module ActionCable private attr_reader :connection - + def beat connection.transmit({ identifier: '_ping', message: Time.now.to_i }.to_json) end end end -end
\ No newline at end of file +end diff --git a/lib/action_cable/connection/identification.rb b/lib/action_cable/connection/identification.rb index 113e41ca3f..1be6f9ac76 100644 --- a/lib/action_cable/connection/identification.rb +++ b/lib/action_cable/connection/identification.rb @@ -16,7 +16,7 @@ module ActionCable # channel instances created off the connection. def identified_by(*identifiers) Array(identifiers).each { |identifier| attr_accessor identifier } - self.identifiers += identifiers + self.identifiers += identifiers end end diff --git a/lib/action_cable/connection/subscriptions.rb b/lib/action_cable/connection/subscriptions.rb index 0411d96413..69e3f60706 100644 --- a/lib/action_cable/connection/subscriptions.rb +++ b/lib/action_cable/connection/subscriptions.rb @@ -24,9 +24,7 @@ module ActionCable id_key = data['identifier'] id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access - subscription_klass = connection.server.channel_classes.detect do |channel_class| - channel_class == id_options[:channel].safe_constantize - end + subscription_klass = connection.server.channel_classes[id_options[:channel]] if subscription_klass subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) diff --git a/lib/action_cable/remote_connections.rb b/lib/action_cable/remote_connections.rb index ae7145891c..1230d905ad 100644 --- a/lib/action_cable/remote_connections.rb +++ b/lib/action_cable/remote_connections.rb @@ -25,7 +25,7 @@ module ActionCable end private - # Represents a single remote connection found via ActionCable.server.remote_connections.where(*). + # Represents a single remote connection found via ActionCable.server.remote_connections.where(*). # Exists for the solely for the purpose of calling #disconnect on that connection. class RemoteConnection class InvalidIdentifiersError < StandardError; end diff --git a/lib/action_cable/server/base.rb b/lib/action_cable/server/base.rb index fe7966e090..43849928b9 100644 --- a/lib/action_cable/server/base.rb +++ b/lib/action_cable/server/base.rb @@ -9,7 +9,7 @@ module ActionCable include ActionCable::Server::Connections cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new } - + def self.logger; config.logger; end delegate :logger, to: :config @@ -36,11 +36,11 @@ module ActionCable @worker_pool ||= ActionCable::Server::Worker.pool(size: config.worker_pool_size) end - # Requires and returns an array of all the channel class constants in this application. + # Requires and returns an hash of all the channel class constants keyed by name. def channel_classes @channel_classes ||= begin config.channel_paths.each { |channel_path| require channel_path } - config.channel_class_names.collect { |name| name.constantize } + config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize } end end @@ -56,7 +56,7 @@ module ActionCable logger.info "[ActionCable] Redis reconnect failed." # logger.info "[ActionCable] Redis reconnected. Closing all the open connections." # @connections.map &:close - end + end end end diff --git a/lib/action_cable/server/broadcasting.rb b/lib/action_cable/server/broadcasting.rb index 4f72ffd96f..037b98951e 100644 --- a/lib/action_cable/server/broadcasting.rb +++ b/lib/action_cable/server/broadcasting.rb @@ -32,7 +32,7 @@ module ActionCable # The redis instance used for broadcasting. Not intended for direct user use. def broadcasting_redis @broadcasting_redis ||= Redis.new(config.redis) - end + end private class Broadcaster diff --git a/lib/action_cable/version.rb b/lib/action_cable/version.rb new file mode 100644 index 0000000000..4947029dcc --- /dev/null +++ b/lib/action_cable/version.rb @@ -0,0 +1,3 @@ +module ActionCable + VERSION = '0.0.3' +end
\ No newline at end of file diff --git a/test/channel/broadcasting_test.rb b/test/channel/broadcasting_test.rb new file mode 100644 index 0000000000..1de04243e5 --- /dev/null +++ b/test/channel/broadcasting_test.rb @@ -0,0 +1,29 @@ +require 'test_helper' +require 'stubs/test_connection' +require 'stubs/room' + +class ActionCable::Channel::BroadcastingTest < ActiveSupport::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + setup do + @connection = TestConnection.new + end + + test "broadcasts_to" do + ActionCable.stubs(:server).returns mock().tap { |m| m.expects(:broadcast).with('action_cable:channel:broadcasting_test:chat:Room#1-Campfire', "Hello World") } + ChatChannel.broadcast_to(Room.new(1), "Hello World") + end + + test "broadcasting_for with an object" do + assert_equal "Room#1-Campfire", ChatChannel.broadcasting_for(Room.new(1)) + end + + test "broadcasting_for with an array" do + assert_equal "Room#1-Campfire:Room#2-Campfire", ChatChannel.broadcasting_for([ Room.new(1), Room.new(2) ]) + end + + test "broadcasting_for with a string" do + assert_equal "hello", ChatChannel.broadcasting_for("hello") + end +end diff --git a/test/channel/naming_test.rb b/test/channel/naming_test.rb new file mode 100644 index 0000000000..89ef6ad8b0 --- /dev/null +++ b/test/channel/naming_test.rb @@ -0,0 +1,10 @@ +require 'test_helper' + +class ActionCable::Channel::NamingTest < ActiveSupport::TestCase + class ChatChannel < ActionCable::Channel::Base + end + + test "channel_name" do + assert_equal "action_cable:channel:naming_test:chat", ChatChannel.channel_name + end +end diff --git a/test/channel/stream_test.rb b/test/channel/stream_test.rb index 1a8c259d11..b0a6f49072 100644 --- a/test/channel/stream_test.rb +++ b/test/channel/stream_test.rb @@ -5,8 +5,10 @@ require 'stubs/room' class ActionCable::Channel::StreamTest < ActiveSupport::TestCase class ChatChannel < ActionCable::Channel::Base def subscribed - @room = Room.new params[:id] - stream_from "test_room_#{@room.id}" + if params[:id] + @room = Room.new params[:id] + stream_from "test_room_#{@room.id}" + end end end @@ -15,10 +17,16 @@ class ActionCable::Channel::StreamTest < ActiveSupport::TestCase end test "streaming start and stop" do - @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe) } + @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("test_room_1") } channel = ChatChannel.new @connection, "{id: 1}", { id: 1 } @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:unsubscribe_proc) } channel.unsubscribe_from_channel end + + test "stream_for" do + @connection.expects(:pubsub).returns mock().tap { |m| m.expects(:subscribe).with("action_cable:channel:stream_test:chat:Room#1-Campfire") } + channel = ChatChannel.new @connection, "" + channel.stream_for Room.new(1) + end end diff --git a/test/connection/subscriptions_test.rb b/test/connection/subscriptions_test.rb index 4e134b6420..24fe8f9300 100644 --- a/test/connection/subscriptions_test.rb +++ b/test/connection/subscriptions_test.rb @@ -20,7 +20,7 @@ class ActionCable::Connection::SubscriptionsTest < ActiveSupport::TestCase setup do @server = TestServer.new - @server.stubs(:channel_classes).returns([ ChatChannel ]) + @server.stubs(:channel_classes).returns(ChatChannel.name => ChatChannel) env = Rack::MockRequest.env_for "/test", 'HTTP_CONNECTION' => 'upgrade', 'HTTP_UPGRADE' => 'websocket' @connection = Connection.new(@server, env) diff --git a/test/stubs/room.rb b/test/stubs/room.rb index 388b5ae33f..246d6a98af 100644 --- a/test/stubs/room.rb +++ b/test/stubs/room.rb @@ -9,4 +9,8 @@ class Room def to_global_id "Room##{id}-#{name}" end + + def to_gid_param + to_global_id.to_param + end end |