aboutsummaryrefslogtreecommitdiffstats
path: root/actioncable/app
diff options
context:
space:
mode:
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})
+ }
+}