reactive_component 0.7.1 → 0.7.2
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 +23 -0
- data/app/channels/reactive_component/channel.rb +24 -9
- data/app/controllers/reactive_component/actions_controller.rb +2 -0
- data/app/javascript/reactive_component/controllers/reactive_renderer_controller.js +22 -1
- data/lib/reactive_component/version.rb +1 -1
- data/lib/reactive_component.rb +4 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ef3f09713ddda3cdec03ffd6dac861df951589a1b077504a7ea1323e6ac8d73
|
|
4
|
+
data.tar.gz: a08ef29771672722c29145c51f15a9b159d0b9487d801dec2a045d0a5ebf0127
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e7142557dca9daff0f7f5b9a6034144228d47578c417e1a9e959694f8ec0e62371fe4757bffc16738ef7ed8879f40cffa53b347887a5a1a9cb153ce63f7756fc
|
|
7
|
+
data.tar.gz: 92e76f033d5883773851f94f551499c78072a33624fe016ce579834be04e1cfcad000c0e8ba6009500fa2477c05fa3ca401cc755c061c2d86b736a430d224d46
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.2] - 2026-09-07
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
- The actions endpoint enforces CSRF itself with `protect_from_forgery`
|
|
7
|
+
instead of relying on the host's `load_defaults`.
|
|
8
|
+
- `live_action` tokens expire after `ReactiveComponent.action_token_ttl`
|
|
9
|
+
(default one day). Before, a token stayed valid forever.
|
|
10
|
+
- `request_update` passes only declared `client_state` fields to the
|
|
11
|
+
component. Any other client key was set as an instance variable and
|
|
12
|
+
handed to the constructor.
|
|
13
|
+
- `request_update` over the channel only answers for records whose
|
|
14
|
+
`broadcasts` stream is the one the subscriber verified. Before, any
|
|
15
|
+
subscriber could pull the rendered data of any record id of the model.
|
|
16
|
+
- The channel resolves the component name with `safe_constantize` and
|
|
17
|
+
requires a class that includes `ReactiveComponent`.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- The renderer controller imports its utils by bare specifier again. The
|
|
21
|
+
0.7.1 relative import resolved to an undigested `/assets/` URL under
|
|
22
|
+
Propshaft and 404ed, which left Turbo stream sources never connecting and
|
|
23
|
+
every system test failing. The bare specifier works for both importmap
|
|
24
|
+
(`pin_all_from`) and npm (`exports`).
|
|
25
|
+
|
|
3
26
|
## [0.7.1] - 2026-09-06
|
|
4
27
|
|
|
5
28
|
### Added
|
|
@@ -7,9 +7,9 @@ module ReactiveComponent
|
|
|
7
7
|
class_attribute :filter_callback, default: nil
|
|
8
8
|
|
|
9
9
|
def subscribed
|
|
10
|
-
stream_name = verified_stream_name
|
|
11
|
-
if stream_name
|
|
12
|
-
stream_from stream_name
|
|
10
|
+
@stream_name = verified_stream_name
|
|
11
|
+
if @stream_name
|
|
12
|
+
stream_from @stream_name
|
|
13
13
|
else
|
|
14
14
|
reject
|
|
15
15
|
end
|
|
@@ -20,12 +20,8 @@ module ReactiveComponent
|
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def request_update(data)
|
|
23
|
-
component_class = data['component'].constantize
|
|
24
23
|
params = data['params'] || {}
|
|
25
|
-
|
|
26
|
-
model_class = component_class.live_model_class
|
|
27
|
-
record_id = data['record_id'] || params.delete('record_id')
|
|
28
|
-
record = model_class.find_by(id: record_id)
|
|
24
|
+
component_class, record = subscribed_component_and_record(data, params)
|
|
29
25
|
return unless record
|
|
30
26
|
|
|
31
27
|
if data['record_id'].present?
|
|
@@ -35,13 +31,32 @@ module ReactiveComponent
|
|
|
35
31
|
transmit({ 'action' => 'remove', 'dom_id' => data['dom_id'] })
|
|
36
32
|
end
|
|
37
33
|
else
|
|
38
|
-
|
|
34
|
+
client_state = params.slice(*component_class._client_state_fields.keys.map(&:to_s))
|
|
35
|
+
result = component_class.build_data(record, **client_state.symbolize_keys)
|
|
39
36
|
transmit({ 'action' => 'render', 'data' => result })
|
|
40
37
|
end
|
|
41
38
|
end
|
|
42
39
|
|
|
43
40
|
private
|
|
44
41
|
|
|
42
|
+
# The component must be a reactive component and the record must broadcast
|
|
43
|
+
# to the stream this subscriber verified: the same stream the wrapper
|
|
44
|
+
# signed into the page. Anything else is a guess at a record id the client
|
|
45
|
+
# was never shown.
|
|
46
|
+
def subscribed_component_and_record(data, params)
|
|
47
|
+
component_class = data['component'].to_s.safe_constantize
|
|
48
|
+
return unless component_class.is_a?(Class) && component_class.include?(ReactiveComponent)
|
|
49
|
+
|
|
50
|
+
record = component_class.live_model_class.find_by(id: data['record_id'] || params.delete('record_id'))
|
|
51
|
+
return unless record
|
|
52
|
+
|
|
53
|
+
stream = ReactiveComponent::Wrapper.find_stream_for(component_class, record)
|
|
54
|
+
signed = Turbo::StreamsChannel.signed_stream_name(stream)
|
|
55
|
+
return unless Turbo::StreamsChannel.verified_stream_name(signed) == @stream_name
|
|
56
|
+
|
|
57
|
+
[component_class, record]
|
|
58
|
+
end
|
|
59
|
+
|
|
45
60
|
def record_matches?(record, params)
|
|
46
61
|
return true unless self.class.filter_callback
|
|
47
62
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Controller } from "@hotwired/stimulus"
|
|
2
2
|
import { createConsumer } from "@rails/actioncable"
|
|
3
|
-
import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage, duplicateIds, strictData } from "
|
|
3
|
+
import { compileTemplate, decompress, morphElement, buildActionBody, routeMessage, duplicateIds, strictData } from "reactive_component/lib/reactive_renderer_utils"
|
|
4
4
|
|
|
5
5
|
const consumer = createConsumer()
|
|
6
6
|
const log = (...args) => {
|
|
@@ -21,6 +21,18 @@ function subscribe(streamValue, controller) {
|
|
|
21
21
|
sub = consumer.subscriptions.create(
|
|
22
22
|
{ channel: "ReactiveComponent::Channel", signed_stream_name: streamValue },
|
|
23
23
|
{
|
|
24
|
+
connected() {
|
|
25
|
+
sub._connected = true
|
|
26
|
+
for (const handler of sub.handlers || []) {
|
|
27
|
+
handler.subscriptionConnected()
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
disconnected() {
|
|
31
|
+
sub._connected = false
|
|
32
|
+
for (const handler of sub.handlers || []) {
|
|
33
|
+
handler.subscriptionDisconnected()
|
|
34
|
+
}
|
|
35
|
+
},
|
|
24
36
|
received: async (message) => {
|
|
25
37
|
const decoded = message.z ? await decompress(message.z) : message
|
|
26
38
|
for (const handler of sub.handlers) {
|
|
@@ -32,6 +44,7 @@ function subscribe(streamValue, controller) {
|
|
|
32
44
|
sub.handlers = new Set()
|
|
33
45
|
}
|
|
34
46
|
sub.handlers.add(controller)
|
|
47
|
+
if (sub._connected) controller.subscriptionConnected()
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
function unsubscribe(streamValue, controller) {
|
|
@@ -90,6 +103,14 @@ export default class extends Controller {
|
|
|
90
103
|
}
|
|
91
104
|
}
|
|
92
105
|
|
|
106
|
+
subscriptionConnected() {
|
|
107
|
+
this.element.setAttribute("data-reactive-renderer-connected", "")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
subscriptionDisconnected() {
|
|
111
|
+
this.element.removeAttribute("data-reactive-renderer-connected")
|
|
112
|
+
}
|
|
113
|
+
|
|
93
114
|
resolveTemplate() {
|
|
94
115
|
if (this.hasTemplateValue) return this.templateValue
|
|
95
116
|
|
data/lib/reactive_component.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/core_ext/integer/time'
|
|
3
4
|
require 'active_support/concern'
|
|
4
5
|
|
|
5
6
|
require_relative 'reactive_component/version'
|
|
@@ -14,6 +15,8 @@ module ReactiveComponent
|
|
|
14
15
|
|
|
15
16
|
mattr_accessor :debug, default: false
|
|
16
17
|
mattr_accessor :renderer, default: nil
|
|
18
|
+
# How long a live_action token minted into a page stays valid.
|
|
19
|
+
mattr_accessor :action_token_ttl, default: 1.day
|
|
17
20
|
|
|
18
21
|
class Error < StandardError; end
|
|
19
22
|
|
|
@@ -204,7 +207,7 @@ module ReactiveComponent
|
|
|
204
207
|
def live_action_token(record)
|
|
205
208
|
live_action_verifier.generate(
|
|
206
209
|
{ c: name, m: record.class.name, r: record.id },
|
|
207
|
-
purpose: :reactive_component_action
|
|
210
|
+
purpose: :reactive_component_action, expires_in: ReactiveComponent.action_token_ttl
|
|
208
211
|
)
|
|
209
212
|
end
|
|
210
213
|
|