From cbd15da0274316bae64caae54880fee87853f480 Mon Sep 17 00:00:00 2001 From: Daniel Rhodes Date: Thu, 17 Mar 2016 15:05:06 +0100 Subject: Added protocol negotiation This is primarily for backwards compatibility for when or if the protocol is changed in future versions. If the server fails to respond with an acceptable protocol, the client disconnects and disables the monitor. --- .../javascripts/action_cable/connection.coffee | 41 +++++++++++++--------- .../javascripts/action_cable/consumer.coffee | 19 ++++++++++ .../javascripts/action_cable/subscription.coffee | 6 ++++ 3 files changed, 50 insertions(+), 16 deletions(-) (limited to 'actioncable/app') diff --git a/actioncable/app/assets/javascripts/action_cable/connection.coffee b/actioncable/app/assets/javascripts/action_cable/connection.coffee index 92272cc5b8..af771f2d82 100644 --- a/actioncable/app/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/app/assets/javascripts/action_cable/connection.coffee @@ -2,7 +2,8 @@ # Encapsulate the cable connection held by the consumer. This is an internal class not intended for direct user manipulation. -{message_types} = ActionCable.INTERNAL +{message_types, protocols} = ActionCable.INTERNAL +[supportedProtocols..., unsupportedProtocol] = protocols class ActionCable.Connection @reopenDelay: 500 @@ -10,6 +11,7 @@ class ActionCable.Connection constructor: (@consumer) -> {@subscriptions} = @consumer @monitor = new ActionCable.ConnectionMonitor this + @disconnected = true send: (data) -> if @isOpen() @@ -23,15 +25,16 @@ class ActionCable.Connection ActionCable.log("Attemped to open WebSocket, but existing socket is #{@getState()}") throw new Error("Existing connection must be closed before opening") else - ActionCable.log("Opening WebSocket, current state is #{@getState()}") + ActionCable.log("Opening WebSocket, current state is #{@getState()}, subprotocols: #{protocols}") @uninstallEventHandlers() if @webSocket? - @webSocket = new WebSocket(@consumer.url) + @webSocket = new WebSocket(@consumer.url, protocols) @installEventHandlers() @monitor.start() true - close: -> - @webSocket?.close() + close: ({allowReconnect} = {allowReconnect: true}) -> + @monitor.stop() unless allowReconnect + @webSocket?.close() if @isActive() reopen: -> ActionCable.log("Reopening WebSocket, current state is #{@getState()}") @@ -46,6 +49,9 @@ class ActionCable.Connection else @open() + getProtocol: -> + @webSocket?.protocol + isOpen: -> @isState("open") @@ -54,6 +60,9 @@ class ActionCable.Connection # Private + isProtocolSupported: -> + @getProtocol() in supportedProtocols + isState: (states...) -> @getState() in states @@ -74,10 +83,12 @@ class ActionCable.Connection events: message: (event) -> + return unless @isSupportedProtocol() {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 @@ -88,20 +99,18 @@ class ActionCable.Connection @subscriptions.notify(identifier, "received", message) open: -> - ActionCable.log("WebSocket onopen event") + ActionCable.log("WebSocket onopen event, using '#{@getProtocol()}' subprotocol") @disconnected = false - @subscriptions.reload() + if not @isProtocolSupported() + ActionCable.log("Protocol is unsupported. Stopping monitor and disconnecting.") + @close(allowReconnect: false) - close: -> + close: (event) -> ActionCable.log("WebSocket onclose event") - @disconnect() + return if @disconnected + @disconnected = true + @monitor.recordDisconnect() + @subscriptions.notifyAll("disconnected", {willAttemptReconnect: @monitor.isRunning()}) error: -> ActionCable.log("WebSocket onerror event") - @disconnect() - - disconnect: -> - return if @disconnected - @disconnected = true - @subscriptions.notifyAll("disconnected") - @monitor.recordDisconnect() diff --git a/actioncable/app/assets/javascripts/action_cable/consumer.coffee b/actioncable/app/assets/javascripts/action_cable/consumer.coffee index 7aae1ed8ed..3298be717f 100644 --- a/actioncable/app/assets/javascripts/action_cable/consumer.coffee +++ b/actioncable/app/assets/javascripts/action_cable/consumer.coffee @@ -14,6 +14,19 @@ # 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 @@ -22,6 +35,12 @@ class ActionCable.Consumer send: (data) -> @connection.send(data) + connect: -> + @connection.open() + + disconnect: -> + @connection.close(allowReconnect: false) + 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 61a3fb1309..8e0805a174 100644 --- a/actioncable/app/assets/javascripts/action_cable/subscription.coffee +++ b/actioncable/app/assets/javascripts/action_cable/subscription.coffee @@ -8,6 +8,12 @@ # 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() # -- cgit v1.2.3