solid_objects 0.13.3 → 0.14.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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +19 -11
  4. data/Rakefile +5 -0
  5. data/app/controllers/solid_objects/transmissions_controller.rb +32 -0
  6. data/config/routes.rb +1 -0
  7. data/docs/correctness.md +9 -0
  8. data/docs/operations.md +23 -1
  9. data/docs/research/solid_queue.md +2 -2
  10. data/docs/roadmap.md +25 -2
  11. data/docs/state-migrations.md +1 -1
  12. data/docs/transmission.md +198 -0
  13. data/examples/at_least_once/actor.rb +14 -0
  14. data/examples/at_least_once/boot.rb +47 -0
  15. data/examples/at_least_once/demo.rb +92 -0
  16. data/examples/at_least_once/effect_worker.rb +40 -0
  17. data/examples/at_least_once/sink.rb +27 -0
  18. data/lib/generators/solid_objects/templates/solid_objects.rb +17 -1
  19. data/lib/solid_objects/actor.rb +11 -0
  20. data/lib/solid_objects/actor_channel.rb +33 -3
  21. data/lib/solid_objects/configuration.rb +7 -1
  22. data/lib/solid_objects/engine.rb +4 -0
  23. data/lib/solid_objects/errors.rb +3 -0
  24. data/lib/solid_objects/transmission.rb +139 -0
  25. data/lib/solid_objects/version.rb +1 -1
  26. data/lib/solid_objects.rb +16 -0
  27. data/sig/generated/controllers/solid_objects/transmissions_controller.rbs +13 -0
  28. data/sig/generated/lib/solid_objects/actor.rbs +3 -0
  29. data/sig/generated/lib/solid_objects/actor_channel.rbs +10 -0
  30. data/sig/generated/lib/solid_objects/configuration.rbs +10 -2
  31. data/sig/generated/lib/solid_objects/errors.rbs +3 -0
  32. data/sig/generated/lib/solid_objects/transmission.rbs +34 -0
  33. data/sig/generated/lib/solid_objects.rbs +3 -0
  34. data/sig/support/framework.rbs +3 -0
  35. metadata +16 -6
