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
@@ -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)
@@ -19,15 +19,16 @@ module SolidObjects
19
19
  @shutdown_requested = false
20
20
  end
21
21
 
22
- # @rbs () -> bool
23
- def run_once
22
+ # @rbs (?now: Time?) -> bool
23
+ def run_once(now: nil)
24
24
  return false if stopped?
25
25
 
26
+ now = normalize_test_time(now)
26
27
  process_registry.heartbeat
27
- reminder = claim_next
28
+ reminder = claim_next(now:)
28
29
  return false unless reminder
29
30
 
30
- enqueue(reminder).present?
31
+ enqueue(reminder, now:).present?
31
32
  rescue
32
33
  release(reminder) if reminder
33
34
  raise
@@ -74,13 +75,14 @@ module SolidObjects
74
75
 
75
76
  attr_reader :process_registry, :database_adapter
76
77
 
77
- # @rbs () -> Reminder?
78
- def claim_next
78
+ # @rbs (now: Time?) -> Reminder?
79
+ def claim_next(now:)
79
80
  database_adapter.transaction do
80
- now = database_adapter.database_now
81
- stale_at = now - SolidObjects.configuration.process_alive_threshold
81
+ database_now = database_adapter.database_now
82
+ due_at = now || database_now
83
+ stale_at = database_now - SolidObjects.configuration.process_alive_threshold
82
84
  relation = Reminder
83
- .where(status: "scheduled", next_run_at: ..now)
85
+ .where(status: "scheduled", next_run_at: ..due_at)
84
86
  .where("claimed_by IS NULL OR claimed_at <= ?", stale_at)
85
87
  .order(:next_run_at, :id)
86
88
  reminder = database_adapter.lock_candidates(relation).first
@@ -88,17 +90,17 @@ module SolidObjects
88
90
 
89
91
  reminder.update!(
90
92
  claimed_by: process_registry.process_record.id,
91
- claimed_at: now
93
+ claimed_at: database_now
92
94
  )
93
95
  reminder
94
96
  end
95
97
  end
96
98
 
97
- # @rbs (Reminder) -> MessageReference?
98
- def enqueue(reminder)
99
+ # @rbs (Reminder, now: Time?) -> MessageReference?
100
+ def enqueue(reminder, now:)
99
101
  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}"
102
+ unless actor_class.definition.messages.key?(reminder.operation.to_sym)
103
+ raise UnknownMessage, "unknown reminder operation #{reminder.operation.inspect}"
102
104
  end
103
105
 
104
106
  mailbox = Mailbox.new(database_adapter:)
@@ -109,12 +111,12 @@ module SolidObjects
109
111
  locked_reminder = Reminder.lock.find_by(id: reminder.id)
110
112
  next unless locked_reminder
111
113
  verify_claim!(locked_reminder)
112
- now = database_adapter.database_now
114
+ schedule_now = now || database_adapter.database_now
113
115
  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",
116
+ reference: Reference.new(actor_type: instance.actor_type, actor_id: instance.actor_id),
117
+ operation: locked_reminder.operation,
118
+ arguments: locked_reminder.arguments,
119
+ delivery_mode: "internal",
118
120
  idempotency_key: "reminder:#{locked_reminder.id}:#{locked_reminder.occurrence}",
119
121
  actor_class:
120
122
  )
@@ -122,7 +124,7 @@ module SolidObjects
122
124
  locked_reminder.update!(
123
125
  status: recurring ? "scheduled" : "completed",
124
126
  occurrence: locked_reminder.occurrence + 1,
125
- next_run_at: recurring ? next_run_at(locked_reminder, now) : locked_reminder.next_run_at,
127
+ next_run_at: recurring ? next_run_at(locked_reminder, schedule_now) : locked_reminder.next_run_at,
126
128
  claimed_by: nil,
127
129
  claimed_at: nil
128
130
  )
@@ -164,5 +166,17 @@ module SolidObjects
164
166
  missed_intervals = ((now - next_run) / interval).floor + 1
165
167
  next_run + (missed_intervals * interval)
166
168
  end
169
+
170
+ # @rbs (Time?) -> Time?
171
+ def normalize_test_time(now)
172
+ return unless now
173
+
174
+ time = now.to_time
175
+ return time if time.to_f.finite?
176
+
177
+ raise ArgumentError
178
+ rescue ArgumentError, NoMethodError, RangeError
179
+ raise ArgumentError, "reminder test time must be a valid time"
180
+ end
167
181
  end
168
182
  end
