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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +52 -6
  3. data/README.md +11 -15
  4. data/lib/action_cable.rb +5 -4
  5. data/lib/action_cable/channel/base.rb +19 -6
  6. data/lib/action_cable/channel/periodic_timers.rb +45 -7
  7. data/lib/action_cable/channel/streams.rb +70 -14
  8. data/lib/action_cable/connection.rb +2 -0
  9. data/lib/action_cable/connection/base.rb +33 -21
  10. data/lib/action_cable/connection/client_socket.rb +17 -9
  11. data/lib/action_cable/connection/faye_client_socket.rb +48 -0
  12. data/lib/action_cable/connection/faye_event_loop.rb +44 -0
  13. data/lib/action_cable/connection/internal_channel.rb +3 -5
  14. data/lib/action_cable/connection/message_buffer.rb +2 -2
  15. data/lib/action_cable/connection/stream.rb +9 -11
  16. data/lib/action_cable/connection/stream_event_loop.rb +10 -1
  17. data/lib/action_cable/connection/web_socket.rb +6 -2
  18. data/lib/action_cable/engine.rb +37 -1
  19. data/lib/action_cable/gem_version.rb +1 -1
  20. data/lib/action_cable/helpers/action_cable_helper.rb +19 -8
  21. data/lib/action_cable/remote_connections.rb +1 -1
  22. data/lib/action_cable/server/base.rb +26 -6
  23. data/lib/action_cable/server/broadcasting.rb +10 -9
  24. data/lib/action_cable/server/configuration.rb +19 -3
  25. data/lib/action_cable/server/connections.rb +3 -3
  26. data/lib/action_cable/server/worker.rb +27 -27
  27. data/lib/action_cable/server/worker/active_record_connection_management.rb +0 -3
  28. data/lib/action_cable/subscription_adapter/async.rb +8 -3
  29. data/lib/action_cable/subscription_adapter/evented_redis.rb +5 -1
  30. data/lib/action_cable/subscription_adapter/postgresql.rb +5 -4
  31. data/lib/action_cable/subscription_adapter/redis.rb +11 -6
  32. data/lib/assets/compiled/action_cable.js +248 -188
  33. data/lib/rails/generators/channel/USAGE +1 -1
  34. data/lib/rails/generators/channel/channel_generator.rb +4 -1
  35. data/lib/rails/generators/channel/templates/assets/cable.js +13 -0
  36. 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
- server.pubsub.broadcast broadcasting, ActiveSupport::JSON.encode(message)
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 = 100
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 ||= Concurrent::TimerTask.new(execution_interval: BEAT_INTERVAL) do
25
- Concurrent.global_io_executor.post { connections.map(&:beat) }
26
- end.tap(&:execute)
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
- @pool = Concurrent::ThreadPoolExecutor.new(
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
- def async_invoke(receiver, method, *args)
24
- @pool.post do
25
- invoke(receiver, method, *args)
26
- end
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 invoke(receiver, method, *args)
30
- begin
31
- self.connection = receiver
31
+ def stopping?
32
+ @executor.shuttingdown?
33
+ end
32
34
 
33
- run_callbacks :work do
34
- receiver.send method, *args
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
- receiver.handle_exception if receiver.respond_to?(:handle_exception)
41
- ensure
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 async_run_periodic_timer(channel, callback)
47
- @pool.post do
48
- run_periodic_timer(channel, callback)
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 run_periodic_timer(channel, callback)
53
- begin
54
- self.connection = channel.connection
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
- run_callbacks :work do
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
- Concurrent.global_io_executor.post { super }
18
+ @event_loop.post { super }
14
19
  end
15
20
 
16
21
  def invoke_callback(*)
17
- Concurrent.global_io_executor.post { super }
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.info "[ActionCable] Redis reconnect failed."
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
- Concurrent.global_io_executor << callback if callback
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
- Concurrent.global_io_executor.post { super }
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
- ::Redis.new(@server.config.cable)
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 ||= self.class.redis_connector.call(@server.config.cable)
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
- Concurrent.global_io_executor << next_callback if next_callback
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
- Concurrent.global_io_executor.post { super }
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 message_types,
56
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
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
- message_types = ActionCable.INTERNAL.message_types;
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.open();
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.isAlive()) {
82
- ActionCable.log("Attemped to open WebSocket, but existing socket is " + (this.getState()));
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 ref;
97
- return (ref = this.webSocket) != null ? ref.close() : void 0;
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.isAlive()) {
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.isAlive = function() {
123
- return (this.webSocket != null) && !this.isState("closing", "closed");
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 ref, states;
287
+ var ref1, states;
128
288
  states = 1 <= arguments.length ? slice.call(arguments, 0) : [];
129
- return ref = this.getState(), indexOf.call(states, ref) >= 0;
289
+ return ref1 = this.getState(), indexOf.call(states, ref1) >= 0;
130
290
  };
131
291
 
132
292
  Connection.prototype.getState = function() {
133
- var ref, state, value;
293
+ var ref1, state, value;
134
294
  for (state in WebSocket) {
135
295
  value = WebSocket[state];
136
- if (value === ((ref = this.webSocket) != null ? ref.readyState : void 0)) {
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, ref, type;
161
- ref = JSON.parse(event.data), identifier = ref.identifier, message = ref.message, type = ref.type;
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.consumer.subscriptions.notify(identifier, "connected");
332
+ return this.subscriptions.notify(identifier, "connected");
165
333
  case message_types.rejection:
166
- return this.consumer.subscriptions.reject(identifier);
334
+ return this.subscriptions.reject(identifier);
167
335
  default:
168
- return this.consumer.subscriptions.notify(identifier, "received", message);
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
- return this.consumer.subscriptions.reload();
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
- return this.disconnect();
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
- return new ActionCable.Subscription(this, params, mixin);
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
- return this.sendCommand(subscription, "subscribe");
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
- return this.sendCommand(subscription, "unsubscribe");
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
- results.push(this.notify(subscription, "rejected"));
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
- return this.subscriptions = (function() {
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
- if (identifier === ActionCable.INTERNAL.identifiers.ping) {
440
- return this.consumer.connection.isOpen();
441
- } else {
442
- return this.consumer.send({
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(subscriptions, params, mixin) {
459
- this.subscriptions = subscriptions;
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
  })();