@@ -0,0 +1,92 @@
1
+ # rbs_inline: enabled
2
+
3
+ # An executable proof for the at-least-once clause: a contract clause
4
+ # nobody can observe firing is decoration. Run with:
5
+ #
6
+ # bundle exec rake at_least_once
7
+ #
8
+ # Phase one crashes an effect worker between the external sink write and
9
+ # the acknowledgement, restarts one, and shows the sink reading 2 with
10
+ # deduplication off. Both deliveries carry the same stable effect id.
11
+ # Phase two repeats the crash with a guard on that id; the sink reads 1.
12
+ # The actor state commits exactly once in both phases.
13
+
14
+ require_relative "boot"
15
+ require_relative "actor"
16
+ require_relative "sink"
17
+ require "fileutils"
18
+ require "json"
19
+ require "rbconfig"
20
+ require "tmpdir"
21
+
22
+ directory = Dir.mktmpdir("solid_objects_at_least_once_")
23
+ database_path = File.join(directory, "state.sqlite3")
24
+ AtLeastOnceBoot.call(database_path)
25
+
26
+ # @rbs (String message) -> void
27
+ def prove(message)
28
+ raise "proof failed: #{message}" unless yield
29
+ end
30
+
31
+ # @rbs (String actor_id) -> void
32
+ def stage_one_delivery(actor_id)
33
+ DeliveryCounter.ref(actor_id).async.deliver
34
+ worker = SolidObjects::Worker.new
35
+ begin
36
+ worker.run_until_idle
37
+ ensure
38
+ worker.stop
39
+ end
40
+ end
41
+
42
+ # @rbs (database_path: String, sink_path: String, mode: String, deduplication: String) -> Integer?
43
+ def run_effect_worker(database_path:, sink_path:, mode:, deduplication:)
44
+ script = File.expand_path("effect_worker.rb", __dir__)
45
+ pid = Process.spawn(
46
+ RbConfig.ruby, script, database_path, sink_path, mode, deduplication,
47
+ chdir: AtLeastOnceBoot::ROOT
48
+ )
49
+ _pid, status = Process.wait2(pid)
50
+ status.exitstatus
51
+ end
52
+
53
+ # @rbs (database_path: String, sink_path: String, deduplication: String) -> void
54
+ def crash_then_recover(database_path:, sink_path:, deduplication:)
55
+ crash = run_effect_worker(database_path:, sink_path:, mode: "crash", deduplication:)
56
+ prove("the first delivery crashed before acknowledgement") { crash == 1 }
57
+ sleep 0.4
58
+ recovery = run_effect_worker(database_path:, sink_path:, mode: "complete", deduplication:)
59
+ prove("the second delivery completed and acknowledged") { recovery == 0 }
60
+ end
61
+
62
+ begin
63
+ sink_off = File.join(directory, "sink-dedup-off.json")
64
+ stage_one_delivery("dedup-off")
65
+ crash_then_recover(database_path:, sink_path: sink_off, deduplication: "off")
66
+ deliveries = AtLeastOnceSink.read(sink_off)
67
+ effect_ids = deliveries.map { |delivery| delivery.fetch("effect_id") }
68
+ state_off = SolidObjects::Instance.find_by!(actor_id: "dedup-off").state.fetch("count")
69
+ prove("the state commit happened exactly once") { state_off == 1 }
70
+ prove("the sink observed the duplicate") { deliveries.length == 2 }
71
+ prove("both deliveries carried the same stable effect id") { effect_ids.uniq.length == 1 }
72
+
73
+ sink_on = File.join(directory, "sink-dedup-on.json")
74
+ stage_one_delivery("dedup-on")
75
+ crash_then_recover(database_path:, sink_path: sink_on, deduplication: "on")
76
+ guarded = AtLeastOnceSink.read(sink_on)
77
+ state_on = SolidObjects::Instance.find_by!(actor_id: "dedup-on").state.fetch("count")
78
+ prove("the state commit happened exactly once") { state_on == 1 }
79
+ prove("the stable effect id absorbed the duplicate") { guarded.length == 1 }
80
+
81
+ puts JSON.pretty_generate(
82
+ duplicate: {
83
+ state_commits: state_off,
84
+ sink_deliveries: deliveries.length,
85
+ same_effect_id: effect_ids.uniq.length == 1,
86
+ attempts: deliveries.map { |delivery| delivery.fetch("attempt") }
87
+ },
88
+ remedy: { state_commits: state_on, sink_deliveries: guarded.length }
89
+ )
90
+ ensure
91
+ FileUtils.remove_entry(directory) if directory
92
+ end
@@ -0,0 +1,40 @@
1
+ # rbs_inline: enabled
2
+
3
+ require_relative "boot"
4
+ require_relative "actor"
5
+ require_relative "sink"
6
+
7
+ database_path, sink_path, mode, deduplication = ARGV
8
+ raise ArgumentError, "usage: effect_worker.rb DATABASE SINK crash|complete on|off" unless deduplication
9
+
10
+ AtLeastOnceBoot.call(database_path.to_s)
11
+
12
+ SolidObjects.register_effect(:record) do |_arguments, context|
13
+ AtLeastOnceSink.record(
14
+ path: sink_path.to_s,
15
+ effect_id: context.id,
16
+ attempt: context.attempt,
17
+ deduplication: deduplication.to_sym
18
+ )
19
+ # A crash between the external write and the acknowledgement: the sink
20
+ # has the delivery, the effect row never completes.
21
+ Process.exit!(1) if mode == "crash"
22
+ nil
23
+ end
24
+
25
+ # Production runs this on the dead-process-cleanup interval; the demo runs
26
+ # it once, after the liveness threshold, to release the crashed claim.
27
+ SolidObjects::ProcessRegistry.cleanup_dead
28
+
29
+ effect_executor = SolidObjects::EffectExecutor.new
30
+ begin
31
+ worked = false
32
+ 200.times do
33
+ worked = effect_executor.run_once
34
+ break if worked
35
+ sleep 0.01
36
+ end
37
+ raise "no effect became claimable" unless worked
38
+ ensure
39
+ effect_executor.stop
40
+ end
@@ -0,0 +1,27 @@
1
+ # rbs_inline: enabled
2
+
3
+ require "json"
4
+
5
+ # The external system in the at-least-once demo: a JSON file that records
6
+ # every delivery it accepts. With deduplication :off it accepts everything,
7
+ # which makes an at-least-once duplicate visible. With deduplication :on it
8
+ # accepts each stable effect id once, which is the documented remedy.
9
+ module AtLeastOnceSink
10
+ # @rbs (String path) -> Array[Hash[String, untyped]]
11
+ def self.read(path)
12
+ JSON.parse(File.read(path))
13
+ rescue Errno::ENOENT
14
+ []
15
+ end
16
+
17
+ # @rbs (path: String, effect_id: String, attempt: Integer, deduplication: Symbol) -> bool
18
+ def self.record(path:, effect_id:, attempt:, deduplication:)
19
+ deliveries = read(path)
20
+ seen = deliveries.any? { |delivery| delivery.fetch("effect_id") == effect_id }
21
+ return false if deduplication == :on && seen
22
+
23
+ deliveries << { "effect_id" => effect_id, "attempt" => attempt }
24
+ File.write(path, JSON.pretty_generate(deliveries))
25
+ true
26
+ end
27
+ end
@@ -37,7 +37,7 @@ SolidObjects.configure do |configuration|
37
37
  #
