solid_objects 0.5.2 → 0.7.0
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/CHANGELOG.md +26 -0
- data/app/assets/javascripts/solid_objects/component_batch_refresh.js +148 -0
- data/app/assets/javascripts/solid_objects/state_payload.js +69 -0
- data/app/controllers/solid_objects/components_controller.rb +84 -0
- data/app/helpers/solid_objects/actor_helper.rb +22 -4
- data/config/routes.rb +1 -0
- data/docs/realtime.md +161 -0
- data/lib/solid_objects/actor.rb +7 -0
- data/lib/solid_objects/actor_channel.rb +55 -4
- data/lib/solid_objects/actor_definition.rb +19 -0
- data/lib/solid_objects/actor_view.rb +14 -7
- data/lib/solid_objects/component_registration.rb +23 -4
- data/lib/solid_objects/component_subscriptions.rb +16 -10
- data/lib/solid_objects/component_token.rb +19 -2
- data/lib/solid_objects/errors.rb +6 -0
- data/lib/solid_objects/executor.rb +22 -7
- data/lib/solid_objects/payload_broadcast.rb +67 -0
- data/lib/solid_objects/stream_token.rb +19 -12
- data/lib/solid_objects/turbo_stream_renderer.rb +34 -5
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +1 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +11 -0
- data/sig/generated/helpers/solid_objects/actor_helper.rbs +2 -2
- data/sig/generated/lib/solid_objects/actor.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +11 -0
- data/sig/generated/lib/solid_objects/actor_definition.rbs +7 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +7 -4
- data/sig/generated/lib/solid_objects/component_registration.rbs +11 -4
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +3 -0
- data/sig/generated/lib/solid_objects/component_token.rbs +7 -2
- data/sig/generated/lib/solid_objects/errors.rbs +6 -0
- data/sig/generated/lib/solid_objects/executor.rbs +7 -4
- data/sig/generated/lib/solid_objects/payload_broadcast.rbs +35 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +5 -2
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +6 -0
- metadata +5 -1
|
@@ -9,6 +9,7 @@ module SolidObjects
|
|
|
9
9
|
# @rbs @messages: Hash[Symbol, Handler]
|
|
10
10
|
# @rbs @queries: Hash[Symbol, Handler]
|
|
11
11
|
# @rbs @observables: Hash[Symbol, Handler]
|
|
12
|
+
# @rbs @payload_broadcasts: Hash[Symbol, Handler]
|
|
12
13
|
# @rbs @state_version: Integer
|
|
13
14
|
# @rbs @state_migrations: Array[StateMigration]
|
|
14
15
|
# @rbs @activation_hooks: Array[Proc]
|
|
@@ -20,6 +21,7 @@ module SolidObjects
|
|
|
20
21
|
:messages,
|
|
21
22
|
:queries,
|
|
22
23
|
:observables,
|
|
24
|
+
:payload_broadcasts,
|
|
23
25
|
:state_version,
|
|
24
26
|
:state_migrations,
|
|
25
27
|
:activation_hooks,
|
|
@@ -31,6 +33,7 @@ module SolidObjects
|
|
|
31
33
|
@messages = {}
|
|
32
34
|
@queries = {}
|
|
33
35
|
@observables = {}
|
|
36
|
+
@payload_broadcasts = {}
|
|
34
37
|
@state_version = 1
|
|
35
38
|
@state_migrations = []
|
|
36
39
|
@activation_hooks = []
|
|
@@ -83,6 +86,21 @@ module SolidObjects
|
|
|
83
86
|
end
|
|
84
87
|
end
|
|
85
88
|
|
|
89
|
+
# @rbs (Symbol | String, Proc) -> Handler
|
|
90
|
+
def add_payload_broadcast(name, block)
|
|
91
|
+
payload_name = name.to_sym
|
|
92
|
+
if payload_broadcasts.key?(payload_name)
|
|
93
|
+
raise InvalidActor, "#{payload_name.inspect} payload broadcast is already defined"
|
|
94
|
+
end
|
|
95
|
+
unless payload_name.to_s.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
96
|
+
raise InvalidActor, "payload broadcast names may contain only letters, digits, and underscores"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
Handler.new(name: payload_name, block:).tap do |handler|
|
|
100
|
+
payload_broadcasts[payload_name] = handler
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
86
104
|
# @rbs (Class) -> ActorDefinition
|
|
87
105
|
def synchronize_instance_messages(actor_class)
|
|
88
106
|
names = actor_message_method_names(actor_class)
|
|
@@ -149,6 +167,7 @@ module SolidObjects
|
|
|
149
167
|
copy.instance_variable_set(:@messages, messages.dup)
|
|
150
168
|
copy.instance_variable_set(:@queries, queries.dup)
|
|
151
169
|
copy.instance_variable_set(:@observables, observables.dup)
|
|
170
|
+
copy.instance_variable_set(:@payload_broadcasts, payload_broadcasts.dup)
|
|
152
171
|
copy.instance_variable_set(:@state_version, state_version)
|
|
153
172
|
copy.instance_variable_set(:@state_migrations, state_migrations.dup)
|
|
154
173
|
copy.instance_variable_set(:@activation_hooks, activation_hooks.dup)
|
|
@@ -37,18 +37,19 @@ module SolidObjects
|
|
|
37
37
|
)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol) -> untyped
|
|
40
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?, ?key: untyped, ?locals: Hash[untyped, untyped], ?refresh: String | Symbol, ?batch: untyped) -> untyped
|
|
41
41
|
def component(
|
|
42
42
|
name,
|
|
43
43
|
observes: nil,
|
|
44
44
|
partial: nil,
|
|
45
45
|
key: nil,
|
|
46
46
|
locals: {},
|
|
47
|
-
refresh: :replace
|
|
47
|
+
refresh: :replace,
|
|
48
|
+
batch: nil
|
|
48
49
|
)
|
|
49
50
|
component_name = normalized_component_name(name)
|
|
50
51
|
unless observes
|
|
51
|
-
validate_static_options!(key:, locals:, refresh:)
|
|
52
|
+
validate_static_options!(key:, locals:, refresh:, batch:)
|
|
52
53
|
return static_component(component_name, partial:)
|
|
53
54
|
end
|
|
54
55
|
if partial
|
|
@@ -67,7 +68,8 @@ module SolidObjects
|
|
|
67
68
|
locals:,
|
|
68
69
|
refresh_method: refresh,
|
|
69
70
|
snapshot:,
|
|
70
|
-
refresh_path
|
|
71
|
+
refresh_path:,
|
|
72
|
+
batch: batch&.to_s
|
|
71
73
|
)
|
|
72
74
|
ensure_unique_component!(registration)
|
|
73
75
|
rendered = ComponentRenderer.new(
|
|
@@ -98,6 +100,11 @@ module SolidObjects
|
|
|
98
100
|
observable_names.dup
|
|
99
101
|
end
|
|
100
102
|
|
|
103
|
+
# @rbs () -> bool
|
|
104
|
+
def batched_components?
|
|
105
|
+
component_registrations.any?(&:batch)
|
|
106
|
+
end
|
|
107
|
+
|
|
101
108
|
# @rbs () -> bool
|
|
102
109
|
def morph_components?
|
|
103
110
|
component_registrations.any?(&:morph?)
|
|
@@ -212,9 +219,9 @@ module SolidObjects
|
|
|
212
219
|
"#{registration.component_key.inspect} is already rendered in this solid_object scope"
|
|
213
220
|
end
|
|
214
221
|
|
|
215
|
-
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol) -> void
|
|
216
|
-
def validate_static_options!(key:, locals:, refresh:)
|
|
217
|
-
return if key.nil? && locals.empty? && refresh.to_s == "replace"
|
|
222
|
+
# @rbs (key: untyped, locals: Hash[untyped, untyped], refresh: String | Symbol, batch: untyped) -> void
|
|
223
|
+
def validate_static_options!(key:, locals:, refresh:, batch:)
|
|
224
|
+
return if key.nil? && locals.empty? && refresh.to_s == "replace" && batch.nil?
|
|
218
225
|
|
|
219
226
|
raise ArgumentError,
|
|
220
227
|
"key, locals, and refresh require an observable component"
|
|
@@ -7,6 +7,7 @@ module SolidObjects
|
|
|
7
7
|
# @rbs @reference: Reference
|
|
8
8
|
# @rbs @component_name: String
|
|
9
9
|
# @rbs @component_key: String | Integer?
|
|
10
|
+
# @rbs @batch: String?
|
|
10
11
|
# @rbs @dependencies: Array[String]
|
|
11
12
|
# @rbs @locals: Hash[String, untyped]
|
|
12
13
|
# @rbs @refresh_method: String
|
|
@@ -18,6 +19,7 @@ module SolidObjects
|
|
|
18
19
|
attr_reader :reference,
|
|
19
20
|
:component_name,
|
|
20
21
|
:component_key,
|
|
22
|
+
:batch,
|
|
21
23
|
:dependencies,
|
|
22
24
|
:locals,
|
|
23
25
|
:refresh_method,
|
|
@@ -26,7 +28,7 @@ module SolidObjects
|
|
|
26
28
|
:refresh_path,
|
|
27
29
|
:token
|
|
28
30
|
|
|
29
|
-
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String) -> void
|
|
31
|
+
# @rbs (reference: Reference, component_name: String, component_key: String | Integer?, dependencies: Array[String], locals: Hash[String, untyped], refresh_method: String, instance_id: Integer, revision: Integer, refresh_path: String, token: String, ?batch: String?) -> void
|
|
30
32
|
def initialize(
|
|
31
33
|
reference:,
|
|
32
34
|
component_name:,
|
|
@@ -37,9 +39,11 @@ module SolidObjects
|
|
|
37
39
|
instance_id:,
|
|
38
40
|
revision:,
|
|
39
41
|
refresh_path:,
|
|
40
|
-
token
|
|
42
|
+
token:,
|
|
43
|
+
batch: nil
|
|
41
44
|
)
|
|
42
45
|
@reference = reference
|
|
46
|
+
@batch = batch
|
|
43
47
|
@component_name = component_name
|
|
44
48
|
@component_key = component_key
|
|
45
49
|
@dependencies = dependencies.freeze
|
|
@@ -52,7 +56,7 @@ module SolidObjects
|
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
class << self
|
|
55
|
-
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String) -> ComponentRegistration
|
|
59
|
+
# @rbs (reference: Reference, component_name: String, component_key: untyped, dependencies: Array[String], locals: Hash[untyped, untyped], refresh_method: String | Symbol, snapshot: ActorSnapshot, refresh_path: String, ?batch: String?) -> ComponentRegistration
|
|
56
60
|
def issue(
|
|
57
61
|
reference:,
|
|
58
62
|
component_name:,
|
|
@@ -61,7 +65,8 @@ module SolidObjects
|
|
|
61
65
|
locals:,
|
|
62
66
|
refresh_method:,
|
|
63
67
|
snapshot:,
|
|
64
|
-
refresh_path
|
|
68
|
+
refresh_path:,
|
|
69
|
+
batch: nil
|
|
65
70
|
)
|
|
66
71
|
token = ComponentToken.generate(
|
|
67
72
|
reference:,
|
|
@@ -70,6 +75,7 @@ module SolidObjects
|
|
|
70
75
|
dependencies:,
|
|
71
76
|
locals:,
|
|
72
77
|
refresh_method:,
|
|
78
|
+
batch:,
|
|
73
79
|
instance_id: snapshot.instance_id,
|
|
74
80
|
revision: snapshot.revision,
|
|
75
81
|
refresh_path:
|
|
@@ -99,6 +105,7 @@ module SolidObjects
|
|
|
99
105
|
reference:,
|
|
100
106
|
component_name: payload.fetch("component_name"),
|
|
101
107
|
component_key: payload["component_key"],
|
|
108
|
+
batch: payload["batch"],
|
|
102
109
|
dependencies:,
|
|
103
110
|
locals: payload.fetch("locals"),
|
|
104
111
|
refresh_method: payload.fetch("refresh_method"),
|
|
@@ -149,6 +156,18 @@ module SolidObjects
|
|
|
149
156
|
locals.merge("component_key" => component_key).freeze
|
|
150
157
|
end
|
|
151
158
|
|
|
159
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
160
|
+
def batch_refresh_url(registrations, instance_id, revision)
|
|
161
|
+
query = URI.encode_www_form(
|
|
162
|
+
[
|
|
163
|
+
[ "instance_id", instance_id ],
|
|
164
|
+
[ "revision", revision ],
|
|
165
|
+
*registrations.map { |registration| [ "tokens[]", registration.token ] }
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
"#{refresh_path}/batch?#{query}"
|
|
169
|
+
end
|
|
170
|
+
|
|
152
171
|
# @rbs (Integer, Integer) -> String
|
|
153
172
|
def refresh_url(instance_id, revision)
|
|
154
173
|
query = URI.encode_www_form(
|
|
@@ -48,16 +48,17 @@ module SolidObjects
|
|
|
48
48
|
observable_name = invalidation.fetch("observable_name")
|
|
49
49
|
instance_id = invalidation.fetch("instance_id")
|
|
50
50
|
revision = invalidation.fetch("revision")
|
|
51
|
-
registrations.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
registration.dom_id,
|
|
55
|
-
instance_id,
|
|
56
|
-
revision
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
refresh(registration, instance_id, revision)
|
|
51
|
+
changed = registrations.select do |registration|
|
|
52
|
+
registration.dependencies.include?(observable_name) &&
|
|
53
|
+
newer_revision?(registration.dom_id, instance_id, revision)
|
|
60
54
|
end
|
|
55
|
+
batched, individual = changed.partition(&:batch)
|
|
56
|
+
streams = individual.map { |registration| refresh(registration, instance_id, revision) }
|
|
57
|
+
batched.group_by(&:batch).each_value do |group|
|
|
58
|
+
group.each { |registration| record_revision(registration, instance_id, revision) }
|
|
59
|
+
streams << TurboStreamRenderer.batch_refresh(group, instance_id, revision)
|
|
60
|
+
end
|
|
61
|
+
streams
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
# @rbs (ActorSnapshot) -> Array[String]
|
|
@@ -90,9 +91,14 @@ module SolidObjects
|
|
|
90
91
|
|
|
91
92
|
attr_reader :registrations, :revisions
|
|
92
93
|
|
|
94
|
+
# @rbs (ComponentRegistration, Integer, Integer) -> void
|
|
95
|
+
def record_revision(registration, instance_id, revision)
|
|
96
|
+
revisions[registration.dom_id] = [ instance_id, revision ]
|
|
97
|
+
end
|
|
98
|
+
|
|
93
99
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
94
100
|
def refresh(registration, instance_id, revision)
|
|
95
|
-
|
|
101
|
+
record_revision(registration, instance_id, revision)
|
|
96
102
|
TurboStreamRenderer.component_refresh(
|
|
97
103
|
registration,
|
|
98
104
|
instance_id,
|
|
@@ -9,6 +9,7 @@ module SolidObjects
|
|
|
9
9
|
MAXIMUM_DEPENDENCIES = 50
|
|
10
10
|
MAXIMUM_LOCALS = 50
|
|
11
11
|
MAXIMUM_COMPONENT_KEY_BYTES = 512
|
|
12
|
+
MAXIMUM_BATCH_BYTES = 64
|
|
12
13
|
REFRESH_METHODS = %w[replace morph].freeze
|
|
13
14
|
RESERVED_LOCALS = %w[actor authorization_context component_key].freeze
|
|
14
15
|
RUBY_KEYWORDS = %w[
|
|
@@ -19,7 +20,7 @@ module SolidObjects
|
|
|
19
20
|
|
|
20
21
|
module_function
|
|
21
22
|
|
|
22
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
23
|
+
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol, ?batch: untyped) -> String
|
|
23
24
|
def generate(
|
|
24
25
|
reference:,
|
|
25
26
|
component_name:,
|
|
@@ -29,13 +30,15 @@ module SolidObjects
|
|
|
29
30
|
refresh_path:,
|
|
30
31
|
component_key: nil,
|
|
31
32
|
locals: {},
|
|
32
|
-
refresh_method: "replace"
|
|
33
|
+
refresh_method: "replace",
|
|
34
|
+
batch: nil
|
|
33
35
|
)
|
|
34
36
|
payload = {
|
|
35
37
|
"actor_type" => reference.actor_type,
|
|
36
38
|
"actor_id" => reference.actor_id,
|
|
37
39
|
"component_name" => component_name,
|
|
38
40
|
"component_key" => Serialization.dump(component_key),
|
|
41
|
+
"batch" => batch&.to_s,
|
|
39
42
|
"dependencies" => dependencies,
|
|
40
43
|
"locals" => Serialization.dump(locals),
|
|
41
44
|
"refresh_method" => refresh_method.to_s,
|
|
@@ -80,6 +83,7 @@ module SolidObjects
|
|
|
80
83
|
|
|
81
84
|
validate_component_name!(payload.fetch("component_name"))
|
|
82
85
|
validate_component_key!(payload["component_key"])
|
|
86
|
+
validate_batch!(payload["batch"])
|
|
83
87
|
validate_dependencies!(payload.fetch("dependencies"))
|
|
84
88
|
validate_locals!(payload.fetch("locals"))
|
|
85
89
|
validate_refresh_method!(payload.fetch("refresh_method"))
|
|
@@ -97,6 +101,7 @@ module SolidObjects
|
|
|
97
101
|
return unless payload.is_a?(Hash)
|
|
98
102
|
|
|
99
103
|
payload["component_key"] = nil unless payload.key?("component_key")
|
|
104
|
+
payload["batch"] = nil unless payload.key?("batch")
|
|
100
105
|
payload["locals"] = {} unless payload.key?("locals")
|
|
101
106
|
payload["refresh_method"] = "replace" unless payload.key?("refresh_method")
|
|
102
107
|
end
|
|
@@ -124,6 +129,18 @@ module SolidObjects
|
|
|
124
129
|
end
|
|
125
130
|
private_class_method :validate_component_key!
|
|
126
131
|
|
|
132
|
+
# @rbs (untyped) -> void
|
|
133
|
+
def validate_batch!(batch)
|
|
134
|
+
return if batch.nil?
|
|
135
|
+
return if batch.is_a?(String) &&
|
|
136
|
+
batch.bytesize.positive? &&
|
|
137
|
+
batch.bytesize <= MAXIMUM_BATCH_BYTES &&
|
|
138
|
+
batch.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
139
|
+
|
|
140
|
+
raise InvalidComponentToken, "invalid actor component batch"
|
|
141
|
+
end
|
|
142
|
+
private_class_method :validate_batch!
|
|
143
|
+
|
|
127
144
|
# @rbs (Array[untyped]) -> void
|
|
128
145
|
def validate_dependencies!(dependencies)
|
|
129
146
|
valid = dependencies.any? &&
|
data/lib/solid_objects/errors.rb
CHANGED
|
@@ -27,7 +27,7 @@ module SolidObjects
|
|
|
27
27
|
result = invoke_actor(message_context)
|
|
28
28
|
ensure_query_did_not_mutate_state!(state_before)
|
|
29
29
|
observable_changes = changed_observables(observables_before, actor.observable_values)
|
|
30
|
-
complete(result, observable_changes)
|
|
30
|
+
complete(result, observable_changes, state_changed: actor.state.to_h != state_before)
|
|
31
31
|
true
|
|
32
32
|
rescue LostActivation
|
|
33
33
|
raise
|
|
@@ -75,8 +75,8 @@ module SolidObjects
|
|
|
75
75
|
end
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
# @rbs (untyped, Hash[String, untyped]) -> void
|
|
79
|
-
def complete(result, observable_changes)
|
|
78
|
+
# @rbs (untyped, Hash[String, untyped], state_changed: bool) -> void
|
|
79
|
+
def complete(result, observable_changes, state_changed:)
|
|
80
80
|
serialized_state = Serialization.dump(
|
|
81
81
|
actor.state.to_h,
|
|
82
82
|
max_bytes: SolidObjects.configuration.max_state_bytes
|
|
@@ -111,7 +111,12 @@ module SolidObjects
|
|
|
111
111
|
enqueued_effects.concat(
|
|
112
112
|
enqueue_actor_messages(locked_message, instance, outbound_message_intents)
|
|
113
113
|
)
|
|
114
|
-
enqueue_broadcasts(
|
|
114
|
+
enqueue_broadcasts(
|
|
115
|
+
locked_message,
|
|
116
|
+
instance,
|
|
117
|
+
observable_changes,
|
|
118
|
+
state_changed:
|
|
119
|
+
)
|
|
115
120
|
claimed_message.destroy!
|
|
116
121
|
end
|
|
117
122
|
|
|
@@ -249,9 +254,14 @@ module SolidObjects
|
|
|
249
254
|
end
|
|
250
255
|
end
|
|
251
256
|
|
|
252
|
-
# @rbs (Message, Instance, Hash[String, untyped]) -> void
|
|
253
|
-
def enqueue_broadcasts(locked_message, instance, observable_changes)
|
|
254
|
-
|
|
257
|
+
# @rbs (Message, Instance, Hash[String, untyped], state_changed: bool) -> void
|
|
258
|
+
def enqueue_broadcasts(locked_message, instance, observable_changes, state_changed:)
|
|
259
|
+
broadcasts = observable_changes
|
|
260
|
+
if broadcasts.empty? && state_changed && payload_broadcasts?
|
|
261
|
+
broadcasts = { PayloadBroadcast::REVISION_OBSERVABLE => {} }
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
broadcasts.each do |observable_name, value|
|
|
255
265
|
Broadcast.create!(
|
|
256
266
|
message: locked_message,
|
|
257
267
|
instance:,
|
|
@@ -266,6 +276,11 @@ module SolidObjects
|
|
|
266
276
|
end
|
|
267
277
|
end
|
|
268
278
|
|
|
279
|
+
# @rbs () -> bool
|
|
280
|
+
def payload_broadcasts?
|
|
281
|
+
actor.class.definition.payload_broadcasts.any?
|
|
282
|
+
end
|
|
283
|
+
|
|
269
284
|
# @rbs (Exception) -> void
|
|
270
285
|
def fail_message(error)
|
|
271
286
|
error_details = serialized_error(error)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class PayloadBroadcast
|
|
5
|
+
MAXIMUM_PAYLOAD_BYTES = 1_048_576
|
|
6
|
+
REVISION_OBSERVABLE = "solid_objects.revision"
|
|
7
|
+
|
|
8
|
+
# @rbs @snapshot: ActorSnapshot
|
|
9
|
+
# @rbs @name: String
|
|
10
|
+
# @rbs @authorization_context: untyped
|
|
11
|
+
|
|
12
|
+
attr_reader :name
|
|
13
|
+
|
|
14
|
+
# @rbs (snapshot: ActorSnapshot, name: String, authorization_context: untyped) -> void
|
|
15
|
+
def initialize(snapshot:, name:, authorization_context:)
|
|
16
|
+
@snapshot = snapshot
|
|
17
|
+
@name = name
|
|
18
|
+
@authorization_context = authorization_context
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# @rbs () -> Hash[String, untyped]
|
|
22
|
+
def call
|
|
23
|
+
handler = snapshot.actor_class.definition.payload_broadcasts[name.to_sym]
|
|
24
|
+
raise UnknownPayloadBroadcast, "unknown payload broadcast #{name.inspect}" unless handler
|
|
25
|
+
|
|
26
|
+
authorize!
|
|
27
|
+
{
|
|
28
|
+
"actor_type" => snapshot.reference.actor_type,
|
|
29
|
+
"actor_id" => snapshot.reference.actor_id,
|
|
30
|
+
"name" => name,
|
|
31
|
+
"instance_id" => snapshot.instance_id,
|
|
32
|
+
"revision" => snapshot.revision,
|
|
33
|
+
"payload" => rendered_payload(handler)
|
|
34
|
+
}
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
attr_reader :snapshot, :authorization_context
|
|
40
|
+
|
|
41
|
+
# @rbs (ActorDefinition::Handler) -> untyped
|
|
42
|
+
def rendered_payload(handler)
|
|
43
|
+
payload = Serialization.dump(
|
|
44
|
+
handler.block.call(snapshot.actor, authorization_context),
|
|
45
|
+
max_bytes: MAXIMUM_PAYLOAD_BYTES
|
|
46
|
+
)
|
|
47
|
+
return payload if payload.is_a?(Hash) || payload.is_a?(Array)
|
|
48
|
+
|
|
49
|
+
raise InvalidPayloadBroadcast,
|
|
50
|
+
"payload broadcast #{name.inspect} must return a JSON object or array"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @rbs () -> void
|
|
54
|
+
def authorize!
|
|
55
|
+
authorized = SolidObjects.configuration.authorize_query.call(
|
|
56
|
+
actor_type: snapshot.reference.actor_type,
|
|
57
|
+
actor_id: snapshot.reference.actor_id,
|
|
58
|
+
message_name: name,
|
|
59
|
+
arguments: {},
|
|
60
|
+
authorization_context:
|
|
61
|
+
)
|
|
62
|
+
return if authorized
|
|
63
|
+
|
|
64
|
+
raise Unauthorized, "actor payload broadcast is not authorized"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -9,13 +9,14 @@ module SolidObjects
|
|
|
9
9
|
|
|
10
10
|
module_function
|
|
11
11
|
|
|
12
|
-
# @rbs (Reference, ?observables: Array[String]?) -> String
|
|
13
|
-
def generate(reference, observables: nil)
|
|
12
|
+
# @rbs (Reference, ?observables: Array[String]?, ?payloads: Array[String]?) -> String
|
|
13
|
+
def generate(reference, observables: nil, payloads: nil)
|
|
14
14
|
identity = {
|
|
15
15
|
"actor_type" => reference.actor_type,
|
|
16
16
|
"actor_id" => reference.actor_id
|
|
17
17
|
}
|
|
18
18
|
identity["observables"] = observables if observables
|
|
19
|
+
identity["payloads"] = payloads if payloads
|
|
19
20
|
validate_identity!(identity)
|
|
20
21
|
verifier.generate(identity, purpose: PURPOSE)
|
|
21
22
|
end
|
|
@@ -36,21 +37,27 @@ module SolidObjects
|
|
|
36
37
|
raise InvalidStreamToken, "invalid actor stream token"
|
|
37
38
|
end
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
validate_names!(identity["observables"], "observables")
|
|
41
|
+
validate_names!(identity["payloads"], "payloads")
|
|
42
|
+
identity
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# @rbs (untyped, String) -> void
|
|
46
|
+
def validate_names!(names, label)
|
|
47
|
+
return unless names
|
|
41
48
|
|
|
42
|
-
valid =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
observable.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
49
|
+
valid = names.is_a?(Array) &&
|
|
50
|
+
names.length <= MAXIMUM_OBSERVABLES &&
|
|
51
|
+
names.uniq.length == names.length &&
|
|
52
|
+
names.all? do |name|
|
|
53
|
+
name.is_a?(String) && name.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
48
54
|
end
|
|
49
|
-
return
|
|
55
|
+
return if valid
|
|
50
56
|
|
|
51
|
-
raise InvalidStreamToken, "invalid actor stream
|
|
57
|
+
raise InvalidStreamToken, "invalid actor stream #{label}"
|
|
52
58
|
end
|
|
53
59
|
private_class_method :validate_identity!
|
|
60
|
+
private_class_method :validate_names!
|
|
54
61
|
|
|
55
62
|
# @rbs () -> ActiveSupport::MessageVerifier
|
|
56
63
|
def verifier
|
|
@@ -16,11 +16,15 @@ module SolidObjects
|
|
|
16
16
|
actor_type: broadcast.instance.actor_type,
|
|
17
17
|
actor_id: broadcast.instance.actor_id
|
|
18
18
|
)
|
|
19
|
-
stream =
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
stream = if broadcast.observable_name == PayloadBroadcast::REVISION_OBSERVABLE
|
|
20
|
+
""
|
|
21
|
+
else
|
|
22
|
+
observable_value(
|
|
23
|
+
reference,
|
|
24
|
+
broadcast.observable_name,
|
|
25
|
+
broadcast.value
|
|
26
|
+
)
|
|
27
|
+
end
|
|
24
28
|
metadata = Base64.urlsafe_encode64(
|
|
25
29
|
JSON.generate(
|
|
26
30
|
"instance_id" => broadcast.instance_id,
|
|
@@ -50,6 +54,31 @@ module SolidObjects
|
|
|
50
54
|
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
|
|
51
55
|
end
|
|
52
56
|
|
|
57
|
+
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
58
|
+
def batch_refresh(registrations, instance_id, revision)
|
|
59
|
+
first = registrations.first
|
|
60
|
+
scope = DomIdentity.scope(first.reference)
|
|
61
|
+
batch = ERB::Util.html_escape(first.batch)
|
|
62
|
+
source = ERB::Util.html_escape(
|
|
63
|
+
first.batch_refresh_url(registrations, instance_id, revision)
|
|
64
|
+
)
|
|
65
|
+
targets = ERB::Util.html_escape(registrations.map(&:dom_id).join(" "))
|
|
66
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-batch-refresh data-batch="#{batch}" data-revision="#{instance_id}:#{revision}" data-targets="#{targets}" data-source="#{source}"></solid-objects-batch-refresh></template></turbo-stream>)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @rbs (Hash[String, untyped]) -> String
|
|
70
|
+
def state_payload(payload)
|
|
71
|
+
reference = Reference.new(
|
|
72
|
+
actor_type: payload.fetch("actor_type"),
|
|
73
|
+
actor_id: payload.fetch("actor_id")
|
|
74
|
+
)
|
|
75
|
+
scope = DomIdentity.scope(reference)
|
|
76
|
+
name = ERB::Util.html_escape(payload.fetch("name"))
|
|
77
|
+
revision = "#{payload.fetch("instance_id")}:#{payload.fetch("revision")}"
|
|
78
|
+
body = ERB::Util.html_escape(JSON.generate(payload.fetch("payload")))
|
|
79
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-payload data-name="#{name}" data-revision="#{revision}">#{body}</solid-objects-payload></template></turbo-stream>)
|
|
80
|
+
end
|
|
81
|
+
|
|
53
82
|
# @rbs (String) -> Hash[String, untyped]?
|
|
54
83
|
def invalidation(stream)
|
|
55
84
|
encoded = stream[INVALIDATION_PATTERN, 1]
|
data/lib/solid_objects.rb
CHANGED
|
@@ -40,6 +40,7 @@ require "solid_objects/component_registration"
|
|
|
40
40
|
require "solid_objects/component_subscriptions"
|
|
41
41
|
require "solid_objects/component_view"
|
|
42
42
|
require "solid_objects/component_renderer"
|
|
43
|
+
require "solid_objects/payload_broadcast"
|
|
43
44
|
require "solid_objects/state_snapshot"
|
|
44
45
|
require "solid_objects/actor_view"
|
|
45
46
|
require "solid_objects/actor_channel"
|
|
@@ -2,11 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class ComponentsController < ActionController::Base
|
|
5
|
+
BATCH_LIMIT: ::Integer
|
|
6
|
+
|
|
5
7
|
# @rbs () -> void
|
|
6
8
|
def show: () -> void
|
|
7
9
|
|
|
10
|
+
# @rbs () -> void
|
|
11
|
+
def batch: () -> void
|
|
12
|
+
|
|
8
13
|
private
|
|
9
14
|
|
|
15
|
+
# @rbs (Hash[Symbol, untyped]) -> void
|
|
16
|
+
def refresh_batch: (Hash[Symbol, untyped]) -> void
|
|
17
|
+
|
|
18
|
+
# @rbs (Array[ComponentRegistration]) -> void
|
|
19
|
+
def validate_single_batch!: (Array[ComponentRegistration]) -> void
|
|
20
|
+
|
|
10
21
|
# @rbs (Hash[Symbol, untyped]) -> void
|
|
11
22
|
def refresh: (Hash[Symbol, untyped]) -> void
|
|
12
23
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
module ActorHelper
|
|
5
|
-
# @rbs (Reference, ?authorization_context: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
-
def solid_object: (Reference, ?authorization_context: untyped) { (ActorView) -> untyped } -> untyped
|
|
5
|
+
# @rbs (Reference, ?authorization_context: untyped, ?payloads: untyped) { (ActorView) -> untyped } -> untyped
|
|
6
|
+
def solid_object: (Reference, ?authorization_context: untyped, ?payloads: untyped) { (ActorView) -> untyped } -> untyped
|
|
7
7
|
end
|
|
8
8
|
end
|
|
@@ -90,6 +90,9 @@ module SolidObjects
|
|
|
90
90
|
# @rbs (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
|
|
91
91
|
def self.observable: (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
|
|
92
92
|
|
|
93
|
+
# @rbs (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
|
|
94
|
+
def self.broadcast_payload: (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
|
|
95
|
+
|
|
93
96
|
# @rbs (?Integer) -> Integer
|
|
94
97
|
def self.state_version: (?Integer) -> Integer
|
|
95
98
|
|
|
@@ -13,9 +13,20 @@ module SolidObjects
|
|
|
13
13
|
|
|
14
14
|
attr_reader scalar_observables: untyped
|
|
15
15
|
|
|
16
|
+
attr_reader payload_names: untyped
|
|
17
|
+
|
|
16
18
|
# @rbs (String) -> void
|
|
17
19
|
def receive_broadcast: (String) -> void
|
|
18
20
|
|
|
21
|
+
# @rbs (ActorSnapshot) -> void
|
|
22
|
+
def transmit_state_payloads: (ActorSnapshot) -> void
|
|
23
|
+
|
|
24
|
+
# @rbs (ActorSnapshot) -> bool
|
|
25
|
+
def newer_payload_revision?: (ActorSnapshot) -> bool
|
|
26
|
+
|
|
27
|
+
# @rbs () -> void
|
|
28
|
+
def validate_payload_names!: () -> void
|
|
29
|
+
|
|
19
30
|
# @rbs (ActorSnapshot) -> void
|
|
20
31
|
def refresh_outdated_components: (ActorSnapshot) -> void
|
|
21
32
|
|
|
@@ -42,6 +42,8 @@ module SolidObjects
|
|
|
42
42
|
|
|
43
43
|
@state_version: Integer
|
|
44
44
|
|
|
45
|
+
@payload_broadcasts: Hash[Symbol, Handler]
|
|
46
|
+
|
|
45
47
|
@observables: Hash[Symbol, Handler]
|
|
46
48
|
|
|
47
49
|
@queries: Hash[Symbol, Handler]
|
|
@@ -58,6 +60,8 @@ module SolidObjects
|
|
|
58
60
|
|
|
59
61
|
attr_reader observables: untyped
|
|
60
62
|
|
|
63
|
+
attr_reader payload_broadcasts: untyped
|
|
64
|
+
|
|
61
65
|
attr_reader state_version: untyped
|
|
62
66
|
|
|
63
67
|
attr_reader state_migrations: untyped
|
|
@@ -81,6 +85,9 @@ module SolidObjects
|
|
|
81
85
|
# @rbs (Symbol | String, Proc?) -> Handler
|
|
82
86
|
def add_observable: (Symbol | String, Proc?) -> Handler
|
|
83
87
|
|
|
88
|
+
# @rbs (Symbol | String, Proc) -> Handler
|
|
89
|
+
def add_payload_broadcast: (Symbol | String, Proc) -> Handler
|
|
90
|
+
|
|
84
91
|
# @rbs (Class) -> ActorDefinition
|
|
85
92
|
def synchronize_instance_messages: (Class) -> ActorDefinition
|
|
86
93
|
|