solid_objects 0.11.0 → 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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/README.md +39 -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/database_adapters/sqlite.rb +7 -1
  26. data/lib/solid_objects/dead_letter_manager.rb +4 -4
  27. data/lib/solid_objects/doctor.rb +8 -8
  28. data/lib/solid_objects/effect_executor.rb +22 -22
  29. data/lib/solid_objects/errors.rb +27 -27
  30. data/lib/solid_objects/executor.rb +40 -38
  31. data/lib/solid_objects/lease.rb +3 -3
  32. data/lib/solid_objects/mailbox.rb +34 -27
  33. data/lib/solid_objects/operation_dispatcher.rb +51 -0
  34. data/lib/solid_objects/payload_broadcast.rb +1 -1
  35. data/lib/solid_objects/reference.rb +60 -29
  36. data/lib/solid_objects/reminder_scheduler.rb +6 -6
  37. data/lib/solid_objects/sync_diagnostics.rb +6 -6
  38. data/lib/solid_objects/turbo_stream_renderer.rb +13 -13
  39. data/lib/solid_objects/version.rb +1 -1
  40. data/lib/solid_objects.rb +1 -0
  41. data/sig/generated/controllers/solid_objects/components_controller.rbs +2 -2
  42. data/sig/generated/lib/solid_objects/actor.rbs +17 -17
  43. data/sig/generated/lib/solid_objects/actor_definition.rbs +4 -4
  44. data/sig/generated/lib/solid_objects/client.rbs +8 -8
  45. data/sig/generated/lib/solid_objects/component_registration.rbs +2 -2
  46. data/sig/generated/lib/solid_objects/component_subscriptions.rbs +8 -8
  47. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
  48. data/sig/generated/lib/solid_objects/effect_executor.rbs +2 -2
  49. data/sig/generated/lib/solid_objects/errors.rbs +18 -18
  50. data/sig/generated/lib/solid_objects/executor.rbs +10 -10
  51. data/sig/generated/lib/solid_objects/lease.rbs +2 -2
  52. data/sig/generated/lib/solid_objects/mailbox.rbs +8 -8
  53. data/sig/generated/lib/solid_objects/operation_dispatcher.rbs +35 -0
  54. data/sig/generated/lib/solid_objects/reference.rbs +7 -4
  55. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +2 -2
  56. data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +8 -8
  57. metadata +5 -2
@@ -68,19 +68,19 @@ module SolidObjects
68
68
  # @rbs @timeout: Numeric
69
69
  # @rbs @actor_type: String
70
70
  # @rbs @actor_id: String
71
- # @rbs @message_name: String
71
+ # @rbs @operation: String
72
72
 
73
- attr_reader :timeout, :actor_type, :actor_id, :message_name
73
+ attr_reader :timeout, :actor_type, :actor_id, :operation
74
74
 
75
- # @rbs (timeout: Numeric, actor_type: String, actor_id: String, message_name: String) -> void
76
- def initialize(timeout:, actor_type:, actor_id:, message_name:)
75
+ # @rbs (timeout: Numeric, actor_type: String, actor_id: String, operation: String) -> void
76
+ def initialize(timeout:, actor_type:, actor_id:, operation:)
77
77
  @timeout = timeout
78
78
  @actor_type = actor_type
79
79
  @actor_id = actor_id
80
- @message_name = message_name
80
+ @operation = operation
81
81
  super(
82
82
  "actor invocation could not be durably enqueued within #{timeout} seconds for " \
83
- "#{actor_type}(#{actor_id.inspect}).#{message_name}"
83
+ "#{actor_type}(#{actor_id.inspect}).#{operation}"
84
84
  )
85
85
  end
86
86
  end
@@ -89,7 +89,7 @@ module SolidObjects
89
89
  # @rbs @timeout: Numeric
90
90
  # @rbs @actor_type: String
91
91
  # @rbs @actor_id: String
92
- # @rbs @message_name: String
92
+ # @rbs @operation: String
93
93
  # @rbs @message_id: Integer
94
94
  # @rbs @request_id: String
95
95
  # @rbs @sequence: Integer
