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.
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
  })();