live_cable 0.3.0 → 0.4.0

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +567 -0
  3. data/README.md +14 -2
  4. data/app/assets/javascript/controllers/live_controller.js +51 -7
  5. data/app/assets/javascript/live_cable.js +2 -0
  6. data/app/assets/javascript/loading.js +58 -6
  7. data/app/assets/javascript/subscriptions.js +292 -22
  8. data/app/channels/live_channel.rb +51 -29
  9. data/app/helpers/live_cable_helper.rb +31 -2
  10. data/lib/live_cable/action_cable_connection.rb +4 -1
  11. data/lib/live_cable/component/broadcasting.rb +37 -6
  12. data/lib/live_cable/component/dispatching.rb +36 -0
  13. data/lib/live_cable/component/lifecycle.rb +1 -1
  14. data/lib/live_cable/component/reactive_variables.rb +33 -5
  15. data/lib/live_cable/component/rendering.rb +30 -14
  16. data/lib/live_cable/component/streaming.rb +35 -4
  17. data/lib/live_cable/component.rb +4 -3
  18. data/lib/live_cable/connection/broadcasting.rb +15 -3
  19. data/lib/live_cable/connection/component_management.rb +12 -8
  20. data/lib/live_cable/connection/error_handling.rb +28 -7
  21. data/lib/live_cable/connection/messaging.rb +40 -52
  22. data/lib/live_cable/connection/state_management.rb +13 -16
  23. data/lib/live_cable/connection.rb +33 -0
  24. data/lib/live_cable/container.rb +17 -11
  25. data/lib/live_cable/defaults_signer.rb +95 -0
  26. data/lib/live_cable/delegation/array.rb +27 -1
  27. data/lib/live_cable/delegation/hash.rb +55 -1
  28. data/lib/live_cable/delegation/methods.rb +25 -1
  29. data/lib/live_cable/delegation/model.rb +12 -0
  30. data/lib/live_cable/delegator.rb +54 -6
  31. data/lib/live_cable/dispatch.rb +54 -0
  32. data/lib/live_cable/engine.rb +3 -1
  33. data/lib/live_cable/forbidden.rb +7 -0
  34. data/lib/live_cable/identity_digest.rb +27 -0
  35. data/lib/live_cable/model_observer.rb +37 -0
  36. data/lib/live_cable/observer_tracking.rb +10 -1
  37. data/lib/live_cable/predicate_builder.rb +13 -0
  38. data/lib/live_cable/render_context.rb +82 -8
  39. data/lib/live_cable/rendering/controller_renderer.rb +26 -0
  40. data/lib/live_cable/rendering/dependency_visitor.rb +68 -24
  41. data/lib/live_cable/rendering/handler.rb +1 -1
  42. data/lib/live_cable/rendering/method_analyzer.rb +18 -4
  43. data/lib/live_cable/rendering/method_collector.rb +13 -5
  44. data/lib/live_cable/rendering/partial.rb +13 -1
  45. data/lib/live_cable/rendering/partial_renderer.rb +59 -7
  46. data/lib/live_cable/rendering/render_result.rb +5 -0
  47. data/lib/live_cable/rendering/renderer.rb +18 -14
  48. data/lib/live_cable/rendering/view_buffer.rb +21 -0
  49. data/lib/live_cable/testing/render_state.rb +5 -3
  50. data/lib/live_cable/testing/test_channel.rb +6 -0
  51. data/lib/live_cable/testing/test_component.rb +3 -3
  52. data/lib/live_cable/testing.rb +9 -5
  53. data/lib/live_cable/version.rb +1 -1
  54. data/lib/live_cable.rb +32 -3
  55. metadata +9 -1
@@ -12,11 +12,24 @@ module LiveCable
12
12
  end
13
13
 
14
14
  def broadcast_subscribe