38
38
  # Prefer policies that bind actor_type and actor_id to a trusted
39
39
  # authorization_context. See:
40
- # https://github.com/cardmagic/solid_objects/blob/main/docs/authorization.md
40
+ # https://github.com/cardmagic/solid-objects-ruby/blob/main/docs/authorization.md
41
41
  # and run:
42
42
  #
43
43
  # bin/rails solid_objects:doctor
@@ -49,6 +49,22 @@ SolidObjects.configure do |configuration|
49
49
  configuration.authorize_subscription = ->(**) { false }
50
50
  configuration.authorize_administration = ->(**) { false }
51
51
 
52
+ # The engine route POST /solid_objects/transmit ingests transmit envelopes
53
+ # from another Solid Objects runtime. It stays denied until its callers are
54
+ # authenticated, because the ingest skips authorize_message by design:
55
+ #
56
+ # configuration.authorize_transmission = lambda do |envelope:, authorization_context:|
57
+ # ActiveSupport::SecurityUtils.secure_compare(
58
+ # authorization_context.request.headers["Authorization"].to_s,
59
+ # "Bearer #{Rails.application.credentials.transmit_token}"
60
+ # )
61
+ # end
62
+ #
63
+ # When the sending runtime names actor types differently, map them here:
64
+ #
65
+ # configuration.transmission_actor_type_resolver = ->(actor_type) { actor_type.sub("browser-", "server-") }
66
+ configuration.authorize_transmission = ->(**) { false }
67
+
52
68
  # Configure component_authorization_context to return the authenticated
53
69
  # principal used for reactive component refreshes.
54
70
 
@@ -190,6 +190,17 @@ module SolidObjects
190
190
  nil
191
191
  end
192
192
 
193
+ # @rbs () -> OperationDispatcher
194
+ def transmit
195
+ OperationDispatcher.new(
196
+ actor_type: self.class.actor_type,
197
+ handlers: self.class.definition.messages
198
+ ) do |operation, arguments|
199
+ emit(Transmission::EFFECT_NAME, operation: operation.to_s, arguments:)
200
+ nil
201
+ end
202
+ end
203
+
193
204
  # @rbs (Symbol | String, **untyped) -> nil
194
205
  def commit_action(name, **arguments)
195
206
  CommitActionIntent.new(
@@ -4,6 +4,14 @@ require "action_cable"
4
4
 
5
5
  module SolidObjects
6
6
  class ActorChannel < ActionCable::Channel::Base
7
+ REJECT_REASONS = {
8
+ UnknownActorType => "unregistered_actor_type",
9
+ InvalidStreamToken => "invalid_stream_token",
10
+ InvalidComponentToken => "invalid_component_token",
11
+ JSON::ParserError => "malformed_component_registration",
12
+ KeyError => "missing_subscription_parameter"
13
+ }.freeze
14
+
7
15
  # @rbs () -> void
8
16
  def subscribed
9
17
  identity = StreamToken.verify(params.fetch("token"))
@@ -15,7 +23,9 @@ module SolidObjects
15
23
  actor_id:,
16
24
  authorization_context: connection
17
25
  )
