Class: SolidObjects::ActorChannel
- Inherits:
-
ActionCable::Channel::Base
- Object
- ActionCable::Channel::Base
- SolidObjects::ActorChannel
- Defined in:
- lib/solid_objects/actor_channel.rb,
sig/generated/lib/solid_objects/actor_channel.rbs
Instance Attribute Summary collapse
-
#component_subscriptions ⇒ Object
readonly
Returns the value of attribute component_subscriptions.
-
#payload_names ⇒ Object
readonly
Returns the value of attribute payload_names.
-
#reference ⇒ Object
readonly
Returns the value of attribute reference.
-
#scalar_observables ⇒ Object
readonly
Returns the value of attribute scalar_observables.
Instance Method Summary collapse
- #newer_payload_revision?(snapshot) ⇒ Boolean
-
#payload_authorization_context(name) ⇒ Object
Resolves the Cable connection to whatever the application uses as an authorization subject, so a payload block and
authorize_querysee the same object a controller render would pass. - #receive_broadcast(stream) ⇒ void
- #refresh_outdated_components(snapshot) ⇒ void
- #scalar_observable_names(snapshot) ⇒ Array[String]
- #subscribed ⇒ void
-
#transmit_state_payload(snapshot, name) ⇒ Boolean
A payload is one subscriber's view of one name.
- #transmit_state_payloads(snapshot) ⇒ void
- #validate_payload_names! ⇒ void
- #validate_scalar_observables! ⇒ void
Instance Attribute Details
#component_subscriptions ⇒ Object (readonly)
Returns the value of attribute component_subscriptions.
52 53 54 |
# File 'lib/solid_objects/actor_channel.rb', line 52 def component_subscriptions @component_subscriptions end |
#payload_names ⇒ Object (readonly)
Returns the value of attribute payload_names.
52 53 54 |
# File 'lib/solid_objects/actor_channel.rb', line 52 def payload_names @payload_names end |
#reference ⇒ Object (readonly)
Returns the value of attribute reference.
52 53 54 |
# File 'lib/solid_objects/actor_channel.rb', line 52 def reference @reference end |
#scalar_observables ⇒ Object (readonly)
Returns the value of attribute scalar_observables.
52 53 54 |
# File 'lib/solid_objects/actor_channel.rb', line 52 def scalar_observables @scalar_observables end |
Instance Method Details
#newer_payload_revision?(snapshot) ⇒ Boolean
135 136 137 138 139 140 |
# File 'lib/solid_objects/actor_channel.rb', line 135 def newer_payload_revision?(snapshot) current = @payload_revision return true unless current (current <=> [ snapshot.instance_id, snapshot.revision ]) == -1 end |
#payload_authorization_context(name) ⇒ Object
Resolves the Cable connection to whatever the application uses as an
authorization subject, so a payload block and authorize_query see the
same object a controller render would pass.
127 128 129 130 131 132 |
# File 'lib/solid_objects/actor_channel.rb', line 127 def (name) callable = SolidObjects.configuration. return callable.call(connection:) unless CallableKeywords.accepts?(callable, :payload_name) callable.call(connection:, payload_name: name) end |
#receive_broadcast(stream) ⇒ void
This method returns an undefined value.
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/solid_objects/actor_channel.rb', line 58 def receive_broadcast(stream) invalidation = TurboStreamRenderer.invalidation(stream) revision_only = invalidation && invalidation.fetch("observable_name") == PayloadBroadcast::REVISION_OBSERVABLE if !revision_only && (!invalidation || scalar_observables.nil? || scalar_observables.include?(invalidation.fetch("observable_name"))) transmit stream end return unless invalidation component_subscriptions .refreshes_for(invalidation) .each { |refresh| transmit refresh } transmit_state_payloads(ActorSnapshot.new(reference)) end |
#refresh_outdated_components(snapshot) ⇒ void
This method returns an undefined value.
158 159 160 161 162 |
# File 'lib/solid_objects/actor_channel.rb', line 158 def refresh_outdated_components(snapshot) component_subscriptions .reconnect_refreshes(snapshot) .each { |refresh| transmit refresh } end |
#scalar_observable_names(snapshot) ⇒ Array[String]
182 183 184 185 186 |
# File 'lib/solid_objects/actor_channel.rb', line 182 def scalar_observable_names(snapshot) return scalar_observables if scalar_observables snapshot.actor_class.definition.observables.keys.map(&:to_s) end |
#subscribed ⇒ void
This method returns an undefined value.
8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/solid_objects/actor_channel.rb', line 8 def subscribed identity = StreamToken.verify(params.fetch("token")) actor_type = identity.fetch("actor_type") actor_id = identity.fetch("actor_id") SolidObjects.registry.fetch(actor_type) = SolidObjects.configuration..call( actor_type:, actor_id:, authorization_context: connection ) return reject unless @reference = Reference.new(actor_type:, actor_id:) @scalar_observables = identity["observables"] @payload_names = identity["payloads"] validate_scalar_observables! validate_payload_names! @component_subscriptions = ComponentSubscriptions.parse( params["components"], reference: ) stream_from StreamName.for(reference), coder: ActiveSupport::JSON do |stream| receive_broadcast(stream) end snapshot = ActorSnapshot.new(reference) scalar_observable_names(snapshot).each do |name| transmit TurboStreamRenderer.observable_value( reference, name, snapshot.observable_value(name) ) end refresh_outdated_components(snapshot) transmit_state_payloads(snapshot) rescue KeyError, JSON::ParserError, InvalidStreamToken, InvalidComponentToken, UnknownActorType reject end |
#transmit_state_payload(snapshot, name) ⇒ Boolean
A payload is one subscriber's view of one name. Letting it raise through here would reject the subscription or abandon the rest of a broadcast, so a failure is confined to the payload that caused it and reported. The exception message is deliberately not instrumented: a payload block reads actor state, so its message is the one place subscriber state could leak into logs.
Returns whether this revision was settled for the name. An unauthorized payload is settled: the decision is stable, so retrying it would only re-deliver its authorized siblings.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/solid_objects/actor_channel.rb', line 102 def transmit_state_payload(snapshot, name) payload = PayloadBroadcast.new( snapshot:, name:, authorization_context: (name) ).call transmit TurboStreamRenderer.state_payload(payload) true rescue Unauthorized true rescue => error SolidObjects.instrument( :payload_broadcast_failed, actor_type: reference.actor_type, actor_id: reference.actor_id, payload_name: name, error_class: error.class.name ) false end |
#transmit_state_payloads(snapshot) ⇒ void
This method returns an undefined value.
77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/solid_objects/actor_channel.rb', line 77 def transmit_state_payloads(snapshot) return if payload_names.nil? || payload_names.empty? return unless newer_payload_revision?(snapshot) # The watermark records what the subscriber has, so a revision with a # failed payload must not advance it: dedup would skip every later # attempt at that revision and the actor may not mutate again for a long # time. Every name is still attempted before the decision is made. attempts = payload_names.map { |name| transmit_state_payload(snapshot, name) } return if attempts.any?(false) @payload_revision = [ snapshot.instance_id, snapshot.revision ] end |
#validate_payload_names! ⇒ void
This method returns an undefined value.
143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/solid_objects/actor_channel.rb', line 143 def validate_payload_names! return unless payload_names broadcasts = SolidObjects .registry .fetch(reference.actor_type) .definition .payload_broadcasts unknown = payload_names.find { |name| !broadcasts.key?(name.to_sym) } return unless unknown raise InvalidStreamToken, "unknown payload broadcast #{unknown.inspect}" end |
#validate_scalar_observables! ⇒ void
This method returns an undefined value.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/solid_objects/actor_channel.rb', line 165 def validate_scalar_observables! return unless scalar_observables observables = SolidObjects .registry .fetch(reference.actor_type) .definition .observables unknown = scalar_observables.find do |name| !observables.key?(name.to_sym) end return unless unknown raise InvalidStreamToken, "unknown scalar observable #{unknown.inspect}" end |