actioncable 5.0.0.beta3 → 5.0.0.beta4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +52 -6
- data/README.md +11 -15
- data/lib/action_cable.rb +5 -4
- data/lib/action_cable/channel/base.rb +19 -6
- data/lib/action_cable/channel/periodic_timers.rb +45 -7
- data/lib/action_cable/channel/streams.rb +70 -14
- data/lib/action_cable/connection.rb +2 -0
- data/lib/action_cable/connection/base.rb +33 -21
- data/lib/action_cable/connection/client_socket.rb +17 -9
- data/lib/action_cable/connection/faye_client_socket.rb +48 -0
- data/lib/action_cable/connection/faye_event_loop.rb +44 -0
- data/lib/action_cable/connection/internal_channel.rb +3 -5
- data/lib/action_cable/connection/message_buffer.rb +2 -2
- data/lib/action_cable/connection/stream.rb +9 -11
- data/lib/action_cable/connection/stream_event_loop.rb +10 -1
- data/lib/action_cable/connection/web_socket.rb +6 -2
- data/lib/action_cable/engine.rb +37 -1
- data/lib/action_cable/gem_version.rb +1 -1
- data/lib/action_cable/helpers/action_cable_helper.rb +19 -8
- data/lib/action_cable/remote_connections.rb +1 -1
- data/lib/action_cable/server/base.rb +26 -6
- data/lib/action_cable/server/broadcasting.rb +10 -9
- data/lib/action_cable/server/configuration.rb +19 -3
- data/lib/action_cable/server/connections.rb +3 -3
- data/lib/action_cable/server/worker.rb +27 -27
- data/lib/action_cable/server/worker/active_record_connection_management.rb +0 -3
- data/lib/action_cable/subscription_adapter/async.rb +8 -3
- data/lib/action_cable/subscription_adapter/evented_redis.rb +5 -1
- data/lib/action_cable/subscription_adapter/postgresql.rb +5 -4
- data/lib/action_cable/subscription_adapter/redis.rb +11 -6
- data/lib/assets/compiled/action_cable.js +248 -188
- data/lib/rails/generators/channel/USAGE +1 -1
- data/lib/rails/generators/channel/channel_generator.rb +4 -1
- data/lib/rails/generators/channel/templates/assets/cable.js +13 -0
- metadata +8 -5
@@ -19,27 +19,28 @@ module ActionCable
|
|
19
19
|
# new Notification data['title'], body: data['body']
|
20
20
|
module Broadcasting
|
21
21
|
# Broadcast a hash directly to a named <tt>broadcasting</tt>. This will later be JSON encoded.
|
22
|
-
def broadcast(broadcasting, message)
|
23
|
-
broadcaster_for(broadcasting).broadcast(message)
|
22
|
+
def broadcast(broadcasting, message, coder: ActiveSupport::JSON)
|
23
|
+
broadcaster_for(broadcasting, coder: coder).broadcast(message)
|
24
24
|
end
|
25
25
|
|
26
26
|
# Returns a broadcaster for a named <tt>broadcasting</tt> that can be reused. Useful when you have an object that
|
27
27
|
# may need multiple spots to transmit to a specific broadcasting over and over.
|
28
|
-
def broadcaster_for(broadcasting)
|
29
|
-
Broadcaster.new(self, broadcasting)
|
28
|
+
def broadcaster_for(broadcasting, coder: ActiveSupport::JSON)
|
29
|
+
Broadcaster.new(self, String(broadcasting), coder: coder)
|
30
30
|
end
|
31
31
|
|
32
32
|
private
|
33
33
|
class Broadcaster
|
34
|
-
attr_reader :server, :broadcasting
|
34
|
+
attr_reader :server, :broadcasting, :coder
|
35
35
|
|
36
|
-
def initialize(server, broadcasting)
|
37
|
-
@server, @broadcasting = server, broadcasting
|
36
|
+
def initialize(server, broadcasting, coder:)
|
37
|
+
@server, @broadcasting, @coder = server, broadcasting, coder
|
38
38
|
end
|
39
39
|
|
40
40
|
def broadcast(message)
|
41
|
-
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message}"
|
42
|
-
|
41
|
+
server.logger.info "[ActionCable] Broadcasting to #{broadcasting}: #{message.inspect}"
|
42
|
+
encoded = coder ? coder.encode(message) : message
|
43
|
+
server.pubsub.broadcast broadcasting, encoded
|
43
44
|
end
|
44
45
|
end
|
45
46
|
end
|
@@ -4,9 +4,9 @@ module ActionCable
|
|
4
4
|
# in a Rails config initializer.
|
5
5
|
class Configuration
|
6
6
|
attr_accessor :logger, :log_tags
|
7
|
-
attr_accessor :connection_class, :worker_pool_size
|
7
|
+
attr_accessor :use_faye, :connection_class, :worker_pool_size
|
8
8
|
attr_accessor :disable_request_forgery_protection, :allowed_request_origins
|
9
|
-
attr_accessor :cable, :url
|
9
|
+
attr_accessor :cable, :url, :mount_path
|
10
10
|
|
11
11
|
attr_accessor :channel_paths # :nodoc:
|
12
12
|
|
@@ -14,7 +14,7 @@ module ActionCable
|
|
14
14
|
@log_tags = []
|
15
15
|
|
16
16
|
@connection_class = ActionCable::Connection::Base
|
17
|
-
@worker_pool_size =
|
17
|
+
@worker_pool_size = 4
|
18
18
|
|
19
19
|
@disable_request_forgery_protection = false
|
20
20
|
end
|
@@ -43,6 +43,22 @@ module ActionCable
|
|
43
43
|
adapter = 'PostgreSQL' if adapter == 'Postgresql'
|
44
44
|
"ActionCable::SubscriptionAdapter::#{adapter}".constantize
|
45
45
|
end
|
46
|
+
|
47
|
+
def event_loop_class
|
48
|
+
if use_faye
|
49
|
+
ActionCable::Connection::FayeEventLoop
|
50
|
+
else
|
51
|
+
ActionCable::Connection::StreamEventLoop
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def client_socket_class
|
56
|
+
if use_faye
|
57
|
+
ActionCable::Connection::FayeClientSocket
|
58
|
+
else
|
59
|
+
ActionCable::Connection::ClientSocket
|
60
|
+
end
|
61
|
+
end
|
46
62
|
end
|
47
63
|
end
|
48
64
|
end
|
@@ -21,9 +21,9 @@ module ActionCable
|
|
21
21
|
# then can't rely on being able to communicate with the connection. To solve this, a 3 second heartbeat runs on all connections. If the beat fails, we automatically
|
22
22
|
# disconnect.
|
23
23
|
def setup_heartbeat_timer
|
24
|
-
@heartbeat_timer ||=
|
25
|
-
|
26
|
-
end
|
24
|
+
@heartbeat_timer ||= event_loop.timer(BEAT_INTERVAL) do
|
25
|
+
event_loop.post { connections.map(&:beat) }
|
26
|
+
end
|
27
27
|
end
|
28
28
|
|
29
29
|
def open_connections_statistics
|
@@ -12,52 +12,52 @@ module ActionCable
|
|
12
12
|
define_callbacks :work
|
13
13
|
include ActiveRecordConnectionManagement
|
14
14
|
|
15
|
+
attr_reader :executor
|
16
|
+
|
15
17
|
def initialize(max_size: 5)
|
16
|
-
@
|
18
|
+
@executor = Concurrent::ThreadPoolExecutor.new(
|
17
19
|
min_threads: 1,
|
18
20
|
max_threads: max_size,
|
19
21
|
max_queue: 0,
|
20
22
|
)
|
21
23
|
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
25
|
+
# Stop processing work: any work that has not already started
|
26
|
+
# running will be discarded from the queue
|
27
|
+
def halt
|
28
|
+
@executor.kill
|
27
29
|
end
|
28
30
|
|
29
|
-
def
|
30
|
-
|
31
|
-
|
31
|
+
def stopping?
|
32
|
+
@executor.shuttingdown?
|
33
|
+
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
rescue Exception => e
|
37
|
-
logger.error "There was an exception - #{e.class}(#{e.message})"
|
38
|
-
logger.error e.backtrace.join("\n")
|
35
|
+
def work(connection)
|
36
|
+
self.connection = connection
|
39
37
|
|
40
|
-
|
41
|
-
|
42
|
-
self.connection = nil
|
38
|
+
run_callbacks :work do
|
39
|
+
yield
|
43
40
|
end
|
41
|
+
ensure
|
42
|
+
self.connection = nil
|
44
43
|
end
|
45
44
|
|
46
|
-
def
|
47
|
-
@
|
48
|
-
|
45
|
+
def async_invoke(receiver, method, *args, connection: receiver)
|
46
|
+
@executor.post do
|
47
|
+
invoke(receiver, method, *args, connection: connection)
|
49
48
|
end
|
50
49
|
end
|
51
50
|
|
52
|
-
def
|
53
|
-
|
54
|
-
|
51
|
+
def invoke(receiver, method, *args, connection:)
|
52
|
+
work(connection) do
|
53
|
+
begin
|
54
|
+
receiver.send method, *args
|
55
|
+
rescue Exception => e
|
56
|
+
logger.error "There was an exception - #{e.class}(#{e.message})"
|
57
|
+
logger.error e.backtrace.join("\n")
|
55
58
|
|
56
|
-
|
57
|
-
callback.respond_to?(:call) ? channel.instance_exec(&callback) : channel.send(callback)
|
59
|
+
receiver.handle_exception if receiver.respond_to?(:handle_exception)
|
58
60
|
end
|
59
|
-
ensure
|
60
|
-
self.connection = nil
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module ActionCable
|
2
2
|
module Server
|
3
3
|
class Worker
|
4
|
-
# Clear active connections between units of work so that way long-running channels or connection processes do not hoard connections.
|
5
4
|
module ActiveRecordConnectionManagement
|
6
5
|
extend ActiveSupport::Concern
|
7
6
|
|
@@ -13,8 +12,6 @@ module ActionCable
|
|
13
12
|
|
14
13
|
def with_database_connections
|
15
14
|
connection.logger.tag(ActiveRecord::Base.logger) { yield }
|
16
|
-
ensure
|
17
|
-
ActiveRecord::Base.clear_active_connections!
|
18
15
|
end
|
19
16
|
end
|
20
17
|
end
|
@@ -5,16 +5,21 @@ module ActionCable
|
|
5
5
|
class Async < Inline # :nodoc:
|
6
6
|
private
|
7
7
|
def new_subscriber_map
|
8
|
-
AsyncSubscriberMap.new
|
8
|
+
AsyncSubscriberMap.new(server.event_loop)
|
9
9
|
end
|
10
10
|
|
11
11
|
class AsyncSubscriberMap < SubscriberMap
|
12
|
+
def initialize(event_loop)
|
13
|
+
@event_loop = event_loop
|
14
|
+
super()
|
15
|
+
end
|
16
|
+
|
12
17
|
def add_subscriber(*)
|
13
|
-
|
18
|
+
@event_loop.post { super }
|
14
19
|
end
|
15
20
|
|
16
21
|
def invoke_callback(*)
|
17
|
-
|
22
|
+
@event_loop.post { super }
|
18
23
|
end
|
19
24
|
end
|
20
25
|
end
|
@@ -51,7 +51,11 @@ module ActionCable
|
|
51
51
|
@redis_connection_for_subscriptions || @server.mutex.synchronize do
|
52
52
|
@redis_connection_for_subscriptions ||= self.class.em_redis_connector.call(@server.config.cable).tap do |redis|
|
53
53
|
redis.on(:reconnect_failed) do
|
54
|
-
@logger.
|
54
|
+
@logger.error "[ActionCable] Redis reconnect failed."
|
55
|
+
end
|
56
|
+
|
57
|
+
redis.on(:failed) do
|
58
|
+
@logger.error "[ActionCable] Redis connection has failed."
|
55
59
|
end
|
56
60
|
end
|
57
61
|
end
|
@@ -42,14 +42,15 @@ module ActionCable
|
|
42
42
|
|
43
43
|
private
|
44
44
|
def listener
|
45
|
-
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
|
45
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
|
46
46
|
end
|
47
47
|
|
48
48
|
class Listener < SubscriberMap
|
49
|
-
def initialize(adapter)
|
49
|
+
def initialize(adapter, event_loop)
|
50
50
|
super()
|
51
51
|
|
52
52
|
@adapter = adapter
|
53
|
+
@event_loop = event_loop
|
53
54
|
@queue = Queue.new
|
54
55
|
|
55
56
|
@thread = Thread.new do
|
@@ -68,7 +69,7 @@ module ActionCable
|
|
68
69
|
case action
|
69
70
|
when :listen
|
70
71
|
pg_conn.exec("LISTEN #{pg_conn.escape_identifier channel}")
|
71
|
-
|
72
|
+
@event_loop.post(&callback) if callback
|
72
73
|
when :unlisten
|
73
74
|
pg_conn.exec("UNLISTEN #{pg_conn.escape_identifier channel}")
|
74
75
|
when :shutdown
|
@@ -98,7 +99,7 @@ module ActionCable
|
|
98
99
|
end
|
99
100
|
|
100
101
|
def invoke_callback(*)
|
101
|
-
|
102
|
+
@event_loop.post { super }
|
102
103
|
end
|
103
104
|
end
|
104
105
|
end
|
@@ -33,25 +33,30 @@ module ActionCable
|
|
33
33
|
end
|
34
34
|
|
35
35
|
def redis_connection_for_subscriptions
|
36
|
-
|
36
|
+
redis_connection
|
37
37
|
end
|
38
38
|
|
39
39
|
private
|
40
40
|
def listener
|
41
|
-
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self) }
|
41
|
+
@listener || @server.mutex.synchronize { @listener ||= Listener.new(self, @server.event_loop) }
|
42
42
|
end
|
43
43
|
|
44
44
|
def redis_connection_for_broadcasts
|
45
45
|
@redis_connection_for_broadcasts || @server.mutex.synchronize do
|
46
|
-
@redis_connection_for_broadcasts ||=
|
46
|
+
@redis_connection_for_broadcasts ||= redis_connection
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
+
def redis_connection
|
51
|
+
self.class.redis_connector.call(@server.config.cable)
|
52
|
+
end
|
53
|
+
|
50
54
|
class Listener < SubscriberMap
|
51
|
-
def initialize(adapter)
|
55
|
+
def initialize(adapter, event_loop)
|
52
56
|
super()
|
53
57
|
|
54
58
|
@adapter = adapter
|
59
|
+
@event_loop = event_loop
|
55
60
|
|
56
61
|
@subscribe_callbacks = Hash.new { |h, k| h[k] = [] }
|
57
62
|
@subscription_lock = Mutex.new
|
@@ -80,7 +85,7 @@ module ActionCable
|
|
80
85
|
|
81
86
|
if callbacks = @subscribe_callbacks[chan]
|
82
87
|
next_callback = callbacks.shift
|
83
|
-
|
88
|
+
@event_loop.post(&next_callback) if next_callback
|
84
89
|
@subscribe_callbacks.delete(chan) if callbacks.empty?
|
85
90
|
end
|
86
91
|
end
|
@@ -129,7 +134,7 @@ module ActionCable
|
|
129
134
|
end
|
130
135
|
|
131
136
|
def invoke_callback(*)
|
132
|
-
|
137
|
+
@event_loop.post { super }
|
133
138
|
end
|
134
139
|
|
135
140
|
private
|
@@ -3,17 +3,19 @@
|
|
3
3
|
|
4
4
|
this.ActionCable = {
|
5
5
|
INTERNAL: {
|
6
|
-
"identifiers": {
|
7
|
-
"ping": "_ping"
|
8
|
-
},
|
9
6
|
"message_types": {
|
7
|
+
"welcome": "welcome",
|
8
|
+
"ping": "ping",
|
10
9
|
"confirmation": "confirm_subscription",
|
11
10
|
"rejection": "reject_subscription"
|
12
|
-
}
|
11
|
+
},
|
12
|
+
"default_mount_path": "/cable",
|
13
|
+
"protocols": ["actioncable-v1-json", "actioncable-unsupported"]
|
13
14
|
},
|
14
15
|
createConsumer: function(url) {
|
16
|
+
var ref;
|
15
17
|
if (url == null) {
|
16
|
-
url = this.getConfig("url");
|
18
|
+
url = (ref = this.getConfig("url")) != null ? ref : this.INTERNAL.default_mount_path;
|
17
19
|
}
|
18
20
|
return new ActionCable.Consumer(this.createWebSocketURL(url));
|
19
21
|
},
|
@@ -52,12 +54,149 @@
|
|
52
54
|
|
53
55
|
}).call(this);
|
54
56
|
(function() {
|
55
|
-
var
|
56
|
-
|
57
|
+
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
58
|
+
|
59
|
+
ActionCable.ConnectionMonitor = (function() {
|
60
|
+
var clamp, now, secondsSince;
|
61
|
+
|
62
|
+
ConnectionMonitor.pollInterval = {
|
63
|
+
min: 3,
|
64
|
+
max: 30
|
65
|
+
};
|
66
|
+
|
67
|
+
ConnectionMonitor.staleThreshold = 6;
|
68
|
+
|
69
|
+
function ConnectionMonitor(connection) {
|
70
|
+
this.connection = connection;
|
71
|
+
this.visibilityDidChange = bind(this.visibilityDidChange, this);
|
72
|
+
this.reconnectAttempts = 0;
|
73
|
+
}
|
74
|
+
|
75
|
+
ConnectionMonitor.prototype.start = function() {
|
76
|
+
if (!this.isRunning()) {
|
77
|
+
this.startedAt = now();
|
78
|
+
delete this.stoppedAt;
|
79
|
+
this.startPolling();
|
80
|
+
document.addEventListener("visibilitychange", this.visibilityDidChange);
|
81
|
+
return ActionCable.log("ConnectionMonitor started. pollInterval = " + (this.getPollInterval()) + " ms");
|
82
|
+
}
|
83
|
+
};
|
84
|
+
|
85
|
+
ConnectionMonitor.prototype.stop = function() {
|
86
|
+
if (this.isRunning()) {
|
87
|
+
this.stoppedAt = now();
|
88
|
+
this.stopPolling();
|
89
|
+
document.removeEventListener("visibilitychange", this.visibilityDidChange);
|
90
|
+
return ActionCable.log("ConnectionMonitor stopped");
|
91
|
+
}
|
92
|
+
};
|
93
|
+
|
94
|
+
ConnectionMonitor.prototype.isRunning = function() {
|
95
|
+
return (this.startedAt != null) && (this.stoppedAt == null);
|
96
|
+
};
|
97
|
+
|
98
|
+
ConnectionMonitor.prototype.recordPing = function() {
|
99
|
+
return this.pingedAt = now();
|
100
|
+
};
|
101
|
+
|
102
|
+
ConnectionMonitor.prototype.recordConnect = function() {
|
103
|
+
this.reconnectAttempts = 0;
|
104
|
+
this.recordPing();
|
105
|
+
delete this.disconnectedAt;
|
106
|
+
return ActionCable.log("ConnectionMonitor recorded connect");
|
107
|
+
};
|
108
|
+
|
109
|
+
ConnectionMonitor.prototype.recordDisconnect = function() {
|
110
|
+
this.disconnectedAt = now();
|
111
|
+
return ActionCable.log("ConnectionMonitor recorded disconnect");
|
112
|
+
};
|
113
|
+
|
114
|
+
ConnectionMonitor.prototype.startPolling = function() {
|
115
|
+
this.stopPolling();
|
116
|
+
return this.poll();
|
117
|
+
};
|
118
|
+
|
119
|
+
ConnectionMonitor.prototype.stopPolling = function() {
|
120
|
+
return clearTimeout(this.pollTimeout);
|
121
|
+
};
|
122
|
+
|
123
|
+
ConnectionMonitor.prototype.poll = function() {
|
124
|
+
return this.pollTimeout = setTimeout((function(_this) {
|
125
|
+
return function() {
|
126
|
+
_this.reconnectIfStale();
|
127
|
+
return _this.poll();
|
128
|
+
};
|
129
|
+
})(this), this.getPollInterval());
|
130
|
+
};
|
131
|
+
|
132
|
+
ConnectionMonitor.prototype.getPollInterval = function() {
|
133
|
+
var interval, max, min, ref;
|
134
|
+
ref = this.constructor.pollInterval, min = ref.min, max = ref.max;
|
135
|
+
interval = 5 * Math.log(this.reconnectAttempts + 1);
|
136
|
+
return Math.round(clamp(interval, min, max) * 1000);
|
137
|
+
};
|
138
|
+
|
139
|
+
ConnectionMonitor.prototype.reconnectIfStale = function() {
|
140
|
+
if (this.connectionIsStale()) {
|
141
|
+
ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + (this.getPollInterval()) + " ms, time disconnected = " + (secondsSince(this.disconnectedAt)) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
|
142
|
+
this.reconnectAttempts++;
|
143
|
+
if (this.disconnectedRecently()) {
|
144
|
+
return ActionCable.log("ConnectionMonitor skipping reopening recent disconnect");
|
145
|
+
} else {
|
146
|
+
ActionCable.log("ConnectionMonitor reopening");
|
147
|
+
return this.connection.reopen();
|
148
|
+
}
|
149
|
+
}
|
150
|
+
};
|
151
|
+
|
152
|
+
ConnectionMonitor.prototype.connectionIsStale = function() {
|
153
|
+
var ref;
|
154
|
+
return secondsSince((ref = this.pingedAt) != null ? ref : this.startedAt) > this.constructor.staleThreshold;
|
155
|
+
};
|
156
|
+
|
157
|
+
ConnectionMonitor.prototype.disconnectedRecently = function() {
|
158
|
+
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
159
|
+
};
|
160
|
+
|
161
|
+
ConnectionMonitor.prototype.visibilityDidChange = function() {
|
162
|
+
if (document.visibilityState === "visible") {
|
163
|
+
return setTimeout((function(_this) {
|
164
|
+
return function() {
|
165
|
+
if (_this.connectionIsStale() || !_this.connection.isOpen()) {
|
166
|
+
ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
|
167
|
+
return _this.connection.reopen();
|
168
|
+
}
|
169
|
+
};
|
170
|
+
})(this), 200);
|
171
|
+
}
|
172
|
+
};
|
173
|
+
|
174
|
+
now = function() {
|
175
|
+
return new Date().getTime();
|
176
|
+
};
|
177
|
+
|
178
|
+
secondsSince = function(time) {
|
179
|
+
return (now() - time) / 1000;
|
180
|
+
};
|
181
|
+
|
182
|
+
clamp = function(number, min, max) {
|
183
|
+
return Math.max(min, Math.min(max, number));
|
184
|
+
};
|
185
|
+
|
186
|
+
return ConnectionMonitor;
|
187
|
+
|
188
|
+
})();
|
189
|
+
|
190
|
+
}).call(this);
|
191
|
+
(function() {
|
192
|
+
var i, message_types, protocols, ref, supportedProtocols, unsupportedProtocol,
|
57
193
|
slice = [].slice,
|
194
|
+
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
|
58
195
|
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
|
59
196
|
|
60
|
-
|
197
|
+
ref = ActionCable.INTERNAL, message_types = ref.message_types, protocols = ref.protocols;
|
198
|
+
|
199
|
+
supportedProtocols = 2 <= protocols.length ? slice.call(protocols, 0, i = protocols.length - 1) : (i = 0, []), unsupportedProtocol = protocols[i++];
|
61
200
|
|
62
201
|
ActionCable.Connection = (function() {
|
63
202
|
Connection.reopenDelay = 500;
|
@@ -65,7 +204,9 @@
|
|
65
204
|
function Connection(consumer) {
|
66
205
|
this.consumer = consumer;
|
67
206
|
this.open = bind(this.open, this);
|
68
|
-
this.
|
207
|
+
this.subscriptions = this.consumer.subscriptions;
|
208
|
+
this.monitor = new ActionCable.ConnectionMonitor(this);
|
209
|
+
this.disconnected = true;
|
69
210
|
}
|
70
211
|
|
71
212
|
Connection.prototype.send = function(data) {
|
@@ -78,29 +219,38 @@
|
|
78
219
|
};
|
79
220
|
|
80
221
|
Connection.prototype.open = function() {
|
81
|
-
if (this.
|
82
|
-
ActionCable.log("
|
222
|
+
if (this.isActive()) {
|
223
|
+
ActionCable.log("Attempted to open WebSocket, but existing socket is " + (this.getState()));
|
83
224
|
throw new Error("Existing connection must be closed before opening");
|
84
225
|
} else {
|
85
|
-
ActionCable.log("Opening WebSocket, current state is " + (this.getState()));
|
226
|
+
ActionCable.log("Opening WebSocket, current state is " + (this.getState()) + ", subprotocols: " + protocols);
|
86
227
|
if (this.webSocket != null) {
|
87
228
|
this.uninstallEventHandlers();
|
88
229
|
}
|
89
|
-
this.webSocket = new WebSocket(this.consumer.url);
|
230
|
+
this.webSocket = new WebSocket(this.consumer.url, protocols);
|
90
231
|
this.installEventHandlers();
|
232
|
+
this.monitor.start();
|
91
233
|
return true;
|
92
234
|
}
|
93
235
|
};
|
94
236
|
|
95
|
-
Connection.prototype.close = function() {
|
96
|
-
var
|
97
|
-
|
237
|
+
Connection.prototype.close = function(arg) {
|
238
|
+
var allowReconnect, ref1;
|
239
|
+
allowReconnect = (arg != null ? arg : {
|
240
|
+
allowReconnect: true
|
241
|
+
}).allowReconnect;
|
242
|
+
if (!allowReconnect) {
|
243
|
+
this.monitor.stop();
|
244
|
+
}
|
245
|
+
if (this.isActive()) {
|
246
|
+
return (ref1 = this.webSocket) != null ? ref1.close() : void 0;
|
247
|
+
}
|
98
248
|
};
|
99
249
|
|
100
250
|
Connection.prototype.reopen = function() {
|
101
251
|
var error, error1;
|
102
252
|
ActionCable.log("Reopening WebSocket, current state is " + (this.getState()));
|
103
|
-
if (this.
|
253
|
+
if (this.isActive()) {
|
104
254
|
try {
|
105
255
|
return this.close();
|
106
256
|
} catch (error1) {
|
@@ -115,25 +265,35 @@
|
|
115
265
|
}
|
116
266
|
};
|
117
267
|
|
268
|
+
Connection.prototype.getProtocol = function() {
|
269
|
+
var ref1;
|
270
|
+
return (ref1 = this.webSocket) != null ? ref1.protocol : void 0;
|
271
|
+
};
|
272
|
+
|
118
273
|
Connection.prototype.isOpen = function() {
|
119
274
|
return this.isState("open");
|
120
275
|
};
|
121
276
|
|
122
|
-
Connection.prototype.
|
123
|
-
return
|
277
|
+
Connection.prototype.isActive = function() {
|
278
|
+
return this.isState("open", "connecting");
|
279
|
+
};
|
280
|
+
|
281
|
+
Connection.prototype.isProtocolSupported = function() {
|
282
|
+
var ref1;
|
283
|
+
return ref1 = this.getProtocol(), indexOf.call(supportedProtocols, ref1) >= 0;
|
124
284
|
};
|
125
285
|
|
126
286
|
Connection.prototype.isState = function() {
|
127
|
-
var
|
287
|
+
var ref1, states;
|
128
288
|
states = 1 <= arguments.length ? slice.call(arguments, 0) : [];
|
129
|
-
return
|
289
|
+
return ref1 = this.getState(), indexOf.call(states, ref1) >= 0;
|
130
290
|
};
|
131
291
|
|
132
292
|
Connection.prototype.getState = function() {
|
133
|
-
var
|
293
|
+
var ref1, state, value;
|
134
294
|
for (state in WebSocket) {
|
135
295
|
value = WebSocket[state];
|
136
|
-
if (value === ((
|
296
|
+
if (value === ((ref1 = this.webSocket) != null ? ref1.readyState : void 0)) {
|
137
297
|
return state.toLowerCase();
|
138
298
|
}
|
139
299
|
}
|
@@ -157,170 +317,55 @@
|
|
157
317
|
|
158
318
|
Connection.prototype.events = {
|
159
319
|
message: function(event) {
|
160
|
-
var identifier, message,
|
161
|
-
|
320
|
+
var identifier, message, ref1, type;
|
321
|
+
if (!this.isProtocolSupported()) {
|
322
|
+
return;
|
323
|
+
}
|
324
|
+
ref1 = JSON.parse(event.data), identifier = ref1.identifier, message = ref1.message, type = ref1.type;
|
162
325
|
switch (type) {
|
326
|
+
case message_types.welcome:
|
327
|
+
this.monitor.recordConnect();
|
328
|
+
return this.subscriptions.reload();
|
329
|
+
case message_types.ping:
|
330
|
+
return this.monitor.recordPing();
|
163
331
|
case message_types.confirmation:
|
164
|
-
return this.
|
332
|
+
return this.subscriptions.notify(identifier, "connected");
|
165
333
|
case message_types.rejection:
|
166
|
-
return this.
|
334
|
+
return this.subscriptions.reject(identifier);
|
167
335
|
default:
|
168
|
-
return this.
|
336
|
+
return this.subscriptions.notify(identifier, "received", message);
|
169
337
|
}
|
170
338
|
},
|
171
339
|
open: function() {
|
172
|
-
ActionCable.log("WebSocket onopen event");
|
340
|
+
ActionCable.log("WebSocket onopen event, using '" + (this.getProtocol()) + "' subprotocol");
|
173
341
|
this.disconnected = false;
|
174
|
-
|
342
|
+
if (!this.isProtocolSupported()) {
|
343
|
+
ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.");
|
344
|
+
return this.close({
|
345
|
+
allowReconnect: false
|
346
|
+
});
|
347
|
+
}
|
175
348
|
},
|
176
|
-
close: function() {
|
349
|
+
close: function(event) {
|
177
350
|
ActionCable.log("WebSocket onclose event");
|
178
|
-
|
351
|
+
if (this.disconnected) {
|
352
|
+
return;
|
353
|
+
}
|
354
|
+
this.disconnected = true;
|
355
|
+
this.monitor.recordDisconnect();
|
356
|
+
return this.subscriptions.notifyAll("disconnected", {
|
357
|
+
willAttemptReconnect: this.monitor.isRunning()
|
358
|
+
});
|
179
359
|
},
|
180
360
|
error: function() {
|
181
|
-
ActionCable.log("WebSocket onerror event");
|
182
|
-
return this.disconnect();
|
361
|
+
return ActionCable.log("WebSocket onerror event");
|
183
362
|
}
|
184
363
|
};
|
185
364
|
|
186
|
-
Connection.prototype.disconnect = function() {
|
187
|
-
if (this.disconnected) {
|
188
|
-
return;
|
189
|
-
}
|
190
|
-
this.disconnected = true;
|
191
|
-
return this.consumer.subscriptions.notifyAll("disconnected");
|
192
|
-
};
|
193
|
-
|
194
365
|
return Connection;
|
195
366
|
|
196
367
|
})();
|
197
368
|
|
198
|
-
}).call(this);
|
199
|
-
(function() {
|
200
|
-
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
|
201
|
-
|
202
|
-
ActionCable.ConnectionMonitor = (function() {
|
203
|
-
var clamp, now, secondsSince;
|
204
|
-
|
205
|
-
ConnectionMonitor.pollInterval = {
|
206
|
-
min: 3,
|
207
|
-
max: 30
|
208
|
-
};
|
209
|
-
|
210
|
-
ConnectionMonitor.staleThreshold = 6;
|
211
|
-
|
212
|
-
ConnectionMonitor.prototype.identifier = ActionCable.INTERNAL.identifiers.ping;
|
213
|
-
|
214
|
-
function ConnectionMonitor(consumer) {
|
215
|
-
this.consumer = consumer;
|
216
|
-
this.visibilityDidChange = bind(this.visibilityDidChange, this);
|
217
|
-
this.consumer.subscriptions.add(this);
|
218
|
-
this.start();
|
219
|
-
}
|
220
|
-
|
221
|
-
ConnectionMonitor.prototype.connected = function() {
|
222
|
-
this.reset();
|
223
|
-
this.pingedAt = now();
|
224
|
-
delete this.disconnectedAt;
|
225
|
-
return ActionCable.log("ConnectionMonitor connected");
|
226
|
-
};
|
227
|
-
|
228
|
-
ConnectionMonitor.prototype.disconnected = function() {
|
229
|
-
return this.disconnectedAt = now();
|
230
|
-
};
|
231
|
-
|
232
|
-
ConnectionMonitor.prototype.received = function() {
|
233
|
-
return this.pingedAt = now();
|
234
|
-
};
|
235
|
-
|
236
|
-
ConnectionMonitor.prototype.reset = function() {
|
237
|
-
return this.reconnectAttempts = 0;
|
238
|
-
};
|
239
|
-
|
240
|
-
ConnectionMonitor.prototype.start = function() {
|
241
|
-
this.reset();
|
242
|
-
delete this.stoppedAt;
|
243
|
-
this.startedAt = now();
|
244
|
-
this.poll();
|
245
|
-
document.addEventListener("visibilitychange", this.visibilityDidChange);
|
246
|
-
return ActionCable.log("ConnectionMonitor started, pollInterval is " + (this.getInterval()) + "ms");
|
247
|
-
};
|
248
|
-
|
249
|
-
ConnectionMonitor.prototype.stop = function() {
|
250
|
-
this.stoppedAt = now();
|
251
|
-
document.removeEventListener("visibilitychange", this.visibilityDidChange);
|
252
|
-
return ActionCable.log("ConnectionMonitor stopped");
|
253
|
-
};
|
254
|
-
|
255
|
-
ConnectionMonitor.prototype.poll = function() {
|
256
|
-
return setTimeout((function(_this) {
|
257
|
-
return function() {
|
258
|
-
if (!_this.stoppedAt) {
|
259
|
-
_this.reconnectIfStale();
|
260
|
-
return _this.poll();
|
261
|
-
}
|
262
|
-
};
|
263
|
-
})(this), this.getInterval());
|
264
|
-
};
|
265
|
-
|
266
|
-
ConnectionMonitor.prototype.getInterval = function() {
|
267
|
-
var interval, max, min, ref;
|
268
|
-
ref = this.constructor.pollInterval, min = ref.min, max = ref.max;
|
269
|
-
interval = 5 * Math.log(this.reconnectAttempts + 1);
|
270
|
-
return clamp(interval, min, max) * 1000;
|
271
|
-
};
|
272
|
-
|
273
|
-
ConnectionMonitor.prototype.reconnectIfStale = function() {
|
274
|
-
if (this.connectionIsStale()) {
|
275
|
-
ActionCable.log("ConnectionMonitor detected stale connection, reconnectAttempts = " + this.reconnectAttempts);
|
276
|
-
this.reconnectAttempts++;
|
277
|
-
if (this.disconnectedRecently()) {
|
278
|
-
return ActionCable.log("ConnectionMonitor skipping reopen because recently disconnected at " + this.disconnectedAt);
|
279
|
-
} else {
|
280
|
-
ActionCable.log("ConnectionMonitor reopening");
|
281
|
-
return this.consumer.connection.reopen();
|
282
|
-
}
|
283
|
-
}
|
284
|
-
};
|
285
|
-
|
286
|
-
ConnectionMonitor.prototype.connectionIsStale = function() {
|
287
|
-
var ref;
|
288
|
-
return secondsSince((ref = this.pingedAt) != null ? ref : this.startedAt) > this.constructor.staleThreshold;
|
289
|
-
};
|
290
|
-
|
291
|
-
ConnectionMonitor.prototype.disconnectedRecently = function() {
|
292
|
-
return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
|
293
|
-
};
|
294
|
-
|
295
|
-
ConnectionMonitor.prototype.visibilityDidChange = function() {
|
296
|
-
if (document.visibilityState === "visible") {
|
297
|
-
return setTimeout((function(_this) {
|
298
|
-
return function() {
|
299
|
-
if (_this.connectionIsStale() || !_this.consumer.connection.isOpen()) {
|
300
|
-
ActionCable.log("ConnectionMonitor reopening stale connection after visibilitychange to " + document.visibilityState);
|
301
|
-
return _this.consumer.connection.reopen();
|
302
|
-
}
|
303
|
-
};
|
304
|
-
})(this), 200);
|
305
|
-
}
|
306
|
-
};
|
307
|
-
|
308
|
-
now = function() {
|
309
|
-
return new Date().getTime();
|
310
|
-
};
|
311
|
-
|
312
|
-
secondsSince = function(time) {
|
313
|
-
return (now() - time) / 1000;
|
314
|
-
};
|
315
|
-
|
316
|
-
clamp = function(number, min, max) {
|
317
|
-
return Math.max(min, Math.min(max, number));
|
318
|
-
};
|
319
|
-
|
320
|
-
return ConnectionMonitor;
|
321
|
-
|
322
|
-
})();
|
323
|
-
|
324
369
|
}).call(this);
|
325
370
|
(function() {
|
326
371
|
var slice = [].slice;
|
@@ -332,25 +377,29 @@
|
|
332
377
|
}
|
333
378
|
|
334
379
|
Subscriptions.prototype.create = function(channelName, mixin) {
|
335
|
-
var channel, params;
|
380
|
+
var channel, params, subscription;
|
336
381
|
channel = channelName;
|
337
382
|
params = typeof channel === "object" ? channel : {
|
338
383
|
channel: channel
|
339
384
|
};
|
340
|
-
|
385
|
+
subscription = new ActionCable.Subscription(this.consumer, params, mixin);
|
386
|
+
return this.add(subscription);
|
341
387
|
};
|
342
388
|
|
343
389
|
Subscriptions.prototype.add = function(subscription) {
|
344
390
|
this.subscriptions.push(subscription);
|
391
|
+
this.consumer.ensureActiveConnection();
|
345
392
|
this.notify(subscription, "initialized");
|
346
|
-
|
393
|
+
this.sendCommand(subscription, "subscribe");
|
394
|
+
return subscription;
|
347
395
|
};
|
348
396
|
|
349
397
|
Subscriptions.prototype.remove = function(subscription) {
|
350
398
|
this.forget(subscription);
|
351
399
|
if (!this.findAll(subscription.identifier).length) {
|
352
|
-
|
400
|
+
this.sendCommand(subscription, "unsubscribe");
|
353
401
|
}
|
402
|
+
return subscription;
|
354
403
|
};
|
355
404
|
|
356
405
|
Subscriptions.prototype.reject = function(identifier) {
|
@@ -360,14 +409,15 @@
|
|
360
409
|
for (i = 0, len = ref.length; i < len; i++) {
|
361
410
|
subscription = ref[i];
|
362
411
|
this.forget(subscription);
|
363
|
-
|
412
|
+
this.notify(subscription, "rejected");
|
413
|
+
results.push(subscription);
|
364
414
|
}
|
365
415
|
return results;
|
366
416
|
};
|
367
417
|
|
368
418
|
Subscriptions.prototype.forget = function(subscription) {
|
369
419
|
var s;
|
370
|
-
|
420
|
+
this.subscriptions = (function() {
|
371
421
|
var i, len, ref, results;
|
372
422
|
ref = this.subscriptions;
|
373
423
|
results = [];
|
@@ -379,6 +429,7 @@
|
|
379
429
|
}
|
380
430
|
return results;
|
381
431
|
}).call(this);
|
432
|
+
return subscription;
|
382
433
|
};
|
383
434
|
|
384
435
|
Subscriptions.prototype.findAll = function(identifier) {
|
@@ -436,14 +487,10 @@
|
|
436
487
|
Subscriptions.prototype.sendCommand = function(subscription, command) {
|
437
488
|
var identifier;
|
438
489
|
identifier = subscription.identifier;
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
command: command,
|
444
|
-
identifier: identifier
|
445
|
-
});
|
446
|
-
}
|
490
|
+
return this.consumer.send({
|
491
|
+
command: command,
|
492
|
+
identifier: identifier
|
493
|
+
});
|
447
494
|
};
|
448
495
|
|
449
496
|
return Subscriptions;
|
@@ -455,15 +502,13 @@
|
|
455
502
|
ActionCable.Subscription = (function() {
|
456
503
|
var extend;
|
457
504
|
|
458
|
-
function Subscription(
|
459
|
-
this.
|
505
|
+
function Subscription(consumer, params, mixin) {
|
506
|
+
this.consumer = consumer;
|
460
507
|
if (params == null) {
|
461
508
|
params = {};
|
462
509
|
}
|
463
510
|
this.identifier = JSON.stringify(params);
|
464
511
|
extend(this, mixin);
|
465
|
-
this.subscriptions.add(this);
|
466
|
-
this.consumer = this.subscriptions.consumer;
|
467
512
|
}
|
468
513
|
|
469
514
|
Subscription.prototype.perform = function(action, data) {
|
@@ -483,7 +528,7 @@
|
|
483
528
|
};
|
484
529
|
|
485
530
|
Subscription.prototype.unsubscribe = function() {
|
486
|
-
return this.subscriptions.remove(this);
|
531
|
+
return this.consumer.subscriptions.remove(this);
|
487
532
|
};
|
488
533
|
|
489
534
|
extend = function(object, properties) {
|
@@ -508,13 +553,28 @@
|
|
508
553
|
this.url = url;
|
509
554
|
this.subscriptions = new ActionCable.Subscriptions(this);
|
510
555
|
this.connection = new ActionCable.Connection(this);
|
511
|
-
this.connectionMonitor = new ActionCable.ConnectionMonitor(this);
|
512
556
|
}
|
513
557
|
|
514
558
|
Consumer.prototype.send = function(data) {
|
515
559
|
return this.connection.send(data);
|
516
560
|
};
|
517
561
|
|
562
|
+
Consumer.prototype.connect = function() {
|
563
|
+
return this.connection.open();
|
564
|
+
};
|
565
|
+
|
566
|
+
Consumer.prototype.disconnect = function() {
|
567
|
+
return this.connection.close({
|
568
|
+
allowReconnect: false
|
569
|
+
});
|
570
|
+
};
|
571
|
+
|
572
|
+
Consumer.prototype.ensureActiveConnection = function() {
|
573
|
+
if (!this.connection.isActive()) {
|
574
|
+
return this.connection.open();
|
575
|
+
}
|
576
|
+
};
|
577
|
+
|
518
578
|
return Consumer;
|
519
579
|
|
520
580
|
})();
|