Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,4 @@ breezy/build/**/*.js
props_template/performance/**/*.png
.tool-versions
testapp/
superglue/
*.sqlite3
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ gemspec

gem 'rails', '~> 7.2.0'
gem 'selenium-webdriver'
gem 'props_template'
gem 'props_template', path: "../props_template"
gem 'standard'
gem 'capybara'
gem 'minitest'
Expand Down
176 changes: 176 additions & 0 deletions app/channels/superglue/streams/broadcasts.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Provides the broadcast actions in synchronous and asynchronous form for the <tt>Superglue::StreamsChannel</tt>.
# See <tt>Superglue::Broadcastable</tt> for the user-facing API that invokes these methods with most of the paperwork filled out already.
#
# Can be used directly using something like <tt>Superglue::StreamsChannel.broadcast_remove_to :entries, target: 1</tt>.
module Superglue::Streams::Broadcasts
# include Superglue::Streams::ActionHelper

# def broadcast_remove_to(*streamables, **opts)
# broadcast_action_to(*streamables, action: :remove, render: false, **opts)
# end

def broadcast_replace_to(*streamables, **opts)
broadcast_action_to(*streamables, action: :replace, **opts)
end

# def broadcast_update_to(*streamables, **opts)
# broadcast_action_to(*streamables, action: :update, **opts)
# end

# def broadcast_before_to(*streamables, **opts)
# broadcast_action_to(*streamables, action: :before, **opts)
# end

# def broadcast_after_to(*streamables, **opts)
# broadcast_action_to(*streamables, action: :after, **opts)
# end

def broadcast_append_to(*streamables, **opts)
broadcast_action_to(*streamables, action: :append, **opts)
end

def broadcast_prepend_to(*streamables, **opts)
broadcast_action_to(*streamables, action: :prepend, **opts)
end

def broadcast_refresh_to(*streamables, **opts)
request_id = Superglue.current_request_id
content = JSON.generate({
type: "message",
action: "refresh",
requestId: request_id,
options: opts
})
broadcast_stream_to(*streamables, content: content)
end

def broadcast_action_to(*streamables, action:, target: nil, targets: nil, options: {}, **rendering)
locals = rendering[:locals] || {}
targets = (target ? [target] : targets)

targets = targets.map do |item|
convert_to_superglue_fragment_id(item)
end

locals[:broadcast_targets] = targets
locals[:broadcast_action] = action
locals[:broadcast_options] = options
rendering[:locals] = locals

broadcast_stream_to(*streamables, content: render_broadcast_action(rendering))
end

def broadcast_replace_later_to(*streamables, **opts)
broadcast_action_later_to(*streamables, action: :replace, **opts)
end

# def broadcast_update_later_to(*streamables, **opts)
# broadcast_action_later_to(*streamables, action: :update, **opts)
# end

# def broadcast_before_later_to(*streamables, **opts)
# broadcast_action_later_to(*streamables, action: :before, **opts)
# end

# def broadcast_after_later_to(*streamables, **opts)
# broadcast_action_later_to(*streamables, action: :after, **opts)
# end

### convert_to_turbo_stream_dom_id ican use this as the fragment name!

def broadcast_append_later_to(*streamables, **opts)
broadcast_action_later_to(*streamables, action: :append, **opts)
end

def broadcast_prepend_later_to(*streamables, **opts)
broadcast_action_later_to(*streamables, action: :prepend, **opts)
end

def broadcast_refresh_later_to(*streamables, request_id: Superglue.current_request_id, **opts)
stream_name = stream_name_from(streamables)

refresh_debouncer_for(*streamables, request_id: request_id).debounce do
content = JSON.generate({
type: "message",
action: "refresh",
requestId: request_id,
options: opts
})

Superglue::Streams::BroadcastStreamJob.perform_later stream_name, content: content
end
end

def broadcast_action_later_to(*streamables, action:, target: nil, targets: nil, options: {}, **rendering)
streamables.flatten!
streamables.compact_blank!

return unless streamables.present?

targets = (target ? [target] : targets).map do |item|
convert_to_superglue_fragment_id(item)
end

# bugs
# ActionBroadcastJOB doesn't take in targets
# targets does not work with multi ids
Superglue::Streams::ActionBroadcastJob.perform_later \
stream_name_from(streamables), action: action, targets: targets, options: options, **rendering
end

# def broadcast_render_to(*streamables, **rendering)
# broadcast_stream_to(*streamables, content: render_format(:superglue_stream, **rendering))
# end

# def broadcast_render_later_to(*streamables, **rendering)
# Superglue::Streams::BroadcastJob.perform_later stream_name_from(streamables), **rendering
# end

