solid_objects 0.11.0 → 0.12.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +62 -20
  4. data/app/controllers/solid_objects/components_controller.rb +4 -4
  5. data/app/models/solid_objects/broadcast.rb +8 -0
  6. data/app/models/solid_objects/message.rb +1 -1
  7. data/app/views/solid_objects/dead_letters/index.html.erb +1 -1
  8. data/app/views/solid_objects/instances/show.html.erb +1 -1
  9. data/benchmark/support.rb +10 -10
  10. data/db/migrate/20260813000000_rename_message_dispatch_columns.rb +53 -0
  11. data/docs/architecture.md +14 -11
  12. data/docs/authorization.md +2 -2
  13. data/docs/database-schema.md +6 -6
  14. data/docs/development.md +13 -1
  15. data/docs/migrating-existing-state.md +1 -3
  16. data/docs/realtime.md +25 -3
  17. data/docs/roadmap.md +9 -5
  18. data/docs/security.md +8 -0
  19. data/examples/application/app/actors/shopping_cart_actor.rb +3 -3
  20. data/lib/solid_objects/actor.rb +49 -37
  21. data/lib/solid_objects/actor_channel.rb +10 -7
  22. data/lib/solid_objects/actor_definition.rb +24 -9
  23. data/lib/solid_objects/actor_view.rb +7 -2
  24. data/lib/solid_objects/application_write_guard.rb +1 -1
  25. data/lib/solid_objects/client.rb +52 -52
  26. data/lib/solid_objects/component_registration.rb +2 -2
  27. data/lib/solid_objects/component_renderer.rb +1 -1
  28. data/lib/solid_objects/component_subscriptions.rb +21 -21
  29. data/lib/solid_objects/database_adapters/sqlite.rb +7 -1
  30. data/lib/solid_objects/dead_letter_manager.rb +4 -4
  31. data/lib/solid_objects/doctor.rb +8 -8
  32. data/lib/solid_objects/effect_executor.rb +30 -22
  33. data/lib/solid_objects/errors.rb +30 -27
  34. data/lib/solid_objects/executor.rb +47 -39
  35. data/lib/solid_objects/lease.rb +3 -3
  36. data/lib/solid_objects/mailbox.rb +34 -27
  37. data/lib/solid_objects/operation_dispatcher.rb +51 -0
  38. data/lib/solid_objects/payload_broadcast.rb +1 -1
  39. data/lib/solid_objects/reference.rb +60 -29
  40. data/lib/solid_objects/reminder_scheduler.rb +34 -20
  41. data/lib/solid_objects/sync_diagnostics.rb +6 -6
  42. data/lib/solid_objects/test_helper.rb +16 -0
  43. data/lib/solid_objects/turbo_stream_renderer.rb +16 -16
  44. data/lib/solid_objects/version.rb +1 -1
  45. data/lib/solid_objects.rb +1 -0
  46. data/sig/generated/controllers/solid_objects/components_controller.rbs +2 -2
  47. data/sig/generated/lib/solid_objects/actor.rbs +19 -19
  48. data/sig/generated/lib/solid_objects/actor_definition.rbs +13 -6
  49. data/sig/generated/lib/solid_objects/client.rbs +8 -8
  50. data/sig/generated/lib/solid_objects/component_registration.rbs +2 -2
  51. data/sig/generated/lib/solid_objects/component_subscriptions.rbs +8 -8
  52. data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
  53. data/sig/generated/lib/solid_objects/effect_executor.rbs +2 -2
  54. data/sig/generated/lib/solid_objects/errors.rbs +21 -18
  55. data/sig/generated/lib/solid_objects/executor.rbs +10 -10
  56. data/sig/generated/lib/solid_objects/lease.rbs +2 -2
  57. data/sig/generated/lib/solid_objects/mailbox.rbs +8 -8
  58. data/sig/generated/lib/solid_objects/operation_dispatcher.rbs +35 -0
  59. data/sig/generated/lib/solid_objects/reference.rbs +7 -4
  60. data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +9 -6
  61. data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +2 -2
  62. data/sig/generated/lib/solid_objects/test_helper.rbs +3 -0
  63. data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +8 -8
  64. data/sig/generated/models/solid_objects/broadcast.rbs +2 -0
  65. metadata +5 -2
@@ -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
@@ -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,
@@ -122,18 +122,18 @@ module SolidObjects
122
122
  actor_id: arguments.fetch("actor_id")
123
123
  )
124
124
  actor_class = SolidObjects.registry.fetch(reference.actor_type)
125
- message_name = arguments.fetch("message_name")
126
- unless actor_class.definition.messages.key?(message_name.to_sym)
127
- raise UnknownMessage, "unknown actor-to-actor message #{message_name.inspect}"
125
+ operation = arguments.fetch("operation")
126
+ unless actor_class.definition.messages.key?(operation.to_sym)
127
+ raise UnknownMessage, "unknown actor-to-actor operation #{operation.inspect}"
128
128
  end
129
129
 
130
130
  available_at = arguments["available_at"]
131
131
  available_at = Time.iso8601(available_at) if available_at
132
132
  Mailbox.new.enqueue(
133
- reference,
134
- message_name,
135
- arguments.fetch("arguments"),
136
- kind: "internal",
133
+ reference:,
134
+ operation:,
135
+ arguments: arguments.fetch("arguments"),
136
+ delivery_mode: "internal",
137
137
  available_at:,
138
138
  idempotency_key: effect.effect_id
139
139
  ).id