@@ -101,7 +101,7 @@ module SolidObjects
101
101
  attr_reader :timeout,
102
102
  :actor_type,
103
103
  :actor_id,
104
- :message_name,
104
+ :operation,
105
105
  :message_id,
106
106
  :request_id,
107
107
  :sequence,
@@ -110,12 +110,12 @@ module SolidObjects
110
110
  :activation,
111
111
  :blocker
112
112
 
113
- # @rbs (timeout: Numeric, actor_type: String, actor_id: String, message_name: String, message_id: Integer, request_id: String, sequence: Integer, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> void
113
+ # @rbs (timeout: Numeric, actor_type: String, actor_id: String, operation: String, message_id: Integer, request_id: String, sequence: Integer, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> void
114
114
  def initialize(
115
115
  timeout:,
116
116
  actor_type:,
117
117
  actor_id:,
118
- message_name:,
118
+ operation:,
119
119
  message_id:,
120
120
  request_id:,
121
121
  sequence:,
@@ -127,7 +127,7 @@ module SolidObjects
127
127
  @timeout = timeout
128
128
  @actor_type = actor_type
129
129
  @actor_id = actor_id
130
- @message_name = message_name
130
+ @operation = operation
131
131
  @message_id = message_id
132
132
  @request_id = request_id
133
133
  @sequence = sequence
@@ -137,7 +137,7 @@ module SolidObjects
137
137
  @blocker = Serialization.readonly_copy(blocker)
138
138
  super(
139
139
  "actor invocation timed out after #{timeout} seconds for " \
140
- "#{actor_type}(#{actor_id.inspect}).#{message_name} " \
140
+ "#{actor_type}(#{actor_id.inspect}).#{operation} " \
141
141
  "message_id=#{message_id} sequence=#{sequence} status=#{status} waiting_on=#{waiting_on}"
142
142
  )
143
143
  end
@@ -157,17 +157,17 @@ module SolidObjects
157
157
  class SyncInsideTransaction < Error
158
158
  # @rbs @actor_type: String
159
159
  # @rbs @actor_id: String
160
- # @rbs @message_name: String
160
+ # @rbs @operation: String
161
161
 
162
- attr_reader :actor_type, :actor_id, :message_name
162
+ attr_reader :actor_type, :actor_id, :operation
163
163
 
164
- # @rbs (actor_type: String, actor_id: String, message_name: String) -> void
165
- def initialize(actor_type:, actor_id:, message_name:)
164
+ # @rbs (actor_type: String, actor_id: String, operation: String) -> void
165
+ def initialize(actor_type:, actor_id:, operation:)
166
166
  @actor_type = actor_type
167
167
  @actor_id = actor_id
168
- @message_name = message_name
168
+ @operation = operation
169
169
  super(
170
- "cannot synchronously invoke #{actor_type}(#{actor_id.inspect}).#{message_name} " \
170
+ "cannot synchronously invoke #{actor_type}(#{actor_id.inspect}).#{operation} " \
171
171
  "inside an open database transaction; call it before the transaction or enqueue it with async"
172
172
  )
173
173
  end
@@ -176,27 +176,27 @@ module SolidObjects
176
176
  class ApplicationWriteForbidden < NonRetryableError
177
177
  # @rbs @actor_type: String
178
178
  # @rbs @actor_id: String
179
- # @rbs @message_name: String
179
+ # @rbs @operation: String
180
180
 
181
- attr_reader :actor_type, :actor_id, :message_name
181
+ attr_reader :actor_type, :actor_id, :operation
182
182
 
183
- # @rbs (actor_type: String, actor_id: String, message_name: String) -> void
184
- def initialize(actor_type:, actor_id:, message_name:)
183
+ # @rbs (actor_type: String, actor_id: String, operation: String) -> void
184
+ def initialize(actor_type:, actor_id:, operation:)
185
185
  @actor_type = actor_type
186
186
  @actor_id = actor_id
187
- @message_name = message_name
187
+ @operation = operation
188
188
  super(
189
- "#{actor_type}(#{actor_id.inspect}).#{message_name} attempted an Active Record write; " \
189
+ "#{actor_type}(#{actor_id.inspect}).#{operation} attempted an Active Record write; " \
190
190
  "mutate actor state, stage a commit action, or emit a durable effect instead"
191
191
  )
192
192
  end
193
193
  end
194
194
 
195
195
  class CommitActionUnavailable < NonRetryableError
196
- # @rbs (actor_type: String, actor_id: String, message_name: String) -> void
197
- def initialize(actor_type:, actor_id:, message_name:)
196
+ # @rbs (actor_type: String, actor_id: String, operation: String) -> void
197
+ def initialize(actor_type:, actor_id:, operation:)
198
198
  super(
199
- "#{actor_type}(#{actor_id.inspect}).#{message_name} staged a commit action, " \
199
+ "#{actor_type}(#{actor_id.inspect}).#{operation} staged a commit action, " \
200
200
  "but Solid Objects uses a separate database from Active Record"
201
201
  )
202
202
  end
@@ -55,17 +55,17 @@ module SolidObjects
55
55
  # @rbs (MessageContext) -> untyped
56
56
  def invoke_actor(message_context)
57
57
  Context.with(actor:, message: message_context) do
58
- actor.invoke(message.message_name, message.arguments)
58
+ actor.invoke(message.operation, message.arguments)
59
59
  end
60
60
  end
61
61
 
62
62
  # @rbs (Hash[String, untyped]) -> void
63
63
  def ensure_query_did_not_mutate_state!(state_before)
64
- return unless message.message_kind == "sync"
65
- return unless actor.class.definition.queries.key?(message.message_name.to_sym)
64
+ return unless message.delivery_mode == "sync"
65
+ return unless actor.class.definition.queries.key?(message.operation.to_sym)
66
66
  return if actor.state.to_h == state_before
67
67
 
68
- raise InvalidActor, "query #{message.message_name.inspect} mutated actor state"
68
+ raise InvalidActor, "query #{message.operation.inspect} mutated actor state"
69
69
  end
70
70
 
71
71
  # @rbs (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
@@ -82,7 +82,7 @@ module SolidObjects
82
82
  max_bytes: SolidObjects.configuration.max_state_bytes
83
83
  )