15
- broadcast({ _status: 'subscribed', id: live_id })
15
+ broadcast({ _status: 'subscribed', id: live_id, _subscribed: true })
16
+
17
+ # Deliver any events queued before this component had a channel of its
18
+ # own (e.g. dispatched while it was rendered inline by a parent). The
19
+ # broadcast_render path flushes events itself, so this only matters for
20
+ # an already-rendered component that subscribes without re-rendering.
21
+ broadcast_events
22
+ end
23
+
24
+ # Sends any queued events on their own, for when there's no render for
25
+ # them to ride along with.
26
+ def broadcast_events
27
+ events = flush_events
28
+ broadcast(_events: events) if events.any?
16
29
  end
17
30
 
18
- # Sent when a received message didn't change any reactive variables,
19
- # so the client can clear its loading state without a re-render.
31
+ # Answers a message batch when the component's own render didn't -
32
+ # nothing changed, or its render went out inside its parent's.
20
33
  def broadcast_ack
21
34
  broadcast({ _ack: true })
22
35
  end
@@ -26,17 +39,35 @@ module LiveCable
26
39
  @subscribed = false
27
40
  end
28
41
 
29
- def broadcast_render
42
+ # @param subscribed [Boolean] whether this is the render sent when the
43
+ # component subscribes
44
+ # @return [Boolean] false when the render changed nothing on the page, so
45
+ # no _refresh went out
46
+ def broadcast_render(subscribed: false)
47
+ sent = false
48
+
30
49
  run_callbacks :render do
31
- data = { _refresh: render.as_json }
50
+ result = render
51
+ events = flush_events
52
+
53
+ # Leaves the reply unclaimed, so the message is answered with an _ack
54
+ if !subscribed && result.is_a?(LiveCable::Rendering::RenderResult) && result.blank?
55
+ broadcast(_events: events) if events.any?
56
+ next
57
+ end
58
+
59
+ data = { _refresh: result.as_json, _reply: live_connection&.take_reply(self) }
60
+ data[:_subscribed] = true if subscribed
32
61
 
33
62
  # Events ride along with the render so the client can fire them
34
63
  # after the DOM has been morphed
35
- events = flush_events
36
64
  data[:_events] = events if events.any?
37
65
 
38
66
  broadcast(data)
67
+ sent = true
39
68
  end
69
+
70
+ sent
40
71
  end
41
72
  end
42
73
  end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LiveCable
4
+ class Component
5
+ module Dispatching
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ define_model_callbacks :dispatch, only: :before
10
+ end
11
+
12
+ # @return [LiveCable::Dispatch, nil] The client message being dispatched
13
+ attr_reader :current_dispatch
14
+
15
+ # Run the before_dispatch callbacks, then the action or reactive write
16
+ # the message asks for, unless a callback threw :abort.
17
+ #
18
+ # @param dispatch [LiveCable::Dispatch] Already checked against the
19
+ # component's actions and writable variables
20
+ def perform_dispatch(dispatch)
21
+ @current_dispatch = dispatch
22
+
23
+ run_callbacks :dispatch do
24
+ if dispatch.reactive?
25
+ public_send("#{dispatch.name}=", dispatch.value)
26
+ else
27
+ action = method(dispatch.name)
28
+ action.arity.positive? ? action.call(dispatch.params) : action.call
29
+ end
30
+ end
31
+ ensure
32
+ @current_dispatch = nil
33
+ end
34
+ end
35
+ end
36
+ end
@@ -44,7 +44,7 @@ module LiveCable
44
44
  broadcast_destroy
45
45
 
46
46
  # Destroy children as well
47
- previous_render_context&.children&.map(&:destroy)
47
+ owned_children.each(&:destroy)
48
48
  end
49
49
 
50
50
  # Allow the component to access the identified_by methods from the connection
@@ -5,6 +5,10 @@ module LiveCable
5
5
  module ReactiveVariables
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # A client may write a shared name only if every class sharing it declares
9
+ # it writable. Keyed by class name so a reload replaces the entry.
10
+ SHARED_DECLARATIONS = Concurrent::Map.new
11
+
8
12
  included do
9
13
  class_attribute :shared_variables, default: []
10
14
  class_attribute :reactive_variables, default: []
