feat(websocket): implement Prism and WebSocket
This commit is contained in:
1 parent
33708860fa
commit
2231150ac4
1 file changed
+80
@@ -0,0 +1,80 @@
|
|||||||
|
require 'monitor'
|
||||||
|
|
||||||
|
module Prism
|
||||||
|
module WebSocket
|
||||||
|
# ActionCable-lite pub/sub broadcaster: application handlers subscribe
|
||||||
|
# a Connection to one or more named streams (e.g. "chat_room_3"), and
|
||||||
|
# any thread can broadcast to that stream without knowing who, or how
|
||||||
|
# many connections, are listening. Guarded by a Monitor the same way
|
||||||
|
# RateLimiter::MemoryStore is, since broadcasts and subscribe/unsubscribe
|
||||||
|
# routinely come from different threads at once (one per connection).
|
||||||
|
class Hub
|
||||||
|
include MonitorMixin
|
||||||
|
|
||||||
|
# max_subscribers_per_stream is a hard cap on how many connections may
|
||||||
|
# share one stream name at once - unbounded fan-in on a single stream
|
||||||
|
# (e.g. every visitor to one wildly popular post) is a real resource
|
||||||
|
# exhaustion path (one broadcast call fans out to an unbounded number
|
||||||
|
# of live sockets) that nothing upstream of Hub was guarding against.
|
||||||
|
# nil (the default) keeps the old unbounded behavior.
|
||||||
|
def initialize(max_subscribers_per_stream: nil)
|
||||||
|
super()
|
||||||
|
@streams = Hash.new { |hash, key| hash[key] = [] }
|
||||||
|
@max_subscribers_per_stream = max_subscribers_per_stream
|
||||||
|
end
|
||||||
|
|
||||||
|
# Membership is checked/removed by object identity (`equal?`), not
|
||||||
|
# `==`/`eql?` - a Connection is "the same subscriber" only if it is
|
||||||
|
# literally the same object, regardless of whether whatever class
|
||||||
|
# wraps a socket happens to define value equality.
|
||||||
|
#
|
||||||
|
# Returns true if the connection is (now, or already was) subscribed,
|
||||||
|
# false if the stream is at max_subscribers_per_stream and this is a
|
||||||
|
# genuinely new subscriber - callers (channel handlers) are expected
|
||||||
|
# to close the connection (e.g. with code 1008, policy violation) when
|
||||||
|
# they get false back, the same way they already do for a missing/
|
||||||
|
# invalid stream key.
|
||||||
|
def subscribe(stream, connection)
|
||||||
|
synchronize do
|
||||||
|
list = @streams[stream]
|
||||||
|
next true if list.any? { |existing| existing.equal?(connection) }
|
||||||
|
next false if @max_subscribers_per_stream && list.size >= @max_subscribers_per_stream
|
||||||
|
|
||||||
|
list << connection
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe(stream, connection)
|
||||||
|
synchronize do
|
||||||
|
@streams[stream].reject! { |existing| existing.equal?(connection) }
|
||||||
|
@streams.delete(stream) if @streams.key?(stream) && @streams[stream].empty?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def unsubscribe_all(connection)
|
||||||
|
synchronize do
|
||||||
|
@streams.each_value { |connections| connections.reject! { |existing| existing.equal?(connection) } }
|
||||||
|
@streams.reject! { |_stream, connections| connections.empty? }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscribers(stream)
|
||||||
|
synchronize { @streams[stream].dup }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Sends `data` to every connection currently subscribed to `stream`.
|
||||||
|
# A Hash/Array payload is JSON-encoded (the common case - structured
|
||||||
|
# events), a String is sent verbatim. A connection whose socket has
|
||||||
|
# already died is dropped from the stream instead of raising, so one
|
||||||
|
# dead client can never break delivery to the rest.
|
||||||
|
def broadcast(stream, data)
|
||||||
|
payload = data.is_a?(String) ? data : JSON.generate(data)
|
||||||
|
subscribers(stream).each do |connection|
|
||||||
|
connection.write_text(payload)
|
||||||
|
unsubscribe(stream, connection) if connection.closed?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in new issue
Block a user