diff options
Diffstat (limited to 'lib/assets/javascripts/cable')
-rw-r--r-- | lib/assets/javascripts/cable/connection.coffee | 84 | ||||
-rw-r--r-- | lib/assets/javascripts/cable/connection.js.coffee | 77 | ||||
-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) |