actioncable 5.2.8.1 → 6.0.0.beta1

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 +58 -112
  3. data/MIT-LICENSE +1 -1
  4. data/README.md +1 -544
  5. data/app/assets/javascripts/action_cable.js +492 -0
  6. data/lib/action_cable/channel/base.rb +5 -1
  7. data/lib/action_cable/channel/test_case.rb +312 -0
  8. data/lib/action_cable/channel.rb +1 -0
  9. data/lib/action_cable/connection/authorization.rb +1 -1
  10. data/lib/action_cable/connection/base.rb +9 -7
  11. data/lib/action_cable/connection/message_buffer.rb +1 -4
  12. data/lib/action_cable/connection/stream.rb +4 -2
  13. data/lib/action_cable/connection/subscriptions.rb +1 -5
  14. data/lib/action_cable/connection/test_case.rb +236 -0
  15. data/lib/action_cable/connection/web_socket.rb +1 -3
  16. data/lib/action_cable/connection.rb +1 -0
  17. data/lib/action_cable/gem_version.rb +4 -4
  18. data/lib/action_cable/server/base.rb +3 -1
  19. data/lib/action_cable/server/worker.rb +5 -7
  20. data/lib/action_cable/subscription_adapter/postgresql.rb +24 -8
  21. data/lib/action_cable/subscription_adapter/redis.rb +2 -1
  22. data/lib/action_cable/subscription_adapter/test.rb +40 -0
  23. data/lib/action_cable/subscription_adapter.rb +1 -0
  24. data/lib/action_cable/test_case.rb +11 -0
  25. data/lib/action_cable/test_helper.rb +133 -0
  26. data/lib/action_cable.rb +15 -7
  27. data/lib/rails/generators/channel/USAGE +4 -5
  28. data/lib/rails/generators/channel/channel_generator.rb +6 -3
  29. data/lib/rails/generators/channel/templates/{assets → javascript}/channel.js.tt +3 -1
  30. data/lib/rails/generators/channel/templates/{assets/cable.js.tt → javascript/consumer.js.tt} +2 -9
  31. data/lib/rails/generators/channel/templates/javascript/index.js.tt +5 -0
  32. data/lib/rails/generators/test_unit/channel_generator.rb +20 -0
  33. data/lib/rails/generators/test_unit/templates/channel_test.rb.tt +8 -0
  34. metadata +24 -17
  35. data/lib/assets/compiled/action_cable.js +0 -601
  36. data/lib/rails/generators/channel/templates/assets/channel.coffee.tt +0 -14
