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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +50 -0
  3. data/README.md +72 -16
  4. data/app/controllers/solid_objects/components_controller.rb +4 -4
  5. data/app/models/solid_objects/message.rb +1 -1
  6. data/app/views/solid_objects/dead_letters/index.html.erb +1 -1
  7. data/app/views/solid_objects/instances/show.html.erb +1 -1
  8. data/benchmark/support.rb +10 -10
  9. data/db/migrate/20260813000000_rename_message_dispatch_columns.rb +53 -0
  10. data/docs/architecture.md +11 -9
  11. data/docs/authorization.md +2 -2
  12. data/docs/database-schema.md +4 -4
  13. data/docs/development.md +1 -1
  14. data/docs/migrating-existing-state.md +1 -3
  15. data/docs/roadmap.md +1 -1
  16. data/lib/solid_objects/actor.rb +43 -32
  17. data/lib/solid_objects/actor_channel.rb +3 -3
  18. data/lib/solid_objects/actor_definition.rb +9 -6
  19. data/lib/solid_objects/actor_view.rb +1 -1
  20. data/lib/solid_objects/application_write_guard.rb +1 -1
  21. data/lib/solid_objects/client.rb +52 -52
  22. data/lib/solid_objects/component_registration.rb +2 -2
  23. data/lib/solid_objects/component_renderer.rb +1 -1
  24. data/lib/solid_objects/component_subscriptions.rb +21 -21
  25. data/lib/solid_objects/configuration.rb +42 -0
  26. data/lib/solid_objects/database_adapters/sqlite.rb +7 -1
  27. data/lib/solid_objects/dead_letter_manager.rb +4 -4
  28. data/lib/solid_objects/doctor.rb +8 -8
  29. data/lib/solid_objects/effect_executor.rb +22 -22
  30. data/lib/solid_objects/errors.rb +27 -27
  31. data/lib/solid_objects/executor.rb +40 -38
  32. data/lib/solid_objects/lease.rb +3 -3
  33. data/lib/solid_objects/mailbox.rb +34 -27
  34. data/lib/solid_objects/operation_dispatcher.rb +51 -0
  35. data/lib/solid_objects/payload_broadcast.rb +1 -1
  36. data/lib/solid_objects/reference.rb +60 -29
  37. data/lib/solid_objects/reminder_scheduler.rb +6 -6
  38. data/lib/solid_objects/supervisor.rb +50 -9
  39. data/lib/solid_objects/sync_diagnostics.rb +6 -6
  40. data/lib/solid_objects/turbo_stream_renderer.rb +13 -13
  41. data/lib/solid_objects/version.rb +1 -1
  42. data/lib/solid_objects.rb +1 -0
  43. data/sig/generated/controllers/solid_objects/components_controller.rbs +2 -2
  44. data/sig/generated/lib/solid_objects/actor.rbs +17 -17
  45. data/sig/generated/lib/solid_objects/actor_definition.rbs +4 -4
  46. data/sig/generated/lib/solid_objects/client.rbs +8 -8
  47. data/sig/generated/lib/solid_objects/component_registration.rbs +2 -2
  48. data/sig/generated/lib/solid_objects/component_subscriptions.rbs +8 -8
  49. data/sig/generated/lib/solid_objects/configuration.rbs +32 -4
  50. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
  51. data/sig/generated/lib/solid_objects/effect_executor.rbs +2 -2
  52. data/sig/generated/lib/solid_objects/errors.rbs +18 -18
  53. data/sig/generated/lib/solid_objects/executor.rbs +10 -10
  54. data/sig/generated/lib/solid_objects/lease.rbs +2 -2
  55. data/sig/generated/lib/solid_objects/mailbox.rbs +8 -8
  56. data/sig/generated/lib/solid_objects/operation_dispatcher.rbs +35 -0
  57. data/sig/generated/lib/solid_objects/reference.rbs +7 -4
  58. data/sig/generated/lib/solid_objects/supervisor.rbs +19 -2
  59. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +2 -2
  60. data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +8 -8
  61. metadata +5 -2
@@ -2,10 +2,10 @@
2
2
 
3
3
  module SolidObjects
4
4
  class Actor
