solid_objects 0.10.3 → 0.12.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 +50 -0
- data/README.md +72 -16
- data/app/controllers/solid_objects/components_controller.rb +4 -4
- data/app/models/solid_objects/message.rb +1 -1
- data/app/views/solid_objects/dead_letters/index.html.erb +1 -1
- data/app/views/solid_objects/instances/show.html.erb +1 -1
- data/benchmark/support.rb +10 -10
- data/db/migrate/20260813000000_rename_message_dispatch_columns.rb +53 -0
- data/docs/architecture.md +11 -9
- data/docs/authorization.md +2 -2
- data/docs/database-schema.md +4 -4
- data/docs/development.md +1 -1
- data/docs/migrating-existing-state.md +1 -3
- data/docs/roadmap.md +1 -1
- data/lib/solid_objects/actor.rb +43 -32
- data/lib/solid_objects/actor_channel.rb +3 -3
- data/lib/solid_objects/actor_definition.rb +9 -6
- data/lib/solid_objects/actor_view.rb +1 -1
- data/lib/solid_objects/application_write_guard.rb +1 -1
- data/lib/solid_objects/client.rb +52 -52
- data/lib/solid_objects/component_registration.rb +2 -2
- data/lib/solid_objects/component_renderer.rb +1 -1
- data/lib/solid_objects/component_subscriptions.rb +21 -21
- data/lib/solid_objects/configuration.rb +42 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +7 -1
- data/lib/solid_objects/dead_letter_manager.rb +4 -4
- data/lib/solid_objects/doctor.rb +8 -8
- data/lib/solid_objects/effect_executor.rb +22 -22
- data/lib/solid_objects/errors.rb +27 -27
- data/lib/solid_objects/executor.rb +40 -38
- data/lib/solid_objects/lease.rb +3 -3
- data/lib/solid_objects/mailbox.rb +34 -27
- data/lib/solid_objects/operation_dispatcher.rb +51 -0
- data/lib/solid_objects/payload_broadcast.rb +1 -1
- data/lib/solid_objects/reference.rb +60 -29
- data/lib/solid_objects/reminder_scheduler.rb +6 -6
- data/lib/solid_objects/supervisor.rb +50 -9
- data/lib/solid_objects/sync_diagnostics.rb +6 -6
- data/lib/solid_objects/turbo_stream_renderer.rb +13 -13
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +1 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +2 -2
- data/sig/generated/lib/solid_objects/actor.rbs +17 -17
- data/sig/generated/lib/solid_objects/actor_definition.rbs +4 -4
- data/sig/generated/lib/solid_objects/client.rbs +8 -8
- data/sig/generated/lib/solid_objects/component_registration.rbs +2 -2
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +8 -8
- data/sig/generated/lib/solid_objects/configuration.rbs +32 -4
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
- data/sig/generated/lib/solid_objects/effect_executor.rbs +2 -2
- data/sig/generated/lib/solid_objects/errors.rbs +18 -18
- data/sig/generated/lib/solid_objects/executor.rbs +10 -10
- data/sig/generated/lib/solid_objects/lease.rbs +2 -2
- data/sig/generated/lib/solid_objects/mailbox.rbs +8 -8
- data/sig/generated/lib/solid_objects/operation_dispatcher.rbs +35 -0
- data/sig/generated/lib/solid_objects/reference.rbs +7 -4
- data/sig/generated/lib/solid_objects/supervisor.rbs +19 -2
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +2 -2
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +8 -8
- metadata +5 -2
data/lib/solid_objects/actor.rb
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class Actor
|
|
5
|
-
EffectIntent = Data.define(:name, :arguments, :
|
|
5
|
+
EffectIntent = Data.define(:name, :arguments, :success_operation, :failure_operation)
|
|
6
6
|
CommitActionIntent = Data.define(:name, :arguments)
|
|
7
7
|
ReminderIntent = Data.define(:name, :at, :arguments, :interval_seconds, :missed_policy)
|
|
8
|
-
OutboundMessageIntent = Data.define(:actor_type, :actor_id, :
|
|
8
|
+
OutboundMessageIntent = Data.define(:actor_type, :actor_id, :operation, :arguments, :available_at, :idempotency_key)
|
|
9
9
|
|
|
10
10
|
class << self
|
|
11
11
|
# @rbs (Class) -> void
|
|
@@ -72,7 +72,7 @@ module SolidObjects
|
|
|
72
72
|
|
|
73
73
|
# @rbs (from: Integer, to: Integer) { (Hash[String, untyped]) -> Hash[String, untyped] } -> ActorDefinition::StateMigration
|
|
74
74
|
def migrate_state(from:, to:, &block)
|
|
75
|
-
definition.add_state_migration(from
|
|
75
|
+
definition.add_state_migration(from:, to:, block:)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# @rbs () { () -> untyped } -> Proc
|
|
@@ -177,8 +177,8 @@ module SolidObjects
|
|
|
177
177
|
EffectIntent.new(
|
|
178
178
|
name: name.to_s,
|
|
179
179
|
arguments: Serialization.dump(arguments),
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
success_operation: on_success&.to_s,
|
|
181
|
+
failure_operation: on_failure&.to_s
|
|
182
182
|
).tap do |intent|
|
|
183
183
|
effect_intents << intent
|
|
184
184
|
end
|
|
@@ -196,8 +196,8 @@ module SolidObjects
|
|
|
196
196
|
nil
|
|
197
197
|
end
|
|
198
198
|
|
|
199
|
-
# @rbs (
|
|
200
|
-
def schedule(
|
|
199
|
+
# @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String) -> OperationDispatcher
|
|
200
|
+
def schedule(at:, every: nil, missed: :latest)
|
|
201
201
|
interval_seconds = every&.to_f
|
|
202
202
|
if interval_seconds && !interval_seconds.positive?
|
|
203
203
|
raise ArgumentError, "reminder interval must be positive"
|
|
@@ -207,31 +207,42 @@ module SolidObjects
|
|
|
207
207
|
raise ArgumentError, "missed reminder policy must be all or latest"
|
|
208
208
|
end
|
|
209
209
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
210
|
+
OperationDispatcher.new(
|
|
211
|
+
actor_type: self.class.actor_type,
|
|
212
|
+
handlers: self.class.definition.messages
|
|
213
|
+
) do |operation, arguments|
|
|
214
|
+
ReminderIntent.new(
|
|
215
|
+
name: operation.to_s,
|
|
216
|
+
at:,
|
|
217
|
+
arguments: Serialization.dump(arguments),
|
|
218
|
+
interval_seconds:,
|
|
219
|
+
missed_policy:
|
|
220
|
+
).tap do |intent|
|
|
221
|
+
reminder_intents << intent
|
|
222
|
+
end
|
|
223
|
+
nil
|
|
218
224
|
end
|
|
219
|
-
nil
|
|
220
225
|
end
|
|
221
226
|
|
|
222
|
-
# @rbs (Reference,
|
|
223
|
-
def send_to(reference,
|
|
224
|
-
|
|
225
|
-
|
|
227
|
+
# @rbs (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
|
|
228
|
+
def send_to(reference, available_at: nil, idempotency_key: nil)
|
|
229
|
+
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
230
|
+
OperationDispatcher.new(
|
|
231
|
+
actor_type: reference.actor_type,
|
|
232
|
+
handlers: actor_class.definition.messages
|
|
233
|
+
) do |operation, arguments|
|
|
234
|
+
stage_outbound_message(reference:, operation:, arguments:, available_at:, idempotency_key:)
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
226
237
|
end
|
|
227
238
|
|
|
228
239
|
# @rbs (Symbol | String, Hash[String, untyped]) -> untyped
|
|
229
|
-
def invoke(
|
|
230
|
-
handler = self.class.definition.messages[
|
|
231
|
-
self.class.definition.queries[
|
|
232
|
-
raise UnknownMessage, "unknown
|
|
240
|
+
def invoke(operation, arguments)
|
|
241
|
+
handler = self.class.definition.messages[operation.to_sym] ||
|
|
242
|
+
self.class.definition.queries[operation.to_sym]
|
|
243
|
+
raise UnknownMessage, "unknown operation #{operation.inspect} for #{self.class.actor_type}" unless handler
|
|
233
244
|
|
|
234
|
-
guard_application_writes(
|
|
245
|
+
guard_application_writes(operation.to_s) do
|
|
235
246
|
instance_exec(**keyword_arguments(arguments), &handler.block)
|
|
236
247
|
end
|
|
237
248
|
end
|
|
@@ -270,12 +281,12 @@ module SolidObjects
|
|
|
270
281
|
end
|
|
271
282
|
end
|
|
272
283
|
|
|
273
|
-
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent
|
|
274
|
-
def stage_outbound_message(reference
|
|
284
|
+
# @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent
|
|
285
|
+
def stage_outbound_message(reference:, operation:, arguments:, available_at: nil, idempotency_key: nil)
|
|
275
286
|
OutboundMessageIntent.new(
|
|
276
287
|
actor_type: reference.actor_type,
|
|
277
288
|
actor_id: reference.actor_id,
|
|
278
|
-
|
|
289
|
+
operation: operation.to_s,
|
|
279
290
|
arguments: Serialization.dump(arguments),
|
|
280
291
|
available_at:,
|
|
281
292
|
idempotency_key:
|
|
@@ -333,11 +344,11 @@ module SolidObjects
|
|
|
333
344
|
end
|
|
334
345
|
|
|
335
346
|
# @rbs (Symbol | String?) -> void
|
|
336
|
-
def validate_effect_callback!(
|
|
337
|
-
return unless
|
|
338
|
-
return if self.class.definition.messages.key?(
|
|
347
|
+
def validate_effect_callback!(operation)
|
|
348
|
+
return unless operation
|
|
349
|
+
return if self.class.definition.messages.key?(operation.to_sym)
|
|
339
350
|
|
|
340
|
-
raise UnknownMessage, "unknown effect callback
|
|
351
|
+
raise UnknownMessage, "unknown effect callback operation #{operation.inspect}"
|
|
341
352
|
end
|
|
342
353
|
end
|
|
343
354
|
end
|
|
@@ -32,9 +32,9 @@ module SolidObjects
|
|
|
32
32
|
snapshot = ActorSnapshot.new(reference)
|
|
33
33
|
scalar_observable_names(snapshot).each do |name|
|
|
34
34
|
transmit TurboStreamRenderer.observable_value(
|
|
35
|
-
reference
|
|
36
|
-
name
|
|
37
|
-
snapshot.observable_value(name)
|
|
35
|
+
reference:,
|
|
36
|
+
name:,
|
|
37
|
+
value: snapshot.observable_value(name)
|
|
38
38
|
)
|
|
39
39
|
end
|
|
40
40
|
refresh_outdated_components(snapshot)
|
|
@@ -45,6 +45,7 @@ module SolidObjects
|
|
|
45
45
|
# @rbs (Symbol | String, default: untyped) -> StateDefinition::Attribute
|
|
46
46
|
def add_attribute(name, default:)
|
|
47
47
|
attribute_name = name.to_sym
|
|
48
|
+
OperationDispatcher.validate_operation_name!(attribute_name)
|
|
48
49
|
if messages.key?(attribute_name) || queries.key?(attribute_name)
|
|
49
50
|
raise InvalidActor, "#{attribute_name.inspect} is already defined"
|
|
50
51
|
end
|
|
@@ -60,7 +61,7 @@ module SolidObjects
|
|
|
60
61
|
|
|
61
62
|
# @rbs (Symbol | String, Proc) -> Handler
|
|
62
63
|
def add_message(name, block)
|
|
63
|
-
add_handler(messages, name
|
|
64
|
+
add_handler(collection: messages, name:, block:)
|
|
64
65
|
end
|
|
65
66
|
|
|
66
67
|
# @rbs (Symbol | String, Proc) -> Handler
|
|
@@ -72,7 +73,7 @@ module SolidObjects
|
|
|
72
73
|
end
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
add_handler(queries, name
|
|
76
|
+
add_handler(collection: queries, name:, block:)
|
|
76
77
|
end
|
|
77
78
|
|
|
78
79
|
# @rbs (Symbol | String, Proc?) -> Handler
|
|
@@ -121,8 +122,8 @@ module SolidObjects
|
|
|
121
122
|
@state_version = version
|
|
122
123
|
end
|
|
123
124
|
|
|
124
|
-
# @rbs (Integer, Integer, Proc) -> StateMigration
|
|
125
|
-
def add_state_migration(from
|
|
125
|
+
# @rbs (from: Integer, to: Integer, block: Proc) -> StateMigration
|
|
126
|
+
def add_state_migration(from:, to:, block:)
|
|
126
127
|
unless to == from + 1
|
|
127
128
|
raise InvalidActor, "state migrations must advance exactly one version"
|
|
128
129
|
end
|
|
@@ -183,6 +184,7 @@ module SolidObjects
|
|
|
183
184
|
|
|
184
185
|
# @rbs (Symbol) -> Handler
|
|
185
186
|
def add_method_message(name)
|
|
187
|
+
OperationDispatcher.validate_operation_name!(name)
|
|
186
188
|
if messages.key?(name) || queries.key?(name)
|
|
187
189
|
raise InvalidActor, "#{name.inspect} is already defined"
|
|
188
190
|
end
|
|
@@ -222,9 +224,10 @@ module SolidObjects
|
|
|
222
224
|
end
|
|
223
225
|
end
|
|
224
226
|
|
|
225
|
-
# @rbs (Hash[Symbol, Handler], Symbol | String, Proc) -> Handler
|
|
226
|
-
def add_handler(collection
|
|
227
|
+
# @rbs (collection: Hash[Symbol, Handler], name: Symbol | String, block: Proc) -> Handler
|
|
228
|
+
def add_handler(collection:, name:, block:)
|
|
227
229
|
handler_name = name.to_sym
|
|
230
|
+
OperationDispatcher.validate_operation_name!(handler_name)
|
|
228
231
|
raise InvalidActor, "#{handler_name.inspect} is already defined" if messages.key?(handler_name) || queries.key?(handler_name)
|
|
229
232
|
|
|
230
233
|
Handler.new(name: handler_name, block:).tap { |handler| collection[handler_name] = handler }
|
|
@@ -154,7 +154,7 @@ module SolidObjects
|
|
|
154
154
|
authorized = SolidObjects.configuration.authorize_query.call(
|
|
155
155
|
actor_type: reference.actor_type,
|
|
156
156
|
actor_id: reference.actor_id,
|
|
157
|
-
|
|
157
|
+
operation: name.to_s,
|
|
158
158
|
arguments: {},
|
|
159
159
|
authorization_context:
|
|
160
160
|
)
|
data/lib/solid_objects/client.rb
CHANGED
|
@@ -12,50 +12,50 @@ module SolidObjects
|
|
|
12
12
|
@mailbox = mailbox
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped) -> MessageReference
|
|
16
|
-
def async(reference
|
|
15
|
+
# @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped) -> MessageReference
|
|
16
|
+
def async(reference:, operation:, arguments:, available_at: nil, idempotency_key: nil, authorization_context: nil)
|
|
17
17
|
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
18
|
-
|
|
19
|
-
raise UnknownMessage, "unknown
|
|
18
|
+
operation_symbol = operation.to_sym
|
|
19
|
+
raise UnknownMessage, "unknown operation #{operation.inspect}" unless actor_class.definition.messages.key?(operation_symbol)
|
|
20
20
|
|
|
21
21
|
authorize!(
|
|
22
|
-
SolidObjects.configuration.authorize_message,
|
|
23
|
-
reference
|
|
24
|
-
|
|
25
|
-
arguments
|
|
22
|
+
hook: SolidObjects.configuration.authorize_message,
|
|
23
|
+
reference:,
|
|
24
|
+
operation:,
|
|
25
|
+
arguments:,
|
|
26
26
|
authorization_context:
|
|
27
27
|
)
|
|
28
28
|
mailbox.enqueue(
|
|
29
|
-
reference
|
|
30
|
-
|
|
31
|
-
arguments
|
|
32
|
-
|
|
29
|
+
reference:,
|
|
30
|
+
operation:,
|
|
31
|
+
arguments:,
|
|
32
|
+
delivery_mode: "async",
|
|
33
33
|
available_at:,
|
|
34
34
|
idempotency_key:
|
|
35
35
|
)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped) -> untyped
|
|
39
|
-
def sync(reference
|
|
38
|
+
# @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped) -> untyped
|
|
39
|
+
def sync(reference:, operation:, arguments:, timeout:, idempotency_key: nil, authorization_context: nil)
|
|
40
40
|
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
41
|
-
|
|
42
|
-
query = actor_class.definition.queries.key?(
|
|
43
|
-
actor_message = actor_class.definition.messages.key?(
|
|
44
|
-
raise UnknownMessage, "unknown
|
|
41
|
+
operation_symbol = operation.to_sym
|
|
42
|
+
query = actor_class.definition.queries.key?(operation_symbol)
|
|
43
|
+
actor_message = actor_class.definition.messages.key?(operation_symbol)
|
|
44
|
+
raise UnknownMessage, "unknown operation #{operation.inspect}" unless query || actor_message
|
|
45
45
|
|
|
46
46
|
authorize!(
|
|
47
|
-
query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
48
|
-
reference
|
|
49
|
-
|
|
50
|
-
arguments
|
|
47
|
+
hook: query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
48
|
+
reference:,
|
|
49
|
+
operation:,
|
|
50
|
+
arguments:,
|
|
51
51
|
authorization_context:
|
|
52
52
|
)
|
|
53
|
-
reject_sync_inside_transaction!(reference,
|
|
53
|
+
reject_sync_inside_transaction!(reference, operation)
|
|
54
54
|
SyncDeadline.with(timeout:) do
|
|
55
55
|
message_reference = enqueue_sync(
|
|
56
|
-
reference
|
|
57
|
-
|
|
58
|
-
arguments
|
|
56
|
+
reference:,
|
|
57
|
+
operation:,
|
|
58
|
+
arguments:,
|
|
59
59
|
idempotency_key:,
|
|
60
60
|
timeout:
|
|
61
61
|
)
|
|
@@ -75,19 +75,19 @@ module SolidObjects
|
|
|
75
75
|
actor_id: message.actor_id
|
|
76
76
|
)
|
|
77
77
|
actor_class = SolidObjects.registry.fetch(reference.actor_type)
|
|
78
|
-
query = actor_class.definition.queries.key?(message.
|
|
79
|
-
actor_message = actor_class.definition.messages.key?(message.
|
|
78
|
+
query = actor_class.definition.queries.key?(message.operation.to_sym)
|
|
79
|
+
actor_message = actor_class.definition.messages.key?(message.operation.to_sym)
|
|
80
80
|
unless query || actor_message
|
|
81
|
-
raise UnknownMessage, "unknown
|
|
81
|
+
raise UnknownMessage, "unknown operation #{message.operation.inspect}"
|
|
82
82
|
end
|
|
83
83
|
authorize!(
|
|
84
|
-
query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
85
|
-
reference
|
|
86
|
-
message.
|
|
87
|
-
message.arguments,
|
|
84
|
+
hook: query ? SolidObjects.configuration.authorize_query : SolidObjects.configuration.authorize_message,
|
|
85
|
+
reference:,
|
|
86
|
+
operation: message.operation,
|
|
87
|
+
arguments: message.arguments,
|
|
88
88
|
authorization_context:
|
|
89
89
|
)
|
|
90
|
-
reject_sync_inside_transaction!(reference, message.
|
|
90
|
+
reject_sync_inside_transaction!(reference, message.operation)
|
|
91
91
|
SynchronousInvocation.new.call(message_reference, timeout:)
|
|
92
92
|
end
|
|
93
93
|
rescue DatabaseDeadlineExceeded
|
|
@@ -98,10 +98,10 @@ module SolidObjects
|
|
|
98
98
|
def snapshot(reference, authorization_context: nil)
|
|
99
99
|
SolidObjects.registry.fetch(reference.actor_type)
|
|
100
100
|
authorize!(
|
|
101
|
-
SolidObjects.configuration.authorize_query,
|
|
102
|
-
reference
|
|
103
|
-
"__snapshot__",
|
|
104
|
-
{},
|
|
101
|
+
hook: SolidObjects.configuration.authorize_query,
|
|
102
|
+
reference:,
|
|
103
|
+
operation: "__snapshot__",
|
|
104
|
+
arguments: {},
|
|
105
105
|
authorization_context:
|
|
106
106
|
)
|
|
107
107
|
StateSnapshot.new(reference)
|
|
@@ -140,13 +140,13 @@ module SolidObjects
|
|
|
140
140
|
|
|
141
141
|
attr_reader :mailbox
|
|
142
142
|
|
|
143
|
-
# @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], idempotency_key: String?, timeout: Numeric) -> MessageReference
|
|
144
|
-
def enqueue_sync(reference
|
|
143
|
+
# @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], idempotency_key: String?, timeout: Numeric) -> MessageReference
|
|
144
|
+
def enqueue_sync(reference:, operation:, arguments:, idempotency_key:, timeout:)
|
|
145
145
|
mailbox.enqueue(
|
|
146
|
-
reference
|
|
147
|
-
|
|
148
|
-
arguments
|
|
149
|
-
|
|
146
|
+
reference:,
|
|
147
|
+
operation:,
|
|
148
|
+
arguments:,
|
|
149
|
+
delivery_mode: "sync",
|
|
150
150
|
idempotency_key:
|
|
151
151
|
)
|
|
152
152
|
rescue DatabaseDeadlineExceeded
|
|
@@ -154,13 +154,13 @@ module SolidObjects
|
|
|
154
154
|
:"sync.enqueue_timeout",
|
|
155
155
|
actor_type: reference.actor_type,
|
|
156
156
|
actor_id: reference.actor_id,
|
|
157
|
-
|
|
157
|
+
operation: operation.to_s
|
|
158
158
|
)
|
|
159
159
|
raise SyncEnqueueTimeout.new(
|
|
160
160
|
timeout:,
|
|
161
161
|
actor_type: reference.actor_type,
|
|
162
162
|
actor_id: reference.actor_id,
|
|
163
|
-
|
|
163
|
+
operation: operation.to_s
|
|
164
164
|
)
|
|
165
165
|
end
|
|
166
166
|
|
|
@@ -176,28 +176,28 @@ module SolidObjects
|
|
|
176
176
|
end
|
|
177
177
|
|
|
178
178
|
# @rbs (Reference, Symbol | String) -> void
|
|
179
|
-
def reject_sync_inside_transaction!(reference,
|
|
179
|
+
def reject_sync_inside_transaction!(reference, operation)
|
|
180
180
|
return unless SolidObjects::Record.connection.transaction_open?
|
|
181
181
|
|
|
182
182
|
SolidObjects.instrument(
|
|
183
183
|
:"sync.transaction_rejected",
|
|
184
184
|
actor_type: reference.actor_type,
|
|
185
185
|
actor_id: reference.actor_id,
|
|
186
|
-
|
|
186
|
+
operation: operation.to_s
|
|
187
187
|
)
|
|
188
188
|
raise SyncInsideTransaction.new(
|
|
189
189
|
actor_type: reference.actor_type,
|
|
190
190
|
actor_id: reference.actor_id,
|
|
191
|
-
|
|
191
|
+
operation: operation.to_s
|
|
192
192
|
)
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
# @rbs (Proc, Reference, Symbol | String, Hash[Symbol | String, untyped], authorization_context: untyped) -> void
|
|
196
|
-
def authorize!(hook
|
|
195
|
+
# @rbs (hook: Proc, reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], authorization_context: untyped) -> void
|
|
196
|
+
def authorize!(hook:, reference:, operation:, arguments:, authorization_context:)
|
|
197
197
|
authorized = hook.call(
|
|
198
198
|
actor_type: reference.actor_type,
|
|
199
199
|
actor_id: reference.actor_id,
|
|
200
|
-
|
|
200
|
+
operation: operation.to_s,
|
|
201
201
|
arguments:,
|
|
202
202
|
authorization_context:
|
|
203
203
|
)
|
|
@@ -156,8 +156,8 @@ module SolidObjects
|
|
|
156
156
|
locals.merge("component_key" => component_key).freeze
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
-
# @rbs (Array[ComponentRegistration], Integer, Integer) -> String
|
|
160
|
-
def batch_refresh_url(registrations
|
|
159
|
+
# @rbs (registrations: Array[ComponentRegistration], instance_id: Integer, revision: Integer) -> String
|
|
160
|
+
def batch_refresh_url(registrations:, instance_id:, revision:)
|
|
161
161
|
query = URI.encode_www_form(
|
|
162
162
|
[
|
|
163
163
|
[ "instance_id", instance_id ],
|
|
@@ -60,7 +60,7 @@ module SolidObjects
|
|
|
60
60
|
authorized = SolidObjects.configuration.authorize_query.call(
|
|
61
61
|
actor_type: snapshot.reference.actor_type,
|
|
62
62
|
actor_id: snapshot.reference.actor_id,
|
|
63
|
-
|
|
63
|
+
operation: observable_name,
|
|
64
64
|
arguments: registration.authorization_arguments,
|
|
65
65
|
authorization_context:
|
|
66
66
|
)
|
|
@@ -50,21 +50,21 @@ module SolidObjects
|
|
|
50
50
|
revision = invalidation.fetch("revision")
|
|
51
51
|
changed = registrations.select do |registration|
|
|
52
52
|
registration.dependencies.include?(observable_name) &&
|
|
53
|
-
newer_revision?(registration.dom_id, instance_id
|
|
53
|
+
newer_revision?(dom_id: registration.dom_id, instance_id:, revision:)
|
|
54
54
|
end
|
|
55
|
-
refresh_streams(changed
|
|
55
|
+
refresh_streams(changed:, instance_id:, revision:)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# @rbs (ActorSnapshot) -> Array[String]
|
|
59
59
|
def reconnect_refreshes(snapshot)
|
|
60
60
|
stale = registrations.select do |registration|
|
|
61
61
|
newer_revision?(
|
|
62
|
-
registration.dom_id,
|
|
63
|
-
snapshot.instance_id,
|
|
64
|
-
snapshot.revision
|
|
62
|
+
dom_id: registration.dom_id,
|
|
63
|
+
instance_id: snapshot.instance_id,
|
|
64
|
+
revision: snapshot.revision
|
|
65
65
|
)
|
|
66
66
|
end
|
|
67
|
-
refresh_streams(stale, snapshot.instance_id, snapshot.revision)
|
|
67
|
+
refresh_streams(changed: stale, instance_id: snapshot.instance_id, revision: snapshot.revision)
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
class << self
|
|
@@ -86,36 +86,36 @@ module SolidObjects
|
|
|
86
86
|
|
|
87
87
|
# Live invalidations and reconnect replays share this, so a reconnecting
|
|
88
88
|
# client pays the same number of requests a connected one does.
|
|
89
|
-
# @rbs (Array[ComponentRegistration], Integer, Integer) -> Array[String]
|
|
90
|
-
def refresh_streams(changed
|
|
89
|
+
# @rbs (changed: Array[ComponentRegistration], instance_id: Integer, revision: Integer) -> Array[String]
|
|
90
|
+
def refresh_streams(changed:, instance_id:, revision:)
|
|
91
91
|
batched, individual = changed.partition(&:batch)
|
|
92
92
|
streams = individual.map do |registration|
|
|
93
|
-
refresh(registration
|
|
93
|
+
refresh(registration:, instance_id:, revision:)
|
|
94
94
|
end
|
|
95
95
|
batched.group_by(&:batch).each_value do |group|
|
|
96
|
-
group.each { |registration| record_revision(registration
|
|
97
|
-
streams << TurboStreamRenderer.batch_refresh(group, instance_id
|
|
96
|
+
group.each { |registration| record_revision(registration:, instance_id:, revision:) }
|
|
97
|
+
streams << TurboStreamRenderer.batch_refresh(registrations: group, instance_id:, revision:)
|
|
98
98
|
end
|
|
99
99
|
streams
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
-
# @rbs (ComponentRegistration, Integer, Integer) -> void
|
|
103
|
-
def record_revision(registration
|
|
102
|
+
# @rbs (registration: ComponentRegistration, instance_id: Integer, revision: Integer) -> void
|
|
103
|
+
def record_revision(registration:, instance_id:, revision:)
|
|
104
104
|
revisions[registration.dom_id] = [ instance_id, revision ]
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
108
|
-
def refresh(registration
|
|
109
|
-
record_revision(registration
|
|
107
|
+
# @rbs (registration: ComponentRegistration, instance_id: Integer, revision: Integer) -> String
|
|
108
|
+
def refresh(registration:, instance_id:, revision:)
|
|
109
|
+
record_revision(registration:, instance_id:, revision:)
|
|
110
110
|
TurboStreamRenderer.component_refresh(
|
|
111
|
-
registration
|
|
112
|
-
instance_id
|
|
113
|
-
revision
|
|
111
|
+
registration:,
|
|
112
|
+
instance_id:,
|
|
113
|
+
revision:
|
|
114
114
|
)
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
-
# @rbs (String, Integer, Integer) -> bool
|
|
118
|
-
def newer_revision?(dom_id
|
|
117
|
+
# @rbs (dom_id: String, instance_id: Integer, revision: Integer) -> bool
|
|
118
|
+
def newer_revision?(dom_id:, instance_id:, revision:)
|
|
119
119
|
current = revisions.fetch(dom_id)
|
|
120
120
|
(current <=> [ instance_id, revision ]) == -1
|
|
121
121
|
end
|
|
@@ -92,6 +92,9 @@ module SolidObjects
|
|
|
92
92
|
:authorize_subscription,
|
|
93
93
|
:authorize_administration
|
|
94
94
|
|
|
95
|
+
# @rbs @additional_components: Array[untyped]
|
|
96
|
+
attr_reader :additional_components
|
|
97
|
+
|
|
95
98
|
# @rbs () -> void
|
|
96
99
|
def initialize
|
|
97
100
|
@table_name_prefix = "solid_objects_"
|
|
@@ -142,6 +145,45 @@ module SolidObjects
|
|
|
142
145
|
@authorize_destroy = ->(**) { false }
|
|
143
146
|
@authorize_subscription = ->(**) { false }
|
|
144
147
|
@authorize_administration = ->(**) { false }
|
|
148
|
+
@additional_components = []
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Registers a long running component that the supervisor runs beside its own
|
|
152
|
+
# workers. An extension gem uses this to share one process, rather than ask
|
|
153
|
+
# an operator to run and monitor a second one.
|
|
154
|
+
#
|
|
155
|
+
# The block must return an object that answers `run`, `request_shutdown`,
|
|
156
|
+
# `stopped?`, and `stop`, which is the contract the built in components
|
|
157
|
+
# already keep. The supervisor calls the block once for each supervisor it
|
|
158
|
+
# builds, and again when it replaces a crashed component, so two
|
|
159
|
+
# supervisors never share one component instance.
|
|
160
|
+
#
|
|
161
|
+
# @rbs (?count: Integer) { () -> untyped } -> void
|
|
162
|
+
def register_component(count: 1, &factory)
|
|
163
|
+
raise ArgumentError, "register_component requires a block" unless factory
|
|
164
|
+
raise ArgumentError, "count must be positive" unless count.positive?
|
|
165
|
+
|
|
166
|
+
count.times { @additional_components << factory }
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# The supervisor checks the contract here rather than at registration,
|
|
170
|
+
# because a component often needs a database connection to exist, and
|
|
171
|
+
# registration happens while the application boots.
|
|
172
|
+
# @rbs () -> Array[untyped]
|
|
173
|
+
def build_additional_components
|
|
174
|
+
additional_components.map { |factory| factory.call.tap { |component| validate_component!(component) } }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# A component that misses part of the contract would hang the supervisor at
|
|
178
|
+
# shutdown, or crash the moment it starts. The build fails instead, where
|
|
179
|
+
# the caller can read the reason.
|
|
180
|
+
# @rbs (untyped) -> void
|
|
181
|
+
def validate_component!(component)
|
|
182
|
+
%i[run request_shutdown stopped? stop].each do |method_name|
|
|
183
|
+
next if component.respond_to?(method_name)
|
|
184
|
+
|
|
185
|
+
raise ArgumentError, "a registered component must respond to #{method_name}"
|
|
186
|
+
end
|
|
145
187
|
end
|
|
146
188
|
|
|
147
189
|
# @rbs () -> self
|
|
@@ -141,7 +141,13 @@ module SolidObjects
|
|
|
141
141
|
def deadline_error?(error)
|
|
142
142
|
return false unless SyncDeadline.active?
|
|
143
143
|
|
|
144
|
-
busy_error?(error)
|
|
144
|
+
busy_error?(error) || reconnect_blocked_error?(error)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @rbs (Exception) -> bool
|
|
148
|
+
def reconnect_blocked_error?(error)
|
|
149
|
+
error.message.include?("SQLite3::CantOpenException") &&
|
|
150
|
+
error.message.include?("PRAGMA journal_mode='wal'")
|
|
145
151
|
end
|
|
146
152
|
|
|
147
153
|
# @rbs (Exception) -> bool
|
|
@@ -16,13 +16,13 @@ module SolidObjects
|
|
|
16
16
|
|
|
17
17
|
original_message = dead_letter.message
|
|
18
18
|
message_reference = Mailbox.new.enqueue(
|
|
19
|
-
Reference.new(
|
|
19
|
+
reference: Reference.new(
|
|
20
20
|
actor_type: dead_letter.actor_type,
|
|
21
21
|
actor_id: dead_letter.actor_id
|
|
22
22
|
),
|
|
23
|
-
dead_letter.
|
|
24
|
-
dead_letter.arguments,
|
|
25
|
-
|
|
23
|
+
operation: dead_letter.operation,
|
|
24
|
+
arguments: dead_letter.arguments,
|
|
25
|
+
delivery_mode: original_message.delivery_mode,
|
|
26
26
|
idempotency_key: "dead-letter:#{dead_letter.id}"
|
|
27
27
|
)
|
|
28
28
|
dead_letter.update!(retried_message_id: message_reference.id)
|
data/lib/solid_objects/doctor.rb
CHANGED
|
@@ -68,7 +68,7 @@ module SolidObjects
|
|
|
68
68
|
activation_generation
|
|
69
69
|
],
|
|
70
70
|
messages: %w[
|
|
71
|
-
id instance_id
|
|
71
|
+
id instance_id delivery_mode arguments sequence attempt_count request_id
|
|
72
72
|
result error rejection completed_at rejected_at
|
|
73
73
|
],
|
|
74
74
|
ready_messages: %w[id message_id instance_id sequence available_at],
|
|
@@ -76,7 +76,7 @@ module SolidObjects
|
|
|
76
76
|
id message_id instance_id process_id activation_token
|
|
77
77
|
activation_generation claimed_at
|
|
78
78
|
],
|
|
79
|
-
reminders: %w[id instance_id
|
|
79
|
+
reminders: %w[id instance_id operation next_run_at status],
|
|
80
80
|
effects: %w[id message_id instance_id effect_id status available_at],
|
|
81
81
|
broadcasts: %w[id message_id instance_id broadcast_id status available_at],
|
|
82
82
|
dead_letters: %w[id message_id instance_id actor_type actor_id attempts]
|
|
@@ -247,10 +247,10 @@ module SolidObjects
|
|
|
247
247
|
probe_registry.register(kind: "caller", metadata: { execution: "doctor" })
|
|
248
248
|
value = SecureRandom.hex(8)
|
|
249
249
|
message_reference = Mailbox.new.enqueue(
|
|
250
|
-
ProbeActor.ref(actor_id),
|
|
251
|
-
:ping,
|
|
252
|
-
{ value: },
|
|
253
|
-
|
|
250
|
+
reference: ProbeActor.ref(actor_id),
|
|
251
|
+
operation: :ping,
|
|
252
|
+
arguments: { value: },
|
|
253
|
+
delivery_mode: "sync"
|
|
254
254
|
)
|
|
255
255
|
result = SynchronousInvocation
|
|
256
256
|
.new(process_registry: probe_registry)
|
|
@@ -314,11 +314,11 @@ module SolidObjects
|
|
|
314
314
|
}
|
|
315
315
|
{
|
|
316
316
|
authorize_message: actor_arguments.merge(
|
|
317
|
-
|
|
317
|
+
operation: "ping",
|
|
318
318
|
arguments: { "value" => "doctor" }
|
|
319
319
|
),
|
|
320
320
|
authorize_query: actor_arguments.merge(
|
|
321
|
-
|
|
321
|
+
operation: "value",
|
|
322
322
|
arguments: {}
|
|
323
323
|
),
|
|
324
324
|
authorize_destroy: actor_arguments,
|