@@ -0,0 +1,492 @@
1
+ (function(global, factory) {
2
+ typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {});
3
+ })(this, function(exports) {
4
+ "use strict";
5
+ var adapters = {
6
+ logger: self.console,
7
+ WebSocket: self.WebSocket
8
+ };
9
+ var logger = {
10
+ log: function log() {
11
+ if (this.enabled) {
12
+ var _adapters$logger;
13
+ for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) {
14
+ messages[_key] = arguments[_key];
15
+ }
16
+ messages.push(Date.now());
17
+ (_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages));
18
+ }
19
+ }
20
+ };
21
+ var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) {
22
+ return typeof obj;
23
+ } : function(obj) {
24
+ return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
25
+ };
26
+ var classCallCheck = function(instance, Constructor) {
27
+ if (!(instance instanceof Constructor)) {
28
+ throw new TypeError("Cannot call a class as a function");
29
+ }
30
+ };
31
+ var now = function now() {
32
+ return new Date().getTime();
33
+ };
34
+ var secondsSince = function secondsSince(time) {
35
+ return (now() - time) / 1e3;
36
+ };
37
+ var clamp = function clamp(number, min, max) {
38
+ return Math.max(min, Math.min(max, number));
39
+ };
40
+ var ConnectionMonitor = function() {
41
+ function ConnectionMonitor(connection) {
42
+ classCallCheck(this, ConnectionMonitor);
43
+ this.visibilityDidChange = this.visibilityDidChange.bind(this);
44
+ this.connection = connection;
45
+ this.reconnectAttempts = 0;
46
+ }
47
+ ConnectionMonitor.prototype.start = function start() {
48
+ if (!this.isRunning()) {
49
+ this.startedAt = now();
50
+ delete this.stoppedAt;
51
+ this.startPolling();
52
+ addEventListener("visibilitychange", this.visibilityDidChange);
53
+ logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms");
54
+ }
55
+ };
56
+ ConnectionMonitor.prototype.stop = function stop() {
57
+ if (this.isRunning()) {
58
+ this.stoppedAt = now();
59
+ this.stopPolling();
60
+ removeEventListener("visibilitychange", this.visibilityDidChange);
61
+ logger.log("ConnectionMonitor stopped");
62
+ }
63
+ };
64
+ ConnectionMonitor.prototype.isRunning = function isRunning() {
65
+ return this.startedAt && !this.stoppedAt;
66
+ };
67
+ ConnectionMonitor.prototype.recordPing = function recordPing() {
68
+ this.pingedAt = now();
69
+ };
70
+ ConnectionMonitor.prototype.recordConnect = function recordConnect() {
71
+ this.reconnectAttempts = 0;
72
+ this.recordPing();
73
+ delete this.disconnectedAt;
74
+ logger.log("ConnectionMonitor recorded connect");
75
+ };
76
+ ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() {
77
+ this.disconnectedAt = now();
78
+ logger.log("ConnectionMonitor recorded disconnect");
79
+ };
80
+ ConnectionMonitor.prototype.startPolling = function startPolling() {
81
+ this.stopPolling();
82
+ this.poll();
83
+ };
84
+ ConnectionMonitor.prototype.stopPolling = function stopPolling() {
85
+ clearTimeout(this.pollTimeout);
86
+ };
87
+ ConnectionMonitor.prototype.poll = function poll() {
88
+ var _this = this;
89
+ this.pollTimeout = setTimeout(function() {
90
+ _this.reconnectIfStale();
91
+ _this.poll();
92
+ }, this.getPollInterval());
93
+ };
94
+ ConnectionMonitor.prototype.getPollInterval = function getPollInterval() {
95
+ var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier;
96
+ var interval = multiplier * Math.log(this.reconnectAttempts + 1);
97
+ return Math.round(clamp(interval, min, max) * 1e3);
98
+ };
99
+ ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() {
100
+ if (this.connectionIsStale()) {
101
+ logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", pollInterval = " + this.getPollInterval() + " ms, time disconnected = " + secondsSince(this.disconnectedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s");
102
+ this.reconnectAttempts++;
103
+ if (this.disconnectedRecently()) {
104
+ logger.log("ConnectionMonitor skipping reopening recent disconnect");
105
+ } else {
106
+ logger.log("ConnectionMonitor reopening");
107
+ this.connection.reopen();
108
+ }
109
+ }
110
+ };
111
+ ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() {
112
+ return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold;
113
+ };
114
+ ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() {
115
+ return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold;
116
+ };
117
+ ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() {
118
+ var _this2 = this;
119
+ if (document.visibilityState === "visible") {
120
+ setTimeout(function() {
121
+ if (_this2.connectionIsStale() || !_this2.connection.isOpen()) {
122
+ logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState);
123
+ _this2.connection.reopen();
124
+ }
125
+ }, 200);
126
+ }
127
+ };
128
+ return ConnectionMonitor;
129
+ }();
130
+ ConnectionMonitor.pollInterval = {
131
+ min: 3,
132
+ max: 30,
133
+ multiplier: 5
134
+ };
135
+ ConnectionMonitor.staleThreshold = 6;
136
+ var INTERNAL = {
137
+ message_types: {
138
+ welcome: "welcome",
139
+ disconnect: "disconnect",
140
+ ping: "ping",
141
+ confirmation: "confirm_subscription",
142
+ rejection: "reject_subscription"
143
+ },
144
+ disconnect_reasons: {
145
+ unauthorized: "unauthorized",
146
+ invalid_request: "invalid_request",
147
+ server_restart: "server_restart"
148
+ },
149
+ default_mount_path: "/cable",
150
+ protocols: [ "actioncable-v1-json", "actioncable-unsupported" ]
151
+ };
152
+ var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols;
153
+ var supportedProtocols = protocols.slice(0, protocols.length - 1);
154
+ var indexOf = [].indexOf;
155
+ var Connection = function() {
156
+ function Connection(consumer) {
157
+ classCallCheck(this, Connection);
158
+ this.open = this.open.bind(this);
159
+ this.consumer = consumer;
160
+ this.subscriptions = this.consumer.subscriptions;
161
+ this.monitor = new ConnectionMonitor(this);
162
+ this.disconnected = true;
163
+ }
164
+ Connection.prototype.send = function send(data) {
165
+ if (this.isOpen()) {
166
+ this.webSocket.send(JSON.stringify(data));
167
+ return true;
168
+ } else {
169
+ return false;
170
+ }
171
+ };
172
+ Connection.prototype.open = function open() {
173
+ if (this.isActive()) {
174
+ logger.log("Attempted to open WebSocket, but existing socket is " + this.getState());
175
+ return false;
176
+ } else {
177
+ logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols);
178
+ if (this.webSocket) {
179
+ this.uninstallEventHandlers();
180
+ }
181
+ this.webSocket = new adapters.WebSocket(this.consumer.url, protocols);
182
+ this.installEventHandlers();
183
+ this.monitor.start();
184
+ return true;
185
+ }
186
+ };
187
+ Connection.prototype.close = function close() {
188
+ var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {
189
+ allowReconnect: true
190
+ }, allowReconnect = _ref.allowReconnect;
191
+ if (!allowReconnect) {
192
+ this.monitor.stop();
193
+ }
194
+ if (this.isActive()) {
195
+ return this.webSocket.close();
196
+ }
197
+ };
198
+ Connection.prototype.reopen = function reopen() {
199
+ logger.log("Reopening WebSocket, current state is " + this.getState());
200
+ if (this.isActive()) {
201
+ try {
202
+ return this.close();
203
+ } catch (error) {
204
+ logger.log("Failed to reopen WebSocket", error);
205
+ } finally {
206
+ logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms");
207
+ setTimeout(this.open, this.constructor.reopenDelay);
208
+ }
209
+ } else {
210
+ return this.open();
211
+ }
212
+ };
213
+ Connection.prototype.getProtocol = function getProtocol() {
214
+ if (this.webSocket) {
215
+ return this.webSocket.protocol;
216
+ }
217
+ };
218
+ Connection.prototype.isOpen = function isOpen() {
219
+ return this.isState("open");
220
+ };
221
+ Connection.prototype.isActive = function isActive() {
222
+ return this.isState("open", "connecting");
223
+ };
224
+ Connection.prototype.isProtocolSupported = function isProtocolSupported() {
225
+ return indexOf.call(supportedProtocols, this.getProtocol()) >= 0;
226
+ };
227
+ Connection.prototype.isState = function isState() {
228
+ for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) {
229
+ states[_key] = arguments[_key];
230
+ }
231
+ return indexOf.call(states, this.getState()) >= 0;
232
+ };
233
+ Connection.prototype.getState = function getState() {
234
+ if (this.webSocket) {
235
+ for (var state in adapters.WebSocket) {
236
+ if (adapters.WebSocket[state] === this.webSocket.readyState) {
237
+ return state.toLowerCase();
238
+ }
239
+ }
240
+ }
241
+ return null;
242
+ };
243
+ Connection.prototype.installEventHandlers = function installEventHandlers() {
244
+ for (var eventName in this.events) {
245
+ var handler = this.events[eventName].bind(this);
246
+ this.webSocket["on" + eventName] = handler;
247
+ }
248
+ };
249
+ Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() {
250
+ for (var eventName in this.events) {
251
+ this.webSocket["on" + eventName] = function() {};
252
+ }
253
+ };
254
+ return Connection;
255
+ }();
256
+ Connection.reopenDelay = 500;
257
+ Connection.prototype.events = {
258
+ message: function message(event) {
259
+ if (!this.isProtocolSupported()) {
260
+ return;
261
+ }
262
+ var _JSON$parse = JSON.parse(event.data), identifier = _JSON$parse.identifier, message = _JSON$parse.message, reason = _JSON$parse.reason, reconnect = _JSON$parse.reconnect, type = _JSON$parse.type;
263
+ switch (type) {
264
+ case message_types.welcome:
265
+ this.monitor.recordConnect();
266
+ return this.subscriptions.reload();
267
+
268
+ case message_types.disconnect:
269
+ logger.log("Disconnecting. Reason: " + reason);
270
+ return this.close({
271
+ allowReconnect: reconnect
272
+ });
273
+
274
+ case message_types.ping:
275
+ return this.monitor.recordPing();
276
+
277
+ case message_types.confirmation:
278
+ return this.subscriptions.notify(identifier, "connected");
279
+
280
+ case message_types.rejection:
281
+ return this.subscriptions.reject(identifier);
282
+
283
+ default:
284
+ return this.subscriptions.notify(identifier, "received", message);
285
+ }
286
+ },
287
+ open: function open() {
288
+ logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol");
289
+ this.disconnected = false;
290
+ if (!this.isProtocolSupported()) {
291
+ logger.log("Protocol is unsupported. Stopping monitor and disconnecting.");
292
+ return this.close({
293
+ allowReconnect: false
294
+ });
295
+ }
296
+ },
297
+ close: function close(event) {
298
+ logger.log("WebSocket onclose event");
299
+ if (this.disconnected) {
300
+ return;
301
+ }
302
+ this.disconnected = true;
303
+ this.monitor.recordDisconnect();
304
+ return this.subscriptions.notifyAll("disconnected", {
305
+ willAttemptReconnect: this.monitor.isRunning()
306
+ });
307
+ },
308
+ error: function error() {
309
+ logger.log("WebSocket onerror event");
310
+ }
311
+ };
312
+ var extend = function extend(object, properties) {
313
+ if (properties != null) {
314
+ for (var key in properties) {
315
+ var value = properties[key];
316
+ object[key] = value;
317
+ }
318
+ }
319
+ return object;
320
+ };
321
+ var Subscription = function() {
322
+ function Subscription(consumer) {
323
+ var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
324
+ var mixin = arguments[2];
325
+ classCallCheck(this, Subscription);
326
+ this.consumer = consumer;
327
+ this.identifier = JSON.stringify(params);
328
+ extend(this, mixin);
329
+ }
330
+ Subscription.prototype.perform = function perform(action) {
331
+ var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
332
+ data.action = action;
333
+ return this.send(data);
334
+ };
335
+ Subscription.prototype.send = function send(data) {
336
+ return this.consumer.send({
337
+ command: "message",
338
+ identifier: this.identifier,
339
+ data: JSON.stringify(data)
340
+ });
341
+ };
342
+ Subscription.prototype.unsubscribe = function unsubscribe() {
343
+ return this.consumer.subscriptions.remove(this);
344
+ };
345
+ return Subscription;
346
+ }();
347
+ var Subscriptions = function() {
348
+ function Subscriptions(consumer) {
349
+ classCallCheck(this, Subscriptions);
350
+ this.consumer = consumer;
351
+ this.subscriptions = [];
352
+ }
353
+ Subscriptions.prototype.create = function create(channelName, mixin) {
354
+ var channel = channelName;
355
+ var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : {
356
+ channel: channel
357
+ };
358
+ var subscription = new Subscription(this.consumer, params, mixin);
359
+ return this.add(subscription);
360
+ };
361
+ Subscriptions.prototype.add = function add(subscription) {
362
+ this.subscriptions.push(subscription);
363
+ this.consumer.ensureActiveConnection();
364
+ this.notify(subscription, "initialized");
365
+ this.sendCommand(subscription, "subscribe");
366
+ return subscription;
367
+ };
368
+ Subscriptions.prototype.remove = function remove(subscription) {
369
+ this.forget(subscription);
370
+ if (!this.findAll(subscription.identifier).length) {
371
+ this.sendCommand(subscription, "unsubscribe");
372
+ }
373
+ return subscription;
374
+ };
375
+ Subscriptions.prototype.reject = function reject(identifier) {
376
+ var _this = this;
377
+ return this.findAll(identifier).map(function(subscription) {
378
+ _this.forget(subscription);
379
+ _this.notify(subscription, "rejected");
380
+ return subscription;
381
+ });
382
+ };
383
+ Subscriptions.prototype.forget = function forget(subscription) {
384
+ this.subscriptions = this.subscriptions.filter(function(s) {
385
+ return s !== subscription;
386
+ });
387
+ return subscription;
388
+ };
389
+ Subscriptions.prototype.findAll = function findAll(identifier) {
390
+ return this.subscriptions.filter(function(s) {
391
+ return s.identifier === identifier;
392
+ });
393
+ };
394
+ Subscriptions.prototype.reload = function reload() {
395
+ var _this2 = this;
396
+ return this.subscriptions.map(function(subscription) {
397
+ return _this2.sendCommand(subscription, "subscribe");
398
+ });
399
+ };
400
+ Subscriptions.prototype.notifyAll = function notifyAll(callbackName) {
401
+ var _this3 = this;
402
+ for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
403
+ args[_key - 1] = arguments[_key];
404
+ }
405
+ return this.subscriptions.map(function(subscription) {
406
+ return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args));
407
+ });
408
+ };
409
+ Subscriptions.prototype.notify = function notify(subscription, callbackName) {
410
+ for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) {
411
+ args[_key2 - 2] = arguments[_key2];
412
+ }
413
+ var subscriptions = void 0;
414
+ if (typeof subscription === "string") {
415
+ subscriptions = this.findAll(subscription);
416
+ } else {
417
+ subscriptions = [ subscription ];
418
+ }
419
+ return subscriptions.map(function(subscription) {
420
+ return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined;
421
+ });
422
+ };
423
+ Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) {
424
+ var identifier = subscription.identifier;
425
+ return this.consumer.send({
426
+ command: command,
427
+ identifier: identifier
428
+ });
429
+ };
430
+ return Subscriptions;
431
+ }();
432
+ var Consumer = function() {
433
+ function Consumer(url) {
434
+ classCallCheck(this, Consumer);
435
+ this.url = url;
436
+ this.subscriptions = new Subscriptions(this);
437
+ this.connection = new Connection(this);
438
+ }
439
+ Consumer.prototype.send = function send(data) {
440
+ return this.connection.send(data);
441
+ };
442
+ Consumer.prototype.connect = function connect() {
443
+ return this.connection.open();
444
+ };
445
+ Consumer.prototype.disconnect = function disconnect() {
446
+ return this.connection.close({
447
+ allowReconnect: false
448
+ });
449
+ };
450
+ Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() {
451
+ if (!this.connection.isActive()) {
452
+ return this.connection.open();
453
+ }
454
+ };
455
+ return Consumer;
456
+ }();
457
+ function createConsumer() {
458
+ var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path;
459
+ return new Consumer(createWebSocketURL(url));
460
+ }
461
+ function getConfig(name) {
462
+ var element = document.head.querySelector("meta[name='action-cable-" + name + "']");
463
+ if (element) {
464
+ return element.getAttribute("content");
465
+ }
466
+ }
467
+ function createWebSocketURL(url) {
468
+ if (url && !/^wss?:/i.test(url)) {
469
+ var a = document.createElement("a");
470
+ a.href = url;
471
+ a.href = a.href;
472
+ a.protocol = a.protocol.replace("http", "ws");
473
+ return a.href;
474
+ } else {
475
+ return url;
476
+ }
477
+ }
478
+ exports.Connection = Connection;
479
+ exports.ConnectionMonitor = ConnectionMonitor;
480
+ exports.Consumer = Consumer;
481
+ exports.INTERNAL = INTERNAL;
482
+ exports.Subscription = Subscription;
483
+ exports.Subscriptions = Subscriptions;
484
+ exports.adapters = adapters;
485
+ exports.logger = logger;
486
+ exports.createConsumer = createConsumer;
487
+ exports.getConfig = getConfig;
488
+ exports.createWebSocketURL = createWebSocketURL;
489
+ Object.defineProperty(exports, "__esModule", {
490
+ value: true
491
+ });
492
+ });
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "set"
4
+ require "active_support/rescuable"
4
5
 
5
6
  module ActionCable
6
7
  module Channel
@@ -99,6 +100,7 @@ module ActionCable
99
100
  include Streams
100
101
  include Naming
101
102
  include Broadcasting
103
+ include ActiveSupport::Rescuable
102
104
 
103
105
  attr_reader :params, :connection, :identifier
104
106
  delegate :logger, to: :connection
@@ -267,10 +269,12 @@ module ActionCable
267
269
  else
268
270
  public_send action
269
271
  end
272
+ rescue Exception => exception
273
+ rescue_with_handler(exception) || raise
270
274
  end
271
275
 
272
276
  def action_signature(action, data)
273
- "#{self.class.name}##{action}".dup.tap do |signature|
277
+ (+"#{self.class.name}##{action}").tap do |signature|
274
278
  if (arguments = data.except("action")).any?
275
279
  signature << "(#{arguments.inspect})"
276
280
  end