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.
This commit is contained in:
Richard Macklin 2018-01-12 16:49:09 -08:00
parent 403c001c56
commit 0eb6b86e96
6 changed files with 232 additions and 283 deletions

View File

@ -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()
})()

View File

@ -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
})()

View File

@ -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.
//

View File

@ -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)
}
}
}

View File

@ -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
})()

View File

@ -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) {