Class: SolidObjects::Actor
- Inherits:
-
Object
- Object
- SolidObjects::Actor
- Defined in:
- lib/solid_objects/actor.rb,
sig/generated/lib/solid_objects/actor.rbs
Direct Known Subclasses
Defined Under Namespace
Classes: CommitActionIntent, EffectIntent, OutboundMessageIntent, ReminderIntent
Constant Summary collapse
- REMINDER_NAME_LIMIT =
The reminders table holds a name in 191 characters.
191- REMINDER_KEY_SEPARATOR =
":"
Instance Attribute Summary collapse
- #actor_id ⇒ Object readonly
-
#commit_action_intents ⇒ Object
readonly
Returns the value of attribute commit_action_intents.
-
#effect_intents ⇒ Object
readonly
Returns the value of attribute effect_intents.
-
#outbound_message_intents ⇒ Object
readonly
Returns the value of attribute outbound_message_intents.
-
#reminder_intents ⇒ Object
readonly
Returns the value of attribute reminder_intents.
- #state ⇒ Object readonly
Class Method Summary collapse
- .actor_type(value = nil) ⇒ String
- .attribute(name, default: nil) ⇒ StateDefinition::Attribute
- .broadcast_payload(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
- .definition ⇒ ActorDefinition
- .ensure_registered! ⇒ Class
- .inherited(subclass) ⇒ void
- .message(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
- .migrate_state(from:, to:) {|arg0| ... } ⇒ ActorDefinition::StateMigration
- .observable(name, broadcast: :invalidation, &block) ⇒ void
- .on_activate { ... } ⇒ Proc
- .on_deactivate { ... } ⇒ Proc
- .query(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
- .ref(actor_id) ⇒ Reference
- .state_version(version = nil) ⇒ Integer
Instance Method Summary collapse
- #activate ⇒ void
- #commit_action(name, **arguments) ⇒ nil
- #current_message ⇒ MessageContext?
- #deactivate ⇒ void
- #discard_intents ⇒ void
- #drain_commit_action_intents ⇒ Array[CommitActionIntent]
- #drain_effect_intents ⇒ Array[EffectIntent]
- #drain_outbound_message_intents ⇒ Array[OutboundMessageIntent]
- #drain_reminder_intents ⇒ Array[ReminderIntent]
- #emit(name, on_success: nil, on_failure: nil, **arguments) ⇒ nil
- #guard_application_writes(operation) { ... } ⇒ Object
-
#initialize(actor_id:, state:) ⇒ Actor
constructor
A new instance of Actor.
- #invoke(operation, arguments) ⇒ Object
- #keyword_arguments(arguments) ⇒ Hash[Symbol, untyped]
- #observable_value(name) ⇒ Object
- #observable_values ⇒ Hash[String, untyped]
- #reject(code, message, details: {}) ⇒ bot
-
#reminder_name(operation:, key:) ⇒ String
A keyed name is the operation, a colon, and the key, so an operation holding a colon of its own would make two different schedules produce one name: an unkeyed "deliver:item" and a "deliver" keyed "item" would share a row, and the second would silently take the first one's alarm.
-
#schedule(at:, every: nil, missed: :latest, key: nil) ⇒ OperationDispatcher
A reminder is identified by its name, and without a key that name is the operation, so one actor holds one alarm per operation.
- #send_to(reference, available_at: nil, idempotency_key: nil) ⇒ OperationDispatcher
- #stage_outbound_message(reference:, operation:, arguments:, available_at: nil, idempotency_key: nil) ⇒ OutboundMessageIntent
- #validate_effect_callback!(operation) ⇒ void
- #validated_reminder_key(key) ⇒ String?
Constructor Details
#initialize(actor_id:, state:) ⇒ Actor
Returns a new instance of Actor.
153 154 155 156 157 158 159 160 |
# File 'lib/solid_objects/actor.rb', line 153 def initialize(actor_id:, state:) @actor_id = actor_id @state = state @effect_intents = [] @commit_action_intents = [] @reminder_intents = [] @outbound_message_intents = [] end |
Instance Attribute Details
#actor_id ⇒ Object (readonly)
150 151 152 |
# File 'lib/solid_objects/actor.rb', line 150 def actor_id @actor_id end |
#commit_action_intents ⇒ Object (readonly)
Returns the value of attribute commit_action_intents.
377 378 379 |
# File 'lib/solid_objects/actor.rb', line 377 def commit_action_intents @commit_action_intents end |
#effect_intents ⇒ Object (readonly)
Returns the value of attribute effect_intents.
377 378 379 |
# File 'lib/solid_objects/actor.rb', line 377 def effect_intents @effect_intents end |
#outbound_message_intents ⇒ Object (readonly)
Returns the value of attribute outbound_message_intents.
377 378 379 |
# File 'lib/solid_objects/actor.rb', line 377 def @outbound_message_intents end |
#reminder_intents ⇒ Object (readonly)
Returns the value of attribute reminder_intents.
377 378 379 |
# File 'lib/solid_objects/actor.rb', line 377 def reminder_intents @reminder_intents end |
#state ⇒ Object (readonly)
150 151 152 |
# File 'lib/solid_objects/actor.rb', line 150 def state @state end |
Class Method Details
.actor_type(value = nil) ⇒ String
23 24 25 26 27 28 29 30 |
# File 'lib/solid_objects/actor.rb', line 23 def actor_type(value = nil) if value @actor_type = value.to_s SolidObjects.register_actor(@actor_type, self) end @actor_type ||= default_actor_type end |
.attribute(name, default: nil) ⇒ StateDefinition::Attribute
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/solid_objects/actor.rb', line 33 def attribute(name, default: nil) attribute_name = name.to_sym ensure_attribute_methods_available!(attribute_name) attribute = definition.add_attribute(attribute_name, default:) @defining_attribute_methods = true begin define_method(attribute_name) { state.fetch(attribute_name) } define_method(:"#{attribute_name}=") { |value| state.write(attribute_name, value) } generated_attribute_methods[attribute_name] = true generated_attribute_methods[:"#{attribute_name}="] = true ensure @defining_attribute_methods = false end attribute end |
.broadcast_payload(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
65 66 67 68 69 |
# File 'lib/solid_objects/actor.rb', line 65 def broadcast_payload(name, &block) raise InvalidActor, "payload broadcasts require a block" unless block definition.add_payload_broadcast(name, block) end |
.definition ⇒ ActorDefinition
101 102 103 |
# File 'lib/solid_objects/actor.rb', line 101 def definition (@definition ||= ActorDefinition.new).(self) end |
.ensure_registered! ⇒ Class
106 107 108 |
# File 'lib/solid_objects/actor.rb', line 106 def ensure_registered! SolidObjects.register_actor(actor_type, self) end |
.inherited(subclass) ⇒ void
This method returns an undefined value.
16 17 18 19 20 |
# File 'lib/solid_objects/actor.rb', line 16 def inherited(subclass) super subclass.instance_variable_set(:@definition, definition.duplicate) subclass.instance_variable_set(:@generated_attribute_methods, generated_attribute_methods.dup) end |
.message(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
50 51 52 |
# File 'lib/solid_objects/actor.rb', line 50 def (name, &block) definition.(name, block) end |
.migrate_state(from:, to:) {|arg0| ... } ⇒ ActorDefinition::StateMigration
78 79 80 |
# File 'lib/solid_objects/actor.rb', line 78 def migrate_state(from:, to:, &block) definition.add_state_migration(from:, to:, block:) end |
.observable(name, broadcast: :invalidation, &block) ⇒ void
This method returns an undefined value.
60 61 62 |
# File 'lib/solid_objects/actor.rb', line 60 def observable(name, broadcast: :invalidation, &block) definition.add_observable(name, block, broadcast:) end |
.on_activate { ... } ⇒ Proc
83 84 85 86 |
# File 'lib/solid_objects/actor.rb', line 83 def on_activate(&block) definition.activation_hooks << block block end |
.on_deactivate { ... } ⇒ Proc
89 90 91 92 |
# File 'lib/solid_objects/actor.rb', line 89 def on_deactivate(&block) definition.deactivation_hooks << block block end |
.query(name) {|arg0, arg1| ... } ⇒ ActorDefinition::Handler
55 56 57 |
# File 'lib/solid_objects/actor.rb', line 55 def query(name, &block) definition.add_query(name, block) end |
.ref(actor_id) ⇒ Reference
95 96 97 98 |
# File 'lib/solid_objects/actor.rb', line 95 def ref(actor_id) ensure_registered! Reference.new(actor_type:, actor_id: actor_id.to_s) end |
.state_version(version = nil) ⇒ Integer
72 73 74 75 |
# File 'lib/solid_objects/actor.rb', line 72 def state_version(version = nil) definition.set_state_version(version) if version definition.state_version end |
Instance Method Details
#activate ⇒ void
This method returns an undefined value.
322 323 324 325 326 |
# File 'lib/solid_objects/actor.rb', line 322 def activate guard_application_writes("on_activate") do self.class.definition.activation_hooks.each { |hook| instance_exec(&hook) } end end |
#commit_action(name, **arguments) ⇒ nil
194 195 196 197 198 199 200 201 202 |
# File 'lib/solid_objects/actor.rb', line 194 def commit_action(name, **arguments) CommitActionIntent.new( name: name.to_s, arguments: Serialization.dump(arguments) ).tap do |intent| commit_action_intents << intent end nil end |
#current_message ⇒ MessageContext?
163 164 165 |
# File 'lib/solid_objects/actor.rb', line 163 def Context. end |
#deactivate ⇒ void
This method returns an undefined value.
329 330 331 332 333 |
# File 'lib/solid_objects/actor.rb', line 329 def deactivate guard_application_writes("on_deactivate") do self.class.definition.deactivation_hooks.each { |hook| instance_exec(&hook) } end end |
#discard_intents ⇒ void
This method returns an undefined value.
368 369 370 371 372 373 |
# File 'lib/solid_objects/actor.rb', line 368 def discard_intents effect_intents.clear commit_action_intents.clear reminder_intents.clear .clear end |
#drain_commit_action_intents ⇒ Array[CommitActionIntent]
353 354 355 |
# File 'lib/solid_objects/actor.rb', line 353 def drain_commit_action_intents commit_action_intents.shift(commit_action_intents.length) end |
#drain_effect_intents ⇒ Array[EffectIntent]
348 349 350 |
# File 'lib/solid_objects/actor.rb', line 348 def drain_effect_intents effect_intents.shift(effect_intents.length) end |
#drain_outbound_message_intents ⇒ Array[OutboundMessageIntent]
363 364 365 |
# File 'lib/solid_objects/actor.rb', line 363 def .shift(.length) end |
#drain_reminder_intents ⇒ Array[ReminderIntent]
358 359 360 |
# File 'lib/solid_objects/actor.rb', line 358 def drain_reminder_intents reminder_intents.shift(reminder_intents.length) end |
#emit(name, on_success: nil, on_failure: nil, **arguments) ⇒ nil
179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/solid_objects/actor.rb', line 179 def emit(name, on_success: nil, on_failure: nil, **arguments) validate_effect_callback!(on_success) validate_effect_callback!(on_failure) EffectIntent.new( name: name.to_s, arguments: Serialization.dump(arguments), success_operation: on_success&.to_s, failure_operation: on_failure&.to_s ).tap do |intent| effect_intents << intent end nil end |
#guard_application_writes(operation) { ... } ⇒ Object
383 384 385 386 387 388 389 390 |
# File 'lib/solid_objects/actor.rb', line 383 def guard_application_writes(operation, &block) ApplicationWriteGuard.call( actor_type: self.class.actor_type, actor_id:, operation:, &block ) end |
#invoke(operation, arguments) ⇒ Object
291 292 293 294 295 296 297 298 299 |
# File 'lib/solid_objects/actor.rb', line 291 def invoke(operation, arguments) handler = self.class.definition.[operation.to_sym] || self.class.definition.queries[operation.to_sym] raise UnknownMessage, "unknown operation #{operation.inspect} for #{self.class.actor_type}" unless handler guard_application_writes(operation.to_s) do instance_exec(**keyword_arguments(arguments), &handler.block) end end |
#keyword_arguments(arguments) ⇒ Hash[Symbol, untyped]
393 394 395 |
# File 'lib/solid_objects/actor.rb', line 393 def keyword_arguments(arguments) arguments.each_with_object({}) { |(key, value), converted| converted[key.to_sym] = value } end |
#observable_value(name) ⇒ Object
311 312 313 314 315 316 317 318 319 |
# File 'lib/solid_objects/actor.rb', line 311 def observable_value(name) observable_name = name.to_sym handler = self.class.definition.observables[observable_name] raise UnknownMessage, "unknown observable #{name.inspect}" unless handler guard_application_writes("observable.#{observable_name}") do Serialization.dump(instance_exec(&handler.block)) end end |
#observable_values ⇒ Hash[String, untyped]
302 303 304 305 306 307 308 |
# File 'lib/solid_objects/actor.rb', line 302 def observable_values guard_application_writes("observables") do self.class.definition.observables.each_with_object({}) do |(name, handler), values| values[name.to_s] = Serialization.dump(instance_exec(&handler.block)) end end end |
#reject(code, message, details: {}) ⇒ bot
168 169 170 171 172 173 174 175 176 |
# File 'lib/solid_objects/actor.rb', line 168 def reject(code, , details: {}) rejection_code = code.to_s unless rejection_code.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/) raise InvalidRejectionCode, "invalid rejection code #{rejection_code.inspect}; expected a letter or underscore followed by letters, digits, or underscores" end raise Rejected.new(code: rejection_code, message:, details:) end |
#reminder_name(operation:, key:) ⇒ String
A keyed name is the operation, a colon, and the key, so an operation holding a colon of its own would make two different schedules produce one name: an unkeyed "deliver:item" and a "deliver" keyed "item" would share a row, and the second would silently take the first one's alarm. Refusing a colon in the operation keeps unkeyed names free of colons, which leaves the two kinds of name disjoint and lets a key hold colons of its own.
The length is checked on the composed name rather than the key alone, because a long operation and a short key can exceed the column just as easily as the reverse. Both are refused here rather than at the insert, once the turn is already doing work.
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 |
# File 'lib/solid_objects/actor.rb', line 261 def reminder_name(operation:, key:) operation_name = operation.to_s if operation_name.include?(REMINDER_KEY_SEPARATOR) raise ArgumentError, "reminder operation #{operation_name.inspect} must not contain #{REMINDER_KEY_SEPARATOR.inspect}" end return operation_name if key.nil? name = "#{operation_name}#{REMINDER_KEY_SEPARATOR}#{key}" if name.length > REMINDER_NAME_LIMIT raise ArgumentError, "reminder name #{name.length} characters exceeds the #{REMINDER_NAME_LIMIT} the database holds" end name end |
#schedule(at:, every: nil, missed: :latest, key: nil) ⇒ OperationDispatcher
A reminder is identified by its name, and without a key that name is the operation, so one actor holds one alarm per operation. A key gives an actor an alarm per item it is waiting on, which is what an actor holding a queue of scheduled work needs; the key is the caller's own identifier for the item, and scheduling the same key again moves that item's alarm.
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/solid_objects/actor.rb', line 210 def schedule(at:, every: nil, missed: :latest, key: nil) interval_seconds = every&.to_f if interval_seconds && !interval_seconds.positive? raise ArgumentError, "reminder interval must be positive" end missed_policy = missed.to_s unless %w[all latest].include?(missed_policy) raise ArgumentError, "missed reminder policy must be all or latest" end reminder_key = validated_reminder_key(key) OperationDispatcher.new( actor_type: self.class.actor_type, handlers: self.class.definition. ) do |operation, arguments| ReminderIntent.new( name: reminder_name(operation:, key: reminder_key), operation: operation.to_s, at:, arguments: Serialization.dump(arguments), interval_seconds:, missed_policy: ).tap do |intent| reminder_intents << intent end nil end end |
#send_to(reference, available_at: nil, idempotency_key: nil) ⇒ OperationDispatcher
279 280 281 282 283 284 285 286 287 288 |
# File 'lib/solid_objects/actor.rb', line 279 def send_to(reference, available_at: nil, idempotency_key: nil) actor_class = SolidObjects.registry.fetch(reference.actor_type) OperationDispatcher.new( actor_type: reference.actor_type, handlers: actor_class.definition. ) do |operation, arguments| (reference:, operation:, arguments:, available_at:, idempotency_key:) nil end end |
#stage_outbound_message(reference:, operation:, arguments:, available_at: nil, idempotency_key: nil) ⇒ OutboundMessageIntent
336 337 338 339 340 341 342 343 344 345 |
# File 'lib/solid_objects/actor.rb', line 336 def (reference:, operation:, arguments:, available_at: nil, idempotency_key: nil) OutboundMessageIntent.new( actor_type: reference.actor_type, actor_id: reference.actor_id, operation: operation.to_s, arguments: Serialization.dump(arguments), available_at:, idempotency_key: ).tap { |intent| << intent } end |
#validate_effect_callback!(operation) ⇒ void
This method returns an undefined value.
398 399 400 401 402 403 |
# File 'lib/solid_objects/actor.rb', line 398 def validate_effect_callback!(operation) return unless operation return if self.class.definition..key?(operation.to_sym) raise UnknownMessage, "unknown effect callback operation #{operation.inspect}" end |
#validated_reminder_key(key) ⇒ String?
240 241 242 243 244 245 246 247 |
# File 'lib/solid_objects/actor.rb', line 240 def validated_reminder_key(key) return nil if key.nil? reminder_key = key.to_s raise ArgumentError, "reminder key must not be empty" if reminder_key.empty? reminder_key end |