aboutsummaryrefslogtreecommitdiffstats
path: root/lib/assets/javascripts/cable
diff options
context:
space:
mode:
Diffstat (limited to 'lib/assets/javascripts/cable')
-rw-r--r--lib/assets/javascripts/cable/connection.coffee84
-rw-r--r--lib/assets/javascripts/cable/connection.js.coffee77
-rw-r--r--lib/assets/javascripts/cable/connection_monitor.coffee (renamed from lib/assets/javascripts/cable/connection_monitor.js.coffee)42
-rw-r--r--lib/assets/javascripts/cable/consumer.coffee (renamed from lib/assets/javascripts/cable/consumer.js.coffee)0
-rw-r--r--lib/assets/javascripts/cable/subscription.coffee (renamed from lib/assets/javascripts/cable/subscription.js.coffee)0
-rw-r--r--lib/assets/javascripts/cable/subscriptions.coffee (renamed from lib/assets/javascripts/cable/subscriptions.js.coffee)38
6 files changed, 139 insertions, 102 deletions
diff --git a/lib/assets/javascripts/cable/connection.coffee b/lib/assets/javascripts/cable/connection.coffee
new file mode 100644
index 0000000000..b2abe8dcb2
--- /dev/null
+++ b/lib/assets/javascripts/cable/connection.coffee
@@ -0,0 +1,84 @@
+# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
+
+{message_types} = Cable.INTERNAL
+
+class Cable.Connection
+ @reopenDelay: 500
+
+ constructor: (@consumer) ->
+ @open()
+
+ send: (data) ->
+ if @isOpen()
+ @webSocket.send(JSON.stringify(data))
+ true
+ else
+ false
+
+ open: =>
+ if @webSocket and not @isState("closed")
+ throw new Error("Existing connection must be closed before opening")
+ else
+ @webSocket = new WebSocket(@consumer.url)
+ @installEventHandlers()
+ true
+
+ close: ->
+ @webSocket?.close()
+
+ reopen: ->
+ if @isState("closed")
+ @open()
+ else
+ try
+ @close()
+ finally
+ setTimeout(@open, @constructor.reopenDelay)
+
+ isOpen: ->
+ @isState("open")
+
+ # Private
+
+ 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
+
+ events:
+ message: (event) ->
+ {identifier, message, type} = JSON.parse(event.data)
+
+ switch type
+ when message_types.confirmation
+ @consumer.subscriptions.notify(identifier, "connected")
+ when message_types.rejection
+ @consumer.subscriptions.reject(identifier)
+ else
+ @consumer.subscriptions.notify(identifier, "received", message)
+
+ open: ->
+ @disconnected = false
+ @consumer.subscriptions.reload()
+
+ close: ->
+ @disconnect()
+
+ error: ->
+ @disconnect()
+
+ disconnect: ->
+ return if @disconnected
+ @disconnected = true
+ @consumer.subscriptions.notifyAll("disconnected")
+
+ toJSON: ->
+ state: @getState()
diff --git a/lib/assets/javascripts/cable/connection.js.coffee b/lib/assets/javascripts/cable/connection.js.coffee
deleted file mode 100644
index 464f0c1ff7..0000000000
--- a/lib/assets/javascripts/cable/connection.js.coffee
+++ /dev/null
@@ -1,77 +0,0 @@
-# Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation.
-class Cable.Connection
- constructor: (@consumer) ->
- @open()
-
- send: (data) ->
- if @isOpen()
- @webSocket.send(JSON.stringify(data))
- true
- else
- false
-
- open: ->
- return if @isState("open", "connecting")
- @webSocket = new WebSocket(@consumer.url)
- @installEventHandlers()
-
- close: ->
- return if @isState("closed", "closing")
- @webSocket?.close()
-
- reopen: ->
- if @isOpen()
- @closeSilently => @open()
- else
- @open()
-
- isOpen: ->
- @isState("open")
-
- # Private
-
- isState: (states...) ->
- @getState() in states
-
- getState: ->
- return state.toLowerCase() for state, value of WebSocket when value is @webSocket?.readyState
- null
-
- closeSilently: (callback = ->) ->
- @uninstallEventHandlers()
- @installEventHandler("close", callback)
- @installEventHandler("error", callback)
- try
- @webSocket.close()
- finally
- @uninstallEventHandlers()
-
- installEventHandlers: ->
- for eventName of @events
- @installEventHandler(eventName)
-
- installEventHandler: (eventName, handler) ->
- handler ?= @events[eventName].bind(this)
- @webSocket.addEventListener(eventName, handler)
-
- uninstallEventHandlers: ->
- for eventName of @events
- @webSocket.removeEventListener(eventName)
-
- events:
- message: (event) ->
- {identifier, message} = JSON.parse(event.data)
- @consumer.subscriptions.notify(identifier, "received", message)
-
- open: ->
- @consumer.subscriptions.reload()
-
- close: ->
- @consumer.subscriptions.notifyAll("disconnected")
-
- error: ->
- @consumer.subscriptions.notifyAll("disconnected")
- @closeSilently()
-
- toJSON: ->
- state: @getState()
diff --git a/lib/assets/javascripts/cable/connection_monitor.js.coffee b/lib/assets/javascripts/cable/connection_monitor.coffee
index cac65d9043..435efcc361 100644
--- a/lib/assets/javascripts/cable/connection_monitor.js.coffee
+++ b/lib/assets/javascripts/cable/connection_monitor.coffee
@@ -1,15 +1,13 @@
# 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 Cable.ConnectionMonitor
- identifier: Cable.PING_IDENTIFIER
-
- pollInterval:
- min: 2
+ @pollInterval:
+ min: 3
max: 30
- staleThreshold:
- startedAt: 4
- pingedAt: 8
+ @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
+
+ identifier: Cable.INTERNAL.identifiers.ping
constructor: (@consumer) ->
@consumer.subscriptions.add(this)
@@ -18,6 +16,10 @@ class Cable.ConnectionMonitor
connected: ->
@reset()
@pingedAt = now()
+ delete @disconnectedAt
+
+ disconnected: ->
+ @disconnectedAt = now()
received: ->
@pingedAt = now()
@@ -30,9 +32,11 @@ class Cable.ConnectionMonitor
delete @stoppedAt
@startedAt = now()
@poll()
+ document.addEventListener("visibilitychange", @visibilityDidChange)
stop: ->
@stoppedAt = now()
+ document.removeEventListener("visibilitychange", @visibilityDidChange)
poll: ->
setTimeout =>
@@ -42,20 +46,28 @@ class Cable.ConnectionMonitor
, @getInterval()
getInterval: ->
- {min, max} = @pollInterval
- interval = 4 * Math.log(@reconnectAttempts + 1)
+ {min, max} = @constructor.pollInterval
+ interval = 5 * Math.log(@reconnectAttempts + 1)
clamp(interval, min, max) * 1000
reconnectIfStale: ->
if @connectionIsStale()
- @reconnectAttempts += 1
- @consumer.connection.reopen()
+ @reconnectAttempts++
+ unless @disconnectedRecently()
+ @consumer.connection.reopen()
connectionIsStale: ->
- if @pingedAt
- secondsSince(@pingedAt) > @staleThreshold.pingedAt
- else
- secondsSince(@startedAt) > @staleThreshold.startedAt
+ secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold
+
+ disconnectedRecently: ->
+ @disconnectedAt and secondsSince(@disconnectedAt) < @constructor.staleThreshold
+
+ visibilityDidChange: =>
+ if document.visibilityState is "visible"
+ setTimeout =>
+ if @connectionIsStale() or not @consumer.connection.isOpen()
+ @consumer.connection.reopen()
+ , 200
toJSON: ->
interval = @getInterval()
diff --git a/lib/assets/javascripts/cable/consumer.js.coffee b/lib/assets/javascripts/cable/consumer.coffee
index 05a7398e79..05a7398e79 100644
--- a/lib/assets/javascripts/cable/consumer.js.coffee
+++ b/lib/assets/javascripts/cable/consumer.coffee
diff --git a/lib/assets/javascripts/cable/subscription.js.coffee b/lib/assets/javascripts/cable/subscription.coffee
index 5b024d4e15..5b024d4e15 100644
--- a/lib/assets/javascripts/cable/subscription.js.coffee
+++ b/lib/assets/javascripts/cable/subscription.coffee
diff --git a/lib/assets/javascripts/cable/subscriptions.js.coffee b/lib/assets/javascripts/cable/subscriptions.coffee
index fe6975c870..7955565f06 100644
--- a/lib/assets/javascripts/cable/subscriptions.js.coffee
+++ b/lib/assets/javascripts/cable/subscriptions.coffee
@@ -9,6 +9,7 @@
class Cable.Subscriptions
constructor: (@consumer) ->
@subscriptions = []
+ @history = []
create: (channelName, mixin) ->
channel = channelName
@@ -20,22 +21,29 @@ class Cable.Subscriptions
add: (subscription) ->
@subscriptions.push(subscription)
@notify(subscription, "initialized")
- if @sendCommand(subscription, "subscribe")
- @notify(subscription, "connected")
-
- reload: ->
- for subscription in @subscriptions
- if @sendCommand(subscription, "subscribe")
- @notify(subscription, "connected")
+ @sendCommand(subscription, "subscribe")
remove: (subscription) ->
- @subscriptions = (s for s in @subscriptions when s isnt subscription)
+ @forget(subscription)
+
unless @findAll(subscription.identifier).length
@sendCommand(subscription, "unsubscribe")
+ reject: (identifier) ->
+ for subscription in @findAll(identifier)
+ @forget(subscription)
+ @notify(subscription, "rejected")
+
+ forget: (subscription) ->
+ @subscriptions = (s for s in @subscriptions when s isnt subscription)
+
findAll: (identifier) ->
s for s in @subscriptions when s.identifier is identifier
+ reload: ->
+ for subscription in @subscriptions
+ @sendCommand(subscription, "subscribe")
+
notifyAll: (callbackName, args...) ->
for subscription in @subscriptions
@notify(subscription, callbackName, args...)
@@ -49,12 +57,22 @@ class Cable.Subscriptions
for subscription in subscriptions
subscription[callbackName]?(args...)
+ if callbackName in ["initialized", "connected", "disconnected", "rejected"]
+ {identifier} = subscription
+ @record(notification: {identifier, callbackName, args})
+
sendCommand: (subscription, command) ->
{identifier} = subscription
- if identifier is Cable.PING_IDENTIFIER
+ if identifier is Cable.INTERNAL.identifiers.ping
@consumer.connection.isOpen()
else
@consumer.send({command, identifier})
+ record: (data) ->
+ data.time = new Date()
+ @history = @history.slice(-19)
+ @history.push(data)
+
toJSON: ->
- subscription.identifier for subscription in @subscriptions
+ history: @history
+ identifiers: (subscription.identifier for subscription in @subscriptions)