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 supportedProtocols = protocols.slice(0, protocols.length - 1) -const Cls = (ActionCable.Connection = class Connection { - static initClass() { - this.reopenDelay = 500 +ActionCable.Connection = (function() { + const indexOf = [].indexOf - 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) + 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 + } + } + + 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) } + } + + 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) } - }, - - 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}) + finally { + ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) + setTimeout(this.open, this.constructor.reopenDelay) } - }, - - 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") + } else { + return this.open() } } - } - 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 + getProtocol() { + return (this.webSocket ? this.webSocket.protocol : undefined) } - } - 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 + isOpen() { + return this.isState("open") } - } - 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) } - } + isActive() { + return this.isState("open", "connecting") + } - 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) + // Private + + 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 WebSocket) { + if (WebSocket[state] === this.webSocket.readyState) { + return state.toLowerCase() + } + } } - finally { - ActionCable.log(`Reopening WebSocket in ${this.constructor.reopenDelay}ms`) - setTimeout(this.open, this.constructor.reopenDelay) + return null + } + + installEventHandlers() { + for (let eventName in this.events) { + const handler = this.events[eventName].bind(this) + this.webSocket[`on${eventName}`] = handler } - } else { - return this.open() + } + + 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") } } - getProtocol() { - return (this.webSocket != null ? this.webSocket.protocol : undefined) - } + return Connection - 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() +})() diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js index c8d2c62cc8..4d2db5b4ae 100644 --- a/actioncable/app/javascript/action_cable/connection_monitor.js +++ b/actioncable/app/javascript/action_cable/connection_monitor.js @@ -1,33 +1,13 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * 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 - */ // Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting // revival reconnections if things go astray. Internal class, not intended for direct user manipulation. -(function() { - let now = undefined - let secondsSince = undefined - let clamp = undefined - const Cls = (ActionCable.ConnectionMonitor = class ConnectionMonitor { - static initClass() { - this.pollInterval = { - min: 3, - max: 30 - } - - this.staleThreshold = 6 - - now = () => new Date().getTime() - - secondsSince = time => (now() - time) / 1000 - - clamp = (number, min, max) => Math.max(min, Math.min(max, number)) - // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) - } +ActionCable.ConnectionMonitor = (function() { + const now = () => new Date().getTime() + const secondsSince = time => (now() - time) / 1000 + + const clamp = (number, min, max) => Math.max(min, Math.min(max, number)) + + class ConnectionMonitor { constructor(connection) { this.visibilityDidChange = this.visibilityDidChange.bind(this) this.connection = connection @@ -40,7 +20,7 @@ delete this.stoppedAt this.startPolling() document.addEventListener("visibilitychange", this.visibilityDidChange) - return ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) + ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`) } } @@ -49,45 +29,45 @@ this.stoppedAt = now() this.stopPolling() document.removeEventListener("visibilitychange", this.visibilityDidChange) - return ActionCable.log("ConnectionMonitor stopped") + ActionCable.log("ConnectionMonitor stopped") } } isRunning() { - return (this.startedAt != null) && (this.stoppedAt == null) + return this.startedAt && !this.stoppedAt } recordPing() { - return this.pingedAt = now() + this.pingedAt = now() } recordConnect() { this.reconnectAttempts = 0 this.recordPing() delete this.disconnectedAt - return ActionCable.log("ConnectionMonitor recorded connect") + ActionCable.log("ConnectionMonitor recorded connect") } recordDisconnect() { this.disconnectedAt = now() - return ActionCable.log("ConnectionMonitor recorded disconnect") + ActionCable.log("ConnectionMonitor recorded disconnect") } // Private startPolling() { this.stopPolling() - return this.poll() + this.poll() } stopPolling() { - return clearTimeout(this.pollTimeout) + clearTimeout(this.pollTimeout) } poll() { - return this.pollTimeout = setTimeout(() => { + this.pollTimeout = setTimeout(() => { this.reconnectIfStale() - return this.poll() + this.poll() } , this.getPollInterval()) } @@ -103,16 +83,16 @@ ActionCable.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`) this.reconnectAttempts++ if (this.disconnectedRecently()) { - return ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") + ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") } else { ActionCable.log("ConnectionMonitor reopening") - return this.connection.reopen() + this.connection.reopen() } } } connectionIsStale() { - return secondsSince(this.pingedAt != null ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold + return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold } disconnectedRecently() { @@ -121,16 +101,25 @@ visibilityDidChange() { if (document.visibilityState === "visible") { - return setTimeout(() => { + setTimeout(() => { if (this.connectionIsStale() || !this.connection.isOpen()) { ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`) - return this.connection.reopen() + this.connection.reopen() } } , 200) } } - }) - Cls.initClass() - return Cls + + } + + ConnectionMonitor.pollInterval = { + min: 3, + max: 30 + } + + ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings) + + return ConnectionMonitor + })() diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js index c2a851b876..731e00996d 100644 --- a/actioncable/app/javascript/action_cable/consumer.js +++ b/actioncable/app/javascript/action_cable/consumer.js @@ -1,8 +1,3 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ //= require ./connection //= require ./subscriptions //= require ./subscription @@ -14,9 +9,9 @@ // // The following example shows how this can be setup: // -// @App = {} -// App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" -// App.appearance = App.cable.subscriptions.create "AppearanceChannel" +// App = {} +// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") +// App.appearance = App.cable.subscriptions.create("AppearanceChannel") // // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. // diff --git a/actioncable/app/javascript/action_cable/index.js.erb b/actioncable/app/javascript/action_cable/index.js.erb index a5fb5b4556..eb85eba722 100644 --- a/actioncable/app/javascript/action_cable/index.js.erb +++ b/actioncable/app/javascript/action_cable/index.js.erb @@ -1,12 +1,3 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS104: Avoid inline assignments - * DS207: Consider shorter variations of null checks - * DS208: Avoid top-level this - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ //= export ActionCable //= require_self //= require ./action_cable/consumer @@ -17,14 +8,16 @@ this.ActionCable = { logger: window.console, createConsumer(url) { - if (url == null) { let left - url = (left = this.getConfig("url")) != null ? left : this.INTERNAL.default_mount_path } + if (url == null) { + const urlConfig = this.getConfig("url") + url = (urlConfig ? urlConfig : this.INTERNAL.default_mount_path) + } return new ActionCable.Consumer(this.createWebSocketURL(url)) }, getConfig(name) { const element = document.head.querySelector(`meta[name='action-cable-${name}']`) - return (element != null ? element.getAttribute("content") : undefined) + return (element ? element.getAttribute("content") : undefined) }, createWebSocketURL(url) { @@ -41,17 +34,17 @@ this.ActionCable = { }, startDebugging() { - return this.debugging = true + this.debugging = true }, stopDebugging() { - return this.debugging = null + this.debugging = null }, log(...messages) { if (this.debugging) { messages.push(Date.now()) - return this.logger.log("[ActionCable]", ...Array.from(messages)) + this.logger.log("[ActionCable]", ...messages) } } } diff --git a/actioncable/app/javascript/action_cable/subscription.js b/actioncable/app/javascript/action_cable/subscription.js index 3659c9ca46..95b9ff6042 100644 --- a/actioncable/app/javascript/action_cable/subscription.js +++ b/actioncable/app/javascript/action_cable/subscription.js @@ -1,37 +1,36 @@ -/* - * decaffeinate suggestions: - * DS102: Remove unnecessary code created because of implicit returns - * 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 - */ // A new subscription is created through the ActionCable.Subscriptions instance available on the consumer. // It provides a number of callbacks and a method for calling remote procedure calls on the corresponding // Channel instance on the server side. // // An example demonstrates the basic functionality: // -// App.appearance = App.cable.subscriptions.create "AppearanceChannel", -// connected: -> -// # Called once the subscription has been successfully completed +// App.appearance = App.cable.subscriptions.create("AppearanceChannel", { +// connected() { +// // Called once the subscription has been successfully completed +// }, // -// disconnected: ({ willAttemptReconnect: boolean }) -> -// # Called when the client has disconnected with the server. -// # The object will have an `willAttemptReconnect` property which -// # says whether the client has the intention of attempting -// # to reconnect. +// disconnected({ willAttemptReconnect: boolean }) { +// // Called when the client has disconnected with the server. +// // The object will have an `willAttemptReconnect` property which +// // says whether the client has the intention of attempting +// // to reconnect. +// }, // -// appear: -> -// @perform 'appear', appearing_on: @appearingOn() +// appear() { +// this.perform('appear', {appearing_on: this.appearingOn()}) +// }, // -// away: -> -// @perform 'away' +// away() { +// this.perform('away') +// }, // -// appearingOn: -> -// $('main').data 'appearing-on' +// appearingOn() { +// $('main').data('appearing-on') +// } +// }) // // The methods #appear and #away forward their intent to the remote AppearanceChannel instance on the server -// by calling the `@perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away). +// by calling the `perform` method with the first parameter being the action (which maps to AppearanceChannel#appear/away). // The second parameter is a hash that'll get JSON encoded and made available on the server in the data parameter. // // This is how the server component would look: @@ -55,32 +54,27 @@ // end // // The "AppearanceChannel" name is automatically mapped between the client-side subscription creation and the server-side Ruby class name. -// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the @perform method. -(function() { - let extend = undefined - const Cls = (ActionCable.Subscription = class Subscription { - static initClass() { - - extend = function(object, properties) { - if (properties != null) { - for (let key in properties) { - const value = properties[key] - object[key] = value - } - } - return object +// The AppearanceChannel#appear/away public methods are exposed automatically to client-side invocation through the perform method. +ActionCable.Subscription = (function() { + const extend = function(object, properties) { + if (properties != null) { + for (let key in properties) { + const value = properties[key] + object[key] = value } } - constructor(consumer, params, mixin) { + return object + } + + class Subscription { + constructor(consumer, params = {}, mixin) { this.consumer = consumer - if (params == null) { params = {} } this.identifier = JSON.stringify(params) extend(this, mixin) } // Perform a channel action with the optional data passed as an attribute - perform(action, data) { - if (data == null) { data = {} } + perform(action, data = {}) { data.action = action return this.send(data) } @@ -92,7 +86,7 @@ unsubscribe() { return this.consumer.subscriptions.remove(this) } - }) - Cls.initClass() - return Cls + } + + return Subscription })() diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js index 105dc51b56..65bdcc4ece 100644 --- a/actioncable/app/javascript/action_cable/subscriptions.js +++ b/actioncable/app/javascript/action_cable/subscriptions.js @@ -1,16 +1,9 @@ -/* - * decaffeinate suggestions: - * DS101: Remove unnecessary use of Array.from - * DS102: Remove unnecessary code created because of implicit returns - * DS205: Consider reworking code to avoid use of IIFEs - * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md - */ // Collection class for creating (and internally managing) channel subscriptions. The only method intended to be triggered by the user // us ActionCable.Subscriptions#create, and it should be called through the consumer like so: // -// @App = {} -// App.cable = ActionCable.createConsumer "ws://example.com/accounts/1" -// App.appearance = App.cable.subscriptions.create "AppearanceChannel" +// App = {} +// App.cable = ActionCable.createConsumer("ws://example.com/accounts/1") +// App.appearance = App.cable.subscriptions.create("AppearanceChannel") // // For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription. ActionCable.Subscriptions = class Subscriptions { @@ -45,34 +38,30 @@ ActionCable.Subscriptions = class Subscriptions { } reject(identifier) { - return (() => { - const result = [] - for (let subscription of Array.from(this.findAll(identifier))) { - this.forget(subscription) - this.notify(subscription, "rejected") - result.push(subscription) - } - return result - })() + return this.findAll(identifier).map((subscription) => { + this.forget(subscription) + this.notify(subscription, "rejected") + return subscription + }) } forget(subscription) { - this.subscriptions = (Array.from(this.subscriptions).filter((s) => s !== subscription)) + this.subscriptions = (this.subscriptions.filter((s) => s !== subscription)) return subscription } findAll(identifier) { - return Array.from(this.subscriptions).filter((s) => s.identifier === identifier) + return this.subscriptions.filter((s) => s.identifier === identifier) } reload() { - return Array.from(this.subscriptions).map((subscription) => + return this.subscriptions.map((subscription) => this.sendCommand(subscription, "subscribe")) } notifyAll(callbackName, ...args) { - return Array.from(this.subscriptions).map((subscription) => - this.notify(subscription, callbackName, ...Array.from(args))) + return this.subscriptions.map((subscription) => + this.notify(subscription, callbackName, ...args)) } notify(subscription, callbackName, ...args) { @@ -83,13 +72,8 @@ ActionCable.Subscriptions = class Subscriptions { subscriptions = [subscription] } - return (() => { - const result = [] - for (subscription of Array.from(subscriptions)) { - result.push((typeof subscription[callbackName] === "function" ? subscription[callbackName](...Array.from(args || [])) : undefined)) - } - return result - })() + return subscriptions.map((subscription) => + (typeof subscription[callbackName] === "function" ? subscription[callbackName](...args) : undefined)) } sendCommand(subscription, command) {