diff --git a/lib/rage/cable/connection.rb b/lib/rage/cable/connection.rb index a933a4e4..1c48c96a 100644 --- a/lib/rage/cable/connection.rb +++ b/lib/rage/cable/connection.rb @@ -133,7 +133,11 @@ def __disconnect(connection) # # Disconnect only the user's specific device # RageCable::Connection.disconnect(current_user_device: [user, "device-123"]) # + # @example Using protocol options + # RageCable::Connection.disconnect(current_user: User.find(1), protocol_options: { reconnect: false }) + # # @param identified_by [Hash] a single key-value pair where the key is a declared identifier + # @param protocol_options [Hash] options passed to the connection's protocol when disconnecting # @raise [ArgumentError] if no identifier, multiple identifiers, or an unknown identifier is provided - # @!method self.disconnect(**identified_by) + # @!method self.disconnect(protocol_options: nil, **identified_by) end diff --git a/lib/rage/cable/protocols/actioncable_v1_json.rb b/lib/rage/cable/protocols/actioncable_v1_json.rb index 6826831b..baab908e 100644 --- a/lib/rage/cable/protocols/actioncable_v1_json.rb +++ b/lib/rage/cable/protocols/actioncable_v1_json.rb @@ -59,6 +59,7 @@ module MESSAGES UNAUTHORIZED = { type: TYPE::DISCONNECT, reason: REASON::UNAUTHORIZED, reconnect: false }.to_json INVALID = { type: TYPE::DISCONNECT, reason: REASON::INVALID, reconnect: true }.to_json DISCONNECT = { type: TYPE::DISCONNECT, reason: REASON::REMOTE, reconnect: true }.to_json + DISCONNECT_NO_RECONNECT = { type: TYPE::DISCONNECT, reason: REASON::REMOTE, reconnect: false }.to_json end HANDSHAKE_HEADERS = { "Sec-WebSocket-Protocol" => "actioncable-v1-json" } @@ -173,7 +174,11 @@ def self.protocol_id # Process the disconnect initiated by the server. # # @param connection [Rage::Cable::WebSocketConnection] the connection object - def self.on_remote_disconnect(connection) - connection.write(MESSAGES::DISCONNECT) + # @param options [Hash, nil] options passed to the protocol when disconnecting + def self.on_remote_disconnect(connection, options) + reconnect = options.nil? || options.fetch(:reconnect, true) + message = reconnect ? MESSAGES::DISCONNECT : MESSAGES::DISCONNECT_NO_RECONNECT + + connection.write(message) end end diff --git a/lib/rage/cable/protocols/base.rb b/lib/rage/cable/protocols/base.rb index f272111b..d95a41a0 100644 --- a/lib/rage/cable/protocols/base.rb +++ b/lib/rage/cable/protocols/base.rb @@ -92,7 +92,8 @@ def supports_rpc? # Process the disconnect initiated by the server. # # @param connection [Rage::Cable::WebSocketConnection] the connection object - def on_remote_disconnect(connection) + # @param options [Hash, nil] options passed to the protocol when disconnecting + def on_remote_disconnect(connection, options) end private diff --git a/lib/rage/cable/remote_disconnect.rb b/lib/rage/cable/remote_disconnect.rb index 6e8d7216..f73ee7c0 100644 --- a/lib/rage/cable/remote_disconnect.rb +++ b/lib/rage/cable/remote_disconnect.rb @@ -29,7 +29,7 @@ module ClassMethods # Disconnect all connections matching the specified identifier. # @example Disconnect all connections for a user # RageCable::Connection.disconnect(current_user: User.find(1)) - def disconnect(**identified_by) + def disconnect(protocol_options: nil, **identified_by) raise ArgumentError, "An identifier must be provided" if identified_by.empty? raise ArgumentError, "Only one identifier can be provided" if identified_by.size > 1 @@ -38,7 +38,14 @@ def disconnect(**identified_by) raise ArgumentError, "Unknown identifier: #{id_pair[0]}" end - Rage::PubSub.publish("", topic: ["rage", name, id_pair]) + message = if protocol_options + raise ArgumentError, "protocol_options must be a Hash" unless protocol_options.is_a?(Hash) + protocol_options.to_json + else + "" + end + + Rage::PubSub.publish(message, topic: ["rage", name, id_pair]) end end @@ -46,8 +53,9 @@ def disconnect(**identified_by) def __remote_disconnect_setup(connection, protocol) __identified_by_map.each do |identified_by| - Rage::PubSub.subscribe(topic: ["rage", self.class.name, identified_by], subscription_id: connection) do - protocol.on_remote_disconnect(connection) + Rage::PubSub.subscribe(topic: ["rage", self.class.name, identified_by], subscription_id: connection) do |message| + protocol_options = JSON.parse(message, symbolize_names: true) unless message.empty? + protocol.on_remote_disconnect(connection, protocol_options) connection.close end end diff --git a/spec/cable/connection_spec.rb b/spec/cable/connection_spec.rb index 4e9c7d57..9bc992b5 100644 --- a/spec/cable/connection_spec.rb +++ b/spec/cable/connection_spec.rb @@ -223,10 +223,29 @@ "TestCableConnection", [:current_user, user] ] - expect(Rage::PubSub).to receive(:publish).with(anything, topic: expected_topic) + expect(Rage::PubSub).to receive(:publish).with("", topic: expected_topic) cable_connection_class.disconnect(current_user: user) end + + it "publishes protocol_options as JSON" do + user = double(id: 1234) + + expected_topic = [ + "rage", + "TestCableConnection", + [:current_user, user] + ] + expect(Rage::PubSub).to receive(:publish).with('{"reconnect":false}', topic: expected_topic) + + cable_connection_class.disconnect(current_user: user, protocol_options: { reconnect: false }) + end + + it "raises ArgumentError if protocol_options is not a Hash" do + expect { + cable_connection_class.disconnect(current_user: double, protocol_options: "invalid") + }.to raise_error(ArgumentError, /protocol_options must be a Hash/) + end end context "with multiple identifiers" do @@ -312,10 +331,24 @@ def connect subject - expect(protocol).to receive(:on_remote_disconnect).with(connection) + expect(protocol).to receive(:on_remote_disconnect).with(connection, nil) + expect(connection).to receive(:close) + + pubsub_callback.call("") + end + + it "passes protocol_options to on_remote_disconnect" do + pubsub_callback = nil + allow(Rage::PubSub).to receive(:subscribe) do |&block| + pubsub_callback = block + end + + subject + + expect(protocol).to receive(:on_remote_disconnect).with(connection, { reconnect: false }) expect(connection).to receive(:close) - pubsub_callback.call + pubsub_callback.call('{"reconnect":false}') end context "with multiple identifiers" do