solid_objects 0.2.1 → 0.4.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 +24 -0
- data/README.md +261 -31
- data/app/controllers/solid_objects/components_controller.rb +78 -0
- data/app/helpers/solid_objects/actor_helper.rb +13 -4
- data/config/routes.rb +1 -0
- data/db/migrate/20260806000000_add_state_revision_to_solid_objects_instances.rb +12 -0
- data/docs/architecture.md +116 -23
- data/docs/authorization.md +53 -3
- data/docs/benchmarks.md +6 -2
- data/docs/correctness.md +45 -1
- data/docs/database-schema.md +18 -5
- data/docs/development.md +40 -0
- data/docs/fit.md +10 -2
- data/docs/migrating-existing-state.md +8 -1
- data/docs/operations.md +74 -36
- data/docs/realtime.md +106 -12
- data/docs/roadmap.md +14 -7
- data/docs/security.md +25 -3
- data/docs/state-migrations.md +2 -0
- data/examples/application/README.md +5 -4
- data/examples/application/app/views/actors/chat_room_actor/_messages.html.erb +1 -1
- data/examples/application/app/views/chat_rooms/show.html.erb +2 -2
- data/examples/application/config/initializers/solid_objects.rb +9 -3
- data/lib/generators/solid_objects/templates/solid_objects.rb +33 -2
- data/lib/solid_objects/activation.rb +33 -4
- data/lib/solid_objects/actor.rb +58 -6
- data/lib/solid_objects/actor_channel.rb +73 -5
- data/lib/solid_objects/actor_definition.rb +2 -0
- data/lib/solid_objects/actor_snapshot.rb +35 -10
- data/lib/solid_objects/actor_view.rb +103 -10
- data/lib/solid_objects/application_write_guard.rb +24 -0
- data/lib/solid_objects/caller_process.rb +28 -0
- data/lib/solid_objects/cli.rb +44 -5
- data/lib/solid_objects/client.rb +98 -5
- data/lib/solid_objects/commit_action_registry.rb +42 -0
- data/lib/solid_objects/component_path_resolver.rb +27 -0
- data/lib/solid_objects/component_registration.rb +117 -0
- data/lib/solid_objects/component_renderer.rb +82 -0
- data/lib/solid_objects/component_subscriptions.rb +109 -0
- data/lib/solid_objects/component_token.rb +139 -0
- data/lib/solid_objects/component_view.rb +67 -0
- data/lib/solid_objects/configuration.rb +39 -1
- data/lib/solid_objects/database_adapter.rb +28 -1
- data/lib/solid_objects/database_adapters/mysql.rb +46 -0
- data/lib/solid_objects/database_adapters/postgresql.rb +29 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +40 -0
- data/lib/solid_objects/errors.rb +156 -0
- data/lib/solid_objects/executor.rb +65 -4
- data/lib/solid_objects/instance_pruner.rb +97 -0
- data/lib/solid_objects/message_pruner.rb +97 -0
- data/lib/solid_objects/message_reference.rb +9 -0
- data/lib/solid_objects/process_pruner.rb +49 -0
- data/lib/solid_objects/reference.rb +5 -0
- data/lib/solid_objects/state_snapshot.rb +41 -0
- data/lib/solid_objects/stream_token.rb +32 -13
- data/lib/solid_objects/sync_deadline.rb +57 -0
- data/lib/solid_objects/sync_diagnostics.rb +133 -0
- data/lib/solid_objects/synchronous_invocation.rb +26 -7
- data/lib/solid_objects/test_helper.rb +78 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +45 -1
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects/worker.rb +1 -1
- data/lib/solid_objects.rb +40 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +22 -0
- data/sig/generated/lib/solid_objects/activation.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor.rbs +29 -0
- data/sig/generated/lib/solid_objects/actor_channel.rbs +20 -0
- data/sig/generated/lib/solid_objects/actor_snapshot.rbs +17 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +31 -2
- data/sig/generated/lib/solid_objects/application_write_guard.rbs +8 -0
- data/sig/generated/lib/solid_objects/caller_process.rbs +11 -0
- data/sig/generated/lib/solid_objects/cli.rbs +11 -2
- data/sig/generated/lib/solid_objects/client.rbs +15 -0
- data/sig/generated/lib/solid_objects/commit_action_registry.rbs +43 -0
- data/sig/generated/lib/solid_objects/component_path_resolver.rbs +13 -0
- data/sig/generated/lib/solid_objects/component_registration.rbs +51 -0
- data/sig/generated/lib/solid_objects/component_renderer.rbs +39 -0
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +40 -0
- data/sig/generated/lib/solid_objects/component_token.rbs +38 -0
- data/sig/generated/lib/solid_objects/component_view.rbs +43 -0
- data/sig/generated/lib/solid_objects/configuration.rbs +35 -7
- data/sig/generated/lib/solid_objects/database_adapter.rbs +9 -0
- data/sig/generated/lib/solid_objects/database_adapters/mysql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/postgresql.rbs +8 -0
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +13 -0
- data/sig/generated/lib/solid_objects/errors.rbs +130 -0
- data/sig/generated/lib/solid_objects/executor.rbs +12 -0
- data/sig/generated/lib/solid_objects/instance_pruner.rbs +36 -0
- data/sig/generated/lib/solid_objects/message_pruner.rbs +42 -0
- data/sig/generated/lib/solid_objects/message_reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/process_pruner.rbs +27 -0
- data/sig/generated/lib/solid_objects/reference.rbs +3 -0
- data/sig/generated/lib/solid_objects/state_snapshot.rbs +30 -0
- data/sig/generated/lib/solid_objects/stream_token.rbs +9 -4
- data/sig/generated/lib/solid_objects/sync_deadline.rbs +31 -0
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +34 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +3 -0
- data/sig/generated/lib/solid_objects/test_helper.rbs +25 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +10 -0
- data/sig/generated/lib/solid_objects.rbs +12 -0
- metadata +34 -1
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Actor
|
|
5
5
|
EffectIntent = Data.define(:name, :arguments, :success_message_name, :failure_message_name)
|
|
6
|
+
CommitActionIntent = Data.define(:name, :arguments)
|
|
6
7
|
ReminderIntent = Data.define(:name, :at, :arguments, :interval_seconds, :missed_policy)
|
|
7
8
|
OutboundMessageIntent = Data.define(:actor_type, :actor_id, :message_name, :arguments, :available_at, :idempotency_key)
|
|
8
9
|
|
|
@@ -131,6 +132,7 @@ module SolidObjects
|
|
|
131
132
|
# @rbs @actor_id: String
|
|
132
133
|
# @rbs @state: State
|
|
133
134
|
# @rbs @effect_intents: Array[EffectIntent]
|
|
135
|
+
# @rbs @commit_action_intents: Array[CommitActionIntent]
|
|
134
136
|
# @rbs @reminder_intents: Array[ReminderIntent]
|
|
135
137
|
# @rbs @outbound_message_intents: Array[OutboundMessageIntent]
|
|
136
138
|
|
|
@@ -141,6 +143,7 @@ module SolidObjects
|
|
|
141
143
|
@actor_id = actor_id
|
|
142
144
|
@state = state
|
|
143
145
|
@effect_intents = []
|
|
146
|
+
@commit_action_intents = []
|
|
144
147
|
@reminder_intents = []
|
|
145
148
|
@outbound_message_intents = []
|
|
146
149
|
end
|
|
@@ -175,6 +178,17 @@ module SolidObjects
|
|
|
175
178
|
nil
|
|
176
179
|
end
|
|
177
180
|
|
|
181
|
+
# @rbs (Symbol | String, **untyped) -> nil
|
|
182
|
+
def commit_action(name, **arguments)
|
|
183
|
+
CommitActionIntent.new(
|
|
184
|
+
name: name.to_s,
|
|
185
|
+
arguments: Serialization.dump(arguments)
|
|
186
|
+
).tap do |intent|
|
|
187
|
+
commit_action_intents << intent
|
|
188
|
+
end
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
178
192
|
# @rbs (Symbol | String, at: Time, ?every: Numeric?, ?missed: Symbol | String, arguments: Hash[Symbol | String, untyped]) -> nil
|
|
179
193
|
def schedule(name, at:, every: nil, missed: :latest, arguments: {})
|
|
180
194
|
interval_seconds = every&.to_f
|
|
@@ -210,24 +224,43 @@ module SolidObjects
|
|
|
210
224
|
self.class.definition.queries[message_name.to_sym]
|
|
211
225
|
raise UnknownMessage, "unknown message #{message_name.inspect} for #{self.class.actor_type}" unless handler
|
|
212
226
|
|
|
213
|
-
|
|
227
|
+
guard_application_writes(message_name.to_s) do
|
|
228
|
+
instance_exec(**keyword_arguments(arguments), &handler.block)
|
|
229
|
+
end
|
|
214
230
|
end
|
|
215
231
|
|
|
216
232
|
# @rbs () -> Hash[String, untyped]
|
|
217
233
|
def observable_values
|
|
218
|
-
|
|
219
|
-
|
|
234
|
+
guard_application_writes("observables") do
|
|
235
|
+
self.class.definition.observables.each_with_object({}) do |(name, handler), values|
|
|
236
|
+
values[name.to_s] = Serialization.dump(instance_exec(&handler.block))
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# @rbs (Symbol | String) -> untyped
|
|
242
|
+
def observable_value(name)
|
|
243
|
+
observable_name = name.to_sym
|
|
244
|
+
handler = self.class.definition.observables[observable_name]
|
|
245
|
+
raise UnknownMessage, "unknown observable #{name.inspect}" unless handler
|
|
246
|
+
|
|
247
|
+
guard_application_writes("observable.#{observable_name}") do
|
|
248
|
+
Serialization.dump(instance_exec(&handler.block))
|
|
220
249
|
end
|
|
221
250
|
end
|
|
222
251
|
|
|
223
252
|
# @rbs () -> void
|
|
224
253
|
def activate
|
|
225
|
-
|
|
254
|
+
guard_application_writes("on_activate") do
|
|
255
|
+
self.class.definition.activation_hooks.each { |hook| instance_exec(&hook) }
|
|
256
|
+
end
|
|
226
257
|
end
|
|
227
258
|
|
|
228
259
|
# @rbs () -> void
|
|
229
260
|
def deactivate
|
|
230
|
-
|
|
261
|
+
guard_application_writes("on_deactivate") do
|
|
262
|
+
self.class.definition.deactivation_hooks.each { |hook| instance_exec(&hook) }
|
|
263
|
+
end
|
|
231
264
|
end
|
|
232
265
|
|
|
233
266
|
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent
|
|
@@ -247,6 +280,11 @@ module SolidObjects
|
|
|
247
280
|
effect_intents.shift(effect_intents.length)
|
|
248
281
|
end
|
|
249
282
|
|
|
283
|
+
# @rbs () -> Array[CommitActionIntent]
|
|
284
|
+
def drain_commit_action_intents
|
|
285
|
+
commit_action_intents.shift(commit_action_intents.length)
|
|
286
|
+
end
|
|
287
|
+
|
|
250
288
|
# @rbs () -> Array[ReminderIntent]
|
|
251
289
|
def drain_reminder_intents
|
|
252
290
|
reminder_intents.shift(reminder_intents.length)
|
|
@@ -260,13 +298,27 @@ module SolidObjects
|
|
|
260
298
|
# @rbs () -> void
|
|
261
299
|
def discard_intents
|
|
262
300
|
effect_intents.clear
|
|
301
|
+
commit_action_intents.clear
|
|
263
302
|
reminder_intents.clear
|
|
264
303
|
outbound_message_intents.clear
|
|
265
304
|
end
|
|
266
305
|
|
|
267
306
|
private
|
|
268
307
|
|
|
269
|
-
attr_reader :effect_intents,
|
|
308
|
+
attr_reader :effect_intents,
|
|
309
|
+
:commit_action_intents,
|
|
310
|
+
:reminder_intents,
|
|
311
|
+
:outbound_message_intents
|
|
312
|
+
|
|
313
|
+
# @rbs (String) { () -> untyped } -> untyped
|
|
314
|
+
def guard_application_writes(operation, &block)
|
|
315
|
+
ApplicationWriteGuard.call(
|
|
316
|
+
actor_type: self.class.actor_type,
|
|
317
|
+
actor_id:,
|
|
318
|
+
operation:,
|
|
319
|
+
&block
|
|
320
|
+
)
|
|
321
|
+
end
|
|
270
322
|
|
|
271
323
|
# @rbs (Hash[String, untyped]) -> Hash[Symbol, untyped]
|
|
272
324
|
def keyword_arguments(arguments)
|
|
@@ -17,13 +17,81 @@ module SolidObjects
|
|
|
17
17
|
)
|
|
18
18
|
return reject unless authorized
|
|
19
19
|
|
|
20
|
-
reference = Reference.new(actor_type:, actor_id:)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
@reference = Reference.new(actor_type:, actor_id:)
|
|
21
|
+
@scalar_observables = identity["observables"]
|
|
22
|
+
validate_scalar_observables!
|
|
23
|
+
@component_subscriptions = ComponentSubscriptions.parse(
|
|
24
|
+
params["components"],
|
|
25
|
+
reference:
|
|
26
|
+
)
|
|
27
|
+
stream_from StreamName.for(reference) do |stream|
|
|
28
|
+
receive_broadcast(stream)
|
|
29
|
+
end
|
|
30
|
+
snapshot = ActorSnapshot.new(reference)
|
|
31
|
+
scalar_observable_names(snapshot).each do |name|
|
|
32
|
+
transmit TurboStreamRenderer.observable_value(
|
|
33
|
+
reference,
|
|
34
|
+
name,
|
|
35
|
+
snapshot.observable_value(name)
|
|
36
|
+
)
|
|
24
37
|
end
|
|
25
|
-
|
|
38
|
+
refresh_outdated_components(snapshot)
|
|
39
|
+
rescue KeyError,
|
|
40
|
+
JSON::ParserError,
|
|
41
|
+
InvalidStreamToken,
|
|
42
|
+
InvalidComponentToken,
|
|
43
|
+
UnknownActorType
|
|
26
44
|
reject
|
|
27
45
|
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
attr_reader :reference, :component_subscriptions, :scalar_observables
|
|
50
|
+
|
|
51
|
+
# @rbs (String) -> void
|
|
52
|
+
def receive_broadcast(stream)
|
|
53
|
+
invalidation = TurboStreamRenderer.invalidation(stream)
|
|
54
|
+
if !invalidation ||
|
|
55
|
+
scalar_observables.nil? ||
|
|
56
|
+
scalar_observables.include?(invalidation.fetch("observable_name"))
|
|
57
|
+
transmit stream
|
|
58
|
+
end
|
|
59
|
+
return unless invalidation
|
|
60
|
+
|
|
61
|
+
component_subscriptions
|
|
62
|
+
.refreshes_for(invalidation)
|
|
63
|
+
.each { |refresh| transmit refresh }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# @rbs (ActorSnapshot) -> void
|
|
67
|
+
def refresh_outdated_components(snapshot)
|
|
68
|
+
component_subscriptions
|
|
69
|
+
.reconnect_refreshes(snapshot)
|
|
70
|
+
.each { |refresh| transmit refresh }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @rbs () -> void
|
|
74
|
+
def validate_scalar_observables!
|
|
75
|
+
return unless scalar_observables
|
|
76
|
+
|
|
77
|
+
observables = SolidObjects
|
|
78
|
+
.registry
|
|
79
|
+
.fetch(reference.actor_type)
|
|
80
|
+
.definition
|
|
81
|
+
.observables
|
|
82
|
+
unknown = scalar_observables.find do |name|
|
|
83
|
+
!observables.key?(name.to_sym)
|
|
84
|
+
end
|
|
85
|
+
return unless unknown
|
|
86
|
+
|
|
87
|
+
raise InvalidStreamToken, "unknown scalar observable #{unknown.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# @rbs (ActorSnapshot) -> Array[String]
|
|
91
|
+
def scalar_observable_names(snapshot)
|
|
92
|
+
return scalar_observables if scalar_observables
|
|
93
|
+
|
|
94
|
+
snapshot.actor_class.definition.observables.keys.map(&:to_s)
|
|
95
|
+
end
|
|
28
96
|
end
|
|
29
97
|
end
|
|
@@ -5,34 +5,59 @@ module SolidObjects
|
|
|
5
5
|
# @rbs @reference: Reference
|
|
6
6
|
# @rbs @actor_class: Class
|
|
7
7
|
# @rbs @actor: Actor
|
|
8
|
+
# @rbs @instance_id: Integer
|
|
9
|
+
# @rbs @revision: Integer
|
|
10
|
+
# @rbs @observable_values: Hash[String, untyped]?
|
|
11
|
+
# @rbs @observable_value_cache: Hash[String, untyped]
|
|
8
12
|
|
|
9
|
-
attr_reader :reference, :actor_class, :actor
|
|
13
|
+
attr_reader :reference, :actor_class, :actor, :instance_id, :revision
|
|
10
14
|
|
|
11
15
|
# @rbs (Reference) -> void
|
|
12
16
|
def initialize(reference)
|
|
13
17
|
@reference = reference
|
|
14
18
|
@actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
19
|
+
@instance = Instance.find_by(
|
|
20
|
+
actor_type: reference.actor_type,
|
|
21
|
+
actor_id: reference.actor_id
|
|
22
|
+
)
|
|
23
|
+
@instance_id = @instance&.id || 0
|
|
24
|
+
@revision = @instance&.state_revision || 0
|
|
15
25
|
@actor = build_actor
|
|
26
|
+
@observable_values = nil
|
|
27
|
+
@observable_value_cache = {}
|
|
16
28
|
end
|
|
17
29
|
|
|
18
30
|
# @rbs () -> Hash[String, untyped]
|
|
19
31
|
def observable_values
|
|
20
|
-
actor.observable_values
|
|
32
|
+
@observable_values ||= Serialization.readonly_copy(actor.observable_values)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (Symbol | String) -> untyped
|
|
36
|
+
def observable_value(name)
|
|
37
|
+
observable_name = name.to_s
|
|
38
|
+
return @observable_value_cache.fetch(observable_name) if @observable_value_cache.key?(observable_name)
|
|
39
|
+
|
|
40
|
+
@observable_value_cache[observable_name] =
|
|
41
|
+
Serialization.readonly_copy(actor.observable_value(observable_name))
|
|
21
42
|
end
|
|
22
43
|
|
|
23
44
|
private
|
|
24
45
|
|
|
46
|
+
attr_reader :instance
|
|
47
|
+
|
|
25
48
|
# @rbs () -> Actor
|
|
26
49
|
def build_actor
|
|
27
|
-
instance = Instance.find_by(
|
|
28
|
-
actor_type: reference.actor_type,
|
|
29
|
-
actor_id: reference.actor_id
|
|
30
|
-
)
|
|
31
50
|
state_version = instance&.state_version || actor_class.state_version
|
|
32
|
-
state_data =
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
51
|
+
state_data = ApplicationWriteGuard.call(
|
|
52
|
+
actor_type: reference.actor_type,
|
|
53
|
+
actor_id: reference.actor_id,
|
|
54
|
+
operation: "state_migration"
|
|
55
|
+
) do
|
|
56
|
+
actor_class.definition.migrate_state(
|
|
57
|
+
state_version,
|
|
58
|
+
instance&.state || {}
|
|
59
|
+
)
|
|
60
|
+
end
|
|
36
61
|
actor_class.new(
|
|
37
62
|
actor_id: reference.actor_id,
|
|
38
63
|
state: State.new(actor_class.definition.state_definition, state_data)
|
|
@@ -6,6 +6,8 @@ module SolidObjects
|
|
|
6
6
|
# @rbs @view_context: untyped
|
|
7
7
|
# @rbs @authorization_context: untyped
|
|
8
8
|
# @rbs @snapshot: ActorSnapshot
|
|
9
|
+
# @rbs @component_registrations: Array[ComponentRegistration]
|
|
10
|
+
# @rbs @observable_names: Array[String]
|
|
9
11
|
|
|
10
12
|
attr_reader :reference
|
|
11
13
|
|
|
@@ -15,6 +17,8 @@ module SolidObjects
|
|
|
15
17
|
@view_context = view_context
|
|
16
18
|
@authorization_context = authorization_context
|
|
17
19
|
@snapshot = ActorSnapshot.new(reference)
|
|
20
|
+
@component_registrations = []
|
|
21
|
+
@observable_names = []
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# @rbs (Symbol | String) -> untyped
|
|
@@ -24,7 +28,8 @@ module SolidObjects
|
|
|
24
28
|
raise UnknownMessage, "unknown observable #{name.inspect}" unless handler
|
|
25
29
|
|
|
26
30
|
authorize_read!(observable_name)
|
|
27
|
-
value = snapshot.
|
|
31
|
+
value = snapshot.observable_value(observable_name)
|
|
32
|
+
observable_names << observable_name.to_s unless observable_names.include?(observable_name.to_s)
|
|
28
33
|
view_context.content_tag(
|
|
29
34
|
:span,
|
|
30
35
|
display_value(value),
|
|
@@ -32,21 +37,54 @@ module SolidObjects
|
|
|
32
37
|
)
|
|
33
38
|
end
|
|
34
39
|
|
|
35
|
-
# @rbs (Symbol | String, ?partial: String?) -> untyped
|
|
36
|
-
def component(name, partial: nil)
|
|
40
|
+
# @rbs (Symbol | String, ?observes: Symbol | String | Array[Symbol | String]?, ?partial: String?) -> untyped
|
|
41
|
+
def component(name, observes: nil, partial: nil)
|
|
37
42
|
component_name = normalized_component_name(name)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
return static_component(component_name, partial:) unless observes
|
|
44
|
+
if partial
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
"reactive components resolve partials by actor and component name"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
dependencies = normalized_dependencies(observes)
|
|
50
|
+
validate_dependencies!(dependencies)
|
|
51
|
+
ensure_unique_component!(component_name)
|
|
52
|
+
refresh_path = component_path_resolver.call(view_context:)
|
|
53
|
+
registration = ComponentRegistration.issue(
|
|
54
|
+
reference:,
|
|
55
|
+
component_name:,
|
|
56
|
+
dependencies:,
|
|
57
|
+
snapshot:,
|
|
58
|
+
refresh_path:
|
|
42
59
|
)
|
|
60
|
+
rendered = ComponentRenderer.new(
|
|
61
|
+
snapshot:,
|
|
62
|
+
component_name:,
|
|
63
|
+
dependencies:,
|
|
64
|
+
view_context:,
|
|
65
|
+
authorization_context:
|
|
66
|
+
).call
|
|
67
|
+
component_registrations << registration
|
|
43
68
|
view_context.content_tag(
|
|
44
|
-
:
|
|
69
|
+
:"turbo-frame",
|
|
45
70
|
rendered,
|
|
46
|
-
id: DomIdentity.component(reference, component_name)
|
|
71
|
+
id: DomIdentity.component(reference, component_name),
|
|
72
|
+
data: {
|
|
73
|
+
solid_objects_revision: "#{snapshot.instance_id}:#{snapshot.revision}"
|
|
74
|
+
}
|
|
47
75
|
)
|
|
48
76
|
end
|
|
49
77
|
|
|
78
|
+
# @rbs () -> Array[String]
|
|
79
|
+
def component_tokens
|
|
80
|
+
component_registrations.map(&:token)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# @rbs () -> Array[String]
|
|
84
|
+
def scalar_observable_names
|
|
85
|
+
observable_names.dup
|
|
86
|
+
end
|
|
87
|
+
|
|
50
88
|
# @rbs () -> State
|
|
51
89
|
def state
|
|
52
90
|
snapshot.actor.state
|
|
@@ -66,7 +104,25 @@ module SolidObjects
|
|
|
66
104
|
|
|
67
105
|
private
|
|
68
106
|
|
|
69
|
-
attr_reader :view_context,
|
|
107
|
+
attr_reader :view_context,
|
|
108
|
+
:authorization_context,
|
|
109
|
+
:snapshot,
|
|
110
|
+
:component_registrations,
|
|
111
|
+
:observable_names
|
|
112
|
+
|
|
113
|
+
# @rbs (String, partial: String?) -> untyped
|
|
114
|
+
def static_component(component_name, partial:)
|
|
115
|
+
authorize_read!(component_name)
|
|
116
|
+
rendered = view_context.render(
|
|
117
|
+
partial: partial || default_partial(component_name),
|
|
118
|
+
locals: { actor: self }
|
|
119
|
+
)
|
|
120
|
+
view_context.content_tag(
|
|
121
|
+
:div,
|
|
122
|
+
rendered,
|
|
123
|
+
id: DomIdentity.component(reference, component_name)
|
|
124
|
+
)
|
|
125
|
+
end
|
|
70
126
|
|
|
71
127
|
# @rbs (Symbol | String) -> void
|
|
72
128
|
def authorize_read!(name)
|
|
@@ -106,6 +162,43 @@ module SolidObjects
|
|
|
106
162
|
component_name
|
|
107
163
|
end
|
|
108
164
|
|
|
165
|
+
# @rbs (Symbol | String | Array[Symbol | String]) -> Array[String]
|
|
166
|
+
def normalized_dependencies(observes)
|
|
167
|
+
dependencies = Array(observes).map(&:to_s).uniq
|
|
168
|
+
if dependencies.empty?
|
|
169
|
+
raise ArgumentError, "reactive components require at least one observable"
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
dependencies
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# @rbs (Array[String]) -> void
|
|
176
|
+
def validate_dependencies!(dependencies)
|
|
177
|
+
unknown = dependencies.find do |dependency|
|
|
178
|
+
!snapshot.actor_class.definition.observables.key?(dependency.to_sym)
|
|
179
|
+
end
|
|
180
|
+
return unless unknown
|
|
181
|
+
|
|
182
|
+
raise UnknownComponentDependency,
|
|
183
|
+
"unknown observable dependency #{unknown.inspect} for #{reference.actor_type}"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# @rbs (String) -> void
|
|
187
|
+
def ensure_unique_component!(component_name)
|
|
188
|
+
return unless component_registrations.any? do |registration|
|
|
189
|
+
registration.component_name == component_name
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
raise ArgumentError,
|
|
193
|
+
"component #{component_name.inspect} is already rendered in this solid_object scope"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# @rbs () -> Proc | ComponentPathResolver
|
|
197
|
+
def component_path_resolver
|
|
198
|
+
SolidObjects.configuration.component_path_resolver ||
|
|
199
|
+
ComponentPathResolver.new
|
|
200
|
+
end
|
|
201
|
+
|
|
109
202
|
# @rbs (String) -> String
|
|
110
203
|
def default_partial(component_name)
|
|
111
204
|
actor_name = snapshot.actor_class.name
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# rbs_inline: enabled
|
|
2
|
+
|
|
3
|
+
module SolidObjects
|
|
4
|
+
class ApplicationWriteGuard
|
|
5
|
+
class << self
|
|
6
|
+
# @rbs (actor_type: String, actor_id: String, operation: String) { () -> untyped } -> untyped
|
|
7
|
+
def call(actor_type:, actor_id:, operation:)
|
|
8
|
+
ActiveRecord::Base.while_preventing_writes { yield }
|
|
9
|
+
rescue ActiveRecord::ReadOnlyError
|
|
10
|
+
SolidObjects.instrument(
|
|
11
|
+
:"actor_code.write_forbidden",
|
|
12
|
+
actor_type:,
|
|
13
|
+
actor_id:,
|
|
14
|
+
operation:
|
|
15
|
+
)
|
|
16
|
+
raise ApplicationWriteForbidden.new(
|
|
17
|
+
actor_type:,
|
|
18
|
+
actor_id:,
|
|
19
|
+
message_name: operation
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -5,12 +5,14 @@ module SolidObjects
|
|
|
5
5
|
# @rbs @mutex: Thread::Mutex
|
|
6
6
|
# @rbs @process_id: Integer?
|
|
7
7
|
# @rbs @registry: ProcessRegistry?
|
|
8
|
+
# @rbs @shutdown_hook_installed: bool
|
|
8
9
|
|
|
9
10
|
# @rbs () -> void
|
|
10
11
|
def initialize
|
|
11
12
|
@mutex = Thread::Mutex.new
|
|
12
13
|
@process_id = nil
|
|
13
14
|
@registry = nil
|
|
15
|
+
@shutdown_hook_installed = false
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# @rbs () -> ProcessRegistry
|
|
@@ -18,11 +20,22 @@ module SolidObjects
|
|
|
18
20
|
mutex.synchronize do
|
|
19
21
|
reset_after_fork
|
|
20
22
|
register unless reusable_registry?
|
|
23
|
+
install_shutdown_hook
|
|
21
24
|
registry.heartbeat
|
|
22
25
|
registry
|
|
23
26
|
end
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
# @rbs () -> bool
|
|
30
|
+
def stop
|
|
31
|
+
mutex.synchronize do
|
|
32
|
+
return false unless @process_id == ::Process.pid
|
|
33
|
+
return false unless registry&.process_record
|
|
34
|
+
|
|
35
|
+
registry.stop.tap { @registry = nil }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
26
39
|
private
|
|
27
40
|
|
|
28
41
|
attr_reader :mutex, :registry
|
|
@@ -53,5 +66,20 @@ module SolidObjects
|
|
|
53
66
|
)
|
|
54
67
|
registry
|
|
55
68
|
end
|
|
69
|
+
|
|
70
|
+
# @rbs () -> void
|
|
71
|
+
def install_shutdown_hook
|
|
72
|
+
return if @shutdown_hook_installed
|
|
73
|
+
|
|
74
|
+
@shutdown_hook_installed = true
|
|
75
|
+
at_exit { stop_after_exit }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# @rbs () -> bool
|
|
79
|
+
def stop_after_exit
|
|
80
|
+
stop
|
|
81
|
+
rescue
|
|
82
|
+
false
|
|
83
|
+
end
|
|
56
84
|
end
|
|
57
85
|
end
|
data/lib/solid_objects/cli.rb
CHANGED
|
@@ -34,7 +34,7 @@ module SolidObjects
|
|
|
34
34
|
# @rbs () -> void
|
|
35
35
|
def status
|
|
36
36
|
boot_application
|
|
37
|
-
authorize_administration!(:status)
|
|
37
|
+
authorize_administration!(:status, resource: "processes")
|
|
38
38
|
rows = SolidObjects::Process.order(:kind, :started_at).map do |process_record|
|
|
39
39
|
{
|
|
40
40
|
id: process_record.id,
|
|
@@ -54,10 +54,49 @@ module SolidObjects
|
|
|
54
54
|
# @rbs () -> void
|
|
55
55
|
def cleanup
|
|
56
56
|
boot_application
|
|
57
|
-
authorize_administration!(:cleanup)
|
|
57
|
+
authorize_administration!(:cleanup, resource: "processes")
|
|
58
58
|
puts JSON.generate(cleaned_processes: ProcessRegistry.cleanup_dead)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
desc "prune_messages", "Preview or delete expired terminal message history"
|
|
62
|
+
option :environment, type: :string, aliases: "-e"
|
|
63
|
+
option :execute, type: :boolean, default: false
|
|
64
|
+
|
|
65
|
+
# @rbs () -> void
|
|
66
|
+
def prune_messages
|
|
67
|
+
boot_application
|
|
68
|
+
authorize_administration!(:prune, resource: "messages")
|
|
69
|
+
pruner = MessagePruner.new
|
|
70
|
+
count = options[:execute] ? pruner.prune : pruner.preview
|
|
71
|
+
puts JSON.generate(mode: options[:execute] ? "execute" : "preview", messages: count)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
desc "prune_instances", "Preview or delete expired idle actor instances"
|
|
75
|
+
option :environment, type: :string, aliases: "-e"
|
|
76
|
+
option :execute, type: :boolean, default: false
|
|
77
|
+
|
|
78
|
+
# @rbs () -> void
|
|
79
|
+
def prune_instances
|
|
80
|
+
boot_application
|
|
81
|
+
authorize_administration!(:prune, resource: "instances")
|
|
82
|
+
pruner = InstancePruner.new
|
|
83
|
+
count = options[:execute] ? pruner.prune : pruner.preview
|
|
84
|
+
puts JSON.generate(mode: options[:execute] ? "execute" : "preview", instances: count)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
desc "prune_processes", "Preview or delete expired stopped process records"
|
|
88
|
+
option :environment, type: :string, aliases: "-e"
|
|
89
|
+
option :execute, type: :boolean, default: false
|
|
90
|
+
|
|
91
|
+
# @rbs () -> void
|
|
92
|
+
def prune_processes
|
|
93
|
+
boot_application
|
|
94
|
+
authorize_administration!(:prune, resource: "processes")
|
|
95
|
+
pruner = ProcessPruner.new
|
|
96
|
+
count = options[:execute] ? pruner.prune : pruner.preview
|
|
97
|
+
puts JSON.generate(mode: options[:execute] ? "execute" : "preview", processes: count)
|
|
98
|
+
end
|
|
99
|
+
|
|
61
100
|
desc "dead_letters", "Print dead actor messages"
|
|
62
101
|
option :environment, type: :string, aliases: "-e"
|
|
63
102
|
|
|
@@ -102,11 +141,11 @@ module SolidObjects
|
|
|
102
141
|
value ? Integer(value) : default
|
|
103
142
|
end
|
|
104
143
|
|
|
105
|
-
# @rbs (Symbol) -> void
|
|
106
|
-
def authorize_administration!(action)
|
|
144
|
+
# @rbs (Symbol, resource: String) -> void
|
|
145
|
+
def authorize_administration!(action, resource:)
|
|
107
146
|
authorized = SolidObjects.configuration.authorize_administration.call(
|
|
108
147
|
action:,
|
|
109
|
-
resource
|
|
148
|
+
resource:,
|
|
110
149
|
resource_id: nil,
|
|
111
150
|
authorization_context: { source: "cli" }
|
|
112
151
|
)
|