aboutsummaryrefslogtreecommitdiffstats
path: root/actioncable/app
diff options
context:
space:
mode:
authorRichard Macklin <richard.github@nrm.com>2017-12-11 14:44:52 +0800
committerRichard Macklin <richard.github@nrm.com>2018-11-02 08:40:35 -0700
commit403c001c56e3980e624da2cb1e1e98d667499d40 (patch)
treefcdcb33626125f00dff1fc11a9447995f8b7abf4 /actioncable/app
parent7b0b37240a20c74197408ac3b53519e7e18347e9 (diff)
downloadrails-403c001c56e3980e624da2cb1e1e98d667499d40.tar.gz
rails-403c001c56e3980e624da2cb1e1e98d667499d40.tar.bz2
rails-403c001c56e3980e624da2cb1e1e98d667499d40.zip
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
Diffstat (limited to 'actioncable/app')
-rw-r--r--actioncable/app/javascript/action_cable/connection.js280
-rw-r--r--actioncable/app/javascript/action_cable/connection_monitor.js231
-rw-r--r--actioncable/app/javascript/action_cable/consumer.js95
-rw-r--r--actioncable/app/javascript/action_cable/index.js.erb73
-rw-r--r--actioncable/app/javascript/action_cable/subscription.js164
-rw-r--r--actioncable/app/javascript/action_cable/subscriptions.js139
6 files changed, 581 insertions, 401 deletions
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()
diff --git a/actioncable/app/javascript/action_cable/connection_monitor.js b/actioncable/app/javascript/action_cable/connection_monitor.js
index 0cc675fa94..c8d2c62cc8 100644
--- a/actioncable/app/javascript/action_cable/connection_monitor.js
+++ b/actioncable/app/javascript/action_cable/connection_monitor.js
@@ -1,95 +1,136 @@
-# 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.
-class ActionCable.ConnectionMonitor
- @pollInterval:
- min: 3
- max: 30
-
- @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
-
- constructor: (@connection) ->
- @reconnectAttempts = 0
-
- start: ->
- unless @isRunning()
- @startedAt = now()
- delete @stoppedAt
- @startPolling()
- document.addEventListener("visibilitychange", @visibilityDidChange)
- ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")
-
- stop: ->
- if @isRunning()
- @stoppedAt = now()
- @stopPolling()
- document.removeEventListener("visibilitychange", @visibilityDidChange)
- ActionCable.log("ConnectionMonitor stopped")
-
- isRunning: ->
- @startedAt? and not @stoppedAt?
-
- recordPing: ->
- @pingedAt = now()
-
- recordConnect: ->
- @reconnectAttempts = 0
- @recordPing()
- delete @disconnectedAt
- ActionCable.log("ConnectionMonitor recorded connect")
-
- recordDisconnect: ->
- @disconnectedAt = now()
- ActionCable.log("ConnectionMonitor recorded disconnect")
-
- # Private
-
- startPolling: ->
- @stopPolling()
- @poll()
-
- stopPolling: ->
- clearTimeout(@pollTimeout)
-
- poll: ->
- @pollTimeout = setTimeout =>
- @reconnectIfStale()
- @poll()
- , @getPollInterval()
-
- getPollInterval: ->
- {min, max} = @constructor.pollInterval
- interval = 5 * Math.log(@reconnectAttempts + 1)
- Math.round(clamp(interval, min, max) * 1000)
-
- reconnectIfStale: ->
- if @connectionIsStale()
- ActionCable.log("ConnectionMonitor detected stale connection. reconnectAttempts = #{@reconnectAttempts}, pollInterval = #{@getPollInterval()} ms, time disconnected = #{secondsSince(@disconnectedAt)} s, stale threshold = #{@constructor.staleThreshold} s")
- @reconnectAttempts++
- if @disconnectedRecently()
- ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
- else
- ActionCable.log("ConnectionMonitor reopening")
- @connection.reopen()
-
- connectionIsStale: ->
- secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
-
- disconnectedRecently: ->
- @disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold
-
- visibilityDidChange: =>
- if document.visibilityState is "visible"
- setTimeout =>
- if @connectionIsStale() or not @connection.isOpen()
- ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}")
- @connection.reopen()
- , 200
-
- now = ->
- new Date().getTime()
-
- secondsSince = (time) ->
- (now() - time) / 1000
-
- clamp = (number, min, max) ->
- Math.max(min, Math.min(max, number))
+/*
+ * 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)
+ }
+
+ constructor(connection) {
+ this.visibilityDidChange = this.visibilityDidChange.bind(this)
+ this.connection = connection
+ this.reconnectAttempts = 0
+ }
+
+ start() {
+ if (!this.isRunning()) {
+ this.startedAt = now()
+ delete this.stoppedAt
+ this.startPolling()
+ document.addEventListener("visibilitychange", this.visibilityDidChange)
+ return ActionCable.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
+ }
+ }
+
+ stop() {
+ if (this.isRunning()) {
+ this.stoppedAt = now()
+ this.stopPolling()
+ document.removeEventListener("visibilitychange", this.visibilityDidChange)
+ return ActionCable.log("ConnectionMonitor stopped")
+ }
+ }
+
+ isRunning() {
+ return (this.startedAt != null) && (this.stoppedAt == null)
+ }
+
+ recordPing() {
+ return this.pingedAt = now()
+ }
+
+ recordConnect() {
+ this.reconnectAttempts = 0
+ this.recordPing()
+ delete this.disconnectedAt
+ return ActionCable.log("ConnectionMonitor recorded connect")
+ }
+
+ recordDisconnect() {
+ this.disconnectedAt = now()
+ return ActionCable.log("ConnectionMonitor recorded disconnect")
+ }
+
+ // Private
+
+ startPolling() {
+ this.stopPolling()
+ return this.poll()
+ }
+
+ stopPolling() {
+ return clearTimeout(this.pollTimeout)
+ }
+
+ poll() {
+ return this.pollTimeout = setTimeout(() => {
+ this.reconnectIfStale()
+ return this.poll()
+ }
+ , this.getPollInterval())
+ }
+
+ getPollInterval() {
+ const {min, max} = this.constructor.pollInterval
+ const interval = 5 * Math.log(this.reconnectAttempts + 1)
+ return Math.round(clamp(interval, min, max) * 1000)
+ }
+
+ reconnectIfStale() {
+ if (this.connectionIsStale()) {
+ ActionCable.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
+ this.reconnectAttempts++
+ if (this.disconnectedRecently()) {
+ return ActionCable.log("ConnectionMonitor skipping reopening recent disconnect")
+ } else {
+ ActionCable.log("ConnectionMonitor reopening")
+ return this.connection.reopen()
+ }
+ }
+ }
+
+ connectionIsStale() {
+ return secondsSince(this.pingedAt != null ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold
+ }
+
+ disconnectedRecently() {
+ return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.constructor.staleThreshold)
+ }
+
+ visibilityDidChange() {
+ if (document.visibilityState === "visible") {
+ return setTimeout(() => {
+ if (this.connectionIsStale() || !this.connection.isOpen()) {
+ ActionCable.log(`ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = ${document.visibilityState}`)
+ return this.connection.reopen()
+ }
+ }
+ , 200)
+ }
+ }
+ })
+ Cls.initClass()
+ return Cls
+})()
diff --git a/actioncable/app/javascript/action_cable/consumer.js b/actioncable/app/javascript/action_cable/consumer.js
index 3298be717f..c2a851b876 100644
--- a/actioncable/app/javascript/action_cable/consumer.js
+++ b/actioncable/app/javascript/action_cable/consumer.js
@@ -1,46 +1,59 @@
-#= require ./connection
-#= require ./subscriptions
-#= require ./subscription
+/*
+ * 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
-# The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
-# the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
-# The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
-# method.
-#
-# 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"
-#
-# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
-#
-# When a consumer is created, it automatically connects with the server.
-#
-# To disconnect from the server, call
-#
-# App.cable.disconnect()
-#
-# and to restart the connection:
-#
-# App.cable.connect()
-#
-# Any channel subscriptions which existed prior to disconnecting will
-# automatically resubscribe.
-class ActionCable.Consumer
- constructor: (@url) ->
- @subscriptions = new ActionCable.Subscriptions this
- @connection = new ActionCable.Connection this
+// The ActionCable.Consumer establishes the connection to a server-side Ruby Connection object. Once established,
+// the ActionCable.ConnectionMonitor will ensure that its properly maintained through heartbeats and checking for stale updates.
+// The Consumer instance is also the gateway to establishing subscriptions to desired channels through the #createSubscription
+// method.
+//
+// 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"
+//
+// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+//
+// When a consumer is created, it automatically connects with the server.
+//
+// To disconnect from the server, call
+//
+// App.cable.disconnect()
+//
+// and to restart the connection:
+//
+// App.cable.connect()
+//
+// Any channel subscriptions which existed prior to disconnecting will
+// automatically resubscribe.
+ActionCable.Consumer = class Consumer {
+ constructor(url) {
+ this.url = url
+ this.subscriptions = new ActionCable.Subscriptions(this)
+ this.connection = new ActionCable.Connection(this)
+ }
- send: (data) ->
- @connection.send(data)
+ send(data) {
+ return this.connection.send(data)
+ }
- connect: ->
- @connection.open()
+ connect() {
+ return this.connection.open()
+ }
- disconnect: ->
- @connection.close(allowReconnect: false)
+ disconnect() {
+ return this.connection.close({allowReconnect: false})
+ }
- ensureActiveConnection: ->
- unless @connection.isActive()
- @connection.open()
+ ensureActiveConnection() {
+ if (!this.connection.isActive()) {
+ return this.connection.open()
+ }
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/index.js.erb b/actioncable/app/javascript/action_cable/index.js.erb
index e0758dae72..a5fb5b4556 100644
--- a/actioncable/app/javascript/action_cable/index.js.erb
+++ b/actioncable/app/javascript/action_cable/index.js.erb
@@ -1,38 +1,57 @@
-#= export ActionCable
-#= require_self
-#= require ./action_cable/consumer
+/*
+ * 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
-@ActionCable =
- INTERNAL: <%= ActionCable::INTERNAL.to_json %>
- WebSocket: window.WebSocket
- logger: window.console
+this.ActionCable = {
+ INTERNAL: <%= ActionCable::INTERNAL.to_json %>,
+ WebSocket: window.WebSocket,
+ logger: window.console,
- createConsumer: (url) ->
- url ?= @getConfig("url") ? @INTERNAL.default_mount_path
- new ActionCable.Consumer @createWebSocketURL(url)
+ createConsumer(url) {
+ if (url == null) { let left
+ url = (left = this.getConfig("url")) != null ? left : this.INTERNAL.default_mount_path }
+ return new ActionCable.Consumer(this.createWebSocketURL(url))
+ },
- getConfig: (name) ->
- element = document.head.querySelector("meta[name='action-cable-#{name}']")
- element?.getAttribute("content")
+ getConfig(name) {
+ const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
+ return (element != null ? element.getAttribute("content") : undefined)
+ },
- createWebSocketURL: (url) ->
- if url and not /^wss?:/i.test(url)
- a = document.createElement("a")
+ createWebSocketURL(url) {
+ if (url && !/^wss?:/i.test(url)) {
+ const a = document.createElement("a")
a.href = url
- # Fix populating Location properties in IE. Otherwise, protocol will be blank.
+ // Fix populating Location properties in IE. Otherwise, protocol will be blank.
a.href = a.href
a.protocol = a.protocol.replace("http", "ws")
- a.href
- else
- url
+ return a.href
+ } else {
+ return url
+ }
+ },
- startDebugging: ->
- @debugging = true
+ startDebugging() {
+ return this.debugging = true
+ },
- stopDebugging: ->
- @debugging = null
+ stopDebugging() {
+ return this.debugging = null
+ },
- log: (messages...) ->
- if @debugging
+ log(...messages) {
+ if (this.debugging) {
messages.push(Date.now())
- @logger.log("[ActionCable]", messages...)
+ return this.logger.log("[ActionCable]", ...Array.from(messages))
+ }
+ }
+}
diff --git a/actioncable/app/javascript/action_cable/subscription.js b/actioncable/app/javascript/action_cable/subscription.js
index 8e0805a174..3659c9ca46 100644
--- a/actioncable/app/javascript/action_cable/subscription.js
+++ b/actioncable/app/javascript/action_cable/subscription.js
@@ -1,72 +1,98 @@
-# 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
-#
-# 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()
-#
-# away: ->
-# @perform 'away'
-#
-# 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).
-# 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:
-#
-# class AppearanceChannel < ApplicationActionCable::Channel
-# def subscribed
-# current_user.appear
-# end
-#
-# def unsubscribed
-# current_user.disappear
-# end
-#
-# def appear(data)
-# current_user.appear on: data['appearing_on']
-# end
-#
-# def away
-# current_user.away
-# end
-# 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.
-class ActionCable.Subscription
- constructor: (@consumer, params = {}, mixin) ->
- @identifier = JSON.stringify(params)
- extend(this, mixin)
+/*
+ * 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
+//
+// 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()
+//
+// away: ->
+// @perform 'away'
+//
+// 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).
+// 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:
+//
+// class AppearanceChannel < ApplicationActionCable::Channel
+// def subscribed
+// current_user.appear
+// end
+//
+// def unsubscribed
+// current_user.disappear
+// end
+//
+// def appear(data)
+// current_user.appear on: data['appearing_on']
+// end
+//
+// def away
+// current_user.away
+// end
+// 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
+ }
+ }
+ 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 = {}) ->
- data.action = action
- @send(data)
+ // Perform a channel action with the optional data passed as an attribute
+ perform(action, data) {
+ if (data == null) { data = {} }
+ data.action = action
+ return this.send(data)
+ }
- send: (data) ->
- @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data))
+ send(data) {
+ return this.consumer.send({command: "message", identifier: this.identifier, data: JSON.stringify(data)})
+ }
- unsubscribe: ->
- @consumer.subscriptions.remove(this)
-
- extend = (object, properties) ->
- if properties?
- for key, value of properties
- object[key] = value
- object
+ unsubscribe() {
+ return this.consumer.subscriptions.remove(this)
+ }
+ })
+ Cls.initClass()
+ return Cls
+})()
diff --git a/actioncable/app/javascript/action_cable/subscriptions.js b/actioncable/app/javascript/action_cable/subscriptions.js
index aa052bf5d8..105dc51b56 100644
--- a/actioncable/app/javascript/action_cable/subscriptions.js
+++ b/actioncable/app/javascript/action_cable/subscriptions.js
@@ -1,66 +1,99 @@
-# 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"
-#
-# For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
-class ActionCable.Subscriptions
- constructor: (@consumer) ->
- @subscriptions = []
+/*
+ * 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"
+//
+// For more details on how you'd configure an actual channel subscription, see ActionCable.Subscription.
+ActionCable.Subscriptions = class Subscriptions {
+ constructor(consumer) {
+ this.consumer = consumer
+ this.subscriptions = []
+ }
- create: (channelName, mixin) ->
- channel = channelName
- params = if typeof channel is "object" then channel else {channel}
- subscription = new ActionCable.Subscription @consumer, params, mixin
- @add(subscription)
+ create(channelName, mixin) {
+ const channel = channelName
+ const params = typeof channel === "object" ? channel : {channel}
+ const subscription = new ActionCable.Subscription(this.consumer, params, mixin)
+ return this.add(subscription)
+ }
- # Private
+ // Private
- add: (subscription) ->
- @subscriptions.push(subscription)
- @consumer.ensureActiveConnection()
- @notify(subscription, "initialized")
- @sendCommand(subscription, "subscribe")
- subscription
+ add(subscription) {
+ this.subscriptions.push(subscription)
+ this.consumer.ensureActiveConnection()
+ this.notify(subscription, "initialized")
+ this.sendCommand(subscription, "subscribe")
+ return subscription
+ }
- remove: (subscription) ->
- @forget(subscription)
- unless @findAll(subscription.identifier).length
- @sendCommand(subscription, "unsubscribe")
- subscription
+ remove(subscription) {
+ this.forget(subscription)
+ if (!this.findAll(subscription.identifier).length) {
+ this.sendCommand(subscription, "unsubscribe")
+ }
+ return subscription
+ }
- reject: (identifier) ->
- for subscription in @findAll(identifier)
- @forget(subscription)
- @notify(subscription, "rejected")
- subscription
+ 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
+ })()
+ }
- forget: (subscription) ->
- @subscriptions = (s for s in @subscriptions when s isnt subscription)
- subscription
+ forget(subscription) {
+ this.subscriptions = (Array.from(this.subscriptions).filter((s) => s !== subscription))
+ return subscription
+ }
- findAll: (identifier) ->
- s for s in @subscriptions when s.identifier is identifier
+ findAll(identifier) {
+ return Array.from(this.subscriptions).filter((s) => s.identifier === identifier)
+ }
- reload: ->
- for subscription in @subscriptions
- @sendCommand(subscription, "subscribe")
+ reload() {
+ return Array.from(this.subscriptions).map((subscription) =>
+ this.sendCommand(subscription, "subscribe"))
+ }
- notifyAll: (callbackName, args...) ->
- for subscription in @subscriptions
- @notify(subscription, callbackName, args...)
+ notifyAll(callbackName, ...args) {
+ return Array.from(this.subscriptions).map((subscription) =>
+ this.notify(subscription, callbackName, ...Array.from(args)))
+ }
- notify: (subscription, callbackName, args...) ->
- if typeof subscription is "string"
- subscriptions = @findAll(subscription)
- else
+ notify(subscription, callbackName, ...args) {
+ let subscriptions
+ if (typeof subscription === "string") {
+ subscriptions = this.findAll(subscription)
+ } else {
subscriptions = [subscription]
+ }
- for subscription in subscriptions
- subscription[callbackName]?(args...)
+ return (() => {
+ const result = []
+ for (subscription of Array.from(subscriptions)) {
+ result.push((typeof subscription[callbackName] === "function" ? subscription[callbackName](...Array.from(args || [])) : undefined))
+ }
+ return result
+ })()
+ }
- sendCommand: (subscription, command) ->
- {identifier} = subscription
- @consumer.send({command, identifier})
+ sendCommand(subscription, command) {
+ const {identifier} = subscription
+ return this.consumer.send({command, identifier})
+ }
+}