actioncable 5.0.0.beta1.1 → 5.0.0.beta2
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 +23 -3
- data/MIT-LICENSE +1 -1
- data/README.md +60 -44
- data/lib/action_cable.rb +2 -1
- data/lib/action_cable/channel/base.rb +2 -2
- data/lib/action_cable/channel/periodic_timers.rb +3 -3
- data/lib/action_cable/channel/streams.rb +4 -4
- data/lib/action_cable/connection.rb +4 -1
- data/lib/action_cable/connection/base.rb +22 -21
- data/lib/action_cable/connection/client_socket.rb +150 -0
- data/lib/action_cable/connection/identification.rb +1 -1
- data/lib/action_cable/connection/internal_channel.rb +6 -6
- data/lib/action_cable/connection/stream.rb +59 -0
- data/lib/action_cable/connection/stream_event_loop.rb +94 -0
- data/lib/action_cable/connection/subscriptions.rb +0 -1
- data/lib/action_cable/connection/web_socket.rb +14 -8
- data/lib/action_cable/engine.rb +3 -3
- data/lib/action_cable/gem_version.rb +1 -1
- data/lib/action_cable/remote_connections.rb +1 -1
- data/lib/action_cable/server.rb +0 -4
- data/lib/action_cable/server/base.rb +19 -22
- data/lib/action_cable/server/broadcasting.rb +1 -8
- data/lib/action_cable/server/configuration.rb +25 -5
- data/lib/action_cable/server/connections.rb +3 -5
- data/lib/action_cable/server/worker.rb +42 -13
- data/lib/action_cable/subscription_adapter.rb +8 -0
- data/lib/action_cable/subscription_adapter/async.rb +22 -0
- data/lib/action_cable/subscription_adapter/base.rb +28 -0
- data/lib/action_cable/subscription_adapter/evented_redis.rb +67 -0
- data/lib/action_cable/subscription_adapter/inline.rb +35 -0
- data/lib/action_cable/subscription_adapter/postgresql.rb +106 -0
- data/lib/action_cable/subscription_adapter/redis.rb +163 -0
- data/lib/action_cable/subscription_adapter/subscriber_map.rb +53 -0
- data/lib/assets/compiled/action_cable.js +473 -0
- data/lib/rails/generators/channel/channel_generator.rb +6 -1
- metadata +21 -99
- data/lib/action_cable/process/logging.rb +0 -10
- data/lib/assets/javascripts/action_cable.coffee.erb +0 -23
- data/lib/assets/javascripts/action_cable/connection.coffee +0 -84
- data/lib/assets/javascripts/action_cable/connection_monitor.coffee +0 -84
- data/lib/assets/javascripts/action_cable/consumer.coffee +0 -31
- data/lib/assets/javascripts/action_cable/subscription.coffee +0 -68
- data/lib/assets/javascripts/action_cable/subscriptions.coffee +0 -78
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'websocket/driver'
|
2
|
+
|
3
|
+
module ActionCable
|
4
|
+
module Connection
|
5
|
+
#--
|
6
|
+
# This class is heavily based on faye-websocket-ruby
|
7
|
+
#
|
8
|
+
# Copyright (c) 2010-2015 James Coglan
|
9
|
+
class ClientSocket # :nodoc:
|
10
|
+
def self.determine_url(env)
|
11
|
+
scheme = secure_request?(env) ? 'wss:' : 'ws:'
|
12
|
+
"#{ scheme }//#{ env['HTTP_HOST'] }#{ env['REQUEST_URI'] }"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.secure_request?(env)
|
16
|
+
return true if env['HTTPS'] == 'on'
|
17
|
+
return true if env['HTTP_X_FORWARDED_SSL'] == 'on'
|
18
|
+
return true if env['HTTP_X_FORWARDED_SCHEME'] == 'https'
|
19
|
+
return true if env['HTTP_X_FORWARDED_PROTO'] == 'https'
|
20
|
+
return true if env['rack.url_scheme'] == 'https'
|
21
|
+
|
22
|
+
return false
|
23
|
+
end
|
24
|
+
|
25
|
+
CONNECTING = 0
|
26
|
+
OPEN = 1
|
27
|
+
CLOSING = 2
|
28
|
+
CLOSED = 3
|
29
|
+
|
30
|
+
attr_reader :env, :url
|
31
|
+
|
32
|
+
def initialize(env, event_target, stream_event_loop)
|
33
|
+
@env = env
|
34
|
+
@event_target = event_target
|
35
|
+
@stream_event_loop = stream_event_loop
|
36
|
+
|
37
|
+
@url = ClientSocket.determine_url(@env)
|
38
|
+
|
39
|
+
@driver = @driver_started = nil
|
40
|
+
@close_params = ['', 1006]
|
41
|
+
|
42
|
+
@ready_state = CONNECTING
|
43
|
+
|
44
|
+
# The driver calls +env+, +url+, and +write+
|
45
|
+
@driver = ::WebSocket::Driver.rack(self)
|
46
|
+
|
47
|
+
@driver.on(:open) { |e| open }
|
48
|
+
@driver.on(:message) { |e| receive_message(e.data) }
|
49
|
+
@driver.on(:close) { |e| begin_close(e.reason, e.code) }
|
50
|
+
@driver.on(:error) { |e| emit_error(e.message) }
|
51
|
+
|
52
|
+
@stream = ActionCable::Connection::Stream.new(@stream_event_loop, self)
|
53
|
+
|
54
|
+
if callback = @env['async.callback']
|
55
|
+
callback.call([101, {}, @stream])
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def start_driver
|
60
|
+
return if @driver.nil? || @driver_started
|
61
|
+
@driver_started = true
|
62
|
+
@driver.start
|
63
|
+
end
|
64
|
+
|
65
|
+
def rack_response
|
66
|
+
start_driver
|
67
|
+
[ -1, {}, [] ]
|
68
|
+
end
|
69
|
+
|
70
|
+
def write(data)
|
71
|
+
@stream.write(data)
|
72
|
+
end
|
73
|
+
|
74
|
+
def transmit(message)
|
75
|
+
return false if @ready_state > OPEN
|
76
|
+
case message
|
77
|
+
when Numeric then @driver.text(message.to_s)
|
78
|
+
when String then @driver.text(message)
|
79
|
+
when Array then @driver.binary(message)
|
80
|
+
else false
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def close(code = nil, reason = nil)
|
85
|
+
code ||= 1000
|
86
|
+
reason ||= ''
|
87
|
+
|
88
|
+
unless code == 1000 or (code >= 3000 and code <= 4999)
|
89
|
+
raise ArgumentError, "Failed to execute 'close' on WebSocket: " +
|
90
|
+
"The code must be either 1000, or between 3000 and 4999. " +
|
91
|
+
"#{code} is neither."
|
92
|
+
end
|
93
|
+
|
94
|
+
@ready_state = CLOSING unless @ready_state == CLOSED
|
95
|
+
@driver.close(reason, code)
|
96
|
+
end
|
97
|
+
|
98
|
+
def parse(data)
|
99
|
+
@driver.parse(data)
|
100
|
+
end
|
101
|
+
|
102
|
+
def client_gone
|
103
|
+
finalize_close
|
104
|
+
end
|
105
|
+
|
106
|
+
def alive?
|
107
|
+
@ready_state == OPEN
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def open
|
112
|
+
return unless @ready_state == CONNECTING
|
113
|
+
@ready_state = OPEN
|
114
|
+
|
115
|
+
@event_target.on_open
|
116
|
+
end
|
117
|
+
|
118
|
+
def receive_message(data)
|
119
|
+
return unless @ready_state == OPEN
|
120
|
+
|
121
|
+
@event_target.on_message(data)
|
122
|
+
end
|
123
|
+
|
124
|
+
def emit_error(message)
|
125
|
+
return if @ready_state >= CLOSING
|
126
|
+
|
127
|
+
@event_target.on_error(message)
|
128
|
+
end
|
129
|
+
|
130
|
+
def begin_close(reason, code)
|
131
|
+
return if @ready_state == CLOSED
|
132
|
+
@ready_state = CLOSING
|
133
|
+
@close_params = [reason, code]
|
134
|
+
|
135
|
+
if @stream
|
136
|
+
@stream.shutdown
|
137
|
+
else
|
138
|
+
finalize_close
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def finalize_close
|
143
|
+
return if @ready_state == CLOSED
|
144
|
+
@ready_state = CLOSED
|
145
|
+
|
146
|
+
@event_target.on_close(*@close_params)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -11,7 +11,7 @@ module ActionCable
|
|
11
11
|
end
|
12
12
|
|
13
13
|
class_methods do
|
14
|
-
# Mark a key as being a connection identifier index that can then used to find the specific connection again later.
|
14
|
+
# Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
|
15
15
|
# Common identifiers are current_user and current_account, but could be anything really.
|
16
16
|
#
|
17
17
|
# Note that anything marked as an identifier will automatically create a delegate by the same name on any
|
@@ -5,24 +5,24 @@ module ActionCable
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
private
|
8
|
-
def
|
8
|
+
def internal_channel
|
9
9
|
"action_cable/#{connection_identifier}"
|
10
10
|
end
|
11
11
|
|
12
12
|
def subscribe_to_internal_channel
|
13
13
|
if connection_identifier.present?
|
14
14
|
callback = -> (message) { process_internal_message(message) }
|
15
|
-
@
|
16
|
-
@
|
15
|
+
@_internal_subscriptions ||= []
|
16
|
+
@_internal_subscriptions << [ internal_channel, callback ]
|
17
17
|
|
18
|
-
|
18
|
+
Concurrent.global_io_executor.post { pubsub.subscribe(internal_channel, callback) }
|
19
19
|
logger.info "Registered connection (#{connection_identifier})"
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
23
|
def unsubscribe_from_internal_channel
|
24
|
-
if @
|
25
|
-
@
|
24
|
+
if @_internal_subscriptions.present?
|
25
|
+
@_internal_subscriptions.each { |channel, callback| Concurrent.global_io_executor.post { pubsub.unsubscribe(channel, callback) } }
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ActionCable
|
2
|
+
module Connection
|
3
|
+
#--
|
4
|
+
# This class is heavily based on faye-websocket-ruby
|
5
|
+
#
|
6
|
+
# Copyright (c) 2010-2015 James Coglan
|
7
|
+
class Stream
|
8
|
+
def initialize(event_loop, socket)
|
9
|
+
@event_loop = event_loop
|
10
|
+
@socket_object = socket
|
11
|
+
@stream_send = socket.env['stream.send']
|
12
|
+
|
13
|
+
@rack_hijack_io = nil
|
14
|
+
|
15
|
+
hijack_rack_socket
|
16
|
+
end
|
17
|
+
|
18
|
+
def each(&callback)
|
19
|
+
@stream_send ||= callback
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
shutdown
|
24
|
+
@socket_object.client_gone
|
25
|
+
end
|
26
|
+
|
27
|
+
def shutdown
|
28
|
+
clean_rack_hijack
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(data)
|
32
|
+
return @rack_hijack_io.write(data) if @rack_hijack_io
|
33
|
+
return @stream_send.call(data) if @stream_send
|
34
|
+
rescue EOFError
|
35
|
+
@socket_object.client_gone
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive(data)
|
39
|
+
@socket_object.parse(data)
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def hijack_rack_socket
|
44
|
+
return unless @socket_object.env['rack.hijack']
|
45
|
+
|
46
|
+
@socket_object.env['rack.hijack'].call
|
47
|
+
@rack_hijack_io = @socket_object.env['rack.hijack_io']
|
48
|
+
|
49
|
+
@event_loop.attach(@rack_hijack_io, self)
|
50
|
+
end
|
51
|
+
|
52
|
+
def clean_rack_hijack
|
53
|
+
return unless @rack_hijack_io
|
54
|
+
@event_loop.detach(@rack_hijack_io, self)
|
55
|
+
@rack_hijack_io = nil
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'nio'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module ActionCable
|
5
|
+
module Connection
|
6
|
+
class StreamEventLoop
|
7
|
+
def initialize
|
8
|
+
@nio = @thread = nil
|
9
|
+
@map = {}
|
10
|
+
@stopping = false
|
11
|
+
@todo = Queue.new
|
12
|
+
|
13
|
+
@spawn_mutex = Mutex.new
|
14
|
+
spawn
|
15
|
+
end
|
16
|
+
|
17
|
+
def attach(io, stream)
|
18
|
+
@todo << lambda do
|
19
|
+
@map[io] = stream
|
20
|
+
@nio.register(io, :r)
|
21
|
+
end
|
22
|
+
wakeup
|
23
|
+
end
|
24
|
+
|
25
|
+
def detach(io, stream)
|
26
|
+
@todo << lambda do
|
27
|
+
@nio.deregister io
|
28
|
+
@map.delete io
|
29
|
+
end
|
30
|
+
wakeup
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
@stopping = true
|
35
|
+
wakeup if @nio
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def spawn
|
40
|
+
return if @thread && @thread.status
|
41
|
+
|
42
|
+
@spawn_mutex.synchronize do
|
43
|
+
return if @thread && @thread.status
|
44
|
+
|
45
|
+
@nio ||= NIO::Selector.new
|
46
|
+
@thread = Thread.new { run }
|
47
|
+
|
48
|
+
return true
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def wakeup
|
53
|
+
spawn || @nio.wakeup
|
54
|
+
end
|
55
|
+
|
56
|
+
def run
|
57
|
+
loop do
|
58
|
+
if @stopping
|
59
|
+
@nio.close
|
60
|
+
break
|
61
|
+
end
|
62
|
+
|
63
|
+
until @todo.empty?
|
64
|
+
@todo.pop(true).call
|
65
|
+
end
|
66
|
+
|
67
|
+
next unless monitors = @nio.select
|
68
|
+
|
69
|
+
monitors.each do |monitor|
|
70
|
+
io = monitor.io
|
71
|
+
stream = @map[io]
|
72
|
+
|
73
|
+
begin
|
74
|
+
stream.receive io.read_nonblock(4096)
|
75
|
+
rescue IO::WaitReadable
|
76
|
+
next
|
77
|
+
rescue
|
78
|
+
# We expect one of EOFError or Errno::ECONNRESET in
|
79
|
+
# normal operation (when the client goes away). But if
|
80
|
+
# anything else goes wrong, this is still the best way
|
81
|
+
# to handle it.
|
82
|
+
begin
|
83
|
+
stream.close
|
84
|
+
rescue
|
85
|
+
@nio.deregister io
|
86
|
+
@map.delete io
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -1,13 +1,11 @@
|
|
1
|
-
require '
|
1
|
+
require 'websocket/driver'
|
2
2
|
|
3
3
|
module ActionCable
|
4
4
|
module Connection
|
5
|
-
#
|
5
|
+
# Wrap the real socket to minimize the externally-presented API
|
6
6
|
class WebSocket
|
7
|
-
|
8
|
-
|
9
|
-
def initialize(env)
|
10
|
-
@websocket = Faye::WebSocket.websocket?(env) ? Faye::WebSocket.new(env) : nil
|
7
|
+
def initialize(env, event_target, stream_event_loop)
|
8
|
+
@websocket = ::WebSocket::Driver.websocket?(env) ? ClientSocket.new(env, event_target, stream_event_loop) : nil
|
11
9
|
end
|
12
10
|
|
13
11
|
def possible?
|
@@ -15,11 +13,19 @@ module ActionCable
|
|
15
13
|
end
|
16
14
|
|
17
15
|
def alive?
|
18
|
-
websocket && websocket.
|
16
|
+
websocket && websocket.alive?
|
19
17
|
end
|
20
18
|
|
21
19
|
def transmit(data)
|
22
|
-
websocket.
|
20
|
+
websocket.transmit data
|
21
|
+
end
|
22
|
+
|
23
|
+
def close
|
24
|
+
websocket.close
|
25
|
+
end
|
26
|
+
|
27
|
+
def rack_response
|
28
|
+
websocket.rack_response
|
23
29
|
end
|
24
30
|
|
25
31
|
protected
|
data/lib/action_cable/engine.rb
CHANGED
@@ -24,11 +24,11 @@ module ActionCable
|
|
24
24
|
options = app.config.action_cable
|
25
25
|
options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?
|
26
26
|
|
27
|
-
app.paths.add "config/
|
27
|
+
app.paths.add "config/cable", with: "config/cable.yml"
|
28
28
|
|
29
29
|
ActiveSupport.on_load(:action_cable) do
|
30
|
-
if (
|
31
|
-
self.
|
30
|
+
if (config_path = Pathname.new(app.config.paths["config/cable"].first)).exist?
|
31
|
+
self.cable = Rails.application.config_for(config_path).with_indifferent_access
|
32
32
|
end
|
33
33
|
|
34
34
|
options.each { |k,v| send("#{k}=", v) }
|
@@ -39,7 +39,7 @@ module ActionCable
|
|
39
39
|
|
40
40
|
# Uses the internal channel to disconnect the connection.
|
41
41
|
def disconnect
|
42
|
-
server.broadcast
|
42
|
+
server.broadcast internal_channel, type: 'disconnect'
|
43
43
|
end
|
44
44
|
|
45
45
|
# Returns all the identifiers that were applied to this connection.
|
data/lib/action_cable/server.rb
CHANGED
@@ -1,7 +1,4 @@
|
|
1
|
-
|
2
|
-
require 'celluloid/current'
|
3
|
-
|
4
|
-
require 'em-hiredis'
|
1
|
+
require 'thread'
|
5
2
|
|
6
3
|
module ActionCable
|
7
4
|
module Server
|
@@ -18,7 +15,12 @@ module ActionCable
|
|
18
15
|
def self.logger; config.logger; end
|
19
16
|
delegate :logger, to: :config
|
20
17
|
|
18
|
+
attr_reader :mutex
|
19
|
+
|
21
20
|
def initialize
|
21
|
+
@mutex = Mutex.new
|
22
|
+
|
23
|
+
@remote_connections = @stream_event_loop = @worker_pool = @channel_classes = @pubsub = nil
|
22
24
|
end
|
23
25
|
|
24
26
|
# Called by rack to setup the server.
|
@@ -34,36 +36,31 @@ module ActionCable
|
|
34
36
|
|
35
37
|
# Gateway to RemoteConnections. See that class for details.
|
36
38
|
def remote_connections
|
37
|
-
@remote_connections ||= RemoteConnections.new(self)
|
39
|
+
@remote_connections || @mutex.synchronize { @remote_connections ||= RemoteConnections.new(self) }
|
40
|
+
end
|
41
|
+
|
42
|
+
def stream_event_loop
|
43
|
+
@stream_event_loop || @mutex.synchronize { @stream_event_loop ||= ActionCable::Connection::StreamEventLoop.new }
|
38
44
|
end
|
39
45
|
|
40
46
|
# The thread worker pool for handling all the connection work on this server. Default size is set by config.worker_pool_size.
|
41
47
|
def worker_pool
|
42
|
-
@worker_pool ||= ActionCable::Server::Worker.
|
48
|
+
@worker_pool || @mutex.synchronize { @worker_pool ||= ActionCable::Server::Worker.new(max_size: config.worker_pool_size) }
|
43
49
|
end
|
44
50
|
|
45
51
|
# Requires and returns a hash of all the channel class constants keyed by name.
|
46
52
|
def channel_classes
|
47
|
-
@channel_classes
|
48
|
-
|
49
|
-
|
53
|
+
@channel_classes || @mutex.synchronize do
|
54
|
+
@channel_classes ||= begin
|
55
|
+
config.channel_paths.each { |channel_path| require channel_path }
|
56
|
+
config.channel_class_names.each_with_object({}) { |name, hash| hash[name] = name.constantize }
|
57
|
+
end
|
50
58
|
end
|
51
59
|
end
|
52
60
|
|
53
|
-
#
|
61
|
+
# Adapter used for all streams/broadcasting.
|
54
62
|
def pubsub
|
55
|
-
@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
|
63
|
+
@pubsub || @mutex.synchronize { @pubsub ||= config.pubsub_adapter.new(self) }
|
67
64
|
end
|
68
65
|
|
69
66
|
# All the identifiers applied to the connection class associated with this server.
|