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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +439 -21
  5. data/lib/action_cable.rb +47 -2
  6. data/lib/action_cable/channel.rb +14 -0
  7. data/lib/action_cable/channel/base.rb +277 -0
  8. data/lib/action_cable/channel/broadcasting.rb +29 -0
  9. data/lib/action_cable/channel/callbacks.rb +35 -0
  10. data/lib/action_cable/channel/naming.rb +22 -0
  11. data/lib/action_cable/channel/periodic_timers.rb +41 -0
  12. data/lib/action_cable/channel/streams.rb +114 -0
  13. data/lib/action_cable/connection.rb +16 -0
  14. data/lib/action_cable/connection/authorization.rb +13 -0
  15. data/lib/action_cable/connection/base.rb +221 -0
  16. data/lib/action_cable/connection/identification.rb +46 -0
  17. data/lib/action_cable/connection/internal_channel.rb +45 -0
  18. data/lib/action_cable/connection/message_buffer.rb +54 -0
  19. data/lib/action_cable/connection/subscriptions.rb +76 -0
  20. data/lib/action_cable/connection/tagged_logger_proxy.rb +40 -0
  21. data/lib/action_cable/connection/web_socket.rb +29 -0
  22. data/lib/action_cable/engine.rb +38 -0
  23. data/lib/action_cable/gem_version.rb +15 -0
  24. data/lib/action_cable/helpers/action_cable_helper.rb +29 -0
  25. data/lib/action_cable/process/logging.rb +10 -0
  26. data/lib/action_cable/remote_connections.rb +64 -0
  27. data/lib/action_cable/server.rb +19 -0
  28. data/lib/action_cable/server/base.rb +77 -0
  29. data/lib/action_cable/server/broadcasting.rb +54 -0
  30. data/lib/action_cable/server/configuration.rb +35 -0
  31. data/lib/action_cable/server/connections.rb +37 -0
  32. data/lib/action_cable/server/worker.rb +42 -0
  33. data/lib/action_cable/server/worker/active_record_connection_management.rb +22 -0
  34. data/lib/action_cable/version.rb +6 -1
  35. data/lib/assets/javascripts/action_cable.coffee.erb +23 -0
  36. data/lib/assets/javascripts/action_cable/connection.coffee +84 -0
  37. data/lib/assets/javascripts/action_cable/connection_monitor.coffee +84 -0
  38. data/lib/assets/javascripts/action_cable/consumer.coffee +31 -0
  39. data/lib/assets/javascripts/action_cable/subscription.coffee +68 -0
  40. data/lib/assets/javascripts/action_cable/subscriptions.coffee +78 -0
  41. data/lib/rails/generators/channel/USAGE +14 -0
  42. data/lib/rails/generators/channel/channel_generator.rb +21 -0
  43. data/lib/rails/generators/channel/templates/assets/channel.coffee +14 -0
  44. data/lib/rails/generators/channel/templates/channel.rb +17 -0
  45. metadata +161 -26
  46. data/.gitignore +0 -9
  47. data/Gemfile +0 -4
  48. data/LICENSE.txt +0 -21
  49. data/Rakefile +0 -2
  50. data/actioncable.gemspec +0 -22
  51. data/bin/console +0 -14
  52. 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,10 @@
1
+ require 'action_cable/server'
2
+ require 'eventmachine'
3
+ require 'celluloid'
4
+
5
+ EM.error_handler do |e|
6
+ puts "Error raised inside the event loop: #{e.message}"
7
+ puts e.backtrace.join("\n")
8
+ end
9
+
10
+ Celluloid.logger = ActionCable.server.logger
@@ -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