From 4a23cb3415eac03d76623112576559a722d1f23d Mon Sep 17 00:00:00 2001 From: David Heinemeier Hansson Date: Fri, 6 Aug 2021 14:00:43 +0200 Subject: [PATCH] Output Action Cable JS without transpiling and as ESM (#42856) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Output Action Cable JS without transpiling and as ESM * Retain umd version under the old name, generate ESM version + duplicate under new name * Precompile JavaScripts for direct asset pipeline use * We've dropped support for IE11 * Include deprecation notice for the old file reference Thanks @rafaelfranca 👍 * Allow app to opt out of precompiling actioncable js assets cc @rafaelfranca * Add changelog entries --- actioncable/.eslintrc | 3 +- actioncable/CHANGELOG.md | 12 + .../app/assets/javascripts/action_cable.js | 429 +++++++---------- .../app/assets/javascripts/actioncable.esm.js | 442 ++++++++++++++++++ .../app/assets/javascripts/actioncable.js | 440 +++++++++++++++++ .../index_with_name_deprecation.js | 2 + actioncable/karma.conf.js | 1 - actioncable/lib/action_cable/engine.rb | 9 + .../lib/action_cable/server/configuration.rb | 1 + actioncable/package.json | 13 +- actioncable/rollup.config.js | 53 ++- actioncable/rollup.config.test.js | 8 +- yarn.lock | 8 +- 13 files changed, 1134 insertions(+), 287 deletions(-) create mode 100644 actioncable/app/assets/javascripts/actioncable.esm.js create mode 100644 actioncable/app/assets/javascripts/actioncable.js create mode 100644 actioncable/app/javascript/action_cable/index_with_name_deprecation.js diff --git a/actioncable/.eslintrc b/actioncable/.eslintrc index 3d9ecd4bce..b85ef26b31 100644 --- a/actioncable/.eslintrc +++ b/actioncable/.eslintrc @@ -3,7 +3,8 @@ "rules": { "semi": ["error", "never"], "quotes": ["error", "double"], - "no-unused-vars": ["error", { "vars": "all", "args": "none" }] + "no-unused-vars": ["error", { "vars": "all", "args": "none" }], + "no-console": "off" }, "plugins": [ "import" diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index d2582883aa..b64fb1bea1 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,3 +1,15 @@ +* Compile ESM package that can be used directly in the browser as actioncable.esm.js. + + *DHH* + +* Move action_cable.js to actioncable.js to match naming convention used for other Rails frameworks, and use JS console to communicate the deprecation. + + *DHH* + +* Stop transpiling the UMD package generated as actioncable.js and drop the IE11 testing that relied on that. + + *DHH* + * Truncate broadcast logging messages. *J Smith* diff --git a/actioncable/app/assets/javascripts/action_cable.js b/actioncable/app/assets/javascripts/action_cable.js index 49a84c65b5..371e0ca4f0 100644 --- a/actioncable/app/assets/javascripts/action_cable.js +++ b/actioncable/app/assets/javascripts/action_cable.js @@ -1,153 +1,113 @@ (function(global, factory) { - typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : factory(global.ActionCable = {}); -})(this, function(exports) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, + factory(global.ActionCable = {})); +})(this, (function(exports) { "use strict"; var adapters = { logger: self.console, WebSocket: self.WebSocket }; var logger = { - log: function log() { + log(...messages) { 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)); + adapters.logger.log("[ActionCable]", ...messages); } } }; - var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function(obj) { - return typeof obj; - } : function(obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; - }; - var classCallCheck = function(instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } - }; - var createClass = function() { - function defineProperties(target, props) { - for (var i = 0; i < props.length; i++) { - var descriptor = props[i]; - descriptor.enumerable = descriptor.enumerable || false; - descriptor.configurable = true; - if ("value" in descriptor) descriptor.writable = true; - Object.defineProperty(target, descriptor.key, descriptor); - } - } - return function(Constructor, protoProps, staticProps) { - if (protoProps) defineProperties(Constructor.prototype, protoProps); - if (staticProps) defineProperties(Constructor, staticProps); - return Constructor; - }; - }(); - var now = function now() { - return new Date().getTime(); - }; - var secondsSince = function secondsSince(time) { - return (now() - time) / 1e3; - }; - var ConnectionMonitor = function() { - function ConnectionMonitor(connection) { - classCallCheck(this, ConnectionMonitor); + const now = () => (new Date).getTime(); + const secondsSince = time => (now() - time) / 1e3; + class ConnectionMonitor { + constructor(connection) { this.visibilityDidChange = this.visibilityDidChange.bind(this); this.connection = connection; this.reconnectAttempts = 0; } - ConnectionMonitor.prototype.start = function start() { + start() { if (!this.isRunning()) { this.startedAt = now(); delete this.stoppedAt; this.startPolling(); addEventListener("visibilitychange", this.visibilityDidChange); - logger.log("ConnectionMonitor started. stale threshold = " + this.constructor.staleThreshold + " s"); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); } - }; - ConnectionMonitor.prototype.stop = function stop() { + } + stop() { if (this.isRunning()) { this.stoppedAt = now(); this.stopPolling(); removeEventListener("visibilitychange", this.visibilityDidChange); logger.log("ConnectionMonitor stopped"); } - }; - ConnectionMonitor.prototype.isRunning = function isRunning() { + } + isRunning() { return this.startedAt && !this.stoppedAt; - }; - ConnectionMonitor.prototype.recordPing = function recordPing() { + } + recordPing() { this.pingedAt = now(); - }; - ConnectionMonitor.prototype.recordConnect = function recordConnect() { + } + recordConnect() { this.reconnectAttempts = 0; this.recordPing(); delete this.disconnectedAt; logger.log("ConnectionMonitor recorded connect"); - }; - ConnectionMonitor.prototype.recordDisconnect = function recordDisconnect() { + } + recordDisconnect() { this.disconnectedAt = now(); logger.log("ConnectionMonitor recorded disconnect"); - }; - ConnectionMonitor.prototype.startPolling = function startPolling() { + } + startPolling() { this.stopPolling(); this.poll(); - }; - ConnectionMonitor.prototype.stopPolling = function stopPolling() { + } + 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 = this.constructor, staleThreshold = _constructor.staleThreshold, reconnectionBackoffRate = _constructor.reconnectionBackoffRate; - var backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); - var jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; - var jitter = jitterMax * Math.random(); + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); return staleThreshold * 1e3 * backoff * (1 + jitter); - }; - ConnectionMonitor.prototype.reconnectIfStale = function reconnectIfStale() { + } + reconnectIfStale() { if (this.connectionIsStale()) { - logger.log("ConnectionMonitor detected stale connection. reconnectAttempts = " + this.reconnectAttempts + ", time stale = " + secondsSince(this.refreshedAt) + " s, stale threshold = " + this.constructor.staleThreshold + " s"); + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); this.reconnectAttempts++; if (this.disconnectedRecently()) { - logger.log("ConnectionMonitor skipping reopening recent disconnect. time disconnected = " + secondsSince(this.disconnectedAt) + " s"); + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); } else { logger.log("ConnectionMonitor reopening"); this.connection.reopen(); } } - }; - ConnectionMonitor.prototype.connectionIsStale = function connectionIsStale() { + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.disconnectedRecently = function disconnectedRecently() { + } + disconnectedRecently() { return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; - }; - ConnectionMonitor.prototype.visibilityDidChange = function visibilityDidChange() { - var _this2 = this; + } + visibilityDidChange() { if (document.visibilityState === "visible") { - setTimeout(function() { - if (_this2.connectionIsStale() || !_this2.connection.isOpen()) { - logger.log("ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = " + document.visibilityState); - _this2.connection.reopen(); + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); } - }, 200); + }), 200); } - }; - createClass(ConnectionMonitor, [ { - key: "refreshedAt", - get: function get$$1() { - return this.pingedAt ? this.pingedAt : this.startedAt; - } - } ]); - return ConnectionMonitor; - }(); + } + } ConnectionMonitor.staleThreshold = 6; ConnectionMonitor.reconnectionBackoffRate = .15; var INTERNAL = { @@ -166,32 +126,31 @@ 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; - var Connection = function() { - function Connection(consumer) { - classCallCheck(this, Connection); + const {message_types: message_types, protocols: protocols} = INTERNAL; + const supportedProtocols = protocols.slice(0, protocols.length - 1); + const indexOf = [].indexOf; + class Connection { + constructor(consumer) { this.open = this.open.bind(this); this.consumer = consumer; this.subscriptions = this.consumer.subscriptions; this.monitor = new ConnectionMonitor(this); this.disconnected = true; } - Connection.prototype.send = function send(data) { + send(data) { if (this.isOpen()) { this.webSocket.send(JSON.stringify(data)); return true; } else { return false; } - }; - Connection.prototype.open = function open() { + } + open() { if (this.isActive()) { - logger.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 { - logger.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(); } @@ -200,90 +159,85 @@ this.monitor.start(); return true; } - }; - Connection.prototype.close = function close() { - var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { - allowReconnect: true - }, allowReconnect = _ref.allowReconnect; + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { if (!allowReconnect) { this.monitor.stop(); } if (this.isActive()) { return this.webSocket.close(); } - }; - Connection.prototype.reopen = function reopen() { - logger.log("Reopening WebSocket, current state is " + this.getState()); + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); if (this.isActive()) { try { return this.close(); } catch (error) { logger.log("Failed to reopen WebSocket", error); } finally { - logger.log("Reopening WebSocket in " + this.constructor.reopenDelay + "ms"); + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); setTimeout(this.open, this.constructor.reopenDelay); } } else { return this.open(); } - }; - Connection.prototype.getProtocol = function getProtocol() { + } + getProtocol() { if (this.webSocket) { return this.webSocket.protocol; } - }; - Connection.prototype.isOpen = function isOpen() { + } + isOpen() { return this.isState("open"); - }; - Connection.prototype.isActive = function isActive() { + } + isActive() { return this.isState("open", "connecting"); - }; - Connection.prototype.isProtocolSupported = function isProtocolSupported() { + } + isProtocolSupported() { return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; - }; - Connection.prototype.isState = function isState() { - for (var _len = arguments.length, states = Array(_len), _key = 0; _key < _len; _key++) { - states[_key] = arguments[_key]; - } + } + isState(...states) { return indexOf.call(states, this.getState()) >= 0; - }; - Connection.prototype.getState = function getState() { + } + getState() { if (this.webSocket) { - for (var state in adapters.WebSocket) { + for (let state in adapters.WebSocket) { if (adapters.WebSocket[state] === this.webSocket.readyState) { return state.toLowerCase(); } } } return null; - }; - Connection.prototype.installEventHandlers = function installEventHandlers() { - for (var eventName in this.events) { - var handler = this.events[eventName].bind(this); - this.webSocket["on" + eventName] = handler; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; } - }; - Connection.prototype.uninstallEventHandlers = function uninstallEventHandlers() { - for (var eventName in this.events) { - this.webSocket["on" + eventName] = function() {}; + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; } - }; - return Connection; - }(); + } + } Connection.reopenDelay = 500; Connection.prototype.events = { - message: function message(event) { + message(event) { if (!this.isProtocolSupported()) { return; } - 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; + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); switch (type) { case message_types.welcome: this.monitor.recordConnect(); return this.subscriptions.reload(); case message_types.disconnect: - logger.log("Disconnecting. Reason: " + reason); + logger.log(`Disconnecting. Reason: ${reason}`); return this.close({ allowReconnect: reconnect }); @@ -301,8 +255,8 @@ return this.subscriptions.notify(identifier, "received", message); } }, - open: function open() { - logger.log("WebSocket onopen event, using '" + this.getProtocol() + "' subprotocol"); + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); this.disconnected = false; if (!this.isProtocolSupported()) { logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); @@ -311,7 +265,7 @@ }); } }, - close: function close(event) { + close(event) { logger.log("WebSocket onclose event"); if (this.disconnected) { return; @@ -322,167 +276,136 @@ willAttemptReconnect: this.monitor.isRunning() }); }, - error: function error() { + error() { logger.log("WebSocket onerror event"); } }; - var extend = function extend(object, properties) { + const extend = function(object, properties) { if (properties != null) { - for (var key in properties) { - var value = properties[key]; + for (let key in properties) { + const value = properties[key]; object[key] = value; } } return object; }; - var Subscription = function() { - function Subscription(consumer) { - var params = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var mixin = arguments[2]; - classCallCheck(this, Subscription); + class Subscription { + constructor(consumer, params = {}, mixin) { this.consumer = consumer; this.identifier = JSON.stringify(params); extend(this, mixin); } - Subscription.prototype.perform = function perform(action) { - var data = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; + perform(action, data = {}) { data.action = action; return this.send(data); - }; - Subscription.prototype.send = function send(data) { + } + send(data) { return this.consumer.send({ command: "message", identifier: this.identifier, data: JSON.stringify(data) }); - }; - Subscription.prototype.unsubscribe = function unsubscribe() { + } + unsubscribe() { return this.consumer.subscriptions.remove(this); - }; - return Subscription; - }(); - var Subscriptions = function() { - function Subscriptions(consumer) { - classCallCheck(this, Subscriptions); + } + } + class Subscriptions { + constructor(consumer) { this.consumer = consumer; this.subscriptions = []; } - Subscriptions.prototype.create = function create(channelName, mixin) { - var channel = channelName; - var params = (typeof channel === "undefined" ? "undefined" : _typeof(channel)) === "object" ? channel : { + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { channel: channel }; - var subscription = new Subscription(this.consumer, params, mixin); + const subscription = new Subscription(this.consumer, params, mixin); return this.add(subscription); - }; - Subscriptions.prototype.add = function add(subscription) { + } + add(subscription) { this.subscriptions.push(subscription); this.consumer.ensureActiveConnection(); this.notify(subscription, "initialized"); this.sendCommand(subscription, "subscribe"); return subscription; - }; - Subscriptions.prototype.remove = function remove(subscription) { + } + remove(subscription) { this.forget(subscription); if (!this.findAll(subscription.identifier).length) { this.sendCommand(subscription, "unsubscribe"); } return subscription; - }; - Subscriptions.prototype.reject = function reject(identifier) { - var _this = this; - return this.findAll(identifier).map(function(subscription) { - _this.forget(subscription); - _this.notify(subscription, "rejected"); + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); return subscription; - }); - }; - Subscriptions.prototype.forget = function forget(subscription) { - this.subscriptions = this.subscriptions.filter(function(s) { - return s !== subscription; - }); + })); + } + forget(subscription) { + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); return subscription; - }; - Subscriptions.prototype.findAll = function findAll(identifier) { - return this.subscriptions.filter(function(s) { - return s.identifier === identifier; - }); - }; - Subscriptions.prototype.reload = function reload() { - var _this2 = this; - return this.subscriptions.map(function(subscription) { - return _this2.sendCommand(subscription, "subscribe"); - }); - }; - Subscriptions.prototype.notifyAll = function notifyAll(callbackName) { - var _this3 = this; - for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - args[_key - 1] = arguments[_key]; - } - return this.subscriptions.map(function(subscription) { - return _this3.notify.apply(_this3, [ subscription, callbackName ].concat(args)); - }); - }; - Subscriptions.prototype.notify = function notify(subscription, callbackName) { - for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { - args[_key2 - 2] = arguments[_key2]; - } - var subscriptions = void 0; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe"))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; if (typeof subscription === "string") { subscriptions = this.findAll(subscription); } else { subscriptions = [ subscription ]; } - return subscriptions.map(function(subscription) { - return typeof subscription[callbackName] === "function" ? subscription[callbackName].apply(subscription, args) : undefined; - }); - }; - Subscriptions.prototype.sendCommand = function sendCommand(subscription, command) { - var identifier = subscription.identifier; + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; return this.consumer.send({ command: command, identifier: identifier }); - }; - return Subscriptions; - }(); - var Consumer = function() { - function Consumer(url) { - classCallCheck(this, Consumer); + } + } + class Consumer { + constructor(url) { this._url = url; this.subscriptions = new Subscriptions(this); this.connection = new Connection(this); } - Consumer.prototype.send = function send(data) { + get url() { + return createWebSocketURL(this._url); + } + send(data) { return this.connection.send(data); - }; - Consumer.prototype.connect = function connect() { + } + connect() { return this.connection.open(); - }; - Consumer.prototype.disconnect = function disconnect() { + } + disconnect() { return this.connection.close({ allowReconnect: false }); - }; - Consumer.prototype.ensureActiveConnection = function ensureActiveConnection() { + } + ensureActiveConnection() { if (!this.connection.isActive()) { return this.connection.open(); } - }; - createClass(Consumer, [ { - key: "url", - get: function get$$1() { - return createWebSocketURL(this._url); - } - } ]); - return Consumer; - }(); + } + } function createWebSocketURL(url) { if (typeof url === "function") { url = url(); } if (url && !/^wss?:/i.test(url)) { - var a = document.createElement("a"); + const a = document.createElement("a"); a.href = url; a.href = a.href; a.protocol = a.protocol.replace("http", "ws"); @@ -491,16 +414,16 @@ return url; } } - function createConsumer() { - var url = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getConfig("url") || INTERNAL.default_mount_path; + function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { return new Consumer(url); } function getConfig(name) { - var element = document.head.querySelector("meta[name='action-cable-" + name + "']"); + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); if (element) { return element.getAttribute("content"); } } + console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8"); exports.Connection = Connection; exports.ConnectionMonitor = ConnectionMonitor; exports.Consumer = Consumer; @@ -508,11 +431,11 @@ exports.Subscription = Subscription; exports.Subscriptions = Subscriptions; exports.adapters = adapters; - exports.createWebSocketURL = createWebSocketURL; - exports.logger = logger; exports.createConsumer = createConsumer; + exports.createWebSocketURL = createWebSocketURL; exports.getConfig = getConfig; + exports.logger = logger; Object.defineProperty(exports, "__esModule", { value: true }); -}); +})); diff --git a/actioncable/app/assets/javascripts/actioncable.esm.js b/actioncable/app/assets/javascripts/actioncable.esm.js new file mode 100644 index 0000000000..d69e8ecd89 --- /dev/null +++ b/actioncable/app/assets/javascripts/actioncable.esm.js @@ -0,0 +1,442 @@ +var adapters = { + logger: self.console, + WebSocket: self.WebSocket +}; + +var logger = { + log(...messages) { + if (this.enabled) { + messages.push(Date.now()); + adapters.logger.log("[ActionCable]", ...messages); + } + } +}; + +const now = () => (new Date).getTime(); + +const secondsSince = time => (now() - time) / 1e3; + +class ConnectionMonitor { + constructor(connection) { + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + addEventListener("visibilitychange", this.visibilityDidChange); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); + } + } + stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + removeEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor stopped"); + } + } + isRunning() { + return this.startedAt && !this.stoppedAt; + } + recordPing() { + this.pingedAt = now(); + } + recordConnect() { + this.reconnectAttempts = 0; + this.recordPing(); + delete this.disconnectedAt; + logger.log("ConnectionMonitor recorded connect"); + } + recordDisconnect() { + this.disconnectedAt = now(); + logger.log("ConnectionMonitor recorded disconnect"); + } + startPolling() { + this.stopPolling(); + this.poll(); + } + stopPolling() { + clearTimeout(this.pollTimeout); + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); + return staleThreshold * 1e3 * backoff * (1 + jitter); + } + reconnectIfStale() { + if (this.connectionIsStale()) { + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); + } else { + logger.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; + } + disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + } + visibilityDidChange() { + if (document.visibilityState === "visible") { + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); + } + }), 200); + } + } +} + +ConnectionMonitor.staleThreshold = 6; + +ConnectionMonitor.reconnectionBackoffRate = .15; + +var INTERNAL = { + message_types: { + welcome: "welcome", + disconnect: "disconnect", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + disconnect_reasons: { + unauthorized: "unauthorized", + invalid_request: "invalid_request", + server_restart: "server_restart" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] +}; + +const {message_types: message_types, protocols: protocols} = INTERNAL; + +const supportedProtocols = protocols.slice(0, protocols.length - 1); + +const indexOf = [].indexOf; + +class Connection { + constructor(consumer) { + this.open = this.open.bind(this); + this.consumer = consumer; + this.subscriptions = this.consumer.subscriptions; + this.monitor = new ConnectionMonitor(this); + this.disconnected = true; + } + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)); + return true; + } else { + return false; + } + } + open() { + if (this.isActive()) { + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); + return false; + } else { + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`); + if (this.webSocket) { + this.uninstallEventHandlers(); + } + this.webSocket = new adapters.WebSocket(this.consumer.url, protocols); + this.installEventHandlers(); + this.monitor.start(); + return true; + } + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { + if (!allowReconnect) { + this.monitor.stop(); + } + if (this.isActive()) { + return this.webSocket.close(); + } + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); + if (this.isActive()) { + try { + return this.close(); + } catch (error) { + logger.log("Failed to reopen WebSocket", error); + } finally { + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); + setTimeout(this.open, this.constructor.reopenDelay); + } + } else { + return this.open(); + } + } + getProtocol() { + if (this.webSocket) { + return this.webSocket.protocol; + } + } + isOpen() { + return this.isState("open"); + } + isActive() { + return this.isState("open", "connecting"); + } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; + } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0; + } + getState() { + if (this.webSocket) { + for (let state in adapters.WebSocket) { + if (adapters.WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase(); + } + } + } + return null; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; + } + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; + } + } +} + +Connection.reopenDelay = 500; + +Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { + return; + } + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); + switch (type) { + case message_types.welcome: + this.monitor.recordConnect(); + return this.subscriptions.reload(); + + case message_types.disconnect: + logger.log(`Disconnecting. Reason: ${reason}`); + return this.close({ + allowReconnect: reconnect + }); + + case message_types.ping: + return this.monitor.recordPing(); + + case message_types.confirmation: + return this.subscriptions.notify(identifier, "connected"); + + case message_types.rejection: + return this.subscriptions.reject(identifier); + + default: + return this.subscriptions.notify(identifier, "received", message); + } + }, + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); + this.disconnected = false; + if (!this.isProtocolSupported()) { + logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); + return this.close({ + allowReconnect: false + }); + } + }, + close(event) { + logger.log("WebSocket onclose event"); + if (this.disconnected) { + return; + } + this.disconnected = true; + this.monitor.recordDisconnect(); + return this.subscriptions.notifyAll("disconnected", { + willAttemptReconnect: this.monitor.isRunning() + }); + }, + error() { + logger.log("WebSocket onerror event"); + } +}; + +const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key]; + object[key] = value; + } + } + return object; +}; + +class Subscription { + constructor(consumer, params = {}, mixin) { + this.consumer = consumer; + this.identifier = JSON.stringify(params); + extend(this, mixin); + } + perform(action, data = {}) { + data.action = action; + return this.send(data); + } + send(data) { + return this.consumer.send({ + command: "message", + identifier: this.identifier, + data: JSON.stringify(data) + }); + } + unsubscribe() { + return this.consumer.subscriptions.remove(this); + } +} + +class Subscriptions { + constructor(consumer) { + this.consumer = consumer; + this.subscriptions = []; + } + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { + channel: channel + }; + const subscription = new Subscription(this.consumer, params, mixin); + return this.add(subscription); + } + add(subscription) { + this.subscriptions.push(subscription); + this.consumer.ensureActiveConnection(); + this.notify(subscription, "initialized"); + this.sendCommand(subscription, "subscribe"); + return subscription; + } + remove(subscription) { + this.forget(subscription); + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe"); + } + return subscription; + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); + return subscription; + })); + } + forget(subscription) { + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); + return subscription; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe"))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription); + } else { + subscriptions = [ subscription ]; + } + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; + return this.consumer.send({ + command: command, + identifier: identifier + }); + } +} + +class Consumer { + constructor(url) { + this._url = url; + this.subscriptions = new Subscriptions(this); + this.connection = new Connection(this); + } + get url() { + return createWebSocketURL(this._url); + } + send(data) { + return this.connection.send(data); + } + connect() { + return this.connection.open(); + } + disconnect() { + return this.connection.close({ + allowReconnect: false + }); + } + ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + } +} + +function createWebSocketURL(url) { + if (typeof url === "function") { + url = url(); + } + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } +} + +function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { + return new Consumer(url); +} + +function getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); + if (element) { + return element.getAttribute("content"); + } +} + +export { Connection, ConnectionMonitor, Consumer, INTERNAL, Subscription, Subscriptions, adapters, createConsumer, createWebSocketURL, getConfig, logger }; diff --git a/actioncable/app/assets/javascripts/actioncable.js b/actioncable/app/assets/javascripts/actioncable.js new file mode 100644 index 0000000000..7bc8ebb5db --- /dev/null +++ b/actioncable/app/assets/javascripts/actioncable.js @@ -0,0 +1,440 @@ +(function(global, factory) { + typeof exports === "object" && typeof module !== "undefined" ? factory(exports) : typeof define === "function" && define.amd ? define([ "exports" ], factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self, + factory(global.ActionCable = {})); +})(this, (function(exports) { + "use strict"; + var adapters = { + logger: self.console, + WebSocket: self.WebSocket + }; + var logger = { + log(...messages) { + if (this.enabled) { + messages.push(Date.now()); + adapters.logger.log("[ActionCable]", ...messages); + } + } + }; + const now = () => (new Date).getTime(); + const secondsSince = time => (now() - time) / 1e3; + class ConnectionMonitor { + constructor(connection) { + this.visibilityDidChange = this.visibilityDidChange.bind(this); + this.connection = connection; + this.reconnectAttempts = 0; + } + start() { + if (!this.isRunning()) { + this.startedAt = now(); + delete this.stoppedAt; + this.startPolling(); + addEventListener("visibilitychange", this.visibilityDidChange); + logger.log(`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`); + } + } + stop() { + if (this.isRunning()) { + this.stoppedAt = now(); + this.stopPolling(); + removeEventListener("visibilitychange", this.visibilityDidChange); + logger.log("ConnectionMonitor stopped"); + } + } + isRunning() { + return this.startedAt && !this.stoppedAt; + } + recordPing() { + this.pingedAt = now(); + } + recordConnect() { + this.reconnectAttempts = 0; + this.recordPing(); + delete this.disconnectedAt; + logger.log("ConnectionMonitor recorded connect"); + } + recordDisconnect() { + this.disconnectedAt = now(); + logger.log("ConnectionMonitor recorded disconnect"); + } + startPolling() { + this.stopPolling(); + this.poll(); + } + stopPolling() { + clearTimeout(this.pollTimeout); + } + poll() { + this.pollTimeout = setTimeout((() => { + this.reconnectIfStale(); + this.poll(); + }), this.getPollInterval()); + } + getPollInterval() { + const {staleThreshold: staleThreshold, reconnectionBackoffRate: reconnectionBackoffRate} = this.constructor; + const backoff = Math.pow(1 + reconnectionBackoffRate, Math.min(this.reconnectAttempts, 10)); + const jitterMax = this.reconnectAttempts === 0 ? 1 : reconnectionBackoffRate; + const jitter = jitterMax * Math.random(); + return staleThreshold * 1e3 * backoff * (1 + jitter); + } + reconnectIfStale() { + if (this.connectionIsStale()) { + logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`); + this.reconnectAttempts++; + if (this.disconnectedRecently()) { + logger.log(`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(this.disconnectedAt)} s`); + } else { + logger.log("ConnectionMonitor reopening"); + this.connection.reopen(); + } + } + } + get refreshedAt() { + return this.pingedAt ? this.pingedAt : this.startedAt; + } + connectionIsStale() { + return secondsSince(this.refreshedAt) > this.constructor.staleThreshold; + } + disconnectedRecently() { + return this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold; + } + visibilityDidChange() { + if (document.visibilityState === "visible") { + setTimeout((() => { + if (this.connectionIsStale() || !this.connection.isOpen()) { + logger.log(`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`); + this.connection.reopen(); + } + }), 200); + } + } + } + ConnectionMonitor.staleThreshold = 6; + ConnectionMonitor.reconnectionBackoffRate = .15; + var INTERNAL = { + message_types: { + welcome: "welcome", + disconnect: "disconnect", + ping: "ping", + confirmation: "confirm_subscription", + rejection: "reject_subscription" + }, + disconnect_reasons: { + unauthorized: "unauthorized", + invalid_request: "invalid_request", + server_restart: "server_restart" + }, + default_mount_path: "/cable", + protocols: [ "actioncable-v1-json", "actioncable-unsupported" ] + }; + const {message_types: message_types, protocols: protocols} = INTERNAL; + const supportedProtocols = protocols.slice(0, protocols.length - 1); + const indexOf = [].indexOf; + class Connection { + constructor(consumer) { + this.open = this.open.bind(this); + this.consumer = consumer; + this.subscriptions = this.consumer.subscriptions; + this.monitor = new ConnectionMonitor(this); + this.disconnected = true; + } + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)); + return true; + } else { + return false; + } + } + open() { + if (this.isActive()) { + logger.log(`Attempted to open WebSocket, but existing socket is ${this.getState()}`); + return false; + } else { + logger.log(`Opening WebSocket, current state is ${this.getState()}, subprotocols: ${protocols}`); + if (this.webSocket) { + this.uninstallEventHandlers(); + } + this.webSocket = new adapters.WebSocket(this.consumer.url, protocols); + this.installEventHandlers(); + this.monitor.start(); + return true; + } + } + close({allowReconnect: allowReconnect} = { + allowReconnect: true + }) { + if (!allowReconnect) { + this.monitor.stop(); + } + if (this.isActive()) { + return this.webSocket.close(); + } + } + reopen() { + logger.log(`Reopening WebSocket, current state is ${this.getState()}`); + if (this.isActive()) { + try { + return this.close(); + } catch (error) { + logger.log("Failed to reopen WebSocket", error); + } finally { + logger.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`); + setTimeout(this.open, this.constructor.reopenDelay); + } + } else { + return this.open(); + } + } + getProtocol() { + if (this.webSocket) { + return this.webSocket.protocol; + } + } + isOpen() { + return this.isState("open"); + } + isActive() { + return this.isState("open", "connecting"); + } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0; + } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0; + } + getState() { + if (this.webSocket) { + for (let state in adapters.WebSocket) { + if (adapters.WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase(); + } + } + } + return null; + } + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this); + this.webSocket[`on${eventName}`] = handler; + } + } + uninstallEventHandlers() { + for (let eventName in this.events) { + this.webSocket[`on${eventName}`] = function() {}; + } + } + } + Connection.reopenDelay = 500; + Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { + return; + } + const {identifier: identifier, message: message, reason: reason, reconnect: reconnect, type: type} = JSON.parse(event.data); + switch (type) { + case message_types.welcome: + this.monitor.recordConnect(); + return this.subscriptions.reload(); + + case message_types.disconnect: + logger.log(`Disconnecting. Reason: ${reason}`); + return this.close({ + allowReconnect: reconnect + }); + + case message_types.ping: + return this.monitor.recordPing(); + + case message_types.confirmation: + return this.subscriptions.notify(identifier, "connected"); + + case message_types.rejection: + return this.subscriptions.reject(identifier); + + default: + return this.subscriptions.notify(identifier, "received", message); + } + }, + open() { + logger.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`); + this.disconnected = false; + if (!this.isProtocolSupported()) { + logger.log("Protocol is unsupported. Stopping monitor and disconnecting."); + return this.close({ + allowReconnect: false + }); + } + }, + close(event) { + logger.log("WebSocket onclose event"); + if (this.disconnected) { + return; + } + this.disconnected = true; + this.monitor.recordDisconnect(); + return this.subscriptions.notifyAll("disconnected", { + willAttemptReconnect: this.monitor.isRunning() + }); + }, + error() { + logger.log("WebSocket onerror event"); + } + }; + const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key]; + object[key] = value; + } + } + return object; + }; + class Subscription { + constructor(consumer, params = {}, mixin) { + this.consumer = consumer; + this.identifier = JSON.stringify(params); + extend(this, mixin); + } + perform(action, data = {}) { + data.action = action; + return this.send(data); + } + send(data) { + return this.consumer.send({ + command: "message", + identifier: this.identifier, + data: JSON.stringify(data) + }); + } + unsubscribe() { + return this.consumer.subscriptions.remove(this); + } + } + class Subscriptions { + constructor(consumer) { + this.consumer = consumer; + this.subscriptions = []; + } + create(channelName, mixin) { + const channel = channelName; + const params = typeof channel === "object" ? channel : { + channel: channel + }; + const subscription = new Subscription(this.consumer, params, mixin); + return this.add(subscription); + } + add(subscription) { + this.subscriptions.push(subscription); + this.consumer.ensureActiveConnection(); + this.notify(subscription, "initialized"); + this.sendCommand(subscription, "subscribe"); + return subscription; + } + remove(subscription) { + this.forget(subscription); + if (!this.findAll(subscription.identifier).length) { + this.sendCommand(subscription, "unsubscribe"); + } + return subscription; + } + reject(identifier) { + return this.findAll(identifier).map((subscription => { + this.forget(subscription); + this.notify(subscription, "rejected"); + return subscription; + })); + } + forget(subscription) { + this.subscriptions = this.subscriptions.filter((s => s !== subscription)); + return subscription; + } + findAll(identifier) { + return this.subscriptions.filter((s => s.identifier === identifier)); + } + reload() { + return this.subscriptions.map((subscription => this.sendCommand(subscription, "subscribe"))); + } + notifyAll(callbackName, ...args) { + return this.subscriptions.map((subscription => this.notify(subscription, callbackName, ...args))); + } + notify(subscription, callbackName, ...args) { + let subscriptions; + if (typeof subscription === "string") { + subscriptions = this.findAll(subscription); + } else { + subscriptions = [ subscription ]; + } + return subscriptions.map((subscription => typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)); + } + sendCommand(subscription, command) { + const {identifier: identifier} = subscription; + return this.consumer.send({ + command: command, + identifier: identifier + }); + } + } + class Consumer { + constructor(url) { + this._url = url; + this.subscriptions = new Subscriptions(this); + this.connection = new Connection(this); + } + get url() { + return createWebSocketURL(this._url); + } + send(data) { + return this.connection.send(data); + } + connect() { + return this.connection.open(); + } + disconnect() { + return this.connection.close({ + allowReconnect: false + }); + } + ensureActiveConnection() { + if (!this.connection.isActive()) { + return this.connection.open(); + } + } + } + function createWebSocketURL(url) { + if (typeof url === "function") { + url = url(); + } + if (url && !/^wss?:/i.test(url)) { + const a = document.createElement("a"); + a.href = url; + a.href = a.href; + a.protocol = a.protocol.replace("http", "ws"); + return a.href; + } else { + return url; + } + } + function createConsumer(url = getConfig("url") || INTERNAL.default_mount_path) { + return new Consumer(url); + } + function getConfig(name) { + const element = document.head.querySelector(`meta[name='action-cable-${name}']`); + if (element) { + return element.getAttribute("content"); + } + } + exports.Connection = Connection; + exports.ConnectionMonitor = ConnectionMonitor; + exports.Consumer = Consumer; + exports.INTERNAL = INTERNAL; + exports.Subscription = Subscription; + exports.Subscriptions = Subscriptions; + exports.adapters = adapters; + exports.createConsumer = createConsumer; + exports.createWebSocketURL = createWebSocketURL; + exports.getConfig = getConfig; + exports.logger = logger; + Object.defineProperty(exports, "__esModule", { + value: true + }); +})); diff --git a/actioncable/app/javascript/action_cable/index_with_name_deprecation.js b/actioncable/app/javascript/action_cable/index_with_name_deprecation.js new file mode 100644 index 0000000000..272ba7bec2 --- /dev/null +++ b/actioncable/app/javascript/action_cable/index_with_name_deprecation.js @@ -0,0 +1,2 @@ +export * from "./index" +console.log("DEPRECATION: action_cable.js has been renamed to actioncable.js – please update your reference before Rails 8") diff --git a/actioncable/karma.conf.js b/actioncable/karma.conf.js index 83e9c98af1..bf7c1b2fbb 100644 --- a/actioncable/karma.conf.js +++ b/actioncable/karma.conf.js @@ -27,7 +27,6 @@ if (process.env.CI) { sl_ff: sauce("firefox", 63), sl_safari: sauce("safari", 12.0, "macOS 10.13"), sl_edge: sauce("microsoftedge", 17.17134, "Windows 10"), - sl_ie_11: sauce("internet explorer", 11, "Windows 8.1"), } config.browsers = Object.keys(config.customLaunchers) diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index 3c37040293..9d95b28d22 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -9,6 +9,7 @@ module ActionCable class Engine < Rails::Engine # :nodoc: config.action_cable = ActiveSupport::OrderedOptions.new config.action_cable.mount_path = ActionCable::INTERNAL[:default_mount_path] + config.action_cable.precompile_assets = true config.eager_load_namespaces << ActionCable @@ -22,6 +23,14 @@ class Engine < Rails::Engine # :nodoc: ActiveSupport.on_load(:action_cable) { self.logger ||= ::Rails.logger } end + initializer "action_cable.asset" do + config.after_initialize do |app| + if Rails.application.config.respond_to?(:assets) && app.config.action_cable.precompile_assets + Rails.application.config.assets.precompile += %w( actioncable.js actioncable.esm.js ) + end + end + end + initializer "action_cable.set_configs" do |app| options = app.config.action_cable options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development? diff --git a/actioncable/lib/action_cable/server/configuration.rb b/actioncable/lib/action_cable/server/configuration.rb index 26209537df..49567ddedb 100644 --- a/actioncable/lib/action_cable/server/configuration.rb +++ b/actioncable/lib/action_cable/server/configuration.rb @@ -9,6 +9,7 @@ class Configuration attr_accessor :connection_class, :worker_pool_size attr_accessor :disable_request_forgery_protection, :allowed_request_origins, :allow_same_origin_as_host attr_accessor :cable, :url, :mount_path + attr_accessor :precompile_assets def initialize @log_tags = [] diff --git a/actioncable/package.json b/actioncable/package.json index 62494a1c74..90ba6332ae 100644 --- a/actioncable/package.json +++ b/actioncable/package.json @@ -2,6 +2,7 @@ "name": "@rails/actioncable", "version": "7.0.0-alpha", "description": "WebSocket framework for Ruby on Rails.", + "module": "app/javascript/action_cable/index.js", "main": "app/assets/javascripts/action_cable.js", "files": [ "app/assets/javascripts/*.js", @@ -23,9 +24,8 @@ }, "homepage": "https://rubyonrails.org/", "devDependencies": { - "babel-core": "^6.25.0", - "babel-plugin-external-helpers": "^6.22.0", - "babel-preset-env": "^1.6.0", + "@rollup/plugin-node-resolve": "^11.0.1", + "@rollup/plugin-commonjs": "^19.0.1", "eslint": "^4.3.0", "eslint-plugin-import": "^2.7.0", "karma": "^3.1.1", @@ -34,11 +34,8 @@ "karma-sauce-launcher": "^1.2.0", "mock-socket": "^2.0.0", "qunit": "^2.8.0", - "rollup": "^0.58.2", - "rollup-plugin-babel": "^3.0.4", - "rollup-plugin-commonjs": "^9.1.0", - "rollup-plugin-node-resolve": "^3.3.0", - "rollup-plugin-uglify": "^3.0.0" + "rollup": "^2.35.1", + "rollup-plugin-terser": "^7.0.2" }, "scripts": { "prebuild": "yarn lint && bundle exec rake assets:codegen", diff --git a/actioncable/rollup.config.js b/actioncable/rollup.config.js index 64727e0887..56ea857e27 100644 --- a/actioncable/rollup.config.js +++ b/actioncable/rollup.config.js @@ -1,24 +1,47 @@ -import babel from "rollup-plugin-babel" -import uglify from "rollup-plugin-uglify" +import resolve from "@rollup/plugin-node-resolve" +import { terser } from "rollup-plugin-terser" -const uglifyOptions = { +const terserOptions = { mangle: false, compress: false, - output: { + format: { beautify: true, indent_level: 2 } } -export default { - input: "app/javascript/action_cable/index.js", - output: { - file: "app/assets/javascripts/action_cable.js", - format: "umd", - name: "ActionCable" +export default [ + { + input: "app/javascript/action_cable/index.js", + output: [ + { + file: "app/assets/javascripts/actioncable.js", + format: "umd", + name: "ActionCable" + }, + + { + file: "app/assets/javascripts/actioncable.esm.js", + format: "es" + } + ], + plugins: [ + resolve(), + terser(terserOptions) + ] }, - plugins: [ - babel(), - uglify(uglifyOptions) - ] -} + + { + input: "app/javascript/action_cable/index_with_name_deprecation.js", + output: { + file: "app/assets/javascripts/action_cable.js", + format: "umd", + name: "ActionCable" + }, + breakOnWarning: false, + plugins: [ + resolve(), + terser(terserOptions) + ] + }, +] \ No newline at end of file diff --git a/actioncable/rollup.config.test.js b/actioncable/rollup.config.test.js index f92ff36240..06ddc8871c 100644 --- a/actioncable/rollup.config.test.js +++ b/actioncable/rollup.config.test.js @@ -1,6 +1,5 @@ -import babel from "rollup-plugin-babel" -import commonjs from "rollup-plugin-commonjs" -import resolve from "rollup-plugin-node-resolve" +import commonjs from "@rollup/plugin-commonjs" +import resolve from "@rollup/plugin-node-resolve" export default { input: "test/javascript/src/test.js", @@ -12,7 +11,6 @@ export default { plugins: [ resolve(), - commonjs(), - babel() + commonjs() ] } diff --git a/yarn.lock b/yarn.lock index 248db3cfb3..1c558772c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5201,7 +5201,7 @@ resolve@^1.1.6, resolve@^1.5.0, resolve@^1.6.0, resolve@^1.8.1: dependencies: path-parse "^1.0.5" -resolve@^1.13.1, resolve@^1.17.0, resolve@^1.19.0, resolve@^1.20.0: +resolve@^1.17.0, resolve@^1.19.0: version "1.20.0" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -5318,9 +5318,9 @@ rollup@^0.58.2: "@types/node" "*" rollup@^2.35.1: - version "2.55.1" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.55.1.tgz#66a444648e2fb603d8e329e77a61c608a6510fda" - integrity sha512-1P9w5fpb6b4qroePh8vHKGIvPNxwoCQhjJpIqfZGHLKpZ0xcU2/XBmFxFbc9697/6bmHpmFTLk5R1dAQhFSo0g== + version "2.53.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.53.3.tgz#14b0e57f0874d4ad23bdbb13050cf70bcd1eabf7" + integrity sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA== optionalDependencies: fsevents "~2.3.2"