diff --git a/.travis.yml b/.travis.yml index 475859b1d7..9097edbfdd 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ before_install: - "travis_retry gem update --system" - "travis_retry gem install bundler" - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - - "[[ $GEM != 'ac:integration' ]] || (cd actioncable && yarn install)" + - "[[ $GEM != 'ac:integration' ]] || yarn install" - "[[ $GEM != 'av:ujs' ]] || nvm install node" - "[[ $GEM != 'av:ujs' ]] || node --version" - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)" diff --git a/actioncable/.gitignore b/actioncable/.gitignore index 53c5720e8c..7fa7c03e03 100644 --- a/actioncable/.gitignore +++ b/actioncable/.gitignore @@ -1,3 +1,4 @@ /app/javascript/action_cable/internal.js +/src /test/javascript/compiled/ /tmp/ diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index 17c7e9a3b7..c0ab0485f3 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,42 @@ +* The ActionCable javascript package has been converted from CoffeeScript + to ES2015, and we now publish the source code in the npm distribution. + + This allows ActionCable users to depend on the javascript source code + rather than the compiled code, which can produce smaller javascript bundles. + + This change includes some breaking changes to optional parts of the + ActionCable javascript API: + + - Configuration of the WebSocket adapter and logger adapter have been moved + from properties of `ActionCable` to properties of `ActionCable.adapters`. + If you are currently configuring these adapters you will need to make + these changes when upgrading: + + ```diff + - ActionCable.WebSocket = MyWebSocket + + ActionCable.adapters.WebSocket = MyWebSocket + ``` + ```diff + - ActionCable.logger = myLogger + + ActionCable.adapters.logger = myLogger + ``` + + - The `ActionCable.startDebugging()` and `ActionCable.stopDebugging()` + methods have been removed and replaced with the property + `ActionCable.logger.enabled`. If you are currently using these methods you + will need to make these changes when upgrading: + + ```diff + - ActionCable.startDebugging() + + ActionCable.logger.enabled = true + ``` + ```diff + - ActionCable.stopDebugging() + + ActionCable.logger.enabled = false + ``` + + *Richard Macklin* + * Add `id` option to redis adapter so now you can distinguish ActionCable's redis connections among others. Also, you can set custom id in options. diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js index 07151b9d25..90e8e6b99b 100644 --- a/actioncable/app/assets/javascripts/action_cable.js +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -1,16 +1,22 @@ (function(global, factory) { - typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : global.ActionCable = factory(); -})(this, function() { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {}); +})(this, function(exports) { "use strict"; - var INTERNAL = { - message_types: { - welcome: "welcome", - ping: "ping", - confirmation: "confirm_subscription", - rejection: "reject_subscription" - }, - default_mount_path: "/cable", - protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] + var adapters = { + logger: window.console, + WebSocket: window.WebSocket + }; + var logger = { + log: function log() { + if (this.enabled) { + var _adapters$logger; + for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) { + messages[_key] = arguments[_key]; + } + messages.push(Date.now()); + (_adapters$logger = adapters.logger).log.apply(_adapters$logger, [ "[ActionCable]" ].concat(messages)); + } + } }; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { return typeof obj; @@ -22,6 +28,121 @@ throw new TypeError("Cannot call a class as a function"); } }; + var now = function now() { + return new Date().getTime(); + }; + var secondsSince = function secondsSince(time) { + return (now() - time) / 1e3; + }; + var clamp = function clamp(number, min, max) { + return Math.max(min, Math.min(max, number)); + }; + var ConnectionMonitor = function() { + function ConnectionMonitor(connection) { + classCallCheck(this, ConnectionMonitor); + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + ConnectionMonitor.prototype.start = function start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + document.addEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); + } + }; + ConnectionMonitor.prototype.stop = function stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + document.removeEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor stopped"); + } + }; + ConnectionMonitor.prototype.isRunning = function isRunning() { + return this.startedAt && !this.stoppedAt; + }; + ConnectionMonitor.prototype.recordPing = function recordPing() { + this.pingedAt = now(); + }; + ConnectionMonitor.prototype.recordConnect = function recordConnect() { + this.reconnectAttempts = 0; + this.recordPing(); + delete this.disconnectedAt; + logger.log("ConnectionMonitor recorded connect"); + }; + ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { + this.disconnectedAt = now(); + logger.log("ConnectionMonitor recorded disconnect"); + }; + ConnectionMonitor.prototype.startPolling = function startPolling() { + this.stopPolling(); + this.poll(); + }; + ConnectionMonitor.prototype.stopPolling = function stopPolling() { + clearTimeout(this.pollTimeout); + }; + ConnectionMonitor.prototype.poll = function poll() { + var _this = this; + this.pollTimeout = setTimeout(function() { + _this.reconnectIfStale(); + _this.poll(); + }, this.getPollInterval()); + }; + ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { + var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier; + var interval = multiplier * Math.log(this.reconnectAttempts + 1); + return Math.round(clamp(interval, min, max) * 1e3); + }; + ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { + if (this.connectionIsStale()) { + 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"); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + logger.log("ConnectionMonitor skipping reopening recent disconnect"); + } else { + logger.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + }; + ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { + return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold; + }; + ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + }; + ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { + var _this2 = this; + if (document.visibilityState === "visible") { + setTimeout(function() { + if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { + logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState); + _this2.connection.reopen(); + } + }, 200); + } + }; + return ConnectionMonitor; + }(); + ConnectionMonitor.pollInterval = { + min: 3, + max: 30, + multiplier: 5 + }; + ConnectionMonitor.staleThreshold = 6; + var INTERNAL = { + message_types: { + welcome: "welcome", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] + }; var message_types = INTERNAL.message_types, protocols = INTERNAL.protocols; var supportedProtocols = protocols.slice(0, protocols.length - 1); var indexOf = [].indexOf; @@ -31,7 +152,7 @@ this.open = this.open.bind(this); this.consumer = consumer; this.subscriptions = this.consumer.subscriptions; - this.monitor = new ActionCable.ConnectionMonitor(this); + this.monitor = new ConnectionMonitor(this); this.disconnected = true; } Connection.prototype.send = function send(data) { @@ -44,14 +165,14 @@ }; Connection.prototype.open = function open() { if (this.isActive()) { - ActionCable.log("Attempted to open WebSocket, but existing socket is " + this.getState()); + logger.log("Attempted to open WebSocket, but existing socket is " + this.getState()); return false; } else { - ActionCable.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols); + logger.log("Opening WebSocket, current state is " + this.getState() + ", subprotocols: " + protocols); if (this.webSocket) { this.uninstallEventHandlers(); } - this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols); + this.webSocket = new adapters.WebSocket(this.consumer.url, protocols); this.installEventHandlers(); this.monitor.start(); return true; @@ -69,14 +190,14 @@ } }; Connection.prototype.reopen = function reopen() { - ActionCable.log("Reopening WebSocket, current state is " + this.getState()); + logger.log("Reopening WebSocket, current state is " + this.getState()); if (this.isActive()) { try { return this.close(); } catch (error) { - ActionCable.log("Failed to reopen WebSocket", error); + logger.log("Failed to reopen WebSocket", error); } finally { - ActionCable.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); + logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); setTimeout(this.open, this.constructor.reopenDelay); } } else { @@ -150,17 +271,17 @@ } }, open: function open() { - ActionCable.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); + logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); this.disconnected = false; if (!this.isProtocolSupported()) { - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting."); + logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); return this.close({ allowReconnect: false }); } }, close: function close(event) { - ActionCable.log("WebSocket onclose event"); + logger.log("WebSocket onclose event"); if (this.disconnected) { return; } @@ -171,139 +292,9 @@ }); }, error: function error() { - ActionCable.log("WebSocket onerror event"); + logger.log("WebSocket onerror event"); } }; - var now = function now() { - return new Date().getTime(); - }; - var secondsSince = function secondsSince(time) { - return (now() - time) / 1e3; - }; - var clamp = function clamp(number, min, max) { - return Math.max(min, Math.min(max, number)); - }; - var ConnectionMonitor = function() { - function ConnectionMonitor(connection) { - classCallCheck(this, ConnectionMonitor); - this.visibilityDidChange = this.visibilityDidChange.bind(this); - this.connection = connection; - this.reconnectAttempts = 0; - } - ConnectionMonitor.prototype.start = function start() { - if (!this.isRunning()) { - this.startedAt = now(); - delete this.stoppedAt; - this.startPolling(); - document.addEventListener("visibilitychange", this.visibilityDidChange); - ActionCable.log("ConnectionMonitor started. pollInterval = " + this.getPollInterval() + " ms"); - } - }; - ConnectionMonitor.prototype.stop = function stop() { - if (this.isRunning()) { - this.stoppedAt = now(); - this.stopPolling(); - document.removeEventListener("visibilitychange", this.visibilityDidChange); - ActionCable.log("ConnectionMonitor stopped"); - } - }; - ConnectionMonitor.prototype.isRunning = function isRunning() { - return this.startedAt && !this.stoppedAt; - }; - ConnectionMonitor.prototype.recordPing = function recordPing() { - this.pingedAt = now(); - }; - ConnectionMonitor.prototype.recordConnect = function recordConnect() { - this.reconnectAttempts = 0; - this.recordPing(); - delete this.disconnectedAt; - ActionCable.log("ConnectionMonitor recorded connect"); - }; - ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { - this.disconnectedAt = now(); - ActionCable.log("ConnectionMonitor recorded disconnect"); - }; - ConnectionMonitor.prototype.startPolling = function startPolling() { - this.stopPolling(); - this.poll(); - }; - ConnectionMonitor.prototype.stopPolling = function stopPolling() { - clearTimeout(this.pollTimeout); - }; - ConnectionMonitor.prototype.poll = function poll() { - var _this = this; - this.pollTimeout = setTimeout(function() { - _this.reconnectIfStale(); - _this.poll(); - }, this.getPollInterval()); - }; - ConnectionMonitor.prototype.getPollInterval = function getPollInterval() { - var _constructor$pollInte = this.constructor.pollInterval, min = _constructor$pollInte.min, max = _constructor$pollInte.max, multiplier = _constructor$pollInte.multiplier; - var interval = multiplier * Math.log(this.reconnectAttempts + 1); - return Math.round(clamp(interval, min, max) * 1e3); - }; - ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { - if (this.connectionIsStale()) { - 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"); - this.reconnectAttempts++; - if (this.disconnectedRecently()) { - ActionCable.log("ConnectionMonitor skipping reopening recent disconnect"); - } else { - ActionCable.log("ConnectionMonitor reopening"); - this.connection.reopen(); - } - } - }; - ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { - return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { - return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { - var _this2 = this; - if (document.visibilityState === "visible") { - setTimeout(function() { - if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { - ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = " + document.visibilityState); - _this2.connection.reopen(); - } - }, 200); - } - }; - return ConnectionMonitor; - }(); - ConnectionMonitor.pollInterval = { - min: 3, - max: 30, - multiplier: 5 - }; - ConnectionMonitor.staleThreshold = 6; - var Consumer = function() { - function Consumer(url) { - classCallCheck(this, Consumer); - this.url = url; - this.subscriptions = new ActionCable.Subscriptions(this); - this.connection = new ActionCable.Connection(this); - } - Consumer.prototype.send = function send(data) { - return this.connection.send(data); - }; - Consumer.prototype.connect = function connect() { - return this.connection.open(); - }; - Consumer.prototype.disconnect = function disconnect() { - return this.connection.close({ - allowReconnect: false - }); - }; - Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { - if (!this.connection.isActive()) { - return this.connection.open(); - } - }; - return Consumer; - }(); var extend = function extend(object, properties) { if (properties != null) { for (var key in properties) { @@ -350,7 +341,7 @@ var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : { channel: channel }; - var subscription = new ActionCable.Subscription(this.consumer, params, mixin); + var subscription = new Subscription(this.consumer, params, mixin); return this.add(subscription); }; Subscriptions.prototype.add = function add(subscription) { @@ -424,53 +415,65 @@ }; return Subscriptions; }(); - var ActionCable = { - Connection: Connection, - ConnectionMonitor: ConnectionMonitor, - Consumer: Consumer, - INTERNAL: INTERNAL, - Subscription: Subscription, - Subscriptions: Subscriptions, - WebSocket: window.WebSocket, - logger: window.console, - createConsumer: function createConsumer(url) { - if (url == null) { - var urlConfig = this.getConfig("url"); - url = urlConfig ? urlConfig : this.INTERNAL.default_mount_path; - } - return new Consumer(this.createWebSocketURL(url)); - }, - getConfig: function getConfig(name) { - var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); - return element ? element.getAttribute("content") : undefined; - }, - createWebSocketURL: function createWebSocketURL(url) { - if (url && !/^wss?:/i.test(url)) { - var a = document.createElement("a"); - a.href = url; - a.href = a.href; - a.protocol = a.protocol.replace("http", "ws"); - return a.href; - } else { - return url; - } - }, - startDebugging: function startDebugging() { - this.debugging = true; - }, - stopDebugging: function stopDebugging() { - this.debugging = null; - }, - log: function log() { - if (this.debugging) { - var _logger; - for (var _len = arguments.length, messages = Array(_len), _key = 0; _key < _len; _key++) { - messages[_key] = arguments[_key]; - } - messages.push(Date.now()); - (_logger = this.logger).log.apply(_logger, [ "[ActionCable]" ].concat(messages)); - } + var Consumer = function() { + function Consumer(url) { + classCallCheck(this, Consumer); + this.url = url; + this.subscriptions = new Subscriptions(this); + this.connection = new Connection(this); } - }; - return ActionCable; + Consumer.prototype.send = function send(data) { + return this.connection.send(data); + }; + Consumer.prototype.connect = function connect() { + return this.connection.open(); + }; + Consumer.prototype.disconnect = function disconnect() { + return this.connection.close({ + allowReconnect: false + }); + }; + Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + }; + return Consumer; + }(); + function createConsumer(url) { + if (url == null) { + var urlConfig = getConfig("url"); + url = urlConfig ? urlConfig : INTERNAL.default_mount_path; + } + return new Consumer(createWebSocketURL(url)); + } + function getConfig(name) { + var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); + return element ? element.getAttribute("content") : undefined; + } + function createWebSocketURL(url) { + if (url && !/^wss?:/i.test(url)) { + var a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } + } + exports.Connection = Connection; + exports.ConnectionMonitor = ConnectionMonitor; + exports.Consumer = Consumer; + exports.INTERNAL = INTERNAL; + exports.Subscription = Subscription; + exports.Subscriptions = Subscriptions; + exports.adapters = adapters; + exports.logger = logger; + exports.createConsumer = createConsumer; + exports.getConfig = getConfig; + exports.createWebSocketURL = createWebSocketURL; + Object.defineProperty(exports, "__esModule", { + value: true + }); }); diff --git a/actioncable/app/javascript/action_cable/adapters.js b/actioncable/app/javascript/action_cable/adapters.js new file mode 100644 index 0000000000..9ba6d338ee --- /dev/null +++ b/actioncable/app/javascript/action_cable/adapters.js @@ -0,0 +1,4 @@ +export default { + logger: window.console, + WebSocket: window.WebSocket +} diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 4ad436f2c0..e3ff8bde24 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -1,5 +1,7 @@ -import ActionCable from "./index" +import adapters from "./adapters" +import ConnectionMonitor from "./connection_monitor" import INTERNAL from "./internal" +import logger from "./logger" // Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. @@ -13,7 +15,7 @@ class Connection { this.open = this.open.bind(this) this.consumer = consumer this.subscriptions = this.consumer.subscriptions - this.monitor = new ActionCable.ConnectionMonitor(this) + this.monitor = new ConnectionMonitor(this) this.disconnected = true } @@ -28,12 +30,12 @@ class Connection { open() { if (this.isActive()) { - ActionCable.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`) return false } else { - ActionCable.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`) if (this.webSocket) { this.uninstallEventHandlers() } - this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) + this.webSocket = new adapters.WebSocket(this.consumer.url, protocols) this.installEventHandlers() this.monitor.start() return true @@ -46,15 +48,15 @@ class Connection { } reopen() { - ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) + logger.log(`Reopening WebSocket, current state is ${this.getState()}`) if (this.isActive()) { try { return this.close() } catch (error) { - ActionCable.log("Failed to reopen WebSocket", error) + logger.log("Failed to reopen WebSocket", error) } finally { - ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) setTimeout(this.open, this.constructor.reopenDelay) } } else { @@ -132,16 +134,16 @@ Connection.prototype.events = { }, open() { - ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) this.disconnected = false if (!this.isProtocolSupported()) { - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + logger.log("Protocol is unsupported. Stopping monitor and disconnecting.") return this.close({allowReconnect: false}) } }, close(event) { - ActionCable.log("WebSocket onclose event") + logger.log("WebSocket onclose event") if (this.disconnected) { return } this.disconnected = true this.monitor.recordDisconnect() @@ -149,7 +151,7 @@ Connection.prototype.events = { }, error() { - ActionCable.log("WebSocket onerror event") + logger.log("WebSocket onerror event") } } diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js index cd1e4602d8..f0e75ae137 100644 --- a/actioncable/app/javascript/action_cable/connection_monitor.js +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -1,4 +1,4 @@ -import ActionCable from "./index" +import logger from "./logger" // Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting // revival reconnections if things go astray. Internal class, not intended for direct user manipulation. @@ -22,7 +22,7 @@ class ConnectionMonitor { delete this.stoppedAt this.startPolling() document.addEventListener("visibilitychange", this.visibilityDidChange) - ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) + logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) } } @@ -31,7 +31,7 @@ class ConnectionMonitor { this.stoppedAt = now() this.stopPolling() document.removeEventListener("visibilitychange", this.visibilityDidChange) - ActionCable.log("ConnectionMonitor stopped") + logger.log("ConnectionMonitor stopped") } } @@ -47,12 +47,12 @@ class ConnectionMonitor { this.reconnectAttempts = 0 this.recordPing() delete this.disconnectedAt - ActionCable.log("ConnectionMonitor recorded connect") + logger.log("ConnectionMonitor recorded connect") } recordDisconnect() { this.disconnectedAt = now() - ActionCable.log("ConnectionMonitor recorded disconnect") + logger.log("ConnectionMonitor recorded disconnect") } // Private @@ -82,12 +82,12 @@ class ConnectionMonitor { reconnectIfStale() { if (this.connectionIsStale()) { - 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`) + 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`) this.reconnectAttempts++ if (this.disconnectedRecently()) { - ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") + logger.log("ConnectionMonitor skipping reopening recent disconnect") } else { - ActionCable.log("ConnectionMonitor reopening") + logger.log("ConnectionMonitor reopening") this.connection.reopen() } } @@ -105,7 +105,7 @@ class ConnectionMonitor { if (document.visibilityState === "visible") { setTimeout(() => { if (this.connectionIsStale() || !this.connection.isOpen()) { - ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`) + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`) this.connection.reopen() } } diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js index c484ceebbd..e8440f39f5 100644 --- a/actioncable/app/javascript/action_cable/consumer.js +++ b/actioncable/app/javascript/action_cable/consumer.js @@ -1,4 +1,5 @@ -import ActionCable from "./index" +import Connection from "./connection" +import Subscriptions from "./subscriptions" // The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established, // the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates. @@ -29,8 +30,8 @@ import ActionCable from "./index" export default class Consumer { constructor(url) { this.url = url - this.subscriptions = new ActionCable.Subscriptions(this) - this.connection = new ActionCable.Connection(this) + this.subscriptions = new Subscriptions(this) + this.connection = new Connection(this) } send(data) { diff --git a/actioncable/app/javascript/action_cable/index.js b/actioncable/app/javascript/action_cable/index.js index eb0c4844df..9f41c14e94 100644 --- a/actioncable/app/javascript/action_cable/index.js +++ b/actioncable/app/javascript/action_cable/index.js @@ -4,55 +4,42 @@ import Consumer from "./consumer" import INTERNAL from "./internal" import Subscription from "./subscription" import Subscriptions from "./subscriptions" +import adapters from "./adapters" +import logger from "./logger" -export default { +export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, Subscriptions, - WebSocket: window.WebSocket, - logger: window.console, + adapters, + logger, +} - createConsumer(url) { - if (url == null) { - const urlConfig = this.getConfig("url") - url = (urlConfig ? urlConfig : this.INTERNAL.default_mount_path) - } - return new Consumer(this.createWebSocketURL(url)) - }, +export function createConsumer(url) { + if (url == null) { + const urlConfig = getConfig("url") + url = (urlConfig ? urlConfig : INTERNAL.default_mount_path) + } + return new Consumer(createWebSocketURL(url)) +} - getConfig(name) { - const element = document.head.querySelector(`meta[name='action-cable-${name}']`) - return (element ? element.getAttribute("content") : undefined) - }, +export function getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`) + return (element ? element.getAttribute("content") : undefined) +} - createWebSocketURL(url) { - if (url && !/^wss?:/i.test(url)) { - const a = document.createElement("a") - a.href = url - // Fix populating Location properties in IE. Otherwise, protocol will be blank. - a.href = a.href - a.protocol = a.protocol.replace("http", "ws") - return a.href - } else { - return url - } - }, - - startDebugging() { - this.debugging = true - }, - - stopDebugging() { - this.debugging = null - }, - - log(...messages) { - if (this.debugging) { - messages.push(Date.now()) - this.logger.log("[ActionCable]", ...messages) - } +export function createWebSocketURL(url) { + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a") + a.href = url + // Fix populating Location properties in IE. Otherwise, protocol will be blank. + a.href = a.href + a.protocol = a.protocol.replace("http", "ws") + return a.href + } else { + return url } } diff --git a/actioncable/app/javascript/action_cable/logger.js b/actioncable/app/javascript/action_cable/logger.js new file mode 100644 index 0000000000..ef4661ead1 --- /dev/null +++ b/actioncable/app/javascript/action_cable/logger.js @@ -0,0 +1,10 @@ +import adapters from "./adapters" + +export default { + log(...messages) { + if (this.enabled) { + messages.push(Date.now()) + adapters.logger.log("[ActionCable]", ...messages) + } + }, +} diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js index 712ff50d28..867cafb407 100644 --- a/actioncable/app/javascript/action_cable/subscriptions.js +++ b/actioncable/app/javascript/action_cable/subscriptions.js @@ -1,4 +1,4 @@ -import ActionCable from "./index" +import Subscription from "./subscription" // Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user // us ActionCable.Subscriptions#create, and it should be called through the consumer like so: @@ -18,7 +18,7 @@ export default class Subscriptions { create(channelName, mixin) { const channel = channelName const params = typeof channel === "object" ? channel : {channel} - const subscription = new ActionCable.Subscription(this.consumer, params, mixin) + const subscription = new Subscription(this.consumer, params, mixin) return this.add(subscription) } diff --git a/actioncable/package.json b/actioncable/package.json index ba641b64a3..db78c1a09a 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -4,7 +4,8 @@ "description": "WebSocket framework for Ruby on Rails.", "main": "app/assets/javascripts/action_cable.js", "files": [ - "app/assets/javascripts/*.js" + "app/assets/javascripts/*.js", + "src/*.js" ], "repository": { "type": "git", @@ -43,6 +44,7 @@ "prebuild": "yarn lint && bundle exec rake assets:codegen", "build": "rollup --config rollup.config.js", "lint": "eslint app/javascript", + "prepublishOnly": "rm -rf src && cp -R app/javascript/action_cable src", "pretest": "bundle exec rake assets:codegen && rollup --config rollup.config.test.js", "test": "karma start" } diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js index 03046526aa..64727e0887 100644 --- a/actioncable/rollup.config.js +++ b/actioncable/rollup.config.js @@ -1,5 +1,3 @@ -import resolve from "rollup-plugin-node-resolve" -import commonjs from "rollup-plugin-commonjs" import babel from "rollup-plugin-babel" import uglify from "rollup-plugin-uglify" @@ -20,8 +18,6 @@ export default { name: "ActionCable" }, plugins: [ - resolve(), - commonjs(), babel(), uglify(uglifyOptions) ] diff --git a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js index f1abea331b..d1dabc9fc4 100644 --- a/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js +++ b/actioncable/test/javascript/src/test_helpers/consumer_test_helper.js @@ -1,5 +1,5 @@ import { WebSocket as MockWebSocket, Server as MockServer } from "mock-socket" -import ActionCable from "../../../../app/javascript/action_cable/index" +import * as ActionCable from "../../../../app/javascript/action_cable/index" import {defer, testURL} from "./index" export default function(name, options, callback) { @@ -14,7 +14,7 @@ export default function(name, options, callback) { return QUnit.test(name, function(assert) { const doneAsync = assert.async() - ActionCable.WebSocket = MockWebSocket + ActionCable.adapters.WebSocket = MockWebSocket const server = new MockServer(options.url) const consumer = ActionCable.createConsumer(options.url) diff --git a/actioncable/test/javascript/src/test_helpers/index.js b/actioncable/test/javascript/src/test_helpers/index.js index 5fa46c21ae..0cd4e260b3 100644 --- a/actioncable/test/javascript/src/test_helpers/index.js +++ b/actioncable/test/javascript/src/test_helpers/index.js @@ -1,4 +1,4 @@ -import ActionCable from "../../../../app/javascript/action_cable/index" +import * as ActionCable from "../../../../app/javascript/action_cable/index" export const testURL = "ws://cable.example.com/" @@ -6,5 +6,5 @@ export function defer(callback) { setTimeout(callback, 1) } -const originalWebSocket = ActionCable.WebSocket -QUnit.testDone(() => ActionCable.WebSocket = originalWebSocket) +const originalWebSocket = ActionCable.adapters.WebSocket +QUnit.testDone(() => ActionCable.adapters.WebSocket = originalWebSocket) diff --git a/actioncable/test/javascript/src/unit/action_cable_test.js b/actioncable/test/javascript/src/unit/action_cable_test.js index 8847d87545..daad900aca 100644 --- a/actioncable/test/javascript/src/unit/action_cable_test.js +++ b/actioncable/test/javascript/src/unit/action_cable_test.js @@ -1,4 +1,4 @@ -import ActionCable from "../../../../app/javascript/action_cable/index" +import * as ActionCable from "../../../../app/javascript/action_cable/index" import {testURL} from "../test_helpers/index" const {module, test} = QUnit @@ -7,23 +7,13 @@ module("ActionCable", () => { module("Adapters", () => { module("WebSocket", () => { test("default is window.WebSocket", assert => { - assert.equal(ActionCable.WebSocket, window.WebSocket) - }) - - test("configurable", assert => { - ActionCable.WebSocket = "" - assert.equal(ActionCable.WebSocket, "") + assert.equal(ActionCable.adapters.WebSocket, window.WebSocket) }) }) module("logger", () => { test("default is window.console", assert => { - assert.equal(ActionCable.logger, window.console) - }) - - test("configurable", assert => { - ActionCable.logger = "" - assert.equal(ActionCable.logger, "") + assert.equal(ActionCable.adapters.logger, window.console) }) }) })