superglue 1.1.0 → 2.0.0.alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/channels/superglue/streams/broadcasts.rb +129 -0
- data/app/channels/superglue/streams/stream_name.rb +25 -0
- data/app/channels/superglue/streams_channel.rb +13 -0
- data/app/controllers/concerns/superglue/request_id_tracking.rb +13 -0
- data/app/helpers/superglue/streams_helper.rb +70 -0
- data/app/jobs/superglue/streams/action_broadcast_job.rb +7 -0
- data/app/jobs/superglue/streams/broadcast_stream_job.rb +7 -0
- data/app/models/concerns/superglue/broadcastable.rb +151 -0
- data/app/models/superglue/debouncer.rb +25 -0
- data/app/models/superglue/thread_debouncer.rb +27 -0
- data/app/views/superglue/layouts/_stream_message.json.props +15 -0
- data/app/views/superglue/layouts/stream.json.props +10 -0
- data/lib/generators/superglue/install/install_generator.rb +4 -1
- data/lib/generators/superglue/install/templates/stream.json.props +15 -0
- data/lib/generators/superglue/install/templates/ts/page_to_page_mapping.ts +3 -4
- data/lib/superglue/engine.rb +55 -0
- data/lib/superglue.rb +27 -35
- metadata +18 -5
- data/lib/superglue/redirection.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ece4f5c86bd486f1c253c62fdbfff95056fb60bbcabc9e951d52945c17b9b1bc
|
4
|
+
data.tar.gz: 665759fb6ce210973e4c49838936161fa685c9e1361432a63745c30e4170c9ce
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9add30dbf5df13ce875abe6510e7ad4f3d9820333341821bbeb8620bfe96c3559168eeff4ea34512ac3210f29e46cf784529f2665720fa25bd24919299e8571c
|
7
|
+
data.tar.gz: 68755c19906211366a20bfc28d56bb68dc47404474071cbf071681cd30093c8315d8895f0be70136beef3c4017658139faf6d601818a0d3f259b1b3c9efdf6b9
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module Superglue::Streams::Broadcasts
|
2
|
+
def broadcast_save_to(*streamables, **opts)
|
3
|
+
broadcast_action_to(*streamables, action: :save, **opts)
|
4
|
+
end
|
5
|
+
|
6
|
+
def broadcast_append_to(*streamables, **opts)
|
7
|
+
broadcast_action_to(*streamables, action: :append, **opts)
|
8
|
+
end
|
9
|
+
|
10
|
+
def broadcast_prepend_to(*streamables, **opts)
|
11
|
+
broadcast_action_to(*streamables, action: :prepend, **opts)
|
12
|
+
end
|
13
|
+
|
14
|
+
def broadcast_refresh_to(*streamables, **opts)
|
15
|
+
request_id = Superglue.current_request_id
|
16
|
+
content = JSON.generate({
|
17
|
+
type: "message",
|
18
|
+
action: "refresh",
|
19
|
+
requestId: request_id,
|
20
|
+
options: opts
|
21
|
+
})
|
22
|
+
broadcast_stream_to(*streamables, content: content)
|
23
|
+
end
|
24
|
+
|
25
|
+
def broadcast_action_to(*streamables, action:, fragment: nil, fragments: nil, save_as: nil, options: {}, **rendering)
|
26
|
+
locals = rendering[:locals] || {}
|
27
|
+
fragments = (fragment ? [fragment] : fragments)
|
28
|
+
|
29
|
+
fragments = fragments.map do |item|
|
30
|
+
convert_to_superglue_fragment_id(item)
|
31
|
+
end
|
32
|
+
|
33
|
+
if save_as
|
34
|
+
options[:saveAs] = convert_to_superglue_fragment_id(save_as)
|
35
|
+
end
|
36
|
+
|
37
|
+
locals[:broadcast_fragment_keys] = fragments
|
38
|
+
locals[:broadcast_action] = action
|
39
|
+
locals[:broadcast_options] = options
|
40
|
+
rendering[:locals] = locals
|
41
|
+
|
42
|
+
broadcast_stream_to(*streamables, content: render_broadcast_action(rendering))
|
43
|
+
end
|
44
|
+
|
45
|
+
def broadcast_save_later_to(*streamables, **opts)
|
46
|
+
broadcast_action_later_to(*streamables, action: :save, **opts)
|
47
|
+
end
|
48
|
+
|
49
|
+
def broadcast_append_later_to(*streamables, **opts)
|
50
|
+
broadcast_action_later_to(*streamables, action: :append, **opts)
|
51
|
+
end
|
52
|
+
|
53
|
+
def broadcast_prepend_later_to(*streamables, **opts)
|
54
|
+
broadcast_action_later_to(*streamables, action: :prepend, **opts)
|
55
|
+
end
|
56
|
+
|
57
|
+
def broadcast_refresh_later_to(*streamables, request_id: Superglue.current_request_id, **opts)
|
58
|
+
stream_name = stream_name_from(streamables)
|
59
|
+
|
60
|
+
refresh_debouncer_for(*streamables, request_id: request_id).debounce do
|
61
|
+
content = JSON.generate({
|
62
|
+
type: "message",
|
63
|
+
action: "refresh",
|
64
|
+
requestId: request_id,
|
65
|
+
options: opts
|
66
|
+
})
|
67
|
+
|
68
|
+
Superglue::Streams::BroadcastStreamJob.perform_later stream_name, content: content
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def broadcast_action_later_to(*streamables, action:, fragment: nil, fragments: nil, save_as: nil, options: {}, **rendering)
|
73
|
+
streamables.flatten!
|
74
|
+
streamables.compact_blank!
|
75
|
+
|
76
|
+
return unless streamables.present?
|
77
|
+
|
78
|
+
fragments = (fragment ? [fragment] : fragments).map do |item|
|
79
|
+
convert_to_superglue_fragment_id(item)
|
80
|
+
end
|
81
|
+
|
82
|
+
if save_as
|
83
|
+
options[:saveAs] = convert_to_superglue_fragment_id(save_as)
|
84
|
+
end
|
85
|
+
|
86
|
+
Superglue::Streams::ActionBroadcastJob.perform_later \
|
87
|
+
stream_name_from(streamables), action: action, fragments: fragments, options: options, **rendering
|
88
|
+
end
|
89
|
+
|
90
|
+
def broadcast_stream_to(*streamables, content:)
|
91
|
+
streamables.flatten!
|
92
|
+
streamables.compact_blank!
|
93
|
+
|
94
|
+
return unless streamables.present?
|
95
|
+
|
96
|
+
ActionCable.server.broadcast stream_name_from(streamables), content
|
97
|
+
end
|
98
|
+
|
99
|
+
def refresh_debouncer_for(*streamables, request_id: nil) # :nodoc:
|
100
|
+
Superglue::ThreadDebouncer.for("superglue-refresh-debouncer-#{stream_name_from(streamables.including(request_id))}")
|
101
|
+
end
|
102
|
+
|
103
|
+
private
|
104
|
+
|
105
|
+
def convert_to_superglue_fragment_id(fragment)
|
106
|
+
fragment_array = Array.wrap(fragment)
|
107
|
+
if fragment_array.any? { |value| value.respond_to?(:to_key) }
|
108
|
+
ActionView::RecordIdentifier.dom_id(*fragment_array)
|
109
|
+
else
|
110
|
+
fragment
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def render_format(format, **rendering)
|
115
|
+
rendering[:layout] = "superglue/layouts/stream_message"
|
116
|
+
ApplicationController.render(formats: [format], **rendering)
|
117
|
+
end
|
118
|
+
|
119
|
+
def render_broadcast_action(rendering)
|
120
|
+
json = rendering.delete(:json)
|
121
|
+
|
122
|
+
if json
|
123
|
+
rendering[:locals] ||= {}
|
124
|
+
rendering[:locals][:broadcast_json] = json
|
125
|
+
end
|
126
|
+
|
127
|
+
render_format(:json, **rendering)
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Superglue::Streams::StreamName
|
2
|
+
def verified_stream_name(signed_stream_name)
|
3
|
+
Superglue.signed_stream_verifier.verified signed_stream_name
|
4
|
+
end
|
5
|
+
|
6
|
+
def signed_stream_name(streamables)
|
7
|
+
Superglue.signed_stream_verifier.generate stream_name_from(streamables)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def verified_stream_name_from_params
|
12
|
+
self.class.verified_stream_name(params[:signed_stream_name])
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def stream_name_from(streamables)
|
19
|
+
if streamables.is_a?(Array)
|
20
|
+
streamables.map { |streamable| stream_name_from(streamable) }.join(":")
|
21
|
+
else
|
22
|
+
streamables.then { |streamable| streamable.try(:to_gid_param) || streamable.to_param }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class Superglue::StreamsChannel < ActionCable::Channel::Base
|
2
|
+
extend Superglue::Streams::StreamName
|
3
|
+
extend Superglue::Streams::Broadcasts
|
4
|
+
include Superglue::Streams::StreamName::ClassMethods
|
5
|
+
|
6
|
+
def subscribed
|
7
|
+
if stream_name = verified_stream_name_from_params
|
8
|
+
stream_from stream_name
|
9
|
+
else
|
10
|
+
reject
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Superglue::RequestIdTracking
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
around_action :superglue_tracking_request_id
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def superglue_tracking_request_id(&block)
|
11
|
+
Superglue.with_request_id(request.headers["X-Superglue-Request-Id"], &block)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module Superglue::StreamsHelper
|
2
|
+
def stream_from_props(*streamables, **attributes)
|
3
|
+
raise ArgumentError, "streamables can't be blank" unless streamables.any?(&:present?)
|
4
|
+
attributes[:channel] = attributes[:channel]&.to_s || "Superglue::StreamsChannel"
|
5
|
+
attributes[:signed_stream_name] = Superglue::StreamsChannel.signed_stream_name(streamables)
|
6
|
+
|
7
|
+
attributes
|
8
|
+
end
|
9
|
+
|
10
|
+
def fragment_id(value)
|
11
|
+
if value.respond_to?(:to_key)
|
12
|
+
ActionView::RecordIdentifier.dom_id(value)
|
13
|
+
elsif value.respond_to?(:broadcast_fragment_default)
|
14
|
+
value.broadcast_fragment_default
|
15
|
+
else
|
16
|
+
value.to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def broadcast_prepend_props(model: nil, fragment: nil, save_as: nil, options: {}, **rendering)
|
21
|
+
if save_as
|
22
|
+
options[:saveAs] ||= fragment_id(save_as)
|
23
|
+
end
|
24
|
+
|
25
|
+
broadcast_action_props(action: "prepend", model:, fragment:, options:, **rendering)
|
26
|
+
end
|
27
|
+
|
28
|
+
def broadcast_append_props(model: nil, fragment: nil, save_as: nil, options: {}, **rendering)
|
29
|
+
if save_as
|
30
|
+
options[:saveAs] ||= fragment_id(save_as)
|
31
|
+
end
|
32
|
+
|
33
|
+
broadcast_action_props(action: "append", model:, fragment:, options:, **rendering)
|
34
|
+
end
|
35
|
+
|
36
|
+
def broadcast_save_props(model: nil, partial: nil, fragment: nil, options: {}, **rendering)
|
37
|
+
if model && !fragment
|
38
|
+
fragment = fragment_id(model)
|
39
|
+
end
|
40
|
+
|
41
|
+
broadcast_action_props(action: "save", model:, fragment:, options:, **rendering)
|
42
|
+
end
|
43
|
+
|
44
|
+
def broadcast_action_props(action:, partial: nil, model: nil, fragment: nil, options: {}, **rendering)
|
45
|
+
if model
|
46
|
+
fragment = model.broadcast_fragment_default if !fragment
|
47
|
+
|
48
|
+
if model.respond_to?(:to_partial_path)
|
49
|
+
rendering[:locals] = (rendering[:locals] || {}).reverse_merge(model.model_name.element.to_sym => model).compact
|
50
|
+
partial ||= model.to_partial_path
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
fragment = fragment_id(fragment)
|
55
|
+
|
56
|
+
if !partial
|
57
|
+
raise StandardError, "A partial is needed to render a stream"
|
58
|
+
end
|
59
|
+
|
60
|
+
json = instance_variable_get(:@__json)
|
61
|
+
|
62
|
+
json.child! do
|
63
|
+
json.fragmentIds [fragment]
|
64
|
+
json.handler action
|
65
|
+
json.options(options)
|
66
|
+
json.data(partial: [partial, rendering]) do
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
class Superglue::Streams::ActionBroadcastJob < ActiveJob::Base
|
2
|
+
discard_on ActiveJob::DeserializationError
|
3
|
+
|
4
|
+
def perform(stream, action:, fragments:, options: {}, **rendering)
|
5
|
+
Superglue::StreamsChannel.broadcast_action_to stream, action: action, fragments: fragments, options: options, **rendering
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,151 @@
|
|
1
|
+
module Superglue::Broadcastable
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
included do
|
5
|
+
thread_mattr_accessor :suppressed_superglue_broadcasts, instance_accessor: false
|
6
|
+
delegate :suppressed_superglue_broadcasts?, to: "self.class"
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def broadcasts_to(stream, inserts_by: :append, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
11
|
+
after_create_commit -> { broadcast_action_later_to(stream.try(:call, self) || send(stream), action: inserts_by, fragment: fragment.try(:call, self) || fragment, save_as: save_as&.try(:call, self), **rendering) }
|
12
|
+
after_update_commit -> { broadcast_save_later_to(stream.try(:call, self) || send(stream), **rendering) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def broadcasts(stream = model_name.plural, inserts_by: :append, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
16
|
+
after_create_commit -> { broadcast_action_later_to(stream, action: inserts_by, fragment: fragment.try(:call, self) || fragment, save_as: save_as&.try(:call, self), **rendering) }
|
17
|
+
after_update_commit -> { broadcast_save_later(**rendering) }
|
18
|
+
end
|
19
|
+
|
20
|
+
def broadcasts_refreshes_to(stream)
|
21
|
+
after_commit -> { broadcast_refresh_later_to(stream.try(:call, self) || send(stream)) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def broadcasts_refreshes(stream = model_name.plural)
|
25
|
+
after_create_commit -> { broadcast_refresh_later_to(stream) }
|
26
|
+
after_update_commit -> { broadcast_refresh_later }
|
27
|
+
after_destroy_commit -> { broadcast_refresh }
|
28
|
+
end
|
29
|
+
|
30
|
+
def broadcast_fragment_default
|
31
|
+
model_name.plural
|
32
|
+
end
|
33
|
+
|
34
|
+
def suppressing_superglue_broadcasts(&block)
|
35
|
+
original, self.suppressed_superglue_broadcasts = suppressed_superglue_broadcasts, true
|
36
|
+
yield
|
37
|
+
ensure
|
38
|
+
self.suppressed_superglue_broadcasts = original
|
39
|
+
end
|
40
|
+
|
41
|
+
def suppressed_superglue_broadcasts?
|
42
|
+
suppressed_superglue_broadcasts
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# add fragment?
|
47
|
+
def broadcast_save_to(*streamables, **rendering)
|
48
|
+
Superglue::StreamsChannel.broadcast_save_to(*streamables, **extract_options_and_add_fragment(rendering, fragment: self)) unless suppressed_superglue_broadcasts?
|
49
|
+
end
|
50
|
+
|
51
|
+
def broadcast_save(**rendering)
|
52
|
+
broadcast_save_to self, **rendering
|
53
|
+
end
|
54
|
+
|
55
|
+
# todo save_as: true
|
56
|
+
def broadcast_append_to(*streamables, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
57
|
+
Superglue::StreamsChannel.broadcast_append_to(*streamables, save_as: save_as, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
58
|
+
end
|
59
|
+
|
60
|
+
def broadcast_append(fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
61
|
+
broadcast_append_to self, fragment: fragment, save_as: save_as, **rendering
|
62
|
+
end
|
63
|
+
|
64
|
+
def broadcast_prepend_to(*streamables, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
65
|
+
Superglue::StreamsChannel.broadcast_prepend_to(*streamables, save_as: save_as, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
66
|
+
end
|
67
|
+
|
68
|
+
def broadcast_prepend(fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
69
|
+
broadcast_prepend_to self, fragment: fragment, save_as: save_as, **rendering
|
70
|
+
end
|
71
|
+
|
72
|
+
def broadcast_refresh_to(*streamables)
|
73
|
+
Superglue::StreamsChannel.broadcast_refresh_to(*streamables) unless suppressed_superglue_broadcasts?
|
74
|
+
end
|
75
|
+
|
76
|
+
def broadcast_refresh
|
77
|
+
broadcast_refresh_to self
|
78
|
+
end
|
79
|
+
|
80
|
+
# todo rename options to js_options
|
81
|
+
def broadcast_action_to(*streamables, action:, fragment: broadcast_fragment_default, options: {}, **rendering)
|
82
|
+
Superglue::StreamsChannel.broadcast_action_to(*streamables, action: action, options: options, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
83
|
+
end
|
84
|
+
|
85
|
+
def broadcast_action(action, fragment: broadcast_fragment_default, options: {}, **rest)
|
86
|
+
broadcast_action_to self, action: action, fragment: fragment, options: options, **rest
|
87
|
+
end
|
88
|
+
|
89
|
+
def broadcast_save_later_to(*streamables, **rendering)
|
90
|
+
Superglue::StreamsChannel.broadcast_save_later_to(*streamables, **extract_options_and_add_fragment(rendering, fragment: self)) unless suppressed_superglue_broadcasts?
|
91
|
+
end
|
92
|
+
|
93
|
+
def broadcast_save_later(**rendering)
|
94
|
+
broadcast_save_later_to self, **rendering
|
95
|
+
end
|
96
|
+
|
97
|
+
def broadcast_append_later_to(*streamables, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
98
|
+
Superglue::StreamsChannel.broadcast_append_later_to(*streamables, save_as: save_as, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
99
|
+
end
|
100
|
+
|
101
|
+
def broadcast_append_later(fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
102
|
+
broadcast_append_later_to self, fragment: fragment, save_as: save_as, **rendering
|
103
|
+
end
|
104
|
+
|
105
|
+
def broadcast_prepend_later_to(*streamables, fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
106
|
+
Superglue::StreamsChannel.broadcast_prepend_later_to(*streamables, save_as: save_as, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
107
|
+
end
|
108
|
+
|
109
|
+
def broadcast_prepend_later(fragment: broadcast_fragment_default, save_as: nil, **rendering)
|
110
|
+
broadcast_prepend_later_to self, fragment: fragment, save_as: save_as, **rendering
|
111
|
+
end
|
112
|
+
|
113
|
+
def broadcast_refresh_later_to(*streamables)
|
114
|
+
Superglue::StreamsChannel.broadcast_refresh_later_to(*streamables, request_id: Superglue.current_request_id) unless suppressed_superglue_broadcasts?
|
115
|
+
end
|
116
|
+
|
117
|
+
def broadcast_refresh_later
|
118
|
+
broadcast_refresh_later_to self
|
119
|
+
end
|
120
|
+
|
121
|
+
def broadcast_action_later_to(*streamables, action:, fragment: broadcast_fragment_default, options: {}, **rendering)
|
122
|
+
Superglue::StreamsChannel.broadcast_action_later_to(*streamables, action: action, options: options, **extract_options_and_add_fragment(rendering, fragment: fragment)) unless suppressed_superglue_broadcasts?
|
123
|
+
end
|
124
|
+
|
125
|
+
def broadcast_action_later(action:, fragment: broadcast_fragment_default, options: {}, **rendering)
|
126
|
+
broadcast_action_later_to self, action: action, fragment: fragment, options: options, **rendering
|
127
|
+
end
|
128
|
+
|
129
|
+
def broadcast_fragment_default
|
130
|
+
self.class.broadcast_fragment_default
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def extract_options_and_add_fragment(rendering = {}, fragment: broadcast_fragment_default)
|
136
|
+
broadcast_rendering_with_defaults(rendering).tap do |options|
|
137
|
+
options[:fragment] = fragment if !options.key?(:fragment) && !options.key?(:fragments)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def broadcast_rendering_with_defaults(options)
|
142
|
+
options.tap do |o|
|
143
|
+
# Add the current instance into the locals with the element name (which is the un-namespaced name)
|
144
|
+
# as the key. This parallels how the ActionView::ObjectRenderer would create a local variable.
|
145
|
+
o[:locals] = (o[:locals] || {}).reverse_merge(model_name.element.to_sym => self).compact
|
146
|
+
|
147
|
+
# if none of these options are passed in, it will set a partial from #to_partial_path
|
148
|
+
o[:partial] ||= to_partial_path
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Superglue::Debouncer
|
2
|
+
attr_reader :delay, :scheduled_task
|
3
|
+
|
4
|
+
DEFAULT_DELAY = 0.5
|
5
|
+
|
6
|
+
def initialize(delay: DEFAULT_DELAY)
|
7
|
+
@delay = delay
|
8
|
+
@scheduled_task = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def debounce(&block)
|
12
|
+
scheduled_task&.cancel unless scheduled_task&.complete?
|
13
|
+
@scheduled_task = Concurrent::ScheduledTask.execute(delay, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
def wait
|
17
|
+
scheduled_task&.wait(wait_timeout)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def wait_timeout
|
23
|
+
delay + 1
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class Superglue::ThreadDebouncer
|
2
|
+
delegate :wait, to: :debouncer
|
3
|
+
|
4
|
+
def self.for(key, delay: Superglue::Debouncer::DEFAULT_DELAY)
|
5
|
+
Thread.current[key] ||= new(key, Thread.current, delay: delay)
|
6
|
+
end
|
7
|
+
|
8
|
+
private_class_method :new
|
9
|
+
|
10
|
+
def initialize(key, thread, delay:)
|
11
|
+
@key = key
|
12
|
+
@debouncer = Superglue::Debouncer.new(delay: delay)
|
13
|
+
@thread = thread
|
14
|
+
end
|
15
|
+
|
16
|
+
def debounce
|
17
|
+
debouncer.debounce do
|
18
|
+
yield.tap do
|
19
|
+
thread[key] = nil
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
attr_reader :key, :debouncer, :thread
|
27
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
json.disable_deferments!
|
2
|
+
|
3
|
+
if local_assigns[:broadcast_json]
|
4
|
+
json.data broadcast_json
|
5
|
+
else
|
6
|
+
json.data do
|
7
|
+
yield
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
json.fragments json.fragments!
|
12
|
+
json.action "handleStreamMessage"
|
13
|
+
json.handler broadcast_action
|
14
|
+
json.fragmentIds broadcast_fragment_keys
|
15
|
+
json.options broadcast_options
|
@@ -30,6 +30,9 @@ module Superglue
|
|
30
30
|
say "Copying application.json.props"
|
31
31
|
copy_file "#{__dir__}/templates/application.json.props", "app/views/layouts/application.json.props"
|
32
32
|
|
33
|
+
say "Copying stream.json.props"
|
34
|
+
copy_file "#{__dir__}/templates/stream.json.props", "app/views/layouts/stream.json.props"
|
35
|
+
|
33
36
|
say "Adding required member methods to ApplicationRecord"
|
34
37
|
add_member_methods
|
35
38
|
|
@@ -37,7 +40,7 @@ module Superglue
|
|
37
40
|
insert_jsx_rendering_defaults
|
38
41
|
|
39
42
|
say "Installing Superglue and friends"
|
40
|
-
run "yarn add react react-dom @reduxjs/toolkit react-redux @thoughtbot/superglue"
|
43
|
+
run "yarn add react react-dom @reduxjs/toolkit react-redux @thoughtbot/superglue@2.0.0-alpha.1"
|
41
44
|
|
42
45
|
if use_typescript
|
43
46
|
run "yarn add -D @types/react-dom @types/react @types/node @thoughtbot/candy_wrapper"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
json.disable_deferments!
|
2
|
+
|
3
|
+
json.data do
|
4
|
+
json.array! do
|
5
|
+
yield json
|
6
|
+
end
|
7
|
+
end
|
8
|
+
json.fragments json.fragments!
|
9
|
+
json.assets [ asset_path('application.js') ]
|
10
|
+
|
11
|
+
if protect_against_forgery?
|
12
|
+
json.csrfToken form_authenticity_token
|
13
|
+
end
|
14
|
+
|
15
|
+
json.action "handleStreamResponse"
|
@@ -1,4 +1,4 @@
|
|
1
|
-
// import your page component
|
1
|
+
// import your page component
|
2
2
|
// e.g import PostsEdit from '../views/posts/edit'
|
3
3
|
|
4
4
|
// Mapping between your props template to Component, you must add to this
|
@@ -28,7 +28,6 @@
|
|
28
28
|
// }
|
29
29
|
// ```
|
30
30
|
//
|
31
|
-
const pageIdentifierToPageComponent = {
|
32
|
-
};
|
31
|
+
const pageIdentifierToPageComponent = {};
|
33
32
|
|
34
|
-
export { pageIdentifierToPageComponent }
|
33
|
+
export { pageIdentifierToPageComponent };
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Superglue
|
2
|
+
module Controller
|
3
|
+
include Helpers
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.include ::Superglue::Rendering
|
7
|
+
return unless base.respond_to?(:helper_method)
|
8
|
+
|
9
|
+
base.helper_method :param_to_dig_path
|
10
|
+
base.helper_method :render_props
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Engine < ::Rails::Engine
|
15
|
+
isolate_namespace Superglue
|
16
|
+
config.eager_load_namespaces << Superglue
|
17
|
+
config.superglue = ActiveSupport::OrderedOptions.new
|
18
|
+
config.superglue.auto_include = true
|
19
|
+
config.autoload_once_paths = %W[
|
20
|
+
#{root}/app/channels
|
21
|
+
#{root}/app/controllers
|
22
|
+
#{root}/app/controllers/concerns
|
23
|
+
#{root}/app/helpers
|
24
|
+
#{root}/app/models
|
25
|
+
#{root}/app/models/concerns
|
26
|
+
#{root}/app/jobs
|
27
|
+
]
|
28
|
+
|
29
|
+
initializer :superglue do |app|
|
30
|
+
ActiveSupport.on_load(:action_controller) do
|
31
|
+
next if self != ActionController::Base
|
32
|
+
|
33
|
+
include Controller
|
34
|
+
include Superglue::RequestIdTracking
|
35
|
+
|
36
|
+
prepend_view_path(
|
37
|
+
Superglue::Resolver.new(Rails.root.join("app/views"))
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
initializer "superglue.helpers" do
|
43
|
+
ActiveSupport.on_load(:action_controller) do
|
44
|
+
helper Superglue::StreamsHelper
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
initializer "superglue.signed_stream_verifier_key" do
|
49
|
+
config.after_initialize do
|
50
|
+
Superglue.signed_stream_verifier_key = config.superglue.signed_stream_verifier_key ||
|
51
|
+
Rails.application.key_generator.generate_key("superglue/signed_stream_verifier_key")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
data/lib/superglue.rb
CHANGED
@@ -1,46 +1,38 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
6
|
-
require
|
1
|
+
require 'superglue/helpers'
|
2
|
+
require 'superglue/rendering'
|
3
|
+
require 'superglue/resolver'
|
4
|
+
require 'superglue/engine'
|
5
|
+
require 'props_template'
|
6
|
+
require 'form_props'
|
7
7
|
|
8
8
|
module Superglue
|
9
|
-
|
10
|
-
include Redirection
|
11
|
-
include Helpers
|
9
|
+
extend ActiveSupport::Autoload
|
12
10
|
|
13
|
-
|
14
|
-
base.include ::Superglue::Rendering
|
15
|
-
if base.respond_to?(:helper_method)
|
16
|
-
base.helper_method :param_to_dig_path
|
17
|
-
base.helper_method :render_props
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
11
|
+
mattr_accessor :draw_routes, default: true
|
21
12
|
|
22
|
-
|
23
|
-
config.superglue = ActiveSupport::OrderedOptions.new
|
24
|
-
config.superglue.auto_include = true
|
13
|
+
thread_mattr_accessor :current_request_id
|
25
14
|
|
26
|
-
|
27
|
-
|
28
|
-
Rails::Generators.hidden_namespaces.uniq!
|
29
|
-
require "generators/rails/scaffold_controller_generator"
|
30
|
-
end
|
15
|
+
class << self
|
16
|
+
attr_writer :signed_stream_verifier_key
|
31
17
|
|
32
|
-
|
33
|
-
ActiveSupport.
|
34
|
-
|
18
|
+
def signed_stream_verifier
|
19
|
+
@signed_stream_verifier ||= ActiveSupport::MessageVerifier.new(
|
20
|
+
signed_stream_verifier_key,
|
21
|
+
digest: 'SHA256',
|
22
|
+
serializer: JSON
|
23
|
+
)
|
24
|
+
end
|
35
25
|
|
36
|
-
|
37
|
-
|
26
|
+
def signed_stream_verifier_key
|
27
|
+
@signed_stream_verifier_key or raise ArgumentError, 'Superglue requires a signed_stream_verifier_key'
|
28
|
+
end
|
38
29
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
30
|
+
def with_request_id(request_id)
|
31
|
+
old_request_id = current_request_id
|
32
|
+
self.current_request_id = request_id
|
33
|
+
yield
|
34
|
+
ensure
|
35
|
+
self.current_request_id = old_request_id
|
44
36
|
end
|
45
37
|
end
|
46
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: superglue
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.0.alpha.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Johny Ho
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-07-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -36,14 +36,14 @@ dependencies:
|
|
36
36
|
requirements:
|
37
37
|
- - "~>"
|
38
38
|
- !ruby/object:Gem::Version
|
39
|
-
version:
|
39
|
+
version: 1.0.0.alpha
|
40
40
|
type: :runtime
|
41
41
|
prerelease: false
|
42
42
|
version_requirements: !ruby/object:Gem::Requirement
|
43
43
|
requirements:
|
44
44
|
- - "~>"
|
45
45
|
- !ruby/object:Gem::Version
|
46
|
-
version:
|
46
|
+
version: 1.0.0.alpha
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: form_props
|
49
49
|
requirement: !ruby/object:Gem::Requirement
|
@@ -65,6 +65,18 @@ extensions: []
|
|
65
65
|
extra_rdoc_files: []
|
66
66
|
files:
|
67
67
|
- README.md
|
68
|
+
- app/channels/superglue/streams/broadcasts.rb
|
69
|
+
- app/channels/superglue/streams/stream_name.rb
|
70
|
+
- app/channels/superglue/streams_channel.rb
|
71
|
+
- app/controllers/concerns/superglue/request_id_tracking.rb
|
72
|
+
- app/helpers/superglue/streams_helper.rb
|
73
|
+
- app/jobs/superglue/streams/action_broadcast_job.rb
|
74
|
+
- app/jobs/superglue/streams/broadcast_stream_job.rb
|
75
|
+
- app/models/concerns/superglue/broadcastable.rb
|
76
|
+
- app/models/superglue/debouncer.rb
|
77
|
+
- app/models/superglue/thread_debouncer.rb
|
78
|
+
- app/views/superglue/layouts/_stream_message.json.props
|
79
|
+
- app/views/superglue/layouts/stream.json.props
|
68
80
|
- lib/generators/superglue/install/install_generator.rb
|
69
81
|
- lib/generators/superglue/install/templates/application.json.props
|
70
82
|
- lib/generators/superglue/install/templates/erb/superglue.html.erb
|
@@ -78,6 +90,7 @@ files:
|
|
78
90
|
- lib/generators/superglue/install/templates/js/layout.jsx
|
79
91
|
- lib/generators/superglue/install/templates/js/page_to_page_mapping.js
|
80
92
|
- lib/generators/superglue/install/templates/js/store.js
|
93
|
+
- lib/generators/superglue/install/templates/stream.json.props
|
81
94
|
- lib/generators/superglue/install/templates/ts/application.tsx
|
82
95
|
- lib/generators/superglue/install/templates/ts/application_visit.ts
|
83
96
|
- lib/generators/superglue/install/templates/ts/components.ts
|
@@ -104,8 +117,8 @@ files:
|
|
104
117
|
- lib/generators/superglue/view_collection/templates/ts/show.tsx
|
105
118
|
- lib/generators/superglue/view_collection/view_collection_generator.rb
|
106
119
|
- lib/superglue.rb
|
120
|
+
- lib/superglue/engine.rb
|
107
121
|
- lib/superglue/helpers.rb
|
108
|
-
- lib/superglue/redirection.rb
|
109
122
|
- lib/superglue/rendering.rb
|
110
123
|
- lib/superglue/resolver.rb
|
111
124
|
homepage: https://github.com/thoughtbot/superglue_rails/
|
@@ -1,29 +0,0 @@
|
|
1
|
-
module Superglue
|
2
|
-
module Redirection
|
3
|
-
def _compute_redirect_to_location(request, options)
|
4
|
-
computed_location = URI.parse(super)
|
5
|
-
next_param = Rack::Utils
|
6
|
-
.parse_nested_query(computed_location.query)
|
7
|
-
|
8
|
-
if request.params[:__] == "0"
|
9
|
-
computed_location.query = next_param.merge({__: "0"}).to_query
|
10
|
-
end
|
11
|
-
|
12
|
-
computed_location.to_s
|
13
|
-
end
|
14
|
-
|
15
|
-
def redirect_back_with_props_at(opts)
|
16
|
-
if request.referrer && params[:props_at]
|
17
|
-
referrer_url = URI.parse(request.referrer)
|
18
|
-
referrer_url.query = Rack::Utils
|
19
|
-
.parse_nested_query(referrer_url.query)
|
20
|
-
.merge({props_at: params[:props_at]})
|
21
|
-
.to_query
|
22
|
-
|
23
|
-
redirect_to referrer_url.to_s, opts
|
24
|
-
else
|
25
|
-
redirect_back(opts)
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|