@@ -29,12 +33,22 @@ module LiveCable
29
33
  end
30
34
 
31
35
  create_reactive_variables(variable, initial_value, shared: shared)
36
+ declare_shared(variable, writable:) if shared
32
37
  end
33
38
 
34
39
  def shared(variable, initial_value = nil)
35
40
  self.shared_variables = (shared_variables || []).dup << variable
36
41
 
37
42
  create_reactive_variables(variable, initial_value, shared: true)
43
+ declare_shared(variable, writable: false)
44
+ end
45
+
46
+ # @return [Boolean] whether a client message may set the variable
47
+ def client_writable?(variable)
48
+ return false unless writable_reactive_variables.include?(variable)
49
+ return true unless shared_reactive_variables.include?(variable)
50
+
51
+ SHARED_DECLARATIONS.fetch(variable, {}).values.all?
38
52
  end
39
53
 
40
54
  def actions(*names)
@@ -48,7 +62,15 @@ module LiveCable
48
62
 
49
63
  private
50
64
 
65
+ def declare_shared(variable, writable:)
66
+ SHARED_DECLARATIONS.compute_if_absent(variable) { Concurrent::Map.new }[name || self] = writable
67
+ end
68
+
51
69
  def create_reactive_variables(variable, initial_value, shared: false)
70
+ unless initial_value.nil? || initial_value.is_a?(Proc)
71
+ raise ArgumentError, "Initial value for :#{variable} must be a proc or nil"
72
+ end
73
+
52
74
  define_method(variable) do
53
75
  container_name = shared ? Connection::SHARED_CONTAINER : live_id
54
76
 
@@ -60,11 +82,12 @@ module LiveCable
60
82
 
61
83
  return if initial_value.nil?
62
84
 
63
- if initial_value.arity.positive?
64
- initial_value.call(self)
65
- else
66
- initial_value.call
67
- end
85
+ prerender_container[variable] =
86
+ if initial_value.arity.positive?
87
+ initial_value.call(self)
88
+ else
89
+ initial_value.call
90
+ end
68
91
  end
69
92
 
70
93
  define_method("#{variable}=") do |value|
@@ -99,6 +122,11 @@ module LiveCable
99
122
  @defaults = (defaults || {}).symbolize_keys
100
123
  end
101
124
 
125
+ # @api private
126
+ def round_trip_defaults
127
+ self.defaults = DefaultsSigner.round_trip(defaults, self.class)
128
+ end
129
+
102
130
  def apply_defaults
103
131
  # Don't set defaults more than once
104
132
  return if defaults_applied
@@ -17,6 +17,8 @@ module LiveCable
17
17
 
18
18
  def render
19
19
  @rendered = true
20
+ return render_in(live_connection.view_context) if live_connection
21
+
20
22
  ApplicationController.renderer.render(self, layout: false)
21
23
  end
22
24
 
@@ -38,13 +40,15 @@ module LiveCable
38
40
  end
39
41
 
40
42
  def render_in(view_context)
43
+ template_changed = false
44
+
41
45
  view, render_context = view_context.with_render_context(self) do
42
- result = view_context.render(template: to_partial_path, locals:)
46
+ result = view_context.render(template: to_partial_path, locals: locals(view_context))
43
47
 
44
48
  unless result.is_a?(LiveCable::Rendering::Partial)