18
- return reject unless authorized
26
+ unless authorized
27
+ return reject_and_report("unauthorized", actor_type:, actor_id:)
28
+ end
19
29
 
20
30
  @reference = Reference.new(actor_type:, actor_id:)
21
31
  @scalar_observables = identity["observables"]
@@ -43,8 +53,8 @@ module SolidObjects
43
53
  JSON::ParserError,
44
54
  InvalidStreamToken,
45
55
  InvalidComponentToken,
46
- UnknownActorType
47
- reject
56
+ UnknownActorType => error
57
+ reject_and_report(reject_reason(error), actor_type:, actor_id:, error:)
48
58
  end
49
59
 
50
60
  private
@@ -54,6 +64,26 @@ module SolidObjects
54
64
  :scalar_observables,
55
65
  :payload_names
56
66
 
67
+ # The exception message stays out of the payload, because a component or
68
+ # payload error can carry actor state into logs.
69
+ # @rbs (String, actor_type: String?, actor_id: String?, ?error: Exception?) -> void
70
+ def reject_and_report(reason, actor_type:, actor_id:, error: nil)
71
+ SolidObjects.instrument(
72
+ :"subscription.rejected",
73
+ reason:,
74
+ actor_type:,
75
+ actor_id:,
76
+ error_class: error&.class&.name
77
+ )
78
+ reject
79
+ end
80
+
81
+ # @rbs (Exception) -> String
82
+ def reject_reason(error)
83
+ match = REJECT_REASONS.find { |error_class, _| error.is_a?(error_class) }
84
+ match ? match.last : "invalid_subscription"
85
+ end
86
+
57
87
  # @rbs (String) -> void
58
88
  def receive_broadcast(stream)
59
89
  invalidation = TurboStreamRenderer.invalidation(stream)
@@ -47,6 +47,8 @@ module SolidObjects
47
47
  # @rbs @authorize_destroy: Proc
48
48
  # @rbs @authorize_subscription: Proc
49
49
  # @rbs @authorize_administration: Proc
50
+ # @rbs @authorize_transmission: Proc
51
+ # @rbs @transmission_actor_type_resolver: Proc
50
52
 
51
53
  attr_accessor :table_name_prefix,
52
54
  :polling_interval,
@@ -92,7 +94,9 @@ module SolidObjects
92
94
  :authorize_query,
93
95
  :authorize_destroy,
94
96
  :authorize_subscription,
95
- :authorize_administration
97
+ :authorize_administration,
98
+ :authorize_transmission,
99
+ :transmission_actor_type_resolver
96
100
 
97
101
  # @rbs @additional_components: Array[untyped]
98
102
  attr_reader :additional_components
@@ -148,6 +152,8 @@ module SolidObjects
148
152
  @authorize_destroy = ->(**) { false }
149
153
  @authorize_subscription = ->(**) { false }
150
154
  @authorize_administration = ->(**) { false }
155
+ @authorize_transmission = ->(**) { false }
156
+ @transmission_actor_type_resolver = ->(actor_type) { actor_type }
151
157
  @additional_components = []
152
158
  end
153
159
 
@@ -15,6 +15,10 @@ module SolidObjects
15
15
  SolidObjects::LogSubscriber.install
16
16
  end
17
17
 
18
+ initializer "solid_objects.actors" do |application|
19
+ application.config.to_prepare { ApplicationActorLoader.new.call }
20
+ end
21
+
18
22
  initializer "solid_objects.database", after: :load_config_initializers do
19
23
  ActiveSupport.on_load(:active_record) do
20
24
  require RECORD_PATH
@@ -19,6 +19,9 @@ module SolidObjects
19
19
  class UnknownMessage < Error
20
20
  end
21
21
 
22
+ class InvalidTransmission < Error
23
+ end
24
+
22
25
  class InvalidPayload < Error
23
26
  end
24
27
 
