actioncable 5.2.4.4 → 6.1.1

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