84
84
  serialized_result = Serialization.dump(
85
- (message.message_kind == "sync") ? result : nil,
85
+ (message.delivery_mode == "sync") ? result : nil,
86
86
  max_bytes: SolidObjects.configuration.max_result_bytes
87
87
  )
88
88
  effect_intents = actor.drain_effect_intents
@@ -112,15 +112,17 @@ module SolidObjects
112
112
  error: nil,
113
113
  completed_at: SolidObjects.database_adapter.database_now
114
114
  )
115
- enqueued_effects.concat(enqueue_effects(locked_message, instance, effect_intents))
115
+ enqueued_effects.concat(
116
+ enqueue_effects(message: locked_message, instance:, intents: effect_intents)
117
+ )
116
118
  moved_reminders.concat(schedule_reminders(instance, reminder_intents))
117
119
  enqueued_effects.concat(
118
- enqueue_actor_messages(locked_message, instance, outbound_message_intents)
120
+ enqueue_actor_messages(message: locked_message, instance:, intents: outbound_message_intents)
119
121
  )
120
122
  enqueue_broadcasts(
121
- locked_message,
122
- instance,
123
- observable_changes,
123
+ message: locked_message,
124
+ instance:,
125
+ observable_changes:,
124
126
  state_changed:
125
127
  )
126
128
  claimed_message.destroy!
@@ -163,12 +165,12 @@ module SolidObjects
163
165
 
164
166
  intents.each do |intent|
165
167
  handler = SolidObjects.commit_action_registry.fetch(intent.name)
166
- execute_commit_action(intent, handler, context)
168
+ execute_commit_action(intent:, handler:, context:)
167
169
  end
168
170
  end
169
171
 