@@ -0,0 +1,139 @@
1
+ # rbs_inline: enabled
2
+
3
+ module SolidObjects
4
+ module Transmission
5
+ REQUIRED_FIELDS = %w[effectId actorType actorId operation].freeze
6
+ IDEMPOTENCY_PREFIX = "transmit:"
7
+ EFFECT_NAME = "solid-objects.transmit"
8
+ UNDELIVERED_STATUSES = %w[pending processing].freeze
9
+
10
+ class << self
11
+ # @rbs (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
12
+ def receive(envelope, resolve_actor_type: :itself.to_proc, mailbox: Mailbox.new, actor_loader: method(:load_application_actors))
13
+ validate!(envelope)
14
+
15
+ actor_type = resolve_actor_type.call(envelope["actorType"]).to_s
16
+ actor_class = fetch_actor_class(actor_type, actor_loader)
17
+ operation = envelope["operation"].to_sym
18
+ unless actor_class.definition.messages.key?(operation)
19
+ raise UnknownMessage, "unknown operation #{envelope["operation"].inspect}"
20
+ end
21
+
22
+ mailbox.enqueue(
23
+ reference: Reference.new(actor_type:, actor_id: envelope["actorId"]),
24
+ operation:,
25
+ arguments: envelope.fetch("arguments", {}),
26
+ delivery_mode: "internal",
27
+ idempotency_key: "#{IDEMPOTENCY_PREFIX}#{envelope["effectId"]}"
28
+ )
29
+ end
30
+
31
+ # @rbs (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
32
+ def deliver_through(effect_name:, arguments:, context:, deliver:)
33
+ staged_envelope(
34
+ arguments,
35
+ effect_id: context.id,
36
+ actor_type: context.actor_type,
37
+ actor_id: context.actor_id
38
+ )
39
+ undelivered_envelopes_through(effect_name:, context:).each do |envelope|
40
+ deliver.call(envelope)
41
+ end
42
+ nil
43
+ end
44
+
45
+ private
46
+
47
+ # @rbs (String, ^() -> bool) -> Class
48
+ def fetch_actor_class(actor_type, actor_loader)
49
+ SolidObjects.registry.fetch(actor_type)
50
+ rescue UnknownActorType
51
+ raise unless actor_loader.call
52
+
53
+ SolidObjects.registry.fetch(actor_type)
54
+ end
55
+
56
+ # @rbs () -> bool
57
+ def load_application_actors
58
+ return false unless defined?(Rails.application) && Rails.application
59
+
60
+ ApplicationActorLoader.new.call
61
+ true
62
+ end
63
+
64
+ # @rbs (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
65
+ def staged_envelope(arguments, effect_id:, actor_type:, actor_id:)
66
+ operation = arguments["operation"]
67
+ unless operation.is_a?(String) && !operation.empty?
68
+ raise InvalidTransmission, "transmit effect arguments require a non-empty operation"
69
+ end
70
+
71
+ target_arguments = arguments["arguments"]
72
+ target_arguments = {} if target_arguments.nil?
73
+ unless target_arguments.is_a?(Hash)
74
+ raise InvalidTransmission, %(transmit effect arguments must hold a JSON object in "arguments")
75
+ end
76
+
77
+ target_type = arguments["actorType"].nil? ? actor_type : arguments["actorType"]
78
+ target_id = arguments["actorId"].nil? ? actor_id : arguments["actorId"]
79
+ { "actorType" => target_type, "actorId" => target_id }.each do |field, value|
80
+ next if value.is_a?(String) && !value.empty?
81
+
82
+ raise InvalidTransmission, "transmit effect #{field} must be a non-empty string"
83
+ end
84
+
85
+ {
86
+ "effectId" => effect_id,
87
+ "actorType" => target_type,
88
+ "actorId" => target_id,
89
+ "operation" => operation,
90
+ "arguments" => target_arguments
91
+ }
92
+ end
93
+
94
+ # @rbs (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
95
+ def undelivered_envelopes_through(effect_name:, context:)
96
+ source_sequence = Message.find(context.source_message_id).sequence
97
+ effects = Effect
98
+ .joins(:message)
99
+ .where(name: effect_name, status: UNDELIVERED_STATUSES)
100
+ .merge(
101
+ Message.where(
102
+ actor_type: context.actor_type,
103
+ actor_id: context.actor_id,
104
+ sequence: ..source_sequence
105
+ )
106
+ )
107
+ .order(Message.arel_table[:sequence].asc, :id)
108
+
109
+ effects.filter_map do |effect|
110
+ staged_envelope(
111
+ effect.arguments,
112
+ effect_id: effect.effect_id,
113
+ actor_type: context.actor_type,
114
+ actor_id: context.actor_id
115
+ )
116
+ rescue InvalidTransmission
117
+ nil
118
+ end
119
+ end
120
+
121
+ # @rbs (untyped) -> void
122
+ def validate!(envelope)
123
+ raise InvalidTransmission, "envelope must be a JSON object" unless envelope.is_a?(Hash)
124
+
125
+ REQUIRED_FIELDS.each do |field|
126
+ value = envelope[field]
127
+ next if value.is_a?(String) && !value.empty?
128
+
129
+ raise InvalidTransmission, "envelope field #{field.inspect} must be a non-empty string"
130
+ end
131
+
132
+ arguments = envelope.fetch("arguments", {})
133
+ return if arguments.is_a?(Hash)
134
+
135
+ raise InvalidTransmission, %(envelope field "arguments" must be a JSON object)
136
+ end
137
+ end
138
+ end
139
+ end
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.13.3"
4
+ VERSION = "0.14.1"
5
5
  end
