actioncable 5.0.0.beta3 → 5.0.0.beta4
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 +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
|
})();
|