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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/README.md +62 -20
- data/app/controllers/solid_objects/components_controller.rb +4 -4
- data/app/models/solid_objects/broadcast.rb +8 -0
- data/app/models/solid_objects/message.rb +1 -1
- data/app/views/solid_objects/dead_letters/index.html.erb +1 -1
- data/app/views/solid_objects/instances/show.html.erb +1 -1
- data/benchmark/support.rb +10 -10
- data/db/migrate/20260813000000_rename_message_dispatch_columns.rb +53 -0
- data/docs/architecture.md +14 -11
- data/docs/authorization.md +2 -2
- data/docs/database-schema.md +6 -6
- data/docs/development.md +13 -1
- data/docs/migrating-existing-state.md +1 -3
- data/docs/realtime.md +25 -3
- data/docs/roadmap.md +9 -5
- data/docs/security.md +8 -0
- data/examples/application/app/actors/shopping_cart_actor.rb +3 -3
- data/lib/solid_objects/actor.rb +49 -37
- data/lib/solid_objects/actor_channel.rb +10 -7
- data/lib/solid_objects/actor_definition.rb +24 -9
- data/lib/solid_objects/actor_view.rb +7 -2
- data/lib/solid_objects/application_write_guard.rb +1 -1
- data/lib/solid_objects/client.rb +52 -52
- data/lib/solid_objects/component_registration.rb +2 -2
- data/lib/solid_objects/component_renderer.rb +1 -1
- data/lib/solid_objects/component_subscriptions.rb +21 -21
- data/lib/solid_objects/database_adapters/sqlite.rb +7 -1
- data/lib/solid_objects/dead_letter_manager.rb +4 -4
- data/lib/solid_objects/doctor.rb +8 -8
- data/lib/solid_objects/effect_executor.rb +30 -22
- data/lib/solid_objects/errors.rb +30 -27
- data/lib/solid_objects/executor.rb +47 -39
- data/lib/solid_objects/lease.rb +3 -3
- data/lib/solid_objects/mailbox.rb +34 -27
- data/lib/solid_objects/operation_dispatcher.rb +51 -0
- data/lib/solid_objects/payload_broadcast.rb +1 -1
- data/lib/solid_objects/reference.rb +60 -29
- data/lib/solid_objects/reminder_scheduler.rb +34 -20
- data/lib/solid_objects/sync_diagnostics.rb +6 -6
- data/lib/solid_objects/test_helper.rb +16 -0
- data/lib/solid_objects/turbo_stream_renderer.rb +16 -16
- data/lib/solid_objects/version.rb +1 -1
- data/lib/solid_objects.rb +1 -0
- data/sig/generated/controllers/solid_objects/components_controller.rbs +2 -2
- data/sig/generated/lib/solid_objects/actor.rbs +19 -19
- data/sig/generated/lib/solid_objects/actor_definition.rbs +13 -6
- data/sig/generated/lib/solid_objects/client.rbs +8 -8
- data/sig/generated/lib/solid_objects/component_registration.rbs +2 -2
- data/sig/generated/lib/solid_objects/component_subscriptions.rbs +8 -8
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +3 -0
- data/sig/generated/lib/solid_objects/effect_executor.rbs +2 -2
- data/sig/generated/lib/solid_objects/errors.rbs +21 -18
- data/sig/generated/lib/solid_objects/executor.rbs +10 -10
- data/sig/generated/lib/solid_objects/lease.rbs +2 -2
- data/sig/generated/lib/solid_objects/mailbox.rbs +8 -8
- data/sig/generated/lib/solid_objects/operation_dispatcher.rbs +35 -0
- data/sig/generated/lib/solid_objects/reference.rbs +7 -4
- data/sig/generated/lib/solid_objects/reminder_scheduler.rbs +9 -6
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +2 -2
- data/sig/generated/lib/solid_objects/test_helper.rbs +3 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +8 -8
- data/sig/generated/models/solid_objects/broadcast.rbs +2 -0
- 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],
|
|
15
|
-
def enqueue(reference
|
|
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
|
-
|
|
26
|
-
normalized_arguments,
|
|
27
|
-
|
|
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],
|
|
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
|
-
|
|
43
|
-
arguments
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
normalized_arguments,
|
|
66
|
-
|
|
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!(
|
|
122
|
-
matches =
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return
|
|
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,
|
|
140
|
-
def create_message(instance
|
|
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
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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 (
|
|
20
|
-
def async(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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 (
|
|
43
|
-
def sync(
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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: ..
|
|
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:
|
|
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.
|
|
101
|
-
raise UnknownMessage, "unknown reminder
|
|
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
|
-
|
|
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.
|
|
116
|
-
locked_reminder.arguments,
|
|
117
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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.
|
|
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
|
|
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
|
|
47
|
-
return morph_component_refresh(registration
|
|
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
|
|
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
|
|
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
|
|
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
|
)
|
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
|
|
10
|
+
attr_reader success_operation(): untyped
|
|
11
11
|
|
|
12
|
-
attr_reader
|
|
12
|
+
attr_reader failure_operation(): untyped
|
|
13
13
|
|
|
14
|
-
def self.new: (untyped name, untyped arguments, untyped
|
|
15
|
-
| (name: untyped, arguments: untyped,
|
|
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, :
|
|
17
|
+
def self.members: () -> [ :name, :arguments, :success_operation, :failure_operation ]
|
|
18
18
|
|
|
19
|
-
def members: () -> [ :name, :arguments, :
|
|
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
|
|
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
|
|
68
|
-
| (actor_type: untyped, actor_id: untyped,
|
|
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, :
|
|
70
|
+
def self.members: () -> [ :actor_type, :actor_id, :operation, :arguments, :available_at, :idempotency_key ]
|
|
71
71
|
|
|
72
|
-
def members: () -> [ :actor_type, :actor_id, :
|
|
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 (
|
|
161
|
-
def schedule: (
|
|
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,
|
|
164
|
-
def send_to: (Reference,
|
|
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]
|