data/lib/solid_objects.rb CHANGED
@@ -63,6 +63,8 @@ require "solid_objects/lease_renewer"
63
63
  # was reachable only through the caller path, so requiring the gem was not
64
64
  # enough to run a role that uses it.
65
65
  require "solid_objects/mailbox"
66
+ require "solid_objects/application_actor_loader"
67
+ require "solid_objects/transmission"
66
68
  require "solid_objects/worker"
67
69
  require "solid_objects/effect_executor"
68
70
  require "solid_objects/reminder_scheduler"
@@ -100,6 +102,20 @@ module SolidObjects
100
102
  effect_registry.register(name, handler)
101
103
  end
102
104
 
105
+ # @rbs (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
106
+ def register_transmit(effect_name: Transmission::EFFECT_NAME, &deliver)
107
+ raise ArgumentError, "register_transmit requires a delivery block" unless deliver
108
+
109
+ register_effect(effect_name) do |arguments, context|
110
+ Transmission.deliver_through(
111
+ effect_name: effect_name.to_s,
112
+ arguments:,
113
+ context:,
114
+ deliver:
115
+ )
116
+ end
117
+ end
118
+
103
119
  # @rbs () -> CommitActionRegistry
104
120
  def commit_action_registry
105
121
  @commit_action_registry ||= CommitActionRegistry.new
@@ -0,0 +1,13 @@
1
+ # Generated from app/controllers/solid_objects/transmissions_controller.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ class TransmissionsController < ActionController::API
5
+ # @rbs () -> void
6
+ def create: () -> void
7
+
8
+ private
9
+
10
+ # @rbs (untyped) -> bool
11
+ def authorized_transmission?: (untyped) -> bool
12
+ end
13
+ end
@@ -161,6 +161,9 @@ module SolidObjects
161
161
  # @rbs (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
162
162
  def emit: (Symbol | String, ?on_success: Symbol | String?, ?on_failure: Symbol | String?, **untyped) -> nil
163
163
 
164
+ # @rbs () -> OperationDispatcher
165
+ def transmit: () -> OperationDispatcher
166
+
164
167
  # @rbs (Symbol | String, **untyped) -> nil
165
168
  def commit_action: (Symbol | String, **untyped) -> nil
166
169
 
@@ -2,6 +2,8 @@
2
2
 
3
3
  module SolidObjects
4
4
  class ActorChannel < ActionCable::Channel::Base
5
+ REJECT_REASONS: untyped
6
+
5
7
  # @rbs () -> void
6
8
  def subscribed: () -> void
7
9
 
@@ -15,6 +17,14 @@ module SolidObjects
15
17
 
16
18
  attr_reader payload_names: untyped
17
19
 
20
+ # The exception message stays out of the payload, because a component or
21
+ # payload error can carry actor state into logs.
22
+ # @rbs (String, actor_type: String?, actor_id: String?, ?error: Exception?) -> void
23
+ def reject_and_report: (String, actor_type: String?, actor_id: String?, ?error: Exception?) -> void
24
+
25
+ # @rbs (Exception) -> String
26
+ def reject_reason: (Exception) -> String
27
+
18
28
  # @rbs (String) -> void