170
- # @rbs (Actor::CommitActionIntent, Proc, CommitActionContext) -> untyped
171
- def execute_commit_action(intent, handler, context)
172
+ # @rbs (intent: Actor::CommitActionIntent, handler: Proc, context: CommitActionContext) -> untyped
173
+ def execute_commit_action(intent:, handler:, context:)
172
174
  payload = {
173
175
  commit_action_name: intent.name,
174
176
  message_id: message.id,
@@ -198,21 +200,21 @@ module SolidObjects
198
200
  raise CommitActionUnavailable.new(
199
201
  actor_type: message.actor_type,
200
202
  actor_id: message.actor_id,
201
- message_name: message.message_name
203
+ operation: message.operation
202
204
  )
203
205
  end
204
206
 
205
- # @rbs (Message, Instance, Array[Actor::EffectIntent]) -> Array[Effect]
206
- def enqueue_effects(locked_message, instance, intents)
207
+ # @rbs (message: Message, instance: Instance, intents: Array[Actor::EffectIntent]) -> Array[Effect]
208
+ def enqueue_effects(message:, instance:, intents:)
207
209
  intents.map do |intent|
208
210
  Effect.create!(
209
- message: locked_message,
211
+ message:,
210
212
  instance:,
211
213
  effect_id: SecureRandom.uuid,
212
214
  name: intent.name,
213
215
  arguments: intent.arguments,
214
- success_message_name: intent.success_message_name,
215
- failure_message_name: intent.failure_message_name,
216
+ success_operation: intent.success_operation,
217
+ failure_operation: intent.failure_operation,
216
218
  status: "pending",
217
219
  max_attempts: SolidObjects.configuration.max_attempts,
218
220
  available_at: SolidObjects.database_adapter.database_now
@@ -237,7 +239,7 @@ module SolidObjects
237
239
  reminder.assign_attributes(
238
240
  actor_type: instance.actor_type,
239
241
  actor_id: instance.actor_id,
240
- message_name: intent.name,
242
+ operation: intent.name,
241
243
  arguments: intent.arguments,
242
244
  next_run_at: intent.at,
243
245
  interval_seconds: intent.interval_seconds,
@@ -268,18 +270,18 @@ module SolidObjects
268
270
  }
269
271
  end
270
272
 
271
- # @rbs (Message, Instance, Array[Actor::OutboundMessageIntent]) -> Array[Effect]
272
- def enqueue_actor_messages(locked_message, instance, intents)
273
+ # @rbs (message: Message, instance: Instance, intents: Array[Actor::OutboundMessageIntent]) -> Array[Effect]
274
+ def enqueue_actor_messages(message:, instance:, intents:)
273
275
  intents.map do |intent|
274
276
  Effect.create!(
275
- message: locked_message,
277
+ message:,
276
278
  instance:,
277
279
  effect_id: SecureRandom.uuid,
278
280
  name: EffectExecutor::ACTOR_MESSAGE_EFFECT,
279
281
  arguments: {
280
282
  "actor_type" => intent.actor_type,
281
283
  "actor_id" => intent.actor_id,
282
- "message_name" => intent.message_name,
284
+ "operation" => intent.operation,
283
285
  "arguments" => intent.arguments,
284
286
  "available_at" => intent.available_at&.iso8601(6),
285
287
  "idempotency_key" => intent.idempotency_key
@@ -291,8 +293,8 @@ module SolidObjects
291
293
  end
292
294
  end
293
295
 
294
- # @rbs (Message, Instance, Hash[String, untyped], state_changed: bool) -> void
295
- def enqueue_broadcasts(locked_message, instance, observable_changes, state_changed:)
296
+ # @rbs (message: Message, instance: Instance, observable_changes: Hash[String, untyped], state_changed: bool) -> void
297
+ def enqueue_broadcasts(message:, instance:, observable_changes:, state_changed:)
296
298
  broadcasts = observable_changes
297
299
  if broadcasts.empty? && state_changed && payload_broadcasts?
298
300
  broadcasts = { PayloadBroadcast::REVISION_OBSERVABLE => {} }
@@ -300,7 +302,7 @@ module SolidObjects
300
302
 
301
303
  broadcasts.each do |observable_name, value|
302
304
  Broadcast.create!(
303
- message: locked_message,
305
+ message:,
304
306
  instance:,
305
307
  broadcast_id: SecureRandom.uuid,
306
308
  observable_name:,
@@ -332,7 +334,7 @@ module SolidObjects
332
334
 
333
335
  if error.is_a?(NonRetryableError) ||
334
336
  locked_message.attempt_count >= locked_message.max_attempts
335
- create_dead_letter(locked_message, error_details, now)
337
+ create_dead_letter(message: locked_message, error_details:, now:)
336
338
  dead = true
337
339
  else
338
340
  ReadyMessage.create!(
@@ -410,20 +412,20 @@ module SolidObjects
410
412
  )
411
413
  end
412
414
 
413
- # @rbs (Message, Hash[String, untyped], Time) -> DeadLetter
414
- def create_dead_letter(locked_message, error_details, now)
415
+ # @rbs (message: Message, error_details: Hash[String, untyped], now: Time) -> DeadLetter
416
+ def create_dead_letter(message:, error_details:, now:)
415
417
  DeadLetter.create!(
416
- message: locked_message,
417
- instance: locked_message.instance,
418
- actor_type: locked_message.actor_type,
419
- actor_id: locked_message.actor_id,
420
- message_name: locked_message.message_name,
421
- arguments: locked_message.arguments,
422
- attempts: locked_message.attempt_count,
418
+ message:,
419
+ instance: message.instance,
420
+ actor_type: message.actor_type,
421
+ actor_id: message.actor_id,
422
+ operation: message.operation,
423
+ arguments: message.arguments,
424
+ attempts: message.attempt_count,
423
425
  exception_class: error_details.fetch("class"),
424
426
  exception_message: error_details.fetch("message"),
425
427
  backtrace: error_details.fetch("backtrace"),
426
- first_failed_at: locked_message.last_failed_at || now,
428
+ first_failed_at: message.last_failed_at || now,
427
429
  last_failed_at: now
428
430
  )
429
431
  end
@@ -11,7 +11,7 @@ module SolidObjects
11
11
  now:,
12
12
  database_adapter:, duration: SolidObjects.configuration.lease_duration
13
13
  )
14
- return if held_by_another_live_owner?(instance, activation_token, now)
14
+ return if held_by_another_live_owner?(instance:, activation_token:, now:)
15
15
 
16
16
  generation = instance.activation_generation + 1
17
17
  expires_at = now + duration
@@ -61,8 +61,8 @@ module SolidObjects
61
61
 
62
62
  private
63
63
 
64
- # @rbs (Instance, String, Time) -> bool
65
- def held_by_another_live_owner?(instance, activation_token, now)
64
+ # @rbs (instance: Instance, activation_token: String, now: Time) -> bool
65
+ def held_by_another_live_owner?(instance:, activation_token:, now:)
66
66
  instance.activation_owner_id.present? &&
67
67
  instance.activation_token != activation_token &&
68
68
  instance.activation_expires_at.present? &&
@@ -11,8 +11,8 @@ module SolidObjects
11
11
  @database_adapter = database_adapter
12
12
  end
13
13
 
14
- # @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], kind: String, ?available_at: Time?, idempotency_key: String?) -> MessageReference
15
- def enqueue(reference, message_name, arguments, kind:, available_at: nil, idempotency_key: nil)
14
+ # @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], delivery_mode: String, ?available_at: Time?, idempotency_key: String?) -> MessageReference
15
+ def enqueue(reference:, operation:, arguments:, delivery_mode:, available_at: nil, idempotency_key: nil)
16
16
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
17
17
  normalized_arguments = Serialization.dump(
18
18
  arguments,
@@ -21,10 +21,10 @@ module SolidObjects
21
21
  message = with_instance_retry do
22
22
  database_adapter.transaction do
23
23
  enqueue_in_transaction(
24
- reference,
25
- message_name,
26
- normalized_arguments,
27
- kind:,
24
+ reference:,
25
+ operation:,
26
+ arguments: normalized_arguments,
27
+ delivery_mode:,
28
28
  available_at:,
29
29
  idempotency_key:,
30
30
  actor_class:
@@ -36,12 +36,12 @@ module SolidObjects
36
36
  MessageReference.from_message(message)
37
37
  end
38
38
 
39
- # @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], kind: String, ?available_at: Time?, idempotency_key: String?, ?actor_class: Class?) -> Message
39
+ # @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], delivery_mode: String, ?available_at: Time?, idempotency_key: String?, ?actor_class: Class?) -> Message
40
40
  def enqueue_in_transaction(
41
- reference,
42
- message_name,
43
- arguments,
44
- kind:,
41
+ reference:,
42
+ operation:,
43
+ arguments:,
44
+ delivery_mode:,
45
45
  available_at: nil,
46
46
  idempotency_key: nil,
47
47
  actor_class: nil
@@ -55,15 +55,22 @@ module SolidObjects
55
55
  instance.lock!
56
56
 
57
57
  existing = find_idempotent_message(instance, idempotency_key)
58
- return validate_idempotent_message!(existing, message_name, kind, normalized_arguments) if existing
58
+ if existing
59
+ return validate_idempotent_message!(
60
+ existing_message: existing,
61
+ operation:,
62
+ delivery_mode:,
63
+ arguments: normalized_arguments
64
+ )
65
+ end
59
66
 
60
67
  enforce_mailbox_limit!(instance)
61
68
  create_message(
62
- instance,
63
- reference,
64
- message_name,
65
- normalized_arguments,
66
- kind:,
69
+ instance:,
70
+ reference:,
71
+ operation:,
72
+ arguments: normalized_arguments,
73
+ delivery_mode:,
67
74
  available_at:,
68
75
  idempotency_key:
69
76
  )
@@ -117,12 +124,12 @@ module SolidObjects
117
124
  Message.find_by(instance_id: instance.id, idempotency_key:)
118
125
  end
119
126
 
120
- # @rbs (Message, Symbol | String, String, untyped) -> Message
121
- def validate_idempotent_message!(message, message_name, kind, arguments)
122
- matches = message.message_name == message_name.to_s &&
123
- message.message_kind == kind &&
124
- message.arguments == arguments
125
- return message if matches
127
+ # @rbs (existing_message: Message, operation: Symbol | String, delivery_mode: String, arguments: untyped) -> Message
128
+ def validate_idempotent_message!(existing_message:, operation:, delivery_mode:, arguments:)
129
+ matches = existing_message.operation == operation.to_s &&
130
+ existing_message.delivery_mode == delivery_mode &&
131
+ existing_message.arguments == arguments
132
+ return existing_message if matches
126
133
 
127
134
  raise IdempotencyConflict, "idempotency key belongs to a different actor invocation"
128
135
  end
@@ -136,8 +143,8 @@ module SolidObjects
136
143
  raise MailboxFull, "mailbox is full for #{instance.actor_type}(#{instance.actor_id.inspect})"
137
144
  end
138
145
 
139
- # @rbs (Instance, Reference, Symbol | String, untyped, kind: String, available_at: Time?, idempotency_key: String?) -> Message
140
- def create_message(instance, reference, message_name, arguments, kind:, available_at:, idempotency_key:)
146
+ # @rbs (instance: Instance, reference: Reference, operation: Symbol | String, arguments: untyped, delivery_mode: String, available_at: Time?, idempotency_key: String?) -> Message
147
+ def create_message(instance:, reference:, operation:, arguments:, delivery_mode:, available_at:, idempotency_key:)
141
148
  sequence = instance.next_message_sequence
142
149
  now = database_adapter.database_now
143
150
  scheduled_at = normalize_availability(available_at, now)
@@ -146,8 +153,8 @@ module SolidObjects
146
153
  instance:,
147
154
  actor_type: reference.actor_type,
148
155
  actor_id: reference.actor_id,
149
- message_name: message_name.to_s,
150
- message_kind: kind,
156
+ operation: operation.to_s,
157
+ delivery_mode:,
151
158
  arguments:,
152
159
  sequence:,
153
160
  max_attempts: SolidObjects.configuration.max_attempts,
@@ -0,0 +1,51 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ class OperationDispatcher
5
+ # @rbs @actor_type: String
6
+ # @rbs @handlers: Hash[Symbol, ActorDefinition::Handler]
7
+ # @rbs @dispatch: Proc
8
+
9
+ # @rbs (actor_type: String, handlers: Hash[Symbol, ActorDefinition::Handler]) { (Symbol, Hash[Symbol, untyped]) -> untyped } -> void
10
+ def initialize(actor_type:, handlers:, &dispatch)
11
+ @actor_type = actor_type
12
+ @handlers = handlers.dup.freeze
13
+ @dispatch = dispatch
14
+ end
15
+
16
+ # @rbs (Symbol, *untyped, **untyped) -> untyped
17
+ def method_missing(name, *arguments, **keywords, &block)
18
+ raise UnknownMessage, "unknown message #{name.inspect} for #{actor_type}" unless handlers.key?(name)
19
+ if arguments.any? || block
20
+ raise ArgumentError, "actor operations accept keyword arguments only"
21
+ end
22
+
23
+ dispatch.call(name, keywords)
24
+ end
25
+
26
+ # @rbs (Symbol, bool) -> bool
27
+ def respond_to_missing?(name, include_private = false)
28
+ handlers.key?(name) || super
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :actor_type, :handlers, :dispatch
34
+
35
+ RESERVED_REFERENCE_METHOD_NAMES = %i[actor_type actor_id async sync destroy snapshot].freeze
36
+ RESERVED_OPERATION_NAMES = (
37
+ public_instance_methods(true) +
38
+ RESERVED_REFERENCE_METHOD_NAMES
39
+ ).map(&:to_sym).uniq.freeze
40
+
41
+ class << self
42
+ # @rbs (Symbol | String) -> void
43
+ def validate_operation_name!(name)
44
+ operation_name = name.to_sym
45
+ return unless RESERVED_OPERATION_NAMES.include?(operation_name)
46
+
47
+ raise InvalidActor, "#{operation_name.inspect} conflicts with actor dispatch"
48
+ end
49
+ end
50
+ end
51
+ end
@@ -83,7 +83,7 @@ module SolidObjects
83
83
  authorized = SolidObjects.configuration.authorize_query.call(
84
84
  actor_type: snapshot.reference.actor_type,
85
85
  actor_id: snapshot.reference.actor_id,
86
- message_name: name,
86
+ operation: name,
87
87
  arguments: {},
88
88
  authorization_context:
89
89
  )
@@ -16,41 +16,50 @@ module SolidObjects
16
16
  freeze
17
17
  end
18
18
 
19
- # @rbs (Symbol | String, ?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped, **untyped) -> MessageReference?
20
- def async(message_name, available_at: nil, idempotency_key: nil, authorization_context: nil, **arguments)
21
- if (actor = Context.current_actor)
22
- actor.stage_outbound_message(
23
- self,
24
- message_name,
25
- arguments,
19
+ # @rbs (?available_at: Time?, ?idempotency_key: String?, ?authorization_context: untyped) -> OperationDispatcher
20
+ def async(available_at: nil, idempotency_key: nil, authorization_context: nil)
21
+ OperationDispatcher.new(
22
+ actor_type:,
23
+ handlers: actor_definition.messages
24
+ ) do |operation, arguments|
25
+ if (actor = Context.current_actor)
26
+ actor.stage_outbound_message(
27
+ reference: self,
28
+ operation:,
29
+ arguments:,
30
+ available_at:,
31
+ idempotency_key:
32
+ )
33
+ next
34
+ end
35
+
36
+ SolidObjects.client.async(
37
+ reference: self,
38
+ operation:,
39
+ arguments:,
26
40
  available_at:,
27
- idempotency_key:
41
+ idempotency_key:,
42
+ authorization_context:
28
43
  )
29
- return
30
44
  end
31
-
32
- SolidObjects.client.async(
33
- self,
34
- message_name,
35
- arguments,
36
- available_at:,
37
- idempotency_key:,
38
- authorization_context:
39
- )
40
45
  end
41
46
 
42
- # @rbs (Symbol | String, ?timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped, **untyped) -> untyped
43
- def sync(message_name, timeout: 5.seconds, idempotency_key: nil, authorization_context: nil, **arguments)
47
+ # @rbs (?timeout: Numeric, ?idempotency_key: String?, ?authorization_context: untyped) -> OperationDispatcher
48
+ def sync(timeout: 5.seconds, idempotency_key: nil, authorization_context: nil)
44
49
  raise ActorCallCycle, "actors cannot synchronously wait for another actor" if Context.current_actor
45
50
 
46
- SolidObjects.client.sync(
47
- self,
48
- message_name,
49
- arguments,
50
- timeout:,
51
- idempotency_key:,
52
- authorization_context:
53
- )
51
+ OperationDispatcher.new(
52
+ actor_type:,
53
+ handlers: actor_definition.messages.merge(actor_definition.queries)
54
+ ) do |operation, arguments|
55
+ invoke_synchronously(
56
+ operation,
57
+ arguments,
58
+ timeout:,
59
+ idempotency_key:,
60
+ authorization_context:
61
+ )
62
+ end
54
63
  end
55
64
 
56
65
  # @rbs (?authorization_context: untyped) -> bool
@@ -66,7 +75,15 @@ module SolidObjects
66
75
  # @rbs (Symbol, *untyped, **untyped) -> untyped
67
76
  def method_missing(name, *arguments, **keywords)
68
77
  return super if arguments.any? || block_given?
69
- return sync(name, **keywords) if actor_message?(name) || actor_query?(name)
78
+ if actor_message?(name) || actor_query?(name)
79
+ return invoke_synchronously(
80
+ name,
81
+ keywords,
82
+ timeout: 5.seconds,
83
+ idempotency_key: nil,
84
+ authorization_context: nil
85
+ )
86
+ end
70
87
 
71
88
  super
72
89
  end
@@ -83,6 +100,20 @@ module SolidObjects
83
100
 
84
101
  private
85
102
 
103
+ # @rbs (Symbol | String, Hash[Symbol, untyped], timeout: Numeric, idempotency_key: String?, authorization_context: untyped) -> untyped
104
+ def invoke_synchronously(operation, arguments, timeout:, idempotency_key:, authorization_context:)
105
+ raise ActorCallCycle, "actors cannot synchronously wait for another actor" if Context.current_actor
106
+
107
+ SolidObjects.client.sync(
108
+ reference: self,
109
+ operation:,
110
+ arguments:,
111
+ timeout:,
112
+ idempotency_key:,
113
+ authorization_context:
114
+ )
115
+ end
116
+
86
117
  # @rbs (Symbol | String) -> bool
87
118
  def actor_message?(name)
88
119
  actor_definition.messages.key?(name.to_sym)
@@ -97,8 +97,8 @@ module SolidObjects
97
97
  # @rbs (Reminder) -> MessageReference?
98
98
  def enqueue(reminder)
99
99
  actor_class = SolidObjects.registry.fetch(reminder.actor_type)
100
- unless actor_class.definition.messages.key?(reminder.message_name.to_sym)
101
- raise UnknownMessage, "unknown reminder message #{reminder.message_name.inspect}"
100
+ unless actor_class.definition.messages.key?(reminder.operation.to_sym)
101
+ raise UnknownMessage, "unknown reminder operation #{reminder.operation.inspect}"
102
102
  end
103
103
 
104
104
  mailbox = Mailbox.new(database_adapter:)
@@ -111,10 +111,10 @@ module SolidObjects
111
111
  verify_claim!(locked_reminder)
112
112
  now = database_adapter.database_now
113
113
  message = mailbox.enqueue_in_transaction(
114
- Reference.new(actor_type: instance.actor_type, actor_id: instance.actor_id),
115
- locked_reminder.message_name,
116
- locked_reminder.arguments,
117
- kind: "internal",
114
+ reference: Reference.new(actor_type: instance.actor_type, actor_id: instance.actor_id),
115
+ operation: locked_reminder.operation,
116
+ arguments: locked_reminder.arguments,
117
+ delivery_mode: "internal",
118
118
  idempotency_key: "reminder:#{locked_reminder.id}:#{locked_reminder.occurrence}",
119
119
  actor_class:
120
120
  )