-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Cable message encoding #24233
Cable message encoding #24233
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,9 +46,7 @@ module Channel | |
# def subscribed | ||
# @room = Chat::Room[params[:room_number]] | ||
# | ||
# stream_for @room, -> (encoded_message) do | ||
# message = ActiveSupport::JSON.decode(encoded_message) | ||
# | ||
# stream_for @room, coder: ActiveSupport::JSON do |message| | ||
# if message['originated_at'].present? | ||
# elapsed_time = (Time.now.to_f - message['originated_at']).round(2) | ||
# | ||
|
@@ -71,16 +69,23 @@ module Streams | |
|
||
# Start streaming from the named <tt>broadcasting</tt> pubsub queue. Optionally, you can pass a <tt>callback</tt> that'll be used | ||
# instead of the default of just transmitting the updates straight to the subscriber. | ||
def stream_from(broadcasting, callback = nil) | ||
# Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. | ||
# Defaults to `coder: nil` which does no decoding, passes raw messages. | ||
def stream_from(broadcasting, callback = nil, coder: nil, &block) | ||
broadcasting = String(broadcasting) | ||
# Don't send the confirmation until pubsub#subscribe is successful | ||
defer_subscription_confirmation! | ||
|
||
callback ||= default_stream_callback(broadcasting) | ||
streams << [ broadcasting, callback ] | ||
if handler = callback || block | ||
handler = -> message { handler.(coder.decode(message)) } if coder | ||
else | ||
handler = default_stream_handler(broadcasting, coder: coder) | ||
end | ||
|
||
streams << [ broadcasting, handler ] | ||
|
||
connection.server.event_loop.post do | ||
pubsub.subscribe(broadcasting, callback, lambda do | ||
pubsub.subscribe(broadcasting, handler, lambda do | ||
transmit_subscription_confirmation | ||
logger.info "#{self.class.name} is streaming from #{broadcasting}" | ||
end) | ||
|
@@ -90,8 +95,11 @@ def stream_from(broadcasting, callback = nil) | |
# Start streaming the pubsub queue for the <tt>model</tt> in this channel. Optionally, you can pass a | ||
# <tt>callback</tt> that'll be used instead of the default of just transmitting the updates straight | ||
# to the subscriber. | ||
def stream_for(model, callback = nil) | ||
stream_from(broadcasting_for([ channel_name, model ]), callback) | ||
# | ||
# Pass `coder: ActiveSupport::JSON` to decode messages as JSON before passing to the callback. | ||
# Defaults to `coder: nil` which does no decoding, passes raw messages. | ||
def stream_for(model, callback = nil, coder: nil, &block) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does it still make sense to support There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure. I meant to track down why we designed for a lambda arg vs a block before deprecating+removing. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
stream_from(broadcasting_for([ channel_name, model ]), callback || block, coder: coder) | ||
end | ||
|
||
# Unsubscribes all streams associated with this channel from the pubsub queue. | ||
|
@@ -109,9 +117,11 @@ def streams | |
@_streams ||= [] | ||
end | ||
|
||
def default_stream_callback(broadcasting) | ||
def default_stream_handler(broadcasting, coder:) | ||
coder ||= ActiveSupport::JSON | ||
|
||
-> (message) do | ||
transmit ActiveSupport::JSON.decode(message), via: "streamed from #{broadcasting}" | ||
transmit coder.decode(message), via: "streamed from #{broadcasting}" | ||
end | ||
end | ||
end | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,8 +51,8 @@ class Base | |
attr_reader :server, :env, :subscriptions, :logger, :worker_pool | ||
delegate :event_loop, :pubsub, to: :server | ||
|
||
def initialize(server, env) | ||
@server, @env = server, env | ||
def initialize(server, env, coder: ActiveSupport::JSON) | ||
@server, @env, @coder = server, env, coder | ||
|
||
@worker_pool = server.worker_pool | ||
@logger = new_tagged_logger | ||
|
@@ -67,7 +67,7 @@ def initialize(server, env) | |
|
||
# Called by the server when a new WebSocket connection is established. This configures the callbacks intended for overwriting by the user. | ||
# This method should not be called directly -- instead rely upon on the #connect (and #disconnect) callbacks. | ||
def process # :nodoc: | ||
def process #:nodoc: | ||
logger.info started_request_message | ||
|
||
if websocket.possible? && allow_request_origin? | ||
|
@@ -77,20 +77,22 @@ def process # :nodoc: | |
end | ||
end | ||
|
||
# Data received over the WebSocket connection is handled by this method. It's expected that everything inbound is JSON encoded. | ||
# The data is routed to the proper channel that the connection has subscribed to. | ||
def receive(data_in_json) | ||
# Decodes WebSocket messages and dispatches them to subscribed channels. | ||
# WebSocket message transfer encoding is always JSON. | ||
def receive(websocket_message) #:nodoc: | ||
send_async :dispatch_websocket_message, websocket_message | ||
end | ||
|
||
def dispatch_websocket_message(websocket_message) #:nodoc: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No longer expect callers to reach in and do async dispatch. Do it ourselves. |
||
if websocket.alive? | ||
subscriptions.execute_command ActiveSupport::JSON.decode(data_in_json) | ||
subscriptions.execute_command decode(websocket_message) | ||
else | ||
logger.error "Received data without a live WebSocket (#{data_in_json.inspect})" | ||
logger.error "Ignoring message processed after the WebSocket was closed: #{websocket_message.inspect})" | ||
end | ||
end | ||
|
||
# Send raw data straight back down the WebSocket. This is not intended to be called directly. Use the #transmit available on the | ||
# Channel instead, as that'll automatically address the correct subscriber and wrap the message in JSON. | ||
def transmit(data) # :nodoc: | ||
websocket.transmit data | ||
def transmit(cable_message) # :nodoc: | ||
websocket.transmit encode(cable_message) | ||
end | ||
|
||
# Close the WebSocket connection. | ||
|
@@ -115,7 +117,7 @@ def statistics | |
end | ||
|
||
def beat | ||
transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i) | ||
transmit type: ActionCable::INTERNAL[:message_types][:ping], message: Time.now.to_i | ||
end | ||
|
||
def on_open # :nodoc: | ||
|
@@ -152,6 +154,14 @@ def cookies | |
attr_reader :message_buffer | ||
|
||
private | ||
def encode(cable_message) | ||
@coder.encode cable_message | ||
end | ||
|
||
def decode(websocket_message) | ||
@coder.decode websocket_message | ||
end | ||
|
||
def handle_open | ||
connect if respond_to?(:connect) | ||
subscribe_to_internal_channel | ||
|
@@ -178,7 +188,7 @@ def send_welcome_message | |
# Send welcome message to the internal connection monitor channel. | ||
# This ensures the connection monitor state is reset after a successful | ||
# websocket connection. | ||
transmit ActiveSupport::JSON.encode(type: ActionCable::INTERNAL[:message_types][:welcome]) | ||
transmit type: ActionCable::INTERNAL[:message_types][:welcome] | ||
end | ||
|
||
def allow_request_origin? | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,15 +30,15 @@ def process! | |
|
||
protected | ||
attr_reader :connection | ||
attr_accessor :buffered_messages | ||
attr_reader :buffered_messages | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💅: Could meld this together with the other There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did that, actually, but I put it back because all the other attrs are multiline. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Funny, peeked in some files and the first two I clicked which used attrs, channels/base and subscriptions, did so on one line. But it doesn't matter 😁 |
||
|
||
private | ||
def valid?(message) | ||
message.is_a?(String) | ||
end | ||
|
||
def receive(message) | ||
connection.send_async :receive, message | ||
connection.receive message | ||
end | ||
|
||
def buffer(message) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,21 +23,21 @@ def execute_command(data) | |
end | ||
|
||
def add(data) | ||
id_options = decode_hash(data['identifier']) | ||
identifier = normalize_identifier(id_options) | ||
id_key = data['identifier'] | ||
id_options = ActiveSupport::JSON.decode(id_key).with_indifferent_access | ||
|
||
subscription_klass = connection.server.channel_classes[id_options[:channel]] | ||
|
||
if subscription_klass | ||
subscriptions[identifier] ||= subscription_klass.new(connection, identifier, id_options) | ||
subscriptions[id_key] ||= subscription_klass.new(connection, id_key, id_options) | ||
else | ||
logger.error "Subscription class not found (#{data.inspect})" | ||
end | ||
end | ||
|
||
def remove(data) | ||
logger.info "Unsubscribing from channel: #{data['identifier']}" | ||
remove_subscription subscriptions[normalize_identifier(data['identifier'])] | ||
remove_subscription subscriptions[data['identifier']] | ||
end | ||
|
||
def remove_subscription(subscription) | ||
|
@@ -46,7 +46,7 @@ def remove_subscription(subscription) | |
end | ||
|
||
def perform_action(data) | ||
find(data).perform_action(decode_hash(data['data'])) | ||
find(data).perform_action ActiveSupport::JSON.decode(data['data']) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doesn't this reintroduce the issue of how we handle JSON that has no backslashes? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep! See the fourth bullet in the commit message for rationale. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok - should we have a sample of what the new payload should look like? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New payload = ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean the fact that we are taking in the identifier and data as "Hashes / JSON objects rather than as opaque JSON-encoded strings" -- this removal effectively breaks compatibility with allowing non-backslashed JSON in, and we should be explicit in communicating this to end users. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Gotcha. There's no new payload; it's the same format. Permitting non-JSON-encoded |
||
end | ||
|
||
def identifiers | ||
|
@@ -63,21 +63,8 @@ def unsubscribe_from_all | |
private | ||
delegate :logger, to: :connection | ||
|
||
def normalize_identifier(identifier) | ||
identifier = ActiveSupport::JSON.encode(identifier) if identifier.is_a?(Hash) | ||
identifier | ||
end | ||
|
||
# If `data` is a Hash, this means that the original JSON | ||
# sent by the client had no backslashes in it, and does | ||
# not need to be decoded again. | ||
def decode_hash(data) | ||
data = ActiveSupport::JSON.decode(data) unless data.is_a?(Hash) | ||
data.with_indifferent_access | ||
end | ||
|
||
def find(data) | ||
if subscription = subscriptions[normalize_identifier(data['identifier'])] | ||
if subscription = subscriptions[data['identifier']] | ||
subscription | ||
else | ||
raise "Unable to find subscription with identifier: #{data['identifier']}" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap the callback in a decoding handler if a coder was provided.