19
29
  def receive_broadcast: (String) -> void
20
30
 
@@ -4,8 +4,6 @@ module SolidObjects
4
4
  class Configuration
5
5
  @table_name_prefix: String
6
6
 
7
- @process_alive_threshold: Float
8
-
9
7
  @shutdown_timeout: Float
10
8
 
11
9
  @supervisor_monitor_interval: Float
@@ -58,6 +56,10 @@ module SolidObjects
58
56
 
59
57
  @authorize_administration: Proc
60
58
 
59
+ @authorize_transmission: Proc
60
+
61
+ @transmission_actor_type_resolver: Proc
62
+
61
63
  @polling_interval: Float
62
64
 
63
65
  @idle_polling_interval: Float
@@ -92,6 +94,8 @@ module SolidObjects
92
94
 
93
95
  @process_heartbeat_interval: Float
94
96
 
97
+ @process_alive_threshold: Float
98
+
95
99
  attr_accessor table_name_prefix: untyped
96
100
 
97
101
  attr_accessor polling_interval: untyped
@@ -182,6 +186,10 @@ module SolidObjects
182
186
 
183
187
  attr_accessor authorize_administration: untyped
184
188
 
189
+ attr_accessor authorize_transmission: untyped
190
+
191
+ attr_accessor transmission_actor_type_resolver: untyped
192
+
185
193
  # @rbs @additional_components: Array[untyped]
186
194
  attr_reader additional_components: untyped
187
195
 
@@ -19,6 +19,9 @@ module SolidObjects
19
19
  class UnknownMessage < Error
20
20
  end
21
21
 
22
+ class InvalidTransmission < Error
23
+ end
24
+
22
25
  class InvalidPayload < Error
23
26
  end
24
27
 
@@ -0,0 +1,34 @@
1
+ # Generated from lib/solid_objects/transmission.rb with RBS::Inline
2
+
3
+ module SolidObjects
4
+ module Transmission
5
+ REQUIRED_FIELDS: untyped
6
+
7
+ IDEMPOTENCY_PREFIX: ::String
8
+
9
+ EFFECT_NAME: ::String
10
+
11
+ UNDELIVERED_STATUSES: untyped
12
+
13
+ # @rbs (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
14
+ def self.receive: (untyped envelope, ?resolve_actor_type: ^(String) -> (String | Symbol), ?mailbox: Mailbox, ?actor_loader: ^() -> bool) -> MessageReference
15
+
16
+ # @rbs (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
17
+ def self.deliver_through: (effect_name: String, arguments: Hash[String, untyped], context: EffectContext, deliver: Proc) -> nil
18
+
19
+ # @rbs (String, ^() -> bool) -> Class
20
+ private def self.fetch_actor_class: (String, ^() -> bool) -> Class
21
+
22
+ # @rbs () -> bool
23
+ private def self.load_application_actors: () -> bool
24
+
25
+ # @rbs (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
26
+ private def self.staged_envelope: (Hash[String, untyped], effect_id: String, actor_type: String, actor_id: String) -> Hash[String, untyped]
27
+
28
+ # @rbs (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
29
+ private def self.undelivered_envelopes_through: (effect_name: String, context: EffectContext) -> Array[Hash[String, untyped]]
30
+
31
+ # @rbs (untyped) -> void
32
+ private def self.validate!: (untyped) -> void
33
+ end
34
+ end
@@ -18,6 +18,9 @@ module SolidObjects
18
18
  # @rbs (String | Symbol) { (Hash[String, untyped], EffectContext) -> untyped } -> Proc
19
19
  def self.register_effect: (String | Symbol) { (Hash[String, untyped], EffectContext) -> untyped } -> Proc
20
20
 
21
+ # @rbs (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
22
+ def self.register_transmit: (?effect_name: String | Symbol) { (Hash[String, untyped]) -> untyped } -> Proc
23
+
21
24
  # @rbs () -> CommitActionRegistry
22
25
  def self.commit_action_registry: () -> CommitActionRegistry
23
26
 
@@ -21,6 +21,9 @@ end
21
21
  module ActionController
22
22
  class Base
23
23
  end
24
+
25
+ class API
26
+ end
24
27
  end
25
28
 
26
29
  module Rails