@@ -8,7 +8,7 @@ module SolidObjects
8
8
  instance = Instance.uncached { Instance.find(message.instance_id) }
9
9
  blocker = earlier_blocker(message)
10
10
  status = message_status(message)
11
- waiting_on = waiting_reason(message, instance, blocker)
11
+ waiting_on = waiting_reason(message:, instance:, blocker:)
12
12
  activation = activation_details(instance)
13
13
  build_error(
14
14
  message,
@@ -38,7 +38,7 @@ module SolidObjects
38
38
  timeout:,
39
39
  actor_type: message_reference.actor_type,
40
40
  actor_id: message_reference.actor_id,
41
- message_name: "unknown",
41
+ operation: "unknown",
42
42
  message_id: message_reference.id,
43
43
  request_id: message_reference.request_id,
44
44
  sequence: message_reference.sequence,
@@ -70,7 +70,7 @@ module SolidObjects
70
70
  timeout:,
71
71
  actor_type: message.actor_type,
72
72
  actor_id: message.actor_id,
73
- message_name: message.message_name,
73
+ operation: message.operation,
74
74
  message_id: message.id,
75
75
  request_id: message.request_id,
76
76
  sequence: message.sequence,
@@ -105,8 +105,8 @@ module SolidObjects
105
105
  "unknown"
106
106
  end
107
107
 
108
- # @rbs (Message, Instance, Message?) -> String
109
- def waiting_reason(message, instance, blocker)
108
+ # @rbs (message: Message, instance: Instance, blocker: Message?) -> String
109
+ def waiting_reason(message:, instance:, blocker:)
110
110
  return "actor_paused" if instance.paused_at
111
111
  return "activation_held" if live_activation?(instance)
112
112
  return "earlier_message" if blocker
@@ -179,7 +179,7 @@ module SolidObjects
179
179
  {
180
180
  "message_id" => blocker.id,
181
181
  "sequence" => blocker.sequence,
182
- "message_name" => blocker.message_name,
182
+ "operation" => blocker.operation,
183
183
  "status" => message_status(blocker)
184
184
  }
185
185
  end
@@ -68,6 +68,22 @@ module SolidObjects
68
68
  runners&.uniq&.each(&:stop)
69
69
  end
70
70
 
71
+ # @rbs (now: Time, ?max_reminders: Integer) -> Integer
72
+ def run_due_reminders(now:, max_reminders: 10_000)
73
+ unless max_reminders.is_a?(Integer) && max_reminders.positive?
74
+ raise ArgumentError, "max_reminders must be a positive integer"
75
+ end
76
+
77
+ scheduler = ReminderScheduler.new
78
+ processed = 0
79
+ while processed < max_reminders && scheduler.run_once(now:)
80
+ processed += 1
81
+ end
82
+ processed
83
+ ensure
84
+ scheduler&.stop
85
+ end
86
+
71
87
  private
72
88
 
73
89
  # @rbs (Array[Symbol]) -> Array[Worker | EffectExecutor | ReminderScheduler | BroadcastExecutor]
@@ -16,14 +16,14 @@ module SolidObjects
16
16
  actor_type: broadcast.instance.actor_type,
17
17
  actor_id: broadcast.instance.actor_id
18
18
  )
19
- stream = if broadcast.observable_name == PayloadBroadcast::REVISION_OBSERVABLE
20
- ""
21
- else
19
+ stream = if broadcast.broadcasts_value?
22
20
  observable_value(
23
- reference,
24
- broadcast.observable_name,
25
- broadcast.value
21
+ reference:,
22
+ name: broadcast.observable_name,
23
+ value: broadcast.value
26
24
  )
25
+ else
26
+ ""
27
27
  end
