Class: SolidObjects::Actor

Inherits:
Object
  • Object
show all
Defined in:
lib/solid_objects/actor.rb,
sig/generated/lib/solid_objects/actor.rbs

Direct Known Subclasses

Doctor::ProbeActor

Defined Under Namespace

Classes: CommitActionIntent, EffectIntent, OutboundMessageIntent, ReminderIntent

Constant Summary collapse

REMINDER_NAME_LIMIT =

The reminders table holds a name in 191 characters.

Returns:

  • (::Integer)
191
REMINDER_KEY_SEPARATOR =

Returns:

  • (::String)
":"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(actor_id:, state:) ⇒ Actor

Returns a new instance of Actor.

RBS:

  • (actor_id: String, state: State) -> void

Parameters:

  • actor_id: (String)
  • state: (State)


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_idObject (readonly)

RBS:

  • @actor_id: String

  • @state: State

  • @effect_intents: Array[EffectIntent]

  • @commit_action_intents: Array[CommitActionIntent]

  • @reminder_intents: Array[ReminderIntent]

  • @outbound_message_intents: Array[OutboundMessageIntent]

Returns:

  • (Object)


150
151
152
# File 'lib/solid_objects/actor.rb', line 150

def actor_id
  @actor_id
end

#commit_action_intentsObject (readonly)

Returns the value of attribute commit_action_intents.

Returns:

  • (Object)


377
378
379
# File 'lib/solid_objects/actor.rb', line 377

def commit_action_intents
  @commit_action_intents
end

#effect_intentsObject (readonly)

Returns the value of attribute effect_intents.

Returns:

  • (Object)


377
378
379
# File 'lib/solid_objects/actor.rb', line 377

def effect_intents
  @effect_intents
end

#outbound_message_intentsObject (readonly)

Returns the value of attribute outbound_message_intents.

Returns:

  • (Object)


377
378
379
# File 'lib/solid_objects/actor.rb', line 377

def outbound_message_intents
  @outbound_message_intents
end

#reminder_intentsObject (readonly)

Returns the value of attribute reminder_intents.

Returns:

  • (Object)


377
378
379
# File 'lib/solid_objects/actor.rb', line 377

def reminder_intents
  @reminder_intents
end

#stateObject (readonly)

RBS:

  • @actor_id: String

  • @state: State

  • @effect_intents: Array[EffectIntent]

  • @commit_action_intents: Array[CommitActionIntent]

  • @reminder_intents: Array[ReminderIntent]

  • @outbound_message_intents: Array[OutboundMessageIntent]

Returns:

  • (Object)


150
151
152
# File 'lib/solid_objects/actor.rb', line 150

def state
  @state
end

Class Method Details

.actor_type(value = nil) ⇒ String

RBS:

  • (?String | Symbol) -> String

Parameters:

  • (String, Symbol)

