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
- #transmit ⇒ OperationDispatcher
- #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.
388 389 390 |
# File 'lib/solid_objects/actor.rb', line 388 def commit_action_intents @commit_action_intents end |
#effect_intents ⇒ Object (readonly)
Returns the value of attribute effect_intents.
388 389 390 |
# File 'lib/solid_objects/actor.rb', line 388 def effect_intents @effect_intents end |
#outbound_message_intents ⇒ Object (readonly)
Returns the value of attribute outbound_message_intents.
388 389 390 |
# File 'lib/solid_objects/actor.rb', line 388 def @outbound_message_intents end |
#reminder_intents ⇒ Object (readonly)
Returns the value of attribute reminder_intents.
388 389 390 |
# File 'lib/solid_objects/actor.rb', line 388 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.
333 334 335 336 337 |
# File 'lib/solid_objects/actor.rb', line 333 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
205 206 207 208 209 210 211 212 213 |
# File 'lib/solid_objects/actor.rb', line 205 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.
340 341 342 343 344 |
# File 'lib/solid_objects/actor.rb', line 340 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.
379 380 381 382 383 384 |
# File 'lib/solid_objects/actor.rb', line 379 def discard_intents effect_intents.clear commit_action_intents.clear reminder_intents.clear .clear end |
#drain_commit_action_intents ⇒ Array[CommitActionIntent]
364 365 366 |
# File 'lib/solid_objects/actor.rb', line 364 def drain_commit_action_intents commit_action_intents.shift(commit_action_intents.length) end |
#drain_effect_intents ⇒ Array[EffectIntent]
359 360 361 |
# File 'lib/solid_objects/actor.rb', line 359 def drain_effect_intents effect_intents.shift(effect_intents.length) end |
#drain_outbound_message_intents ⇒ Array[OutboundMessageIntent]
374 375 376 |
# File 'lib/solid_objects/actor.rb', line 374 def .shift(.length) end |
#drain_reminder_intents ⇒ Array[ReminderIntent]
369 370 371 |
# File 'lib/solid_objects/actor.rb', line 369 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
394 395 396 397 398 399 400 401 |
# File 'lib/solid_objects/actor.rb', line 394 def guard_application_writes(operation, &block) ApplicationWriteGuard.call( actor_type: self.class.actor_type, actor_id:, operation:, &block ) end |
#invoke(operation, arguments) ⇒ Object
302 303 304 305 306 307 308 309 310 |
# File 'lib/solid_objects/actor.rb', line 302 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]
404 405 406 |
# File 'lib/solid_objects/actor.rb', line 404 def keyword_arguments(arguments) arguments.each_with_object({}) { |(key, value), converted| converted[key.to_sym] = value } end |
#observable_value(name) ⇒ Object
322 323 324 325 326 327 328 329 330 |
# File 'lib/solid_objects/actor.rb', line 322 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]
313 314 315 316 317 318 319 |
# File 'lib/solid_objects/actor.rb', line 313 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.
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 |
# File 'lib/solid_objects/actor.rb', line 272 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.
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/solid_objects/actor.rb', line 221 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
290 291 292 293 294 295 296 297 298 299 |
# File 'lib/solid_objects/actor.rb', line 290 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
347 348 349 350 351 352 353 354 355 356 |
# File 'lib/solid_objects/actor.rb', line 347 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 |
#transmit ⇒ OperationDispatcher
194 195 196 197 198 199 200 201 202 |
# File 'lib/solid_objects/actor.rb', line 194 def transmit OperationDispatcher.new( actor_type: self.class.actor_type, handlers: self.class.definition. ) do |operation, arguments| emit(Transmission::EFFECT_NAME, operation: operation.to_s, arguments:) nil end end |
#validate_effect_callback!(operation) ⇒ void
This method returns an undefined value.
409 410 411 412 413 414 |
# File 'lib/solid_objects/actor.rb', line 409 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?
251 252 253 254 255 256 257 258 |
# File 'lib/solid_objects/actor.rb', line 251 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 |