28
28
  metadata = Base64.urlsafe_encode64(
29
29
  JSON.generate(
@@ -35,16 +35,16 @@ module SolidObjects
35
35
  "#{stream}<!--#{INVALIDATION_MARKER}#{metadata}-->"
36
36
  end
37
37
 
38
- # @rbs (Reference, Symbol | String, untyped) -> String
39
- def observable_value(reference, name, value)
38
+ # @rbs (reference: Reference, name: Symbol | String, value: untyped) -> String
39
+ def observable_value(reference:, name:, value:)
40
40
  target = DomIdentity.observable(reference, name)
41
41
  content = ERB::Util.html_escape(display_value(value))
42
42
  %(<turbo-stream action="replace" target="#{target}"><template><span id="#{target}">#{content}</span></template></turbo-stream>)
43
43
  end
44
44
 
45
- # @rbs (ComponentRegistration, Integer, Integer) -> String
46
- def component_refresh(registration, instance_id, revision)
47
- return morph_component_refresh(registration, instance_id, revision) if registration.morph?
45
+ # @rbs (registration: ComponentRegistration, instance_id: Integer, revision: Integer) -> String
46
+ def component_refresh(registration:, instance_id:, revision:)
47
+ return morph_component_refresh(registration:, instance_id:, revision:) if registration.morph?
48
48
 
49
49
  target = registration.dom_id
50
50
  source = ERB::Util.html_escape(
@@ -54,13 +54,13 @@ module SolidObjects
54
54
  %(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
55
55
  end
56
56
 
57
- # @rbs (Array[ComponentRegistration], Integer, Integer) -> String
58
- def batch_refresh(registrations, instance_id, revision)
57
+ # @rbs (registrations: Array[ComponentRegistration], instance_id: Integer, revision: Integer) -> String
58
+ def batch_refresh(registrations:, instance_id:, revision:)
59
59
  first = registrations.first
60
60
  scope = DomIdentity.scope(first.reference)
61
61
  batch = ERB::Util.html_escape(first.batch)
62
62
  source = ERB::Util.html_escape(
63
- first.batch_refresh_url(registrations, instance_id, revision)
63
+ first.batch_refresh_url(registrations:, instance_id:, revision:)
64
64
  )
65
65
  targets = ERB::Util.html_escape(registrations.map(&:dom_id).join(" "))
66
66
  %(<turbo-stream action="append" target="#{scope}"><template><solid-objects-batch-refresh data-batch="#{batch}" data-revision="#{instance_id}:#{revision}" data-targets="#{targets}" data-source="#{source}"></solid-objects-batch-refresh></template></turbo-stream>)
@@ -104,8 +104,8 @@ module SolidObjects
104
104
  end
105
105
  private_class_method :display_value
106
106
 
107
- # @rbs (ComponentRegistration, Integer, Integer) -> String
108
- def morph_component_refresh(registration, instance_id, revision)
107
+ # @rbs (registration: ComponentRegistration, instance_id: Integer, revision: Integer) -> String
108
+ def morph_component_refresh(registration:, instance_id:, revision:)
109
109
  source = ERB::Util.html_escape(
110
110
  registration.refresh_url(instance_id, revision)
111
111
  )
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.11.0"
4
+ VERSION = "0.12.1"
5
5
  end
data/lib/solid_objects.rb CHANGED
@@ -21,6 +21,7 @@ require "solid_objects/serialization"
21
21
  require "solid_objects/context"
22
22
  require "solid_objects/actor_registry"
23
23
  require "solid_objects/state"
24
+ require "solid_objects/operation_dispatcher"
24
25
  require "solid_objects/actor_definition"
25
26
  require "solid_objects/application_write_guard"
26
27
  require "solid_objects/actor"
@@ -39,7 +39,7 @@ module SolidObjects
39
39
  # @rbs () -> untyped
40
40
  def component_view_context: () -> untyped
41
41
 
42
- # @rbs (ComponentRegistration, ActorSnapshot, untyped) -> String
43
- def component_frame: (ComponentRegistration, ActorSnapshot, untyped) -> String
42
+ # @rbs (registration: ComponentRegistration, snapshot: ActorSnapshot, rendered: untyped) -> String
43
+ def component_frame: (registration: ComponentRegistration, snapshot: ActorSnapshot, rendered: untyped) -> String
44
44
  end
45
45
  end
@@ -7,16 +7,16 @@ module SolidObjects
7
7
 
8
8
  attr_reader arguments(): untyped
9
9
 
10
- attr_reader success_message_name(): untyped
10
+ attr_reader success_operation(): untyped
11
11
 
12
- attr_reader failure_message_name(): untyped
12
+ attr_reader failure_operation(): untyped
13
13
 
14
- def self.new: (untyped name, untyped arguments, untyped success_message_name, untyped failure_message_name) -> instance
15
- | (name: untyped, arguments: untyped, success_message_name: untyped, failure_message_name: untyped) -> instance
14
+ def self.new: (untyped name, untyped arguments, untyped success_operation, untyped failure_operation) -> instance
15
+ | (name: untyped, arguments: untyped, success_operation: untyped, failure_operation: untyped) -> instance
16
16
 
17
- def self.members: () -> [ :name, :arguments, :success_message_name, :failure_message_name ]
17
+ def self.members: () -> [ :name, :arguments, :success_operation, :failure_operation ]
18
18
 
19
- def members: () -> [ :name, :arguments, :success_message_name, :failure_message_name ]
19
+ def members: () -> [ :name, :arguments, :success_operation, :failure_operation ]
20
20
  end
21
21
 
22
22
  class CommitActionIntent < Data
@@ -56,7 +56,7 @@ module SolidObjects
56
56
 
57
57
  attr_reader actor_id(): untyped
58
58
 
59
- attr_reader message_name(): untyped
59
+ attr_reader operation(): untyped
60
60
 
61
61
  attr_reader arguments(): untyped
62
62
 
@@ -64,12 +64,12 @@ module SolidObjects
64
64
 
65
65
  attr_reader idempotency_key(): untyped
66
66
 
67
- def self.new: (untyped actor_type, untyped actor_id, untyped message_name, untyped arguments, untyped available_at, untyped idempotency_key) -> instance
68
- | (actor_type: untyped, actor_id: untyped, message_name: untyped, arguments: untyped, available_at: untyped, idempotency_key: untyped) -> instance
67
+ def self.new: (untyped actor_type, untyped actor_id, untyped operation, untyped arguments, untyped available_at, untyped idempotency_key) -> instance
68
+ | (actor_type: untyped, actor_id: untyped, operation: untyped, arguments: untyped, available_at: untyped, idempotency_key: untyped) -> instance
69
69
 
70
- def self.members: () -> [ :actor_type, :actor_id, :message_name, :arguments, :available_at, :idempotency_key ]
70
+ def self.members: () -> [ :actor_type, :actor_id, :operation, :arguments, :available_at, :idempotency_key ]
71
71
 
72
- def members: () -> [ :actor_type, :actor_id, :message_name, :arguments, :available_at, :idempotency_key ]
72
+ def members: () -> [ :actor_type, :actor_id, :operation, :arguments, :available_at, :idempotency_key ]
73
73
  end
74
74
 
75
75
  # @rbs (Class) -> void
@@ -87,8 +87,8 @@ module SolidObjects
87
87
  # @rbs (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler
88
88
  def self.query: (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler
89
89
 
90
- # @rbs (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
91
- def self.observable: (Symbol | String) ?{ () -> untyped } -> ActorDefinition::Handler
90
+ # @rbs (Symbol | String, ?broadcast: Symbol) ?{ () -> untyped } -> ActorDefinition::Handler
91
+ def self.observable: (Symbol | String, ?broadcast: Symbol) ?{ () -> untyped } -> ActorDefinition::Handler
92
92
 
93
93
  # @rbs (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
94
94
  def self.broadcast_payload: (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler
@@ -157,11 +157,11 @@ module SolidObjects
157
157
  # @rbs (Symbol | String, **untyped) -> nil
158
158
  def commit_action: (Symbol | String, **untyped) -> nil
159
159
 
160
- # @rbs (Symbol | String, at: Time, ?every: Numeric?, ?missed: Symbol | String, arguments: Hash[Symbol | String, untyped]) -> nil
161
- def schedule: (Symbol | String, at: Time, arguments: Hash[Symbol | String, untyped], ?every: Numeric?, ?missed: Symbol | String) -> nil
160
+ # @rbs (at: Time, ?every: Numeric?, ?missed: Symbol | String) -> OperationDispatcher
161
+ def schedule: (at: Time, ?every: Numeric?, ?missed: Symbol | String) -> OperationDispatcher
162
162
 
163
- # @rbs (Reference, Symbol | String, ?available_at: Time?, ?idempotency_key: String?, **untyped) -> nil
164
- def send_to: (Reference, Symbol | String, ?available_at: Time?, ?idempotency_key: String?, **untyped) -> nil
163
+ # @rbs (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
164
+ def send_to: (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher
165
165
 
166
166
  # @rbs (Symbol | String, Hash[String, untyped]) -> untyped
167
167
  def invoke: (Symbol | String, Hash[String, untyped]) -> untyped
@@ -178,8 +178,8 @@ module SolidObjects
178
178
  # @rbs () -> void
179
179
  def deactivate: () -> void
180
180
 
181
- # @rbs (Reference, Symbol | String, Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent
182
- def stage_outbound_message: (Reference, Symbol | String, Hash[Symbol | String, untyped], idempotency_key: String?, ?available_at: Time?) -> OutboundMessageIntent
181
+ # @rbs (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent
182
+ def stage_outbound_message: (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], idempotency_key: String?, ?available_at: Time?) -> OutboundMessageIntent
183
183
 
184
184
  # @rbs () -> Array[EffectIntent]
185
185
  def drain_effect_intents: () -> Array[EffectIntent]