Returns:

  • (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

RBS:

  • (Symbol | String, ?default: untyped) -> StateDefinition::Attribute

Parameters:

  • (Symbol, String)
  • default: (Object) (defaults to: nil)

Returns:



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

RBS:

  • (Symbol | String) { (untyped, untyped) -> untyped } -> ActorDefinition::Handler

Parameters:

  • (Symbol, String)

Yields:

Yield Parameters:

  • arg0 (Object)
  • arg1 (Object)

Yield Returns:

  • (Object)

Returns:



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

.definitionActorDefinition

RBS:

  • () -> ActorDefinition

Returns:



101
102
103
# File 'lib/solid_objects/actor.rb', line 101

def definition
  (@definition ||= ActorDefinition.new).synchronize_instance_messages(self)
end

.ensure_registered!Class

RBS:

  • () -> Class

Returns:

  • (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.

RBS:

  • (Class) -> void

Parameters:

  • (Class)


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

RBS:

  • (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler

Parameters:

  • (Symbol, String)

Yields:

Yield Parameters:

  • arg0 (Object)
  • arg1 (Object)

Yield Returns:

  • (Object)

Returns:



50
51
52
# File 'lib/solid_objects/actor.rb', line 50

def message(name, &block)
  definition.add_message(name, block)
end

.migrate_state(from:, to:) {|arg0| ... } ⇒ ActorDefinition::StateMigration

RBS:

  • (from: Integer, to: Integer) { (Hash[String, untyped]) -> Hash[String, untyped] } -> ActorDefinition::StateMigration

Parameters:

  • from: (Integer)
  • to: (Integer)

Yields:

Yield Parameters:

  • arg0 (Hash[String, untyped])

Yield Returns:

  • (Hash[String, untyped])

Returns:



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.

RBS:

  • (Symbol | String, ?broadcast: Symbol) ?{ () -> untyped } -> ActorDefinition::Handler

Parameters:

  • (Symbol, String)
  • broadcast: (Symbol) (defaults to: :invalidation)


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

RBS:

  • () { () -> untyped } -> Proc

Yields:

Yield Returns:

  • (Object)

Returns:

  • (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

RBS:

  • () { () -> untyped } -> Proc

Yields:

Yield Returns:

  • (Object)

Returns:

  • (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

RBS:

  • (Symbol | String) { (*untyped, **untyped) -> untyped } -> ActorDefinition::Handler

Parameters:

  • (Symbol, String)

Yields:

Yield Parameters:

  • arg0 (Object)
  • arg1 (Object)

Yield Returns:

  • (Object)

Returns:



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

RBS:

  • (String | Integer) -> Reference

Parameters:

  • (String, Integer)

Returns:



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

RBS:

  • (?Integer) -> Integer

Parameters:

  • (Integer)

Returns:

  • (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

#activatevoid

This method returns an undefined value.

RBS:

  • () -> void



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

RBS:

  • (Symbol | String, **untyped) -> nil

Parameters:

  • (Symbol, String)
  • (Object)

Returns:

  • (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_messageMessageContext?

RBS:

  • () -> MessageContext?

Returns:



163
164
165
# File 'lib/solid_objects/actor.rb', line 163

def current_message
  Context.current_message
end

#deactivatevoid

This method returns an undefined value.

RBS:

  • () -> void



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_intentsvoid

This method returns an undefined value.

RBS:

  • () -> void



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
  outbound_message_intents.clear
end

#drain_commit_action_intentsArray[CommitActionIntent]

RBS:

  • () -> Array[CommitActionIntent]

Returns:



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_intentsArray[EffectIntent]

RBS:

  • () -> Array[EffectIntent]

Returns:



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_intentsArray[OutboundMessageIntent]

RBS:

  • () -> Array[OutboundMessageIntent]

Returns:



363
364
365
# File 'lib/solid_objects/actor.rb', line 363

def drain_outbound_message_intents
  outbound_message_intents.shift(outbound_message_intents.length)
end

#drain_reminder_intentsArray[ReminderIntent]

RBS:

  • () -> Array[ReminderIntent]

Returns:



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

RBS:

  • (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil

Parameters:

  • (Symbol, String)
  • on_success: (Symbol, String, nil) (defaults to: nil)
  • on_failure: (Symbol, String, nil) (defaults to: nil)
  • (Object)

Returns:

  • (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

RBS:

  • (String) { () -> untyped } -> untyped

Parameters:

  • (String)

Yields:

Yield Returns:

  • (Object)

Returns:

  • (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

RBS:

  • (Symbol | String, Hash[String, untyped]) -> untyped

Parameters:

  • (Symbol, String)
  • (Hash[String, untyped])

Returns:

  • (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.messages[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]

RBS:

  • (Hash[String, untyped]) -> Hash[Symbol, untyped]

Parameters:

  • (Hash[String, untyped])

Returns:

  • (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

RBS:

  • (Symbol | String) -> untyped

Parameters:

  • (Symbol, String)

Returns:

  • (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_valuesHash[String, untyped]

RBS:

  • () -> Hash[String, untyped]

Returns:

  • (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

RBS:

  • (Symbol | String, String, ?details: Hash[String | Symbol, untyped]) -> bot

Parameters:

  • (Symbol, String)
  • (String)
  • details: (Hash[String | Symbol, untyped]) (defaults to: {})

Returns:

  • (bot)


168
169
170
171
172
173
174
175
176
# File 'lib/solid_objects/actor.rb', line 168

def reject(code, message, 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.

RBS:

  • (operation: Symbol | String, key: String?) -> String

Parameters:

  • operation: (Symbol, String)
  • key: (String, nil)

Returns:

  • (String)


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.

RBS:

  • (at: Time, ?every: Numeric?, ?missed: Symbol | String, ?key: (String | Symbol | Integer)?) -> OperationDispatcher

Parameters:

  • at: (Time)
  • every: (Numeric, nil) (defaults to: nil)
  • missed: (Symbol, String) (defaults to: :latest)
  • key: (String, Symbol, Integer, nil) (defaults to: nil)

Returns:



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.messages
  ) 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

RBS:

  • (Reference, ?available_at: Time?, ?idempotency_key: String?) -> OperationDispatcher

Parameters:

  • (Reference)
  • available_at: (Time, nil) (defaults to: nil)
  • idempotency_key: (String, nil) (defaults to: nil)

Returns:



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.messages
  ) do |operation, arguments|
    stage_outbound_message(reference:, operation:, arguments:, available_at:, idempotency_key:)
    nil
  end
end

#stage_outbound_message(reference:, operation:, arguments:, available_at: nil, idempotency_key: nil) ⇒ OutboundMessageIntent

RBS:

  • (reference: Reference, operation: Symbol | String, arguments: Hash[Symbol | String, untyped], ?available_at: Time?, idempotency_key: String?) -> OutboundMessageIntent

Parameters:

  • reference: (Reference)
  • operation: (Symbol, String)
  • arguments: (Hash[Symbol | String, untyped])
  • idempotency_key: (String, nil) (defaults to: nil)
  • available_at: (Time, nil) (defaults to: nil)

Returns:



336
337
338
339
340
341
342
343
344
345
# File 'lib/solid_objects/actor.rb', line 336

def stage_outbound_message(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| outbound_message_intents << intent }
end

#validate_effect_callback!(operation) ⇒ void

This method returns an undefined value.

RBS:

  • (Symbol | String?) -> void

Parameters:

  • (Symbol, String, nil)


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.messages.key?(operation.to_sym)

  raise UnknownMessage, "unknown effect callback operation #{operation.inspect}"
end

#validated_reminder_key(key) ⇒ String?

RBS:

  • ((String | Symbol | Integer)?) -> String?

Parameters:

  • (String, Symbol, Integer, nil)

Returns:

  • (String, nil)


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