actioncable 0.0.0 → 5.0.0.beta1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/MIT-LICENSE +20 -0
- data/README.md +439 -21
- data/lib/action_cable.rb +47 -2
- data/lib/action_cable/channel.rb +14 -0
- data/lib/action_cable/channel/base.rb +277 -0
- data/lib/action_cable/channel/broadcasting.rb +29 -0
- data/lib/action_cable/channel/callbacks.rb +35 -0
- data/lib/action_cable/channel/naming.rb +22 -0
- data/lib/action_cable/channel/periodic_timers.rb +41 -0
- data/lib/action_cable/channel/streams.rb +114 -0
- data/lib/action_cable/connection.rb +16 -0
- data/lib/action_cable/connection/authorization.rb +13 -0
- data/lib/action_cable/connection/base.rb +221 -0
- data/lib/action_cable/connection/identification.rb +46 -0
- data/lib/action_cable/connection/internal_channel.rb +45 -0
- data/lib/action_cable/connection/message_buffer.rb +54 -0
- data/lib/action_cable/connection/subscriptions.rb +76 -0
- data/lib/action_cable/connection/tagged_logger_proxy.rb +40 -0
- data/lib/action_cable/connection/web_socket.rb +29 -0
- data/lib/action_cable/engine.rb +38 -0
- data/lib/action_cable/gem_version.rb +15 -0
- data/lib/action_cable/helpers/action_cable_helper.rb +29 -0
- data/lib/action_cable/process/logging.rb +10 -0
- data/lib/action_cable/remote_connections.rb +64 -0
- data/lib/action_cable/server.rb +19 -0
- data/lib/action_cable/server/base.rb +77 -0
- data/lib/action_cable/server/broadcasting.rb +54 -0
- data/lib/action_cable/server/configuration.rb +35 -0
- data/lib/action_cable/server/connections.rb +37 -0
- data/lib/action_cable/server/worker.rb +42 -0
- data/lib/action_cable/server/worker/active_record_connection_management.rb +22 -0
- data/lib/action_cable/version.rb +6 -1
- data/lib/assets/javascripts/action_cable.coffee.erb +23 -0
- data/lib/assets/javascripts/action_cable/connection.coffee +84 -0
- data/lib/assets/javascripts/action_cable/connection_monitor.coffee +84 -0
- data/lib/assets/javascripts/action_cable/consumer.coffee +31 -0
- data/lib/assets/javascripts/action_cable/subscription.coffee +68 -0
- data/lib/assets/javascripts/action_cable/subscriptions.coffee +78 -0
- data/lib/rails/generators/channel/USAGE +14 -0
- data/lib/rails/generators/channel/channel_generator.rb +21 -0
- data/lib/rails/generators/channel/templates/assets/channel.coffee +14 -0
- data/lib/rails/generators/channel/templates/channel.rb +17 -0
- metadata +161 -26
- data/.gitignore +0 -9
- data/Gemfile +0 -4
- data/LICENSE.txt +0 -21
- data/Rakefile +0 -2
- data/actioncable.gemspec +0 -22
- data/bin/console +0 -14
- data/bin/setup +0 -7
@@ -0,0 +1,54 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Connection
|
3
|
+
# Allows us to buffer messages received from the WebSocket before the Connection has been fully initialized and is ready to receive them.
|
4
|
+
# Entirely internal operation and should not be used directly by the user.
|
5
|
+
class MessageBuffer
|
6
|
+
def initialize(connection)
|
7
|
+
@connection = connection
|
8
|
+
@buffered_messages = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def append(message)
|
12
|
+
if valid? message
|
13
|
+
if processing?
|
14
|
+
receive message
|
15
|
+
else
|
16
|
+
buffer message
|
17
|
+
end
|
18
|
+
else
|
19
|
+
connection.logger.error "Couldn't handle non-string message: #{message.class}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def processing?
|
24
|
+
@processing
|
25
|
+
end
|
26
|
+
|
27
|
+
def process!
|
28
|
+
@processing = true
|
29
|
+
receive_buffered_messages
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
attr_reader :connection
|
34
|
+
attr_accessor :buffered_messages
|
35
|
+
|
36
|
+
private
|
37
|
+
def valid?(message)
|
38
|
+
message.is_a?(String)
|
39
|
+
end
|
40
|
+
|
41
|
+
def receive(message)
|
42
|
+
connection.send_async :receive, message
|
43
|
+
end
|
44
|
+
|
45
|
+
def buffer(message)
|
46
|
+
buffered_messages << message
|
47
|
+
end
|
48
|
+
|
49
|
+
def receive_buffered_messages
|
50
|
+
receive buffered_messages.shift until buffered_messages.empty?
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
# Collection class for all the channel subscriptions established on a given connection. Responsible for routing incoming commands that arrive on
|
6
|
+
# the connection to the proper channel. Should not be used directly by the user.
|
7
|
+
class Subscriptions
|
8
|
+
def initialize(connection)
|
9
|
+
@connection = connection
|
10
|
+
@subscriptions = {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute_command(data)
|
14
|
+
case data['command']
|
15
|
+
when 'subscribe' then add data
|
16
|
+
when 'unsubscribe' then remove data
|
17
|
+
when 'message' then perform_action data
|
18
|
+
else
|
19
|
+
logger.error "Received unrecognized command in #{data.inspect}"
|
20
|
+
end
|
21
|
+
rescue Exception => e
|
22
|
+
logger.error "Could not execute command from #{data.inspect}) [#{e.class} - #{e.message}]: #{e.backtrace.first(5).join(" | ")}"
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(data)
|
26
|
+
id_key = data['identifier']
|
27
|
+
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access
|
28
|
+
|
29
|
+
subscription_klass = connection.server.channel_classes[id_options[:channel]]
|
30
|
+
|
31
|
+
if subscription_klass
|
32
|
+
subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options)
|
33
|
+
else
|
34
|
+
logger.error "Subscription class not found (#{data.inspect})"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove(data)
|
39
|
+
logger.info "Unsubscribing from channel: #{data['identifier']}"
|
40
|
+
remove_subscription subscriptions[data['identifier']]
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove_subscription(subscription)
|
44
|
+
subscription.unsubscribe_from_channel
|
45
|
+
subscriptions.delete(subscription.identifier)
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform_action(data)
|
49
|
+
find(data).perform_action ActiveSupport::JSON.decode(data['data'])
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
def identifiers
|
54
|
+
subscriptions.keys
|
55
|
+
end
|
56
|
+
|
57
|
+
def unsubscribe_from_all
|
58
|
+
subscriptions.each { |id, channel| channel.unsubscribe_from_channel }
|
59
|
+
end
|
60
|
+
|
61
|
+
protected
|
62
|
+
attr_reader :connection, :subscriptions
|
63
|
+
|
64
|
+
private
|
65
|
+
delegate :logger, to: :connection
|
66
|
+
|
67
|
+
def find(data)
|
68
|
+
if subscription = subscriptions[data['identifier']]
|
69
|
+
subscription
|
70
|
+
else
|
71
|
+
raise "Unable to find subscription with identifier: #{data['identifier']}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Connection
|
3
|
+
# Allows the use of per-connection tags against the server logger. This wouldn't work using the traditional
|
4
|
+
# <tt>ActiveSupport::TaggedLogging</tt> enhanced Rails.logger, as that logger will reset the tags between requests.
|
5
|
+
# The connection is long-lived, so it needs its own set of tags for its independent duration.
|
6
|
+
class TaggedLoggerProxy
|
7
|
+
attr_reader :tags
|
8
|
+
|
9
|
+
def initialize(logger, tags:)
|
10
|
+
@logger = logger
|
11
|
+
@tags = tags.flatten
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_tags(*tags)
|
15
|
+
@tags += tags.flatten
|
16
|
+
@tags = @tags.uniq
|
17
|
+
end
|
18
|
+
|
19
|
+
def tag(logger)
|
20
|
+
if logger.respond_to?(:tagged)
|
21
|
+
current_tags = tags - logger.formatter.current_tags
|
22
|
+
logger.tagged(*current_tags) { yield }
|
23
|
+
else
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
%i( debug info warn error fatal unknown ).each do |severity|
|
29
|
+
define_method(severity) do |message|
|
30
|
+
log severity, message
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
protected
|
35
|
+
def log(type, message)
|
36
|
+
tag(@logger) { @logger.send type, message }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'faye/websocket'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
# Decorate the Faye::WebSocket with helpers we need.
|
6
|
+
class WebSocket
|
7
|
+
delegate :rack_response, :close, :on, to: :websocket
|
8
|
+
|
9
|
+
def initialize(env)
|
10
|
+
@websocket = Faye::WebSocket.websocket?(env) ? Faye::WebSocket.new(env) : nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def possible?
|
14
|
+
websocket
|
15
|
+
end
|
16
|
+
|
17
|
+
def alive?
|
18
|
+
websocket && websocket.ready_state == Faye::WebSocket::API::OPEN
|
19
|
+
end
|
20
|
+
|
21
|
+
def transmit(data)
|
22
|
+
websocket.send data
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
attr_reader :websocket
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "rails"
|
2
|
+
require "action_cable"
|
3
|
+
require "action_cable/helpers/action_cable_helper"
|
4
|
+
require "active_support/core_ext/hash/indifferent_access"
|
5
|
+
|
6
|
+
module ActionCable
|
7
|
+
class Railtie < Rails::Engine # :nodoc:
|
8
|
+
config.action_cable = ActiveSupport::OrderedOptions.new
|
9
|
+
config.action_cable.url = '/cable'
|
10
|
+
|
11
|
+
config.eager_load_namespaces << ActionCable
|
12
|
+
|
13
|
+
initializer "action_cable.helpers" do
|
14
|
+
ActiveSupport.on_load(:action_view) do
|
15
|
+
include ActionCable::Helpers::ActionCableHelper
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
initializer "action_cable.logger" do
|
20
|
+
ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger }
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "action_cable.set_configs" do |app|
|
24
|
+
options = app.config.action_cable
|
25
|
+
options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
|
26
|
+
|
27
|
+
app.paths.add "config/redis/cable", with: "config/redis/cable.yml"
|
28
|
+
|
29
|
+
ActiveSupport.on_load(:action_cable) do
|
30
|
+
if (redis_cable_path = Pathname.new(app.config.paths["config/redis/cable"].first)).exist?
|
31
|
+
self.redis = Rails.application.config_for(redis_cable_path).with_indifferent_access
|
32
|
+
end
|
33
|
+
|
34
|
+
options.each { |k,v| send("#{k}=", v) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ActionCable
|
2
|
+
# Returns the version of the currently loaded Action Cable as a <tt>Gem::Version</tt>.
|
3
|
+
def self.gem_version
|
4
|
+
Gem::Version.new VERSION::STRING
|
5
|
+
end
|
6
|
+
|
7
|
+
module VERSION
|
8
|
+
MAJOR = 5
|
9
|
+
MINOR = 0
|
10
|
+
TINY = 0
|
11
|
+
PRE = "beta1"
|
12
|
+
|
13
|
+
STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Helpers
|
3
|
+
module ActionCableHelper
|
4
|
+
# Returns an "action-cable-url" meta tag with the value of the url specified in your
|
5
|
+
# configuration. Ensure this is above your javascript tag:
|
6
|
+
#
|
7
|
+
# <head>
|
8
|
+
# <%= action_cable_meta_tag %>
|
9
|
+
# <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
|
10
|
+
# </head>
|
11
|
+
#
|
12
|
+
# This is then used by ActionCable to determine the url of your websocket server.
|
13
|
+
# Your CoffeeScript can then connect to the server without needing to specify the
|
14
|
+
# url directly:
|
15
|
+
#
|
16
|
+
# #= require cable
|
17
|
+
# @App = {}
|
18
|
+
# App.cable = Cable.createConsumer()
|
19
|
+
#
|
20
|
+
# Make sure to specify the correct server location in each of your environments
|
21
|
+
# config file:
|
22
|
+
#
|
23
|
+
# config.action_cable.url = "ws://example.com:28080"
|
24
|
+
def action_cable_meta_tag
|
25
|
+
tag "meta", name: "action-cable-url", content: Rails.application.config.action_cable.url
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActionCable
|
2
|
+
# If you need to disconnect a given connection, you go through the RemoteConnections. You find the connections you're looking for by
|
3
|
+
# searching the identifier declared on the connection. Example:
|
4
|
+
#
|
5
|
+
# module ApplicationCable
|
6
|
+
# class Connection < ActionCable::Connection::Base
|
7
|
+
# identified_by :current_user
|
8
|
+
# ....
|
9
|
+
# end
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# ActionCable.server.remote_connections.where(current_user: User.find(1)).disconnect
|
13
|
+
#
|
14
|
+
# That will disconnect all the connections established for User.find(1) across all servers running on all machines (because it uses
|
15
|
+
# the internal channel that all these servers are subscribed to).
|
16
|
+
class RemoteConnections
|
17
|
+
attr_reader :server
|
18
|
+
|
19
|
+
def initialize(server)
|
20
|
+
@server = server
|
21
|
+
end
|
22
|
+
|
23
|
+
def where(identifier)
|
24
|
+
RemoteConnection.new(server, identifier)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
# Represents a single remote connection found via ActionCable.server.remote_connections.where(*).
|
29
|
+
# Exists for the solely for the purpose of calling #disconnect on that connection.
|
30
|
+
class RemoteConnection
|
31
|
+
class InvalidIdentifiersError < StandardError; end
|
32
|
+
|
33
|
+
include Connection::Identification, Connection::InternalChannel
|
34
|
+
|
35
|
+
def initialize(server, ids)
|
36
|
+
@server = server
|
37
|
+
set_identifier_instance_vars(ids)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Uses the internal channel to disconnect the connection.
|
41
|
+
def disconnect
|
42
|
+
server.broadcast internal_redis_channel, type: 'disconnect'
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns all the identifiers that were applied to this connection.
|
46
|
+
def identifiers
|
47
|
+
server.connection_identifiers
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
attr_reader :server
|
52
|
+
|
53
|
+
def set_identifier_instance_vars(ids)
|
54
|
+
raise InvalidIdentifiersError unless valid_identifiers?(ids)
|
55
|
+
ids.each { |k,v| instance_variable_set("@#{k}", v) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def valid_identifiers?(ids)
|
59
|
+
keys = ids.keys
|
60
|
+
identifiers.all? { |id| keys.include?(id) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'eventmachine'
|
2
|
+
EventMachine.epoll if EventMachine.epoll?
|
3
|
+
EventMachine.kqueue if EventMachine.kqueue?
|
4
|
+
|
5
|
+
module ActionCable
|
6
|
+
module Server
|
7
|
+
extend ActiveSupport::Autoload
|
8
|
+
|
9
|
+
eager_autoload do
|
10
|
+
autoload :Base
|
11
|
+
autoload :Broadcasting
|
12
|
+
autoload :Connections
|
13
|
+
autoload :Configuration
|
14
|
+
|
15
|
+
autoload :Worker
|
16
|
+
autoload :ActiveRecordConnectionManagement, 'action_cable/server/worker/active_record_connection_management'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# FIXME: Cargo culted fix from https://github.com/celluloid/celluloid-pool/issues/10
|
2
|
+
require 'celluloid/current'
|
3
|
+
|
4
|
+
require 'em-hiredis'
|
5
|
+
|
6
|
+
module ActionCable
|
7
|
+
module Server
|
8
|
+
# A singleton ActionCable::Server instance is available via ActionCable.server. It's used by the rack process that starts the cable server, but
|
9
|
+
# also by the user to reach the RemoteConnections instead for finding and disconnecting connections across all servers.
|
10
|
+
#
|
11
|
+
# Also, this is the server instance used for broadcasting. See Broadcasting for details.
|
12
|
+
class Base
|
13
|
+
include ActionCable::Server::Broadcasting
|
14
|
+
include ActionCable::Server::Connections
|
15
|
+
|
16
|
+
cattr_accessor(:config, instance_accessor: true) { ActionCable::Server::Configuration.new }
|
17
|
+
|
18
|
+
def self.logger; config.logger; end
|
19
|
+
delegate :logger, to: :config
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
end
|
23
|
+
|
24
|
+
# Called by rack to setup the server.
|
25
|
+
def call(env)
|
26
|
+
setup_heartbeat_timer
|
27
|
+
config.connection_class.new(self, env).process
|
28
|
+
end
|
29
|
+
|
30
|
+
# Disconnect all the connections identified by `identifiers` on this server or any others via RemoteConnections.
|
31
|
+
def disconnect(identifiers)
|
32
|
+
remote_connections.where(identifiers).disconnect
|
33
|
+
end
|
34
|
+
|
35
|
+
# Gateway to RemoteConnections. See that class for details.
|
36
|
+
def remote_connections
|
37
|
+
@remote_connections ||= RemoteConnections.new(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
|
41
|
+
def worker_pool
|
42
|
+
@worker_pool ||= ActionCable::Server::Worker.pool(size: config.worker_pool_size)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Requires and returns a hash of all the channel class constants keyed by name.
|
46
|
+
def channel_classes
|
47
|
+
@channel_classes ||= begin
|
48
|
+
config.channel_paths.each { |channel_path| require channel_path }
|
49
|
+
config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# The redis pubsub adapter used for all streams/broadcasting.
|
54
|
+
def pubsub
|
55
|
+
@pubsub ||= redis.pubsub
|
56
|
+
end
|
57
|
+
|
58
|
+
# The EventMachine Redis instance used by the pubsub adapter.
|
59
|
+
def redis
|
60
|
+
@redis ||= EM::Hiredis.connect(config.redis[:url]).tap do |redis|
|
61
|
+
redis.on(:reconnect_failed) do
|
62
|
+
logger.info "[ActionCable] Redis reconnect failed."
|
63
|
+
# logger.info "[ActionCable] Redis reconnected. Closing all the open connections."
|
64
|
+
# @connections.map &:close
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# All the identifiers applied to the connection class associated with this server.
|
70
|
+
def connection_identifiers
|
71
|
+
config.connection_class.identifiers
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
ActiveSupport.run_load_hooks(:action_cable, Base.config)
|
76
|
+
end
|
77
|
+
end
|