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
This commit is contained in:
Richard Macklin 2017-12-11 14:44:52 +08:00
parent 7b0b37240a
commit 403c001c56
6 changed files with 544 additions and 364 deletions

View File

@ -1,116 +1,164 @@
#= require ./connection_monitor
/*
* 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.
// 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
const {message_types, protocols} = ActionCable.INTERNAL
const adjustedLength = Math.max(protocols.length, 1),
supportedProtocols = protocols.slice(0, adjustedLength - 1),
unsupportedProtocol = protocols[adjustedLength - 1]
class ActionCable.Connection
@reopenDelay: 500
const Cls = (ActionCable.Connection = class Connection {
static initClass() {
this.reopenDelay = 500
constructor: (@consumer) ->
{@subscriptions} = @consumer
@monitor = new ActionCable.ConnectionMonitor this
@disconnected = true
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)
}
},
send: (data) ->
if @isOpen()
@webSocket.send(JSON.stringify(data))
true
else
false
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})
}
},
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(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()})
},
close: ({allowReconnect} = {allowReconnect: true}) ->
@monitor.stop() unless allowReconnect
@webSocket?.close() if @isActive()
error() {
return ActionCable.log("WebSocket onerror event")
}
}
}
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()
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
}
getProtocol: ->
@webSocket?.protocol
send(data) {
if (this.isOpen()) {
this.webSocket.send(JSON.stringify(data))
return true
} else {
return false
}
}
isOpen: ->
@isState("open")
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
}
}
isActive: ->
@isState("open", "connecting")
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) }
}
# Private
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()
}
}
isProtocolSupported: ->
@getProtocol() in supportedProtocols
getProtocol() {
return (this.webSocket != null ? this.webSocket.protocol : undefined)
}
isState: (states...) ->
@getState() in states
isOpen() {
return this.isState("open")
}
getState: ->
return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState
null
isActive() {
return this.isState("open", "connecting")
}
installEventHandlers: ->
for eventName of @events
handler = @events[eventName].bind(this)
@webSocket["on#{eventName}"] = handler
return
// Private
uninstallEventHandlers: ->
for eventName of @events
@webSocket["on#{eventName}"] = ->
return
isProtocolSupported() {
let needle
return (needle = this.getProtocol(), Array.from(supportedProtocols).includes(needle))
}
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)
isState(...states) {
let needle
return (needle = this.getState(), Array.from(states).includes(needle))
}
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)
getState() {
for (let state in WebSocket) { const value = WebSocket[state]; if (value === (this.webSocket != null ? this.webSocket.readyState : undefined)) { return state.toLowerCase() } }
return null
}
close: (event) ->
ActionCable.log("WebSocket onclose event")
return if @disconnected
@disconnected = true
@monitor.recordDisconnect()
@subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()})
installEventHandlers() {
for (let eventName in this.events) {
const handler = this.events[eventName].bind(this)
this.webSocket[`on${eventName}`] = handler
}
}
error: ->
ActionCable.log("WebSocket onerror event")
uninstallEventHandlers() {
for (let eventName in this.events) {
this.webSocket[`on${eventName}`] = function() {}
}
}
})
Cls.initClass()

View File

@ -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
/*
* 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)
}
@staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
constructor(connection) {
this.visibilityDidChange = this.visibilityDidChange.bind(this)
this.connection = connection
this.reconnectAttempts = 0
}
constructor: (@connection) ->
@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`)
}
}
start: ->
unless @isRunning()
@startedAt = now()
delete @stoppedAt
@startPolling()
document.addEventListener("visibilitychange", @visibilityDidChange)
ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms")
stop() {
if (this.isRunning()) {
this.stoppedAt = now()
this.stopPolling()
document.removeEventListener("visibilitychange", this.visibilityDidChange)
return ActionCable.log("ConnectionMonitor stopped")
}
}
stop: ->
if @isRunning()
@stoppedAt = now()
@stopPolling()
document.removeEventListener("visibilitychange", @visibilityDidChange)
ActionCable.log("ConnectionMonitor stopped")
isRunning() {
return (this.startedAt != null) && (this.stoppedAt == null)
}
isRunning: ->
@startedAt? and not @stoppedAt?
recordPing() {
return this.pingedAt = now()
}
recordPing: ->
@pingedAt = now()
recordConnect() {
this.reconnectAttempts = 0
this.recordPing()
delete this.disconnectedAt
return ActionCable.log("ConnectionMonitor recorded connect")
}
recordConnect: ->
@reconnectAttempts = 0
@recordPing()
delete @disconnectedAt
ActionCable.log("ConnectionMonitor recorded connect")
recordDisconnect() {
this.disconnectedAt = now()
return ActionCable.log("ConnectionMonitor recorded disconnect")
}
recordDisconnect: ->
@disconnectedAt = now()
ActionCable.log("ConnectionMonitor recorded disconnect")
// Private
# Private
startPolling() {
this.stopPolling()
return this.poll()
}
startPolling: ->
@stopPolling()
@poll()
stopPolling() {
return clearTimeout(this.pollTimeout)
}
stopPolling: ->
clearTimeout(@pollTimeout)
poll() {
return this.pollTimeout = setTimeout(() => {
this.reconnectIfStale()
return this.poll()
}
, this.getPollInterval())
}
poll: ->
@pollTimeout = setTimeout =>
@reconnectIfStale()
@poll()
, @getPollInterval()
getPollInterval() {
const {min, max} = this.constructor.pollInterval
const interval = 5 * Math.log(this.reconnectAttempts + 1)
return Math.round(clamp(interval, min, max) * 1000)
}
getPollInterval: ->
{min, max} = @constructor.pollInterval
interval = 5 * Math.log(@reconnectAttempts + 1)
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()
}
}
}
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() {
return secondsSince(this.pingedAt != null ? this.pingedAt : this.startedAt) > this.constructor.staleThreshold
}
connectionIsStale: ->
secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
disconnectedRecently() {
return this.disconnectedAt && (secondsSince(this.disconnectedAt) < this.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))
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
})()

View File

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

View File

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

View File

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

View File

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