@@ -150,10 +150,14 @@ module SolidObjects
150
150
  locked_effect = Effect.lock.find(effect.id)
151
151
  verify_claim!(locked_effect)
152
152
  result_message = enqueue_result_message(
153
- locked_effect,
154
- locked_effect.success_message_name,
155
- "success",
156
- { "effect_id" => locked_effect.effect_id, "result" => serialized_result }
153
+ effect: locked_effect,
154
+ operation: locked_effect.success_operation,
155
+ outcome: "success",
156
+ arguments: {
157
+ "effect_id" => locked_effect.effect_id,
158
+ "arguments" => locked_effect.arguments,
159
+ "result" => serialized_result
160
+ }
157
161
  )
158
162
  locked_effect.update!(
159
163
  status: "completed",
@@ -188,10 +192,14 @@ module SolidObjects
188
192
  }
189
193
  if dead
190
194
  result_message = enqueue_result_message(
191
- locked_effect,
192
- locked_effect.failure_message_name,
193
- "failure",
194
- { "effect_id" => locked_effect.effect_id, "error" => error_details }
195
+ effect: locked_effect,
196
+ operation: locked_effect.failure_operation,
197
+ outcome: "failure",
198
+ arguments: {
199
+ "effect_id" => locked_effect.effect_id,
200
+ "arguments" => locked_effect.arguments,
201
+ "error" => error_details
202
+ }
195
203
  )
196
204
  end
197
205
  locked_effect.update!(
@@ -216,18 +224,18 @@ module SolidObjects
216
224
  raise LostActivation, "effect claim changed"
217
225
  end
218
226
 
219
- # @rbs (Effect, String?, String, Hash[String, untyped]) -> Message?
220
- def enqueue_result_message(effect, message_name, outcome, arguments)
221
- return unless message_name
227
+ # @rbs (effect: Effect, operation: String?, outcome: String, arguments: Hash[String, untyped]) -> Message?
228
+ def enqueue_result_message(effect:, operation:, outcome:, arguments:)
229
+ return unless operation
222
230
 
223
231
  Mailbox.new.enqueue_in_transaction(
224
- Reference.new(
232
+ reference: Reference.new(
225
233
  actor_type: effect.instance.actor_type,
226
234
  actor_id: effect.instance.actor_id
227
235
  ),
228
- message_name,
229
- arguments,
230
- kind: "internal",
236
+ operation:,
237
+ arguments:,
238
+ delivery_mode: "internal",
231
239
  idempotency_key: "effect:#{effect.effect_id}:#{outcome}"
232
240
  )
233
241
  end
@@ -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
@@ -205,6 +205,9 @@ module SolidObjects
205
205
  class UnknownCommitAction < NonRetryableError
206
206
  end
207
207
 
208
+ class InvalidRejectionCode < NonRetryableError
209
+ end
210
+
208
211
  class Rejected < Error
209
212
  # @rbs @code: String
210
213
  # @rbs @details: Hash[String, untyped]
@@ -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,20 +293,26 @@ 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 => {} }
299
301
  end
300
302
 
301
303
  broadcasts.each do |observable_name, value|
304
+ stored_value = if observable_name != PayloadBroadcast::REVISION_OBSERVABLE &&
305
+ actor.class.definition.broadcasts_observable_value?(observable_name)
306
+ value
307
+ else
308
+ {}
309
+ end
302
310
  Broadcast.create!(
303
- message: locked_message,
311
+ message:,
304
312
  instance:,
305
313
  broadcast_id: SecureRandom.uuid,
306
314
  observable_name:,
307
- value:,
315
+ value: stored_value,
308
316
  state_version: actor.class.state_version,
309
317
  activation_generation: activation.lease.generation,
310
318
  status: "pending",
@@ -332,7 +340,7 @@ module SolidObjects
332
340
 
333
341
  if error.is_a?(NonRetryableError) ||
334
342
  locked_message.attempt_count >= locked_message.max_attempts
335
- create_dead_letter(locked_message, error_details, now)
343
+ create_dead_letter(message: locked_message, error_details:, now:)
336
344
  dead = true
337
345
  else
338
346
  ReadyMessage.create!(
@@ -410,20 +418,20 @@ module SolidObjects
410
418
  )
411
419
  end
412
420
 
413
- # @rbs (Message, Hash[String, untyped], Time) -> DeadLetter
414
- def create_dead_letter(locked_message, error_details, now)
421
+ # @rbs (message: Message, error_details: Hash[String, untyped], now: Time) -> DeadLetter
422
+ def create_dead_letter(message:, error_details:, now:)
415
423
  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,
424
+ message:,
425
+ instance: message.instance,
426
+ actor_type: message.actor_type,
427
+ actor_id: message.actor_id,
428
+ operation: message.operation,
429
+ arguments: message.arguments,
430
+ attempts: message.attempt_count,
423
431
  exception_class: error_details.fetch("class"),
424
432
  exception_message: error_details.fetch("message"),
425
433
  backtrace: error_details.fetch("backtrace"),
426
- first_failed_at: locked_message.last_failed_at || now,
434
+ first_failed_at: message.last_failed_at || now,
427
435
  last_failed_at: now
428
436
  )
429
437
  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? &&