45
- warn(
46
- "[LiveCable Warning] #{to_partial_path} was rendered without using a .live.erb template, " \
47
- 'this will be less performant.'
49
+ LiveCable.warn_once(
50
+ "[LiveCable] #{to_partial_path} was rendered without a .live.erb template; " \
51
+ 'the full template diff is sent on every change, which is less performant.'
48
52
  )
49
53
 
50
54
  next result
@@ -87,10 +91,8 @@ module LiveCable
87
91
  end
88
92
 
89
93
  if previous_render_context
90
- # Children from skipped parts are preserved — their part simply didn't
91
- # re-evaluate this cycle, so they haven't actually gone away.
92
- preserved = render_context.preserved_children_from(previous_render_context)
93
- destroyed = previous_render_context.children - render_context.children - preserved
94
+ render_context.inherit_skipped(previous_render_context) unless template_changed
95
+ destroyed = previous_render_context.owned_children - render_context.owned_children
94
96
  destroyed.each(&:destroy)
95
97
  end
96
98
 
@@ -130,6 +132,11 @@ module LiveCable
130
132
  previous_render_context&.children || []
131
133
  end
132
134
 
135
+ # @return [Array<LiveCable::Component>]
136
+ def owned_children
137
+ previous_render_context&.owned_children || []
138
+ end
139
+
133
140
  private
134
141
 
135
142
  # @return [Hash<String, Boolean>]
@@ -145,9 +152,10 @@ module LiveCable
145
152
  matches = html.match(/(?:\n\s*|^\s*|<!--.*?-->)<([a-zA-Z0-9-]+)/)
146
153
 
147
154
  unless matches
155
+ preview = html.to_s.strip[0, 80]
148
156
  raise LiveCable::Error,
149
157
  "#{self.class.name} template must have a single root HTML element " \
150
- '(could not find an opening tag in first part of rendered output)'
158
+ "(could not find an opening tag in the first part of rendered output: #{preview.inspect})"
151
159
  end
152
160
 
153
161
  attributes = {
@@ -157,17 +165,25 @@ module LiveCable
157
165
  'data-live-status-value' => subscribed? ? 'subscribed' : 'disconnected',
158
166
  }
159
167
 
160
- attributes['live-defaults'] = defaults.to_json unless live_connection
168
+ # Sign the defaults so the client can't tamper with them on the round
169
+ # trip and set non-writable reactive variables at subscribe time.
170
+ unless live_connection
171
+ attributes['live-defaults'] = LiveCable::DefaultsSigner.sign(defaults, live_id)
172
+ end
161
173
 
162
- html.insert(matches.end(1), " #{view_context.tag.attributes(attributes)}".html_safe)
163
- html
174
+ # Build a new string rather than mutating `html` in place with
175
+ # String#insert - the rendered part may be frozen, which would raise
176
+ # a FrozenError.
177
+ insert_at = matches.end(1)
178
+ rendered_attributes = " #{view_context.tag.attributes(attributes)}"
179
+ "#{html[0...insert_at]}#{rendered_attributes}#{html[insert_at..]}".html_safe
164
180
  end
165
181
 
166
- def locals
182
+ def locals(view_context)
167
183
  identifiers = channel ? channel.connection.identifiers.to_a : []
168
184
 
169
185
  (all_reactive_variables | (self.class.shared_variables || []) | identifiers).
170
- to_h { |v| [v, public_send(v)] }.
186
+ to_h { |v| [v, view_context.live_cable_unwrap(public_send(v))] }.
171
187
  merge(
172
188
  component: self
173
189
  )
@@ -11,6 +11,13 @@ module LiveCable
11
11
  additional_streams.each do |stream_name|
12
12
  channel.stop_stream_from(stream_name)
13
13
  end
14
+
15
+ additional_streams.clear
16
+ end
17
+
18
+ def stop_stream_from(channel_name)
19
+ channel.stop_stream_from(channel_name)
20
+ additional_streams.delete(channel_name)
14
21
  end
15
22
 
16
23
  def stream_from(channel_name, callback = nil, coder: nil, &block)
@@ -19,11 +26,35 @@ module LiveCable
19
26
  channel.stream_from(channel_name, coder:) do |payload|
20
27
  callback ||= block
21
28
 
22
- begin
23
- callback.call(payload)
24
- live_connection.broadcast_changeset
29
+ # Stopping a stream, as disconnect does, can't take back a broadcast
30
+ # already queued, which may run after it or wait on the lock below
31
+ connection = live_connection
32
+ next unless connection
33
+
34
+ # Held across the callback and the render together: the callback's
35
+ # writes are what race an action on another worker, so locking only
36
+ # the broadcast would leave the race in place.
37
+ connection.synchronize do
38
+ next unless live_connection == connection
39
+ next unless additional_streams.include?(channel_name)
40
+
41
+ connection.reset_changeset
42
+
43
+ # A rescue_from handler that takes the error may have changed state
44
+ # to show it, so render in that case too; after an _error there is
45
+ # nothing left to render into.
46
+ handled = begin
47
+ callback.call(payload)
48
+ true
49
+ rescue StandardError => error
50
+ connection.rescue_error(self, error)
51
+ end
52
+
53
+ connection.broadcast_changeset if handled
54
+ # Nothing above this rescues a stream callback, so a raising
55
+ # rescue_from handler or render must not escape to ActionCable
25
56
  rescue StandardError => error
26
- live_connection.handle_error(self, error)
57
+ connection.handle_error(self, error)
27
58
  end
28
59
  end
29
60
  end
@@ -6,6 +6,7 @@ module LiveCable
6
6
  include ReactiveVariables
7
7
  include Identification
8
8
  include Lifecycle
9
+ include Dispatching
9
10
  include Broadcasting
10
11
  include Events
11
12
  include Rendering
@@ -17,6 +18,9 @@ module LiveCable
17
18
  # @return [String]
18
19
  attr_reader :id
19
20
 
21
+ # @return [Hash]
22
+ attr_reader :defaults
23
+
20
24
  def initialize(id, **defaults)
21
25
  @id = self.class.resolve_id(id)
22
26
  @rendered = false
@@ -32,9 +36,6 @@ module LiveCable
32
36
  # @return [Boolean]
33
37
  attr_reader :subscribed
34
38
 
35
- # @return [Hash]
36
- attr_reader :defaults
37
-
38
39
  # @return [LiveCable::Channel, nil]
39
40
  attr_reader :channel
40
41
  end
@@ -8,6 +8,12 @@ module LiveCable
8
8
  # @return [Array<LiveCable::Component>] Components that were broadcast to
9
9
  # (rendered or errored), including children rendered by their parents
10
10
  def broadcast_changeset
11
+ synchronize { broadcast_changeset_unsynchronized }
12
+ end
13
+
14
+ private
15
+
16
+ def broadcast_changeset_unsynchronized
11
17
  rendered = []
12
18
  shared_changeset = containers[SHARED_CONTAINER]&.changeset
13
19
 
@@ -32,10 +38,16 @@ module LiveCable
32
38
 
33
39
  # Deliver events from components that didn't broadcast a render this
34
40
  # cycle (no state change, or rendered inline by a parent) - rendered
35
- # components already flushed their events with the refresh
41
+ # components already flushed their events with the refresh.
42
+ #
43
+ # A component rendered inline by a parent has no channel of its own
44
+ # yet, so it can't deliver anything. Leave its events queued (don't
45
+ # flush) so they're delivered when its own subscription connects,
46
+ # rather than silently dropped here.
36
47
  components.each_value do |component|
37
- events = component.flush_events
38
- component.broadcast(_events: events) if events.any?
48
+ next unless component.subscribed?
49
+
50
+ component.broadcast_events
39
51
  end
40
52
 
41
53
  rendered
@@ -10,19 +10,23 @@ module LiveCable
10
10
  end
11
11
 
12
12
  def add_component(component)
13
- component.live_connection = self
14
- components[component.live_id] = component
13
+ synchronize do
14
+ component.live_connection = self
15
+ components[component.live_id] = component
16
+ end
15
17
  end
16
18
 
17
19
  def remove_component(component)
18
- # Clean up the container to break observer reference chains
19
- container = containers.delete(component.live_id)
20
- container&.cleanup
20
+ synchronize do
21
+ # Clean up the container to break observer reference chains
22
+ container = containers.delete(component.live_id)
23
+ container&.cleanup
21
24
 
22
- components.delete(component.live_id)
25
+ components.delete(component.live_id)
23
26
 
24
- # Clean up shared container if no components remain
25
- cleanup_shared_container
27
+ # Clean up shared container if no components remain
28
+ cleanup_shared_container
29
+ end
26
30
  end
27
31
 
28
32
  private
@@ -5,6 +5,24 @@ module LiveCable
5
5
  module ErrorHandling
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # Offer an error raised by the component's own code - an action, a
9
+ # reactive write, a stream callback - to its rescue_from handlers, and
10
+ # fall back to #handle_error when none matches.
11
+ #
12
+ # A handled error is a processed message, not a failed one: no _error
13
+ # goes out, so the caller must still answer with a render or an _ack or
14
+ # the client's loading state never clears.
15
+ #
16
+ # @param component [LiveCable::Component]
17
+ # @param error [Exception]
18
+ # @return [Boolean] true when a handler took the error
19
+ def rescue_error(component, error)
20
+ return true if component.rescue_with_handler(error)
21
+
22
+ handle_error(component, error)
23
+ false
24
+ end
25
+
8
26
  # Report an error and replace the component on the client with an
9
27
  # error box.
10
28
  #
@@ -16,16 +34,19 @@ module LiveCable
16
34
  # through; needed when the component failed before it connected to
17
35
  # one, or when there is no component at all
18
36
  def handle_error(component, error, channel: nil)
19
- Rails.error.report(error)
37
+ synchronize do
38
+ take_reply(component)
39
+ Rails.error.report(error)
20
40
 
21
- html = error_html(component, error)
41
+ html = error_html(component, error)
22
42
 
23
- # Destroy children first so their _status:destroy messages arrive before _error
24
- component&.rendered_children&.each(&:destroy)
43
+ # Destroy children first so their _status:destroy messages arrive before _error
44
+ component&.owned_children&.each(&:destroy)
25
45
 
26
- # Broadcast the error - JS replaces the DOM and calls unsubscribe(),
27
- # which triggers LiveChannel#unsubscribed -> component.disconnect for server cleanup
28
- (channel || component)&.broadcast(_error: html)
46
+ # Broadcast the error - JS replaces the DOM and calls unsubscribe(),
47
+ # which triggers LiveChannel#unsubscribed -> component.disconnect for server cleanup
48
+ (channel || component)&.broadcast(_error: html)
49
+ end
29
50
  end
30
51
 
31
52
  private
@@ -6,79 +6,67 @@ module LiveCable
6
6
  extend ActiveSupport::Concern
7
7
 
8
8
  def receive(component, data)
9
- reset_changeset
9
+ synchronize do
10
+ reset_changeset
10
11
 
11
- return unless data['messages'].present?
12
+ return unless data['messages'].present?
12
13
 
13
- # An error broadcasts an _error, which is itself the batch's one
14
- # response - so a failed message must suppress the trailing _ack
15
- errored = false
16
- data['messages'].each do |message|
17
- errored = true unless action(component, message)
18
- end
19
-
20
- rendered = broadcast_changeset
21
-
22
- # Guarantee exactly one response per message batch so the client can
23
- # clear its loading state even when nothing changed
24
- component.broadcast_ack unless errored || rendered.include?(component)
25
- end
26
-
27
- # @return [Boolean] true when the message was processed, false when an
28
- # error was handled (and an _error broadcast in its place)
29
- def action(component, data)
30
- params = parse_params(data)
14
+ @reply_to = component
31
15
 
32
- if data['_action']
33
- action = data['_action']&.to_sym
34
-
35
- if action == :_reactive
36
- return reactive(component, data)
37
- end
38
-
39
- unless component.class.allowed_actions.include?(action)
40
- raise LiveCable::Error, "Unauthorized action: #{action}"
16
+ data['messages'].each do |message|
17
+ action(component, message)
41
18
  end
42
19
 
43
- method = component.method(action)
20
+ broadcast_changeset
44
21
 
45
- if method.arity.positive?
46
- method.call(params)
47
- else
48
- method.call
49
- end
22
+ # Guarantee exactly one reply per message batch so the client can
23
+ # clear its loading state: the component's own render, an _error,
24
+ # or this
25
+ component.broadcast_ack if take_reply(component)
26
+ ensure
27
+ @reply_to = nil
50
28
  end
29
+ end
51
30
 
31
+ # @return [Boolean] true, once, for the frame that answers the message
32
+ # batch the component sent
33
+ def take_reply(component)
34
+ return false unless component && @reply_to.equal?(component)
35
+
36
+ @reply_to = nil
52
37
  true
53
- rescue StandardError => e
54
- handle_error(component, e)
55
- false
56
38
  end
57
39
 
58
- # @return [Boolean] true when applied, false when an error was handled
59
- def reactive(component, data)
60
- unless component.class.writable_reactive_variables.include?(data['name'].to_sym)
61
- raise LiveCable::Error, "Non-writable reactive variable: #{data['name']}"
62
- end
40
+ # @return [Boolean] true when the message was processed (including one a
41
+ # before_dispatch callback halted, or whose error the component's
42
+ # rescue_from took), false when an _error was broadcast in its place
43
+ def action(component, data)
44
+ return true unless data['_action']
63
45
 
64
- component.public_send("#{data['name']}=", data['value'])
46
+ dispatch = Dispatch.from_message(data)
47
+ ensure_exposed(component, dispatch)
48
+ component.perform_dispatch(dispatch)
65
49
 
66
50
  true
67
- rescue StandardError => e
51
+ rescue LiveCable::Forbidden => e
68
52
  handle_error(component, e)
69
53
  false
54
+ rescue StandardError => e
55
+ rescue_error(component, e)
70
56
  end
71
57
 
72
58
  private
73
59
 
74
- def parse_params(data)
75
- params = data['params'] || ''
60
+ def ensure_exposed(component, dispatch)
61
+ if dispatch.reactive?
62
+ return if component.class.client_writable?(dispatch.name)
63
+
64
+ raise LiveCable::Forbidden, "Non-writable reactive variable: #{dispatch.name}"
65
+ end
66
+
67
+ return if component.class.allowed_actions.include?(dispatch.name)
76
68
 
77
- ActionController::Parameters.new(
78
- ActionDispatch::ParamBuilder.from_pairs(
79
- ActionDispatch::QueryParser.each_pair(params)
80
- )
81
- )
69
+ raise LiveCable::Forbidden, "Unauthorized action: #{dispatch.name}"
82
70
  end
83
71
  end
84
72
  end
@@ -10,7 +10,8 @@ module LiveCable
10
10
 
11
11
  return container[variable] if container.key?(variable)
12
12
 
13
- container[variable] = process_initial_value(component, variable, initial_value)
13
+ container[variable] = process_initial_value(component, initial_value)
14
+ container[variable]
14
15
  end
15
16
 
16
17
  def set(container_name, variable, value)
@@ -31,25 +32,21 @@ module LiveCable
31
32
  shared_changeset = containers[SHARED_CONTAINER]&.changeset
32
33
  container = containers[component.live_id]
33
34
 
34
- container.changeset | component.shared_reactive_variables.intersection(shared_changeset)
35
+ changeset = container.changeset | component.shared_reactive_variables.intersection(shared_changeset)
36
+ return changeset if changeset.empty?
37
+
38
+ changeset | component.shared_variables
35
39
  end
36
40
 
37
41
  private
38
42
 
39
- def process_initial_value(component, variable, initial_value)
40
- case initial_value
41
- when nil
42
- nil
43
- when Proc
44
- args = []
45
- args << component if initial_value.arity.positive?
46
-
47
- initial_value.call(*args)
48
- else
49
- raise LiveCable::Error, "Initial value for \":#{variable}\" must be a proc or nil"
50
- end
51
- rescue StandardError => e
52
- handle_error(component, e)
43
+ def process_initial_value(component, initial_value)
44
+ return if initial_value.nil?
45
+
46
+ args = []
47
+ args << component if initial_value.arity.positive?
48
+
49
+ initial_value.call(*args)
53
50
  end
54
51
  end
55
52
  end