aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml19
-rw-r--r--Gemfile.lock2
-rw-r--r--README.md1
-rw-r--r--actioncable.gemspec5
-rw-r--r--lib/action_cable.rb3
-rw-r--r--lib/action_cable/channel.rb2
-rw-r--r--lib/action_cable/channel/base.rb2
-rw-r--r--lib/action_cable/channel/broadcasting.rb29
-rw-r--r--lib/action_cable/channel/naming.rb22
-rw-r--r--lib/action_cable/channel/streams.rb41
-rw-r--r--lib/action_cable/connection/base.rb12
-rw-r--r--lib/action_cable/connection/heartbeat.rb6
-rw-r--r--lib/action_cable/connection/identification.rb2
-rw-r--r--lib/action_cable/connection/subscriptions.rb4
-rw-r--r--lib/action_cable/remote_connections.rb2
-rw-r--r--lib/action_cable/server/base.rb8
-rw-r--r--lib/action_cable/server/broadcasting.rb2
-rw-r--r--lib/action_cable/version.rb3
-rw-r--r--test/channel/broadcasting_test.rb29
-rw-r--r--test/channel/naming_test.rb10
-rw-r--r--test/channel/stream_test.rb14
-rw-r--r--test/connection/subscriptions_test.rb2
-rw-r--r--test/stubs/room.rb4
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)
diff --git a/README.md b/README.md
index 5e5e8f645b..fc74a084a9 100644
--- a/README.md
+++ b/README.md
@@ -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