diff options
Diffstat (limited to 'actioncable/app/assets')
6 files changed, 89 insertions, 72 deletions
diff --git a/actioncable/app/assets/javascripts/action_cable.coffee.erb b/actioncable/app/assets/javascripts/action_cable.coffee.erb index 6a8b4eeb85..f0422d9d9c 100644 --- a/actioncable/app/assets/javascripts/action_cable.coffee.erb +++ b/actioncable/app/assets/javascripts/action_cable.coffee.erb @@ -4,12 +4,13 @@ @ActionCable = INTERNAL: <%= ActionCable::INTERNAL.to_json %> - createConsumer: (url = @getConfig("url")) -> + createConsumer: (url) -> + url ?= @getConfig("url") ? @INTERNAL.default_mount_path new ActionCable.Consumer @createWebSocketURL(url) getConfig: (name) -> element = document.head.querySelector("meta[name='action-cable-#{name}']") - element?.getAttribute("content") ? '/cable' + element?.getAttribute("content") createWebSocketURL: (url) -> if url and not /^wss?:/i.test(url) diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee index 4244322a1e..92272cc5b8 100644 --- a/actioncable/app/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee @@ -1,3 +1,5 @@ +#= require ./connection_monitor + # Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. {message_types} = ActionCable.INTERNAL @@ -6,11 +8,10 @@ class ActionCable.Connection @reopenDelay: 500 constructor: (@consumer) -> + {@subscriptions} = @consumer + @monitor = new ActionCable.ConnectionMonitor this send: (data) -> - unless @isOpen() - @open() - if @isOpen() @webSocket.send(JSON.stringify(data)) true @@ -18,7 +19,7 @@ class ActionCable.Connection false open: => - if @isAlive() + if @isActive() ActionCable.log("Attemped to open WebSocket, but existing socket is #{@getState()}") throw new Error("Existing connection must be closed before opening") else @@ -26,6 +27,7 @@ class ActionCable.Connection @uninstallEventHandlers() if @webSocket? @webSocket = new WebSocket(@consumer.url) @installEventHandlers() + @monitor.start() true close: -> @@ -33,7 +35,7 @@ class ActionCable.Connection reopen: -> ActionCable.log("Reopening WebSocket, current state is #{@getState()}") - if @isAlive() + if @isActive() try @close() catch error @@ -47,10 +49,10 @@ class ActionCable.Connection isOpen: -> @isState("open") - # Private + isActive: -> + @isState("open", "connecting") - isAlive: -> - @webSocket? and not @isState("closing", "closed") + # Private isState: (states...) -> @getState() in states @@ -73,19 +75,22 @@ class ActionCable.Connection events: message: (event) -> {identifier, message, type} = JSON.parse(event.data) - switch type + when message_types.welcome + @monitor.recordConnect() + when message_types.ping + @monitor.recordPing() when message_types.confirmation - @consumer.subscriptions.notify(identifier, "connected") + @subscriptions.notify(identifier, "connected") when message_types.rejection - @consumer.subscriptions.reject(identifier) + @subscriptions.reject(identifier) else - @consumer.subscriptions.notify(identifier, "received", message) + @subscriptions.notify(identifier, "received", message) open: -> ActionCable.log("WebSocket onopen event") @disconnected = false - @consumer.subscriptions.reload() + @subscriptions.reload() close: -> ActionCable.log("WebSocket onclose event") @@ -98,4 +103,5 @@ class ActionCable.Connection disconnect: -> return if @disconnected @disconnected = true - @consumer.subscriptions.notifyAll("disconnected") + @subscriptions.notifyAll("disconnected") + @monitor.recordDisconnect() diff --git a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee index 75a6f1fb07..0cc675fa94 100644 --- a/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee +++ b/actioncable/app/assets/javascripts/action_cable/connection_monitor.coffee @@ -7,61 +7,69 @@ class ActionCable.ConnectionMonitor @staleThreshold: 6 # Server::Connections::BEAT_INTERVAL * 2 (missed two pings) - identifier: ActionCable.INTERNAL.identifiers.ping + constructor: (@connection) -> + @reconnectAttempts = 0 - constructor: (@consumer) -> - @consumer.subscriptions.add(this) - @start() + start: -> + unless @isRunning() + @startedAt = now() + delete @stoppedAt + @startPolling() + document.addEventListener("visibilitychange", @visibilityDidChange) + ActionCable.log("ConnectionMonitor started. pollInterval = #{@getPollInterval()} ms") - connected: -> - @reset() - @pingedAt = now() - delete @disconnectedAt - ActionCable.log("ConnectionMonitor connected") + stop: -> + if @isRunning() + @stoppedAt = now() + @stopPolling() + document.removeEventListener("visibilitychange", @visibilityDidChange) + ActionCable.log("ConnectionMonitor stopped") - disconnected: -> - @disconnectedAt = now() + isRunning: -> + @startedAt? and not @stoppedAt? - received: -> + recordPing: -> @pingedAt = now() - reset: -> + recordConnect: -> @reconnectAttempts = 0 + @recordPing() + delete @disconnectedAt + ActionCable.log("ConnectionMonitor recorded connect") - start: -> - @reset() - delete @stoppedAt - @startedAt = now() + recordDisconnect: -> + @disconnectedAt = now() + ActionCable.log("ConnectionMonitor recorded disconnect") + + # Private + + startPolling: -> + @stopPolling() @poll() - document.addEventListener("visibilitychange", @visibilityDidChange) - ActionCable.log("ConnectionMonitor started, pollInterval is #{@getInterval()}ms") - stop: -> - @stoppedAt = now() - document.removeEventListener("visibilitychange", @visibilityDidChange) - ActionCable.log("ConnectionMonitor stopped") + stopPolling: -> + clearTimeout(@pollTimeout) poll: -> - setTimeout => - unless @stoppedAt - @reconnectIfStale() - @poll() - , @getInterval() + @pollTimeout = setTimeout => + @reconnectIfStale() + @poll() + , @getPollInterval() - getInterval: -> + getPollInterval: -> {min, max} = @constructor.pollInterval interval = 5 * Math.log(@reconnectAttempts + 1) - clamp(interval, min, max) * 1000 + Math.round(clamp(interval, min, max) * 1000) reconnectIfStale: -> if @connectionIsStale() - ActionCable.log("ConnectionMonitor detected stale connection, reconnectAttempts = #{@reconnectAttempts}") + 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 reopen because recently disconnected at #{@disconnectedAt}") + ActionCable.log("ConnectionMonitor skipping reopening recent disconnect") else ActionCable.log("ConnectionMonitor reopening") - @consumer.connection.reopen() + @connection.reopen() connectionIsStale: -> secondsSince(@pingedAt ? @startedAt) > @constructor.staleThreshold @@ -72,9 +80,9 @@ class ActionCable.ConnectionMonitor visibilityDidChange: => if document.visibilityState is "visible" setTimeout => - if @connectionIsStale() or not @consumer.connection.isOpen() - ActionCable.log("ConnectionMonitor reopening stale connection after visibilitychange to #{document.visibilityState}") - @consumer.connection.reopen() + if @connectionIsStale() or not @connection.isOpen() + ActionCable.log("ConnectionMonitor reopening stale connection on visibilitychange. visbilityState = #{document.visibilityState}") + @connection.reopen() , 200 now = -> diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee index 717c0641a9..7aae1ed8ed 100644 --- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee +++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee @@ -1,5 +1,4 @@ #= require ./connection -#= require ./connection_monitor #= require ./subscriptions #= require ./subscription @@ -19,7 +18,10 @@ class ActionCable.Consumer constructor: (@url) -> @subscriptions = new ActionCable.Subscriptions this @connection = new ActionCable.Connection this - @connectionMonitor = new ActionCable.ConnectionMonitor this send: (data) -> @connection.send(data) + + ensureActiveConnection: -> + unless @connection.isActive() + @connection.open() diff --git a/actioncable/app/assets/javascripts/action_cable/subscription.coffee b/actioncable/app/assets/javascripts/action_cable/subscription.coffee index 339d676933..61a3fb1309 100644 --- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee +++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee @@ -1,5 +1,5 @@ -# 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 +# 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: @@ -7,13 +7,13 @@ # App.appearance = App.cable.subscriptions.create "AppearanceChannel", # connected: -> # # Called once the subscription has been successfully completed -# +# # appear: -> # @perform 'appear', appearing_on: @appearingOn() -# +# # away: -> # @perform 'away' -# +# # appearingOn: -> # $('main').data 'appearing-on' # @@ -27,15 +27,15 @@ # 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 @@ -44,11 +44,9 @@ # 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: (@subscriptions, params = {}, mixin) -> + constructor: (@consumer, params = {}, mixin) -> @identifier = JSON.stringify(params) extend(this, mixin) - @subscriptions.add(this) - @consumer = @subscriptions.consumer # Perform a channel action with the optional data passed as an attribute perform: (action, data = {}) -> @@ -59,7 +57,7 @@ class ActionCable.Subscription @consumer.send(command: "message", identifier: @identifier, data: JSON.stringify(data)) unsubscribe: -> - @subscriptions.remove(this) + @consumer.subscriptions.remove(this) extend = (object, properties) -> if properties? diff --git a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee index ae041ffa2b..aa052bf5d8 100644 --- a/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee +++ b/actioncable/app/assets/javascripts/action_cable/subscriptions.coffee @@ -13,28 +13,33 @@ class ActionCable.Subscriptions create: (channelName, mixin) -> channel = channelName params = if typeof channel is "object" then channel else {channel} - new ActionCable.Subscription this, params, mixin + subscription = new ActionCable.Subscription @consumer, params, mixin + @add(subscription) # Private add: (subscription) -> @subscriptions.push(subscription) + @consumer.ensureActiveConnection() @notify(subscription, "initialized") @sendCommand(subscription, "subscribe") + subscription remove: (subscription) -> @forget(subscription) - unless @findAll(subscription.identifier).length @sendCommand(subscription, "unsubscribe") + subscription reject: (identifier) -> for subscription in @findAll(identifier) @forget(subscription) @notify(subscription, "rejected") + subscription forget: (subscription) -> @subscriptions = (s for s in @subscriptions when s isnt subscription) + subscription findAll: (identifier) -> s for s in @subscriptions when s.identifier is identifier @@ -58,7 +63,4 @@ class ActionCable.Subscriptions sendCommand: (subscription, command) -> {identifier} = subscription - if identifier is ActionCable.INTERNAL.identifiers.ping - @consumer.connection.isOpen() - else - @consumer.send({command, identifier}) + @consumer.send({command, identifier}) |