From 7b0b37240a20c74197408ac3b53519e7e18347e9 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Mon, 11 Dec 2017 14:29:45 +0800 Subject: Move actioncable javascript to app/javascript and change .coffee -> .js - Rename action_cable/*.coffee -> *.js - Move app/assets/javascripts/* -> app/javascript/* - Rename action_cable.js.erb -> action_cable/index.js.erb Renaming the extension to .js is in preparation for converting these files from coffeescript to ES2015. Moving the files to app/javascript and putting the entry point in index.js.erb changes the structure of ActionCable's javascript to match the structure of ActiveStorage's javascript. (We are doing the file moving and renaming in a separate commit to ensure that the git history of the files will be preserved - i.e. git will track these as file renames rather than unrelated file additions/deletions. In particular, git blame will still trace back to the original authorship.) --- .../app/javascript/action_cable/connection.js | 116 +++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 actioncable/app/javascript/action_cable/connection.js (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js new file mode 100644 index 0000000000..7fd68cad2f --- /dev/null +++ b/actioncable/app/javascript/action_cable/connection.js @@ -0,0 +1,116 @@ +#= require ./connection_monitor + +# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. + +{message_types, protocols} = ActionCable.INTERNAL +[supportedProtocols..., unsupportedProtocol] = protocols + +class ActionCable.Connection + @reopenDelay: 500 + + constructor: (@consumer) -> + {@subscriptions} = @consumer + @monitor = new ActionCable.ConnectionMonitor this + @disconnected = true + + send: (data) -> + if @isOpen() + @webSocket.send(JSON.stringify(data)) + true + else + false + + open: => + if @isActive() + ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") + false + else + ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") + @uninstallEventHandlers() if @webSocket? + @webSocket = new ActionCable.WebSocket(@consumer.url, protocols) + @installEventHandlers() + @monitor.start() + true + + close: ({allowReconnect} = {allowReconnect: true}) -> + @monitor.stop() unless allowReconnect + @webSocket?.close() if @isActive() + + reopen: -> + ActionCable.log("Reopening WebSocket, current state is #{@getState()}") + if @isActive() + try + @close() + catch error + ActionCable.log("Failed to reopen WebSocket", error) + finally + ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms") + setTimeout(@open, @constructor.reopenDelay) + else + @open() + + getProtocol: -> + @webSocket?.protocol + + isOpen: -> + @isState("open") + + isActive: -> + @isState("open", "connecting") + + # Private + + isProtocolSupported: -> + @getProtocol() in supportedProtocols + + isState: (states...) -> + @getState() in states + + getState: -> + return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState + null + + installEventHandlers: -> + for eventName of @events + handler = @events[eventName].bind(this) + @webSocket["on#{eventName}"] = handler + return + + uninstallEventHandlers: -> + for eventName of @events + @webSocket["on#{eventName}"] = -> + return + + events: + message: (event) -> + return unless @isProtocolSupported() + {identifier, message, type} = JSON.parse(event.data) + switch type + when message_types.welcome + @monitor.recordConnect() + @subscriptions.reload() + when message_types.ping + @monitor.recordPing() + when message_types.confirmation + @subscriptions.notify(identifier, "connected") + when message_types.rejection + @subscriptions.reject(identifier) + else + @subscriptions.notify(identifier, "received", message) + + open: -> + ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol") + @disconnected = false + if not @isProtocolSupported() + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + @close(allowReconnect: false) + + close: (event) -> + ActionCable.log("WebSocket onclose event") + return if @disconnected + @disconnected = true + @monitor.recordDisconnect() + @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()}) + + error: -> + ActionCable.log("WebSocket onerror event") -- cgit v1.2.3 From 403c001c56e3980e624da2cb1e1e98d667499d40 Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Mon, 11 Dec 2017 14:44:52 +0800 Subject: Run decaffeinate on action_cable/*.js Using [decaffeinate], we have converted these files from coffeescript syntax to ES2015 syntax. Decaffeinate is very conservative in the conversion process to ensure exact coffeescript semantics are preserved. Most of the time, it's safe to clean up the code, and decaffeinate has left suggestions regarding potential cleanups we can take. I'll tackle those cleanups separately. After running decaffeinate, I ran: ``` eslint --fix app/javascript ``` using the eslint configuration from ActiveStorage to automatically correct lint violations in the decaffeinated output. This removed 189 extra semicolons and changed one instance of single quotes to double quotes. Note: decaffeinate and eslint can't parse ERB syntax. So I worked around that by temporarily quoting the ERB: ```diff @ActionCable = - INTERNAL: <%= ActionCable::INTERNAL.to_json %> + INTERNAL: "<%= ActionCable::INTERNAL.to_json %>" WebSocket: window.WebSocket logger: window.console ``` and then removing those quotes after running decaffeinate and eslint. [decaffeinate]: https://github.com/decaffeinate/decaffeinate --- .../app/javascript/action_cable/connection.js | 280 ++++++++++++--------- 1 file changed, 164 insertions(+), 116 deletions(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 7fd68cad2f..462db965d1 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -1,116 +1,164 @@ -#= require ./connection_monitor - -# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. - -{message_types, protocols} = ActionCable.INTERNAL -[supportedProtocols..., unsupportedProtocol] = protocols - -class ActionCable.Connection - @reopenDelay: 500 - - constructor: (@consumer) -> - {@subscriptions} = @consumer - @monitor = new ActionCable.ConnectionMonitor this - @disconnected = true - - send: (data) -> - if @isOpen() - @webSocket.send(JSON.stringify(data)) - true - else - false - - open: => - if @isActive() - ActionCable.log("Attempted to open WebSocket, but existing socket is #{@getState()}") - false - else - ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") - @uninstallEventHandlers() if @webSocket? - @webSocket = new ActionCable.WebSocket(@consumer.url, protocols) - @installEventHandlers() - @monitor.start() - true - - close: ({allowReconnect} = {allowReconnect: true}) -> - @monitor.stop() unless allowReconnect - @webSocket?.close() if @isActive() - - reopen: -> - ActionCable.log("Reopening WebSocket, current state is #{@getState()}") - if @isActive() - try - @close() - catch error - ActionCable.log("Failed to reopen WebSocket", error) - finally - ActionCable.log("Reopening WebSocket in #{@constructor.reopenDelay}ms") - setTimeout(@open, @constructor.reopenDelay) - else - @open() - - getProtocol: -> - @webSocket?.protocol - - isOpen: -> - @isState("open") - - isActive: -> - @isState("open", "connecting") - - # Private - - isProtocolSupported: -> - @getProtocol() in supportedProtocols - - isState: (states...) -> - @getState() in states - - getState: -> - return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState - null - - installEventHandlers: -> - for eventName of @events - handler = @events[eventName].bind(this) - @webSocket["on#{eventName}"] = handler - return - - uninstallEventHandlers: -> - for eventName of @events - @webSocket["on#{eventName}"] = -> - return - - events: - message: (event) -> - return unless @isProtocolSupported() - {identifier, message, type} = JSON.parse(event.data) - switch type - when message_types.welcome - @monitor.recordConnect() - @subscriptions.reload() - when message_types.ping - @monitor.recordPing() - when message_types.confirmation - @subscriptions.notify(identifier, "connected") - when message_types.rejection - @subscriptions.reject(identifier) - else - @subscriptions.notify(identifier, "received", message) - - open: -> - ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol") - @disconnected = false - if not @isProtocolSupported() - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") - @close(allowReconnect: false) - - close: (event) -> - ActionCable.log("WebSocket onclose event") - return if @disconnected - @disconnected = true - @monitor.recordDisconnect() - @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()}) - - error: -> - ActionCable.log("WebSocket onerror event") +/* + * decaffeinate suggestions: + * DS101: Remove unnecessary use of Array.from + * DS102: Remove unnecessary code created because of implicit returns + * DS104: Avoid inline assignments + * DS201: Simplify complex destructure assignments + * DS204: Change includes calls to have a more natural evaluation order + * DS206: Consider reworking classes to avoid initClass + * DS207: Consider shorter variations of null checks + * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md + */ +//= require ./connection_monitor + +// Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. + +const {message_types, protocols} = ActionCable.INTERNAL +const adjustedLength = Math.max(protocols.length, 1), + supportedProtocols = protocols.slice(0, adjustedLength - 1), + unsupportedProtocol = protocols[adjustedLength - 1] + +const Cls = (ActionCable.Connection = class Connection { + static initClass() { + this.reopenDelay = 500 + + this.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { return } + const {identifier, message, type} = JSON.parse(event.data) + switch (type) { + case message_types.welcome: + this.monitor.recordConnect() + return this.subscriptions.reload() + 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() { + ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) + this.disconnected = false + if (!this.isProtocolSupported()) { + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + return this.close({allowReconnect: false}) + } + }, + + close(event) { + ActionCable.log("WebSocket onclose event") + if (this.disconnected) { return } + this.disconnected = true + this.monitor.recordDisconnect() + return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) + }, + + error() { + return ActionCable.log("WebSocket onerror event") + } + } + } + + constructor(consumer) { + this.open = this.open.bind(this) + this.consumer = consumer; + ({subscriptions: this.subscriptions} = this.consumer) + this.monitor = new ActionCable.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()) { + ActionCable.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}`) + if (this.webSocket != null) { this.uninstallEventHandlers() } + this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) + this.installEventHandlers() + this.monitor.start() + return true + } + } + + close(param) { + if (param == null) { param = {allowReconnect: true} } + const {allowReconnect} = param + if (!allowReconnect) { this.monitor.stop() } + if (this.isActive()) { return (this.webSocket != null ? this.webSocket.close() : undefined) } + } + + reopen() { + ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) + if (this.isActive()) { + try { + return this.close() + } catch (error) { + return ActionCable.log("Failed to reopen WebSocket", error) + } + finally { + ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + setTimeout(this.open, this.constructor.reopenDelay) + } + } else { + return this.open() + } + } + + getProtocol() { + return (this.webSocket != null ? this.webSocket.protocol : undefined) + } + + isOpen() { + return this.isState("open") + } + + isActive() { + return this.isState("open", "connecting") + } + + // Private + + isProtocolSupported() { + let needle + return (needle = this.getProtocol(), Array.from(supportedProtocols).includes(needle)) + } + + isState(...states) { + let needle + return (needle = this.getState(), Array.from(states).includes(needle)) + } + + getState() { + for (let state in WebSocket) { const value = WebSocket[state]; if (value === (this.webSocket != null ? this.webSocket.readyState : undefined)) { 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() {} + } + } +}) +Cls.initClass() -- cgit v1.2.3 From 0eb6b86e9606cace49afba0b35ec18916c73646e Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Fri, 12 Jan 2018 16:49:09 -0800 Subject: Refactor decaffeinate output to more natural/idiomatic javascript - Remove unnecessary Array.from usages from subscriptions.js These were all Arrays before, so Array.from is a no-op - Remove unnecessary IIFEs from subscriptions.js - Manually decaffeinate sample ActionCable code in comments Here the coffeescript -> ES2015 conversion was done by hand rather than using decaffeinate, because these code samples were simple enough. - Refactor ActionCable.Subscription to avoid initClass - Refactor ActionCable.Subscription to use ES2015 default parameters - Refactor ActionCable.ConnectionMonitor to avoid initClass - Refactor ActionCable.ConnectionMonitor to use shorter variations of null checks - Remove unnecessary code created because of implicit returns in ConnectionMonitor This removes the `return` statements that were returning the value of console.log and those from private methods whose return value was not being used. - Refactor ActionCable.Connection to avoid initClass - Refactor Connection#isProtocolSupported and #isState This addresses these three decaffeinate cleanup suggestions: - DS101: Remove unnecessary use of Array.from - DS104: Avoid inline assignments - DS204: Change includes calls to have a more natural evaluation order It also removes the use of Array.prototype.includes, which means we don't have to worry about providing a polyfill or requiring that end users provide one. - Refactor ActionCable.Connection to use ES2015 default parameters - Refactor ActionCable.Connection to use shorter variations of null checks - Remove return statements that return the value of console.log() in ActionCable.Connection - Simplify complex destructure assignment in connection.js decaffeinate had inserted ``` adjustedLength = Math.max(protocols.length, 1) ``` to be safe, but we know that there has to always be at least one protocol, so we don't have to worry about protocols.length being 0 here. - Refactor Connection#getState The decaffeinate translation of this method was not very clear, so we've rewritten it to be more natural. - Simplify destructure assignment in connection.js - Remove unnecessary use of Array.from from action_cable.js.erb - Refactor ActionCable#createConsumer and #getConfig This addresses these two decaffeinate cleanup suggestions: - DS104: Avoid inline assignments - DS207: Consider shorter variations of null checks - Remove unnecessary code created because of implicit returns in action_cable.js.erb This removes the `return` statements that were returning the value of console.log and those from methods that just set and unset the `debugging` flag. - Remove decaffeinate suggestion about avoiding top-level this In this case, the top-level `this` is intentional, so it's okay to ignore this suggestion. - Remove decaffeinate suggestions about removing unnecessary returns I did remove some of the return statements in previous commits, where it seemed appropriate. However, the rest of these should probably remain because the return values have been exposed through the public API. If we want to break that contract, we can do so, but I think it should be done deliberately as part of a breaking-API change (separate from this coffeescript -> ES2015 conversion) - Remove unused `unsupportedProtocol` variable from connection.js Leaving this would cause eslint to fail - Refactor Subscriptions methods to avoid `for` ... `of` syntax Babel transpiles `for` ... `of` syntax to use `Symbol.iterator`, which would require a polyfill in applications that support older browsers. The `for` ... `of` syntax was produced by running `decaffeinate`, but in these instances a simpler `map` should be sufficient and avoid any `Symbol` issues. --- .../app/javascript/action_cable/connection.js | 268 ++++++++++----------- 1 file changed, 131 insertions(+), 137 deletions(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 462db965d1..540f597303 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -1,164 +1,158 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS201: Simplify complex destructure assignments - * DS204: Change includes calls to have a more natural evaluation order - * DS206: Consider reworking classes to avoid initClass - * DS207: Consider shorter variations of null checks - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ //= require ./connection_monitor // Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. const {message_types, protocols} = ActionCable.INTERNAL -const adjustedLength = Math.max(protocols.length, 1), - supportedProtocols = protocols.slice(0, adjustedLength - 1), - unsupportedProtocol = protocols[adjustedLength - 1] - -const Cls = (ActionCable.Connection = class Connection { - static initClass() { - this.reopenDelay = 500 - - this.prototype.events = { - message(event) { - if (!this.isProtocolSupported()) { return } - const {identifier, message, type} = JSON.parse(event.data) - switch (type) { - case message_types.welcome: - this.monitor.recordConnect() - return this.subscriptions.reload() - 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() { - ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) - this.disconnected = false - if (!this.isProtocolSupported()) { - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") - return this.close({allowReconnect: false}) - } - }, - - close(event) { - ActionCable.log("WebSocket onclose event") - if (this.disconnected) { return } - this.disconnected = true - this.monitor.recordDisconnect() - return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) - }, - - error() { - return ActionCable.log("WebSocket onerror event") - } +const supportedProtocols = protocols.slice(0, protocols.length - 1) + +ActionCable.Connection = (function() { + const indexOf = [].indexOf + + class Connection { + constructor(consumer) { + this.open = this.open.bind(this) + this.consumer = consumer + this.subscriptions = this.consumer.subscriptions + this.monitor = new ActionCable.ConnectionMonitor(this) + this.disconnected = true } - } - - constructor(consumer) { - this.open = this.open.bind(this) - this.consumer = consumer; - ({subscriptions: this.subscriptions} = this.consumer) - this.monitor = new ActionCable.ConnectionMonitor(this) - this.disconnected = true - } - send(data) { - if (this.isOpen()) { - this.webSocket.send(JSON.stringify(data)) - return true - } else { - return false + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)) + return true + } else { + return false + } } - } - open() { - if (this.isActive()) { - ActionCable.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}`) - if (this.webSocket != null) { this.uninstallEventHandlers() } - this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) - this.installEventHandlers() - this.monitor.start() - return true + open() { + if (this.isActive()) { + ActionCable.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}`) + if (this.webSocket) { this.uninstallEventHandlers() } + this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) + this.installEventHandlers() + this.monitor.start() + return true + } } - } - close(param) { - if (param == null) { param = {allowReconnect: true} } - const {allowReconnect} = param - if (!allowReconnect) { this.monitor.stop() } - if (this.isActive()) { return (this.webSocket != null ? this.webSocket.close() : undefined) } - } + close({allowReconnect} = {allowReconnect: true}) { + if (!allowReconnect) { this.monitor.stop() } + if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) } + } - reopen() { - ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) - if (this.isActive()) { - try { - return this.close() - } catch (error) { - return ActionCable.log("Failed to reopen WebSocket", error) - } - finally { - ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) - setTimeout(this.open, this.constructor.reopenDelay) + reopen() { + ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) + if (this.isActive()) { + try { + return this.close() + } catch (error) { + ActionCable.log("Failed to reopen WebSocket", error) + } + finally { + ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + setTimeout(this.open, this.constructor.reopenDelay) + } + } else { + return this.open() } - } else { - return this.open() } - } - getProtocol() { - return (this.webSocket != null ? this.webSocket.protocol : undefined) - } + getProtocol() { + return (this.webSocket ? this.webSocket.protocol : undefined) + } - isOpen() { - return this.isState("open") - } + isOpen() { + return this.isState("open") + } - isActive() { - return this.isState("open", "connecting") - } + isActive() { + return this.isState("open", "connecting") + } - // Private + // Private - isProtocolSupported() { - let needle - return (needle = this.getProtocol(), Array.from(supportedProtocols).includes(needle)) - } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0 + } - isState(...states) { - let needle - return (needle = this.getState(), Array.from(states).includes(needle)) - } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0 + } - getState() { - for (let state in WebSocket) { const value = WebSocket[state]; if (value === (this.webSocket != null ? this.webSocket.readyState : undefined)) { return state.toLowerCase() } } - return null - } + getState() { + if (this.webSocket) { + for (let state in WebSocket) { + if (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 + 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() {} + } + } + } - 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, message, type} = JSON.parse(event.data) + switch (type) { + case message_types.welcome: + this.monitor.recordConnect() + return this.subscriptions.reload() + 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() { + ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) + this.disconnected = false + if (!this.isProtocolSupported()) { + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + return this.close({allowReconnect: false}) + } + }, + + close(event) { + ActionCable.log("WebSocket onclose event") + if (this.disconnected) { return } + this.disconnected = true + this.monitor.recordDisconnect() + return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) + }, + + error() { + ActionCable.log("WebSocket onerror event") } } -}) -Cls.initClass() + + return Connection + +})() -- cgit v1.2.3 From c96139af71e6f7c36e25bccea6b05ccd9523531a Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Sat, 20 Jan 2018 23:33:32 -0800 Subject: Convert ActionCable javascript to ES2015 modules with modern build environment We've replaced the sprockets `//= require` directives with ES2015 imports. As a result, the ActionCable javascript can now be compiled with rollup (like ActiveStorage already is). - Rename action_cable/index.js.erb -> action_cable/index.js - Add rake task to generate a javascript module of the ActionCable::INTERNAL ruby hash This will allow us to get rid of ERB from the actioncable javascript, since it is only used to interpolate ActionCable::INTERNAL.to_json. - Import INTERNAL directly in ActionCable Connection module This is necessary to remove a load-order dependency conflict in the rollup-compiled build. Using ActionCable.INTERNAL would result in a runtime error: ``` TypeError: Cannot read property 'INTERNAL' of undefined ``` because ActionCable.INTERNAL is not set before the Connection module is executed. All other ActionCable.* references are executed inside of the body of a function, so there is no load-order dependency there. - Add eslint and eslint-plugin-import devDependencies to actioncable These will be used to add a linting setup to actioncable like the one in activestorage. - Add .eslintrc to actioncable This lint configuration was copied from activestorage - Add lint script to actioncable This is the same as the lint script in activestorage - Add babel-core, babel-plugin-external-helpers, and babel-preset-env devDependencies to actioncable These will be used to add ES2015 transpilation support to actioncable like we have in activestorage. - Add .babelrc to actioncable This configuration was copied from activestorage - Enable loose mode in ActionCable's babel config This generates a smaller bundle when compiled - Add rollup devDependencies to actioncable These will be used to add a modern build pipeline to actioncable like the one in activestorage. - Add rollup config to actioncable This is essentially the same as the rollup config from activestorage - Add prebuild and build scripts to actioncable package These scripts were copied from activestorage - Invoke code generation task as part of actioncable's prebuild script This will guarantee that the action_cable/internal.js module is available at build time (which is important, because two other modules now depend on it). - Update actioncable package to reference the rollup-compiled files Now that we have a fully functional rollup pipeline in actioncable, we can use the compiled output in our npm package. - Remove build section from ActionCable blade config Now that rollup is responsible for building ActionCable, we can remove that responsibility from Blade. - Remove assets:compile and assets:verify tasks from ActionCable Now that we've added a compiled ActionCable bundle to version control, we don't need to compile and verify it at publish-time. (We're following the pattern set in ActiveStorage.) - Include compiled ActionCable javascript bundle in published gem This is necessary to maintain support for depending on the ActionCable javascript through the Sprockets asset pipeline. - Add compiled ActionCable bundle to version control This mirrors what we do in ActiveStorage, and allows ActionCable to continue to be consumed via the sprockets-based asset pipeline when using a git source instead of a published version of the gem. --- .../app/javascript/action_cable/connection.js | 248 ++++++++++----------- 1 file changed, 123 insertions(+), 125 deletions(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 540f597303..4ad436f2c0 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -1,158 +1,156 @@ -//= require ./connection_monitor +import ActionCable from "./index" +import INTERNAL from "./internal" // Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. -const {message_types, protocols} = ActionCable.INTERNAL +const {message_types, protocols} = INTERNAL const supportedProtocols = protocols.slice(0, protocols.length - 1) -ActionCable.Connection = (function() { - const indexOf = [].indexOf +const indexOf = [].indexOf - class Connection { - constructor(consumer) { - this.open = this.open.bind(this) - this.consumer = consumer - this.subscriptions = this.consumer.subscriptions - this.monitor = new ActionCable.ConnectionMonitor(this) - this.disconnected = true - } +class Connection { + constructor(consumer) { + this.open = this.open.bind(this) + this.consumer = consumer + this.subscriptions = this.consumer.subscriptions + this.monitor = new ActionCable.ConnectionMonitor(this) + this.disconnected = true + } - send(data) { - if (this.isOpen()) { - this.webSocket.send(JSON.stringify(data)) - return true - } else { - return false - } + send(data) { + if (this.isOpen()) { + this.webSocket.send(JSON.stringify(data)) + return true + } else { + return false } + } - open() { - if (this.isActive()) { - ActionCable.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}`) - if (this.webSocket) { this.uninstallEventHandlers() } - this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) - this.installEventHandlers() - this.monitor.start() - return true - } + open() { + if (this.isActive()) { + ActionCable.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}`) + if (this.webSocket) { this.uninstallEventHandlers() } + this.webSocket = new ActionCable.WebSocket(this.consumer.url, protocols) + this.installEventHandlers() + this.monitor.start() + return true } + } - close({allowReconnect} = {allowReconnect: true}) { - if (!allowReconnect) { this.monitor.stop() } - if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) } - } + close({allowReconnect} = {allowReconnect: true}) { + if (!allowReconnect) { this.monitor.stop() } + if (this.isActive()) { return (this.webSocket ? this.webSocket.close() : undefined) } + } - reopen() { - ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) - if (this.isActive()) { - try { - return this.close() - } catch (error) { - ActionCable.log("Failed to reopen WebSocket", error) - } - finally { - ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) - setTimeout(this.open, this.constructor.reopenDelay) - } - } else { - return this.open() + reopen() { + ActionCable.log(`Reopening WebSocket, current state is ${this.getState()}`) + if (this.isActive()) { + try { + return this.close() + } catch (error) { + ActionCable.log("Failed to reopen WebSocket", error) } + finally { + ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + setTimeout(this.open, this.constructor.reopenDelay) + } + } else { + return this.open() } + } - getProtocol() { - return (this.webSocket ? this.webSocket.protocol : undefined) - } + getProtocol() { + return (this.webSocket ? this.webSocket.protocol : undefined) + } - isOpen() { - return this.isState("open") - } + isOpen() { + return this.isState("open") + } - isActive() { - return this.isState("open", "connecting") - } + isActive() { + return this.isState("open", "connecting") + } - // Private + // Private - isProtocolSupported() { - return indexOf.call(supportedProtocols, this.getProtocol()) >= 0 - } + isProtocolSupported() { + return indexOf.call(supportedProtocols, this.getProtocol()) >= 0 + } - isState(...states) { - return indexOf.call(states, this.getState()) >= 0 - } + isState(...states) { + return indexOf.call(states, this.getState()) >= 0 + } - getState() { - if (this.webSocket) { - for (let state in WebSocket) { - if (WebSocket[state] === this.webSocket.readyState) { - return state.toLowerCase() - } + getState() { + if (this.webSocket) { + for (let state in WebSocket) { + if (WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase() } } - return null } + return null + } - installEventHandlers() { - for (let eventName in this.events) { - const 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 } + } - uninstallEventHandlers() { - for (let eventName in this.events) { - this.webSocket[`on${eventName}`] = function() {} - } + 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, message, type} = JSON.parse(event.data) - switch (type) { - case message_types.welcome: - this.monitor.recordConnect() - return this.subscriptions.reload() - 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() { - ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) - this.disconnected = false - if (!this.isProtocolSupported()) { - ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") - return this.close({allowReconnect: false}) - } - }, - - close(event) { - ActionCable.log("WebSocket onclose event") - if (this.disconnected) { return } - this.disconnected = true - this.monitor.recordDisconnect() - return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) - }, - - error() { - ActionCable.log("WebSocket onerror event") +} + +Connection.reopenDelay = 500 + +Connection.prototype.events = { + message(event) { + if (!this.isProtocolSupported()) { return } + const {identifier, message, type} = JSON.parse(event.data) + switch (type) { + case message_types.welcome: + this.monitor.recordConnect() + return this.subscriptions.reload() + 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() { + ActionCable.log(`WebSocket onopen event, using '${this.getProtocol()}' subprotocol`) + this.disconnected = false + if (!this.isProtocolSupported()) { + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + return this.close({allowReconnect: false}) } + }, + + close(event) { + ActionCable.log("WebSocket onclose event") + if (this.disconnected) { return } + this.disconnected = true + this.monitor.recordDisconnect() + return this.subscriptions.notifyAll("disconnected", {willAttemptReconnect: this.monitor.isRunning()}) + }, + + error() { + ActionCable.log("WebSocket onerror event") } +} - return Connection - -})() +export default Connection -- cgit v1.2.3 From aa1ba9cb244b1e03d36aaa941ae4e91c6713b77e Mon Sep 17 00:00:00 2001 From: rmacklin <1863540+rmacklin@users.noreply.github.com> Date: Sat, 1 Dec 2018 13:25:02 -0800 Subject: Remove circular dependency warnings in ActionCable javascript and publish source modules with fine-grained exports (#34370) * Replace several ActionCable.* references with finer-grained imports This reduces the number of circular dependencies among the module imports from 4: ``` (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/connection.js -> app/javascript/action_cable/index.js (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/connection_monitor.js -> app/javascript/action_cable/index.js (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/consumer.js -> app/javascript/action_cable/index.js (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/subscriptions.js -> app/javascript/action_cable/index.js ``` to 2: ``` (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/connection.js -> app/javascript/action_cable/index.js (!) Circular dependency: app/javascript/action_cable/index.js -> app/javascript/action_cable/connection.js -> app/javascript/action_cable/connection_monitor.js -> app/javascript/action_cable/index.js ``` * Remove tests that only test javascript object property assignment These tests really only assert that you can assign a property to the ActionCable global object. That's true for pretty much any object in javascript (it would only be false if the object has been frozen, or has explicitly set some properties to be nonconfigurable). * Refactor ActionCable to provide individual named exports By providing individual named exports rather than a default export which is an object with all of those properties, we enable applications to only import the functions they need: any unused functions will be removed via tree shaking. Additionally, this restructuring removes the remaining circular dependencies by extracting the separate adapters and logger modules, so there are now no warnings when compiling the ActionCable bundle. Note: This produces two small breaking API changes: - The `ActionCable.WebSocket` getter and setter would be moved to `ActionCable.adapters.WebSocket`. If a user is currently configuring this, when upgrading they'd need to either add a delegated getter/setter themselves, or change it like this: ```diff - ActionCable.WebSocket = MyWebSocket + ActionCable.adapters.WebSocket = MyWebSocket ``` Applications which don't change the WebSocket adapter would not need any changes for this when upgrading. - Similarly, the `ActionCable.logger` getter and setter would be moved to `ActionCable.adapters.logger`. If a user is currently configuring this, when upgrading they'd need to either add a delegated getter/setter themselves, or change it like this: ```diff - ActionCable.logger = myLogger + ActionCable.adapters.logger = myLogger ``` Applications which don't change the logger would not need any changes for this when upgrading. These two aspects of the public API have to change because there's no way to export a property setter for `WebSocket` (or `logger`) such that this: ```js import ActionCable from "actioncable" ActionCable.WebSocket = MyWebSocket ``` would actually update `adapters.WebSocket`. (We can only offer that if we have two separate source files like if `index.js` uses `import * as ActionCable from "./action_cable" and then exports a wrapper which has delegated getters and setters for those properties.) This API change is very minor - it should be easy for applications to add the `adapters.` prefix in their assignments or to patch in delegated setters. And especially because most applications in the wild are not ever changing the default value of `ActionCable.WebSocket` or `ActionCable.logger` (because the default values are perfect), this API breakage is worth the tree-shaking benefits we gain. * Include source code in published actioncable npm package This allows actioncable users to ship smaller javascript bundles to visitors using modern browsers, as demonstrated in this repository: https://github.com/rmacklin/actioncable-es2015-build-example In that example, the bundle shrinks by 2.8K (25.2%) when you simply change the actioncable import to point to the untranspiled src. If you go a step further, like this: ``` diff --git a/app/scripts/main.js b/app/scripts/main.js index 17bc031..1a2b2e0 100644 --- a/app/scripts/main.js +++ b/app/scripts/main.js @@ -1,6 +1,6 @@ -import ActionCable from 'actioncable'; +import * as ActionCable from 'actioncable'; let cable = ActionCable.createConsumer('wss://cable.example.com'); cable.subscriptions.create('AppearanceChannel', { ``` then the bundle shrinks by 3.6K (31.7%)! In addition to allowing smaller bundles for those who ship untranspiled code to modern browsers, including the source code in the published package can be useful in other ways: 1. Users can import individual modules rather than the whole library 2. As a result of (1), users can also monkey patch parts of actioncable by importing the relevant module, modifying the exported object, and then importing the rest of actioncable (which would then use the patched object). Note: This is the same enhancement that we made to activestorage in c0368ad090b79c19300a4aa133bb188b2d9ab611 * Remove unused commonjs & resolve plugins from ActionCable rollup config These were added when we copied the rollup config from ActiveStorage, but ActionCable does not have any commonjs dependencies (it doesn't have any external dependencies at all), so these plugins are unnecessary here * Change ActionCable.startDebugging() -> ActionCable.logger.enabled=true and ActionCable.stopDebugging() -> ActionCable.logger.enabled=false This API is simpler and more clearly describes what it does * Change Travis configuration to run yarn install at the root for ActionCable builds This is necessary now that the repository is using Yarn Workspaces --- .../app/javascript/action_cable/connection.js | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') 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") } } -- cgit v1.2.3 From 2bb4fdef5efc70327c018e982ff809a29ac6708b Mon Sep 17 00:00:00 2001 From: Richard Macklin Date: Sat, 1 Dec 2018 14:48:24 -0800 Subject: Replace reference to WebSocket global with ActionCable.adapters.WebSocket The WebSocket dependency of ActionCable.Connection was made configurable in 66901c1849efae74c8a58fe0cb36afd487c067cc However, the reference here in Connection#getState was not updated to use the configurable property. This change remedies that and adds a test to verify it. Additionally, it backfills a test to ensure that Connection#open uses the configurable property. --- actioncable/app/javascript/action_cable/connection.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index e3ff8bde24..8aa4fe1266 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -88,8 +88,8 @@ class Connection { getState() { if (this.webSocket) { - for (let state in WebSocket) { - if (WebSocket[state] === this.webSocket.readyState) { + for (let state in adapters.WebSocket) { + if (adapters.WebSocket[state] === this.webSocket.readyState) { return state.toLowerCase() } } -- cgit v1.2.3 From 58dbc1c2ed0e372d9cae4c9e3baebb679a726dc3 Mon Sep 17 00:00:00 2001 From: Mick Staugaard Date: Thu, 11 Oct 2018 13:47:16 -0700 Subject: Stop trying to reconnect on unauthorized cable connections --- actioncable/app/javascript/action_cable/connection.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'actioncable/app/javascript/action_cable/connection.js') diff --git a/actioncable/app/javascript/action_cable/connection.js b/actioncable/app/javascript/action_cable/connection.js index 8aa4fe1266..b2910cb2a6 100644 --- a/actioncable/app/javascript/action_cable/connection.js +++ b/actioncable/app/javascript/action_cable/connection.js @@ -117,11 +117,14 @@ Connection.reopenDelay = 500 Connection.prototype.events = { message(event) { if (!this.isProtocolSupported()) { return } - const {identifier, message, type} = JSON.parse(event.data) + const {identifier, message, reason, reconnect, 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: -- cgit v1.2.3