5
- EffectIntent = Data.define(:name, :arguments, :success_message_name, :failure_message_name)
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, :message_name, :arguments, :available_at, :idempotency_key)
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, to, block)
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
- success_message_name: on_success&.to_s,
181
- failure_message_name: on_failure&.to_s
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 (Symbol | String, at: Time, ?every: Numeric?, ?missed: Symbol | String, arguments: Hash[Symbol | String, untyped]) -> nil
200
- def schedule(name, at:, every: nil, missed: :latest, arguments: {})
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
- ReminderIntent.new(
211
- name: name.to_s,
212
- at:,
213
- arguments: Serialization.dump(arguments),
214
- interval_seconds:,
215
- missed_policy:
216
- ).tap do |intent|
217
- reminder_intents << intent
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, Symbol | String, ?available_at: Time?, ?idempotency_key: String?, **untyped) -> nil
223
- def send_to(reference, message_name, available_at: nil, idempotency_key: nil, **arguments)
224
- stage_outbound_message(reference, message_name, arguments, available_at:, idempotency_key:)
225
- nil
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(message_name, arguments)
230
- handler = self.class.definition.messages[message_name.to_sym] ||
231
- self.class.definition.queries[message_name.to_sym]
232
- raise UnknownMessage, "unknown message #{message_name.inspect} for #{self.class.actor_type}" unless handler
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(message_name.to_s) do
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, message_name, arguments, available_at: nil, idempotency_key: nil)
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
- message_name: message_name.to_s,
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!(message_name)
337
- return unless message_name
338
- return if self.class.definition.messages.key?(message_name.to_sym)
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 message #{message_name.inspect}"
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, block)
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, block)
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, to, block)
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, name, block)
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
- message_name: name.to_s,
157
+ operation: name.to_s,
158
158
  arguments: {},
159
159
  authorization_context:
160
160
  )
@@ -16,7 +16,7 @@ module SolidObjects
16
16
  raise ApplicationWriteForbidden.new(
17
17
  actor_type:,
18
18
  actor_id:,
19
- message_name: operation
19
+ operation:
20
20
  )
21
21
  end
22
22
  end
@@ -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, message_name, arguments, available_at: nil, idempotency_key: nil, authorization_context: nil)
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
- message_symbol = message_name.to_sym
19
- raise UnknownMessage, "unknown message #{message_name.inspect}" unless actor_class.definition.messages.key?(message_symbol)
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
- message_name,
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
- message_name,
31
- arguments,
32
- kind: "async",
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, message_name, arguments, timeout:, idempotency_key: nil, authorization_context: nil)
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
- message_symbol = message_name.to_sym
42
- query = actor_class.definition.queries.key?(message_symbol)
43
- actor_message = actor_class.definition.messages.key?(message_symbol)
44
- raise UnknownMessage, "unknown message #{message_name.inspect}" unless query || actor_message
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
- message_name,
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, message_name)
53
+ reject_sync_inside_transaction!(reference, operation)
54
54
  SyncDeadline.with(timeout:) do
55
55
  message_reference = enqueue_sync(
56
- reference,
57
- message_name,
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.message_name.to_sym)
79
- actor_message = actor_class.definition.messages.key?(message.message_name.to_sym)
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 message #{message.message_name.inspect}"
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.message_name,
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.message_name)
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, message_name, arguments, idempotency_key:, timeout:)
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
- message_name,
148
- arguments,
149
- kind: "sync",
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
- message_name: message_name.to_s
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
- message_name: message_name.to_s
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, message_name)
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
- message_name: message_name.to_s
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
- message_name: message_name.to_s
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, reference, message_name, arguments, authorization_context:)
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
- message_name: message_name.to_s,
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, instance_id, revision)
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
- message_name: observable_name,
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, revision)
53
+ newer_revision?(dom_id: registration.dom_id, instance_id:, revision:)
54
54
  end
55
- refresh_streams(changed, instance_id, revision)
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, instance_id, revision)
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, instance_id, revision)
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, instance_id, revision) }
97
- streams << TurboStreamRenderer.batch_refresh(group, instance_id, revision)
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, instance_id, revision)
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, instance_id, revision)
109
- record_revision(registration, instance_id, revision)
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, instance_id, revision)
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.message_name,
24
- dead_letter.arguments,
25
- kind: original_message.message_kind,
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)
@@ -68,7 +68,7 @@ module SolidObjects
68
68
  activation_generation
69
69
  ],
70
70
  messages: %w[
71
- id instance_id message_kind arguments sequence attempt_count request_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 message_name next_run_at status],
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
- kind: "sync"
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
- message_name: "ping",
317
+ operation: "ping",
318
318
  arguments: { "value" => "doctor" }
319
319
  ),
320
320
  authorize_query: actor_arguments.merge(
321
- message_name: "value",
321
+ operation: "value",
322
322
  arguments: {}
323
323
  ),
324
324
  authorize_destroy: actor_arguments,