def broadcast_stream_to(*streamables, content:)
streamables.flatten!
streamables.compact_blank!

return unless streamables.present?

ActionCable.server.broadcast stream_name_from(streamables), content
end

def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
Superglue::ThreadDebouncer.for("superglue-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
end

private

def convert_to_superglue_fragment_id(target, include_selector: false)
target_array = Array.wrap(target)
if target_array.any? { |value| value.respond_to?(:to_key) }
ActionView::RecordIdentifier.dom_id(*target_array)
else
target
end
end

def render_format(format, **rendering)
rendering[:layout] = "superglue/layouts/fragment"
ApplicationController.render(formats: [format], **rendering)
end

def render_broadcast_action(rendering)
# content = rendering.delete(:content) # i should remove content
# html = rendering.delete(:html) # i should add json and stringify it
render = rendering.delete(:render)
json = rendering.delete(:json)

if render == false
nil
elsif rendering.present?
if json
rendering[:partial] = "superglue/body"
rendering[:locals] ||= {}
rendering[:locals][:broadcast_json] = json
end

render_format(:json, **rendering)
end
end
end
32 changes: 32 additions & 0 deletions app/channels/superglue/streams/stream_name.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Stream names are how we identify which updates should go to which users. All streams run over the same
# <tt>Superglue::StreamsChannel</tt>, but each with their own subscription. Since stream names are exposed directly to the user
# via the HTML stream subscription tags, we need to ensure that the name isn't tampered with, so the names are signed
# upon generation and verified upon receipt. All verification happens through the <tt>Superglue.signed_stream_verifier</tt>.
module Superglue::Streams::StreamName
# Used by <tt>Superglue::StreamsChannel</tt> to verify a signed stream name.
def verified_stream_name(signed_stream_name)
Superglue.signed_stream_verifier.verified signed_stream_name
end

# Used by <tt>Superglue::StreamsHelper#Superglue_stream_from(*streamables)</tt> to generate a signed stream name.
def signed_stream_name(streamables)
Superglue.signed_stream_verifier.generate stream_name_from(streamables)
end

module ClassMethods
# Can be used by custom Superglue stream channels to obtain signed stream name from <tt>params</tt>
def verified_stream_name_from_params
self.class.verified_stream_name(params[:signed_stream_name])
end
end

private

def stream_name_from(streamables)
if streamables.is_a?(Array)
streamables.map { |streamable| stream_name_from(streamable) }.join(':')
else
streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
end
end
end
13 changes: 13 additions & 0 deletions app/channels/superglue/streams_channel.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class Superglue::StreamsChannel < ActionCable::Channel::Base
extend Superglue::Streams::StreamName
extend Superglue::Streams::Broadcasts
include Superglue::Streams::StreamName::ClassMethods

def subscribed
if stream_name = verified_stream_name_from_params
stream_from stream_name
else
reject
end
end
end
13 changes: 13 additions & 0 deletions app/controllers/concerns/superglue/request_id_tracking.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module Superglue::RequestIdTracking
extend ActiveSupport::Concern

included do
around_action :superglue_tracking_request_id
end

private

def superglue_tracking_request_id(&block)
Superglue.with_request_id(request.headers["X-Superglue-Request-Id"], &block)
end
end
12 changes: 12 additions & 0 deletions app/helpers/superglue/streams/action_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Superglue::Streams::ActionHelper
private

def convert_to_superglue_fragment_id(target, include_selector: false)
target_array = Array.wrap(target)
if target_array.any? { |value| value.respond_to?(:to_key) }
ActionView::RecordIdentifier.dom_id(*target_array)
else
target
end
end
end
8 changes: 8 additions & 0 deletions app/jobs/superglue/streams/action_broadcast_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# The job that powers all the <tt>broadcast_$action_later</tt> broadcasts available in <tt>Turbo::Streams::Broadcasts</tt>.
class Superglue::Streams::ActionBroadcastJob < ActiveJob::Base
discard_on ActiveJob::DeserializationError

def perform(stream, action:, targets:, options: {}, **rendering)
Superglue::StreamsChannel.broadcast_action_to stream, action: action, targets: targets, options: options, **rendering
end
end
7 changes: 7 additions & 0 deletions app/jobs/superglue/streams/broadcast_stream_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class Superglue::Streams::BroadcastStreamJob < ActiveJob::Base
discard_on ActiveJob::DeserializationError

def perform(stream, content:)
Superglue::StreamsChannel.broadcast_stream_to(stream, content: content)
end
end
Loading
Loading