57 lines
2.2 KiB
Ruby
57 lines
2.2 KiB
Ruby
require 'uri'
|
|
require 'json'
|
|
|
|
# ActionCable-lite channel for posts#show: anyone with a post open can
|
|
# subscribe (GET /cable?post_id=N, upgraded to a WebSocket) to hear about
|
|
# moderation events on that post's comments - approved, rejected, or
|
|
# removed - the instant they happen, instead of polling or reloading.
|
|
# CommentsController broadcasts through the same Hub every time one of
|
|
# those three actions changes the post's *visible* (approved) comment
|
|
# count; this channel itself never accepts anything back from the client,
|
|
# it only ever pushes.
|
|
class PostActivityChannel
|
|
# A hard cap on live viewers per post - without one, one post going viral
|
|
# would mean one Hub#broadcast call fanning out to an unbounded number of
|
|
# live sockets on every single moderation action. Real, but generous
|
|
# enough that the demo's handful of viewers never trips it by accident.
|
|
MAX_VIEWERS_PER_POST = 500
|
|
|
|
def self.hub
|
|
@hub ||= Prism::WebSocket::Hub.new(max_subscribers_per_stream: MAX_VIEWERS_PER_POST)
|
|
end
|
|
|
|
def self.stream_for(post_id)
|
|
"post_#{post_id}_comments"
|
|
end
|
|
|
|
def on_open(connection)
|
|
post_id = post_id_from(connection)
|
|
return connection.close(1008, 'post_id query parameter is required') unless post_id
|
|
unless self.class.hub.subscribe(self.class.stream_for(post_id), connection)
|
|
return connection.close(1008, 'this post already has the maximum number of live viewers')
|
|
end
|
|
connection.write_text(JSON.generate(event: 'subscribed', post_id: post_id))
|
|
end
|
|
|
|
def on_message(connection, _message)
|
|
# Read-only stream - nothing a client sends is ever acted on.
|
|
end
|
|
|
|
def on_close(connection)
|
|
self.class.hub.unsubscribe_all(connection)
|
|
end
|
|
|
|
private
|
|
|
|
# The handshake request never reaches a Prism::Request/Params, just the
|
|
# raw Rack env the server captured before the Upgrade (see
|
|
# Prism::Server#handle_websocket) - so the query string is decoded by
|
|
# hand here instead of through Prism::Params#nested_from, which only
|
|
# ever runs as part of an ordinary Request#params call.
|
|
def post_id_from(connection)
|
|
query = URI.decode_www_form(connection.env['QUERY_STRING'].to_s).to_h
|
|
id = query['post_id'].to_s
|
|
id.empty? ? nil : id
|
|
end
|
|
end
|