phlex-reactive 0.4.0 → 0.4.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 +46 -0
- data/app/controllers/phlex/reactive/actions_controller.rb +41 -7
- data/lib/phlex/reactive/streamable.rb +5 -2
- data/lib/phlex/reactive/version.rb +1 -1
- data/lib/phlex/reactive.rb +25 -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: 8ec8a731ff4ae0e729288d805070e04c8860b7a51591a69b946ed42bd4996d97
|
|
4
|
+
data.tar.gz: '018dafe55b68a05a46ef52c4e896803a9933beecf50e87f1d428073065fd3205'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 949549bc92c39def933aea8d83cf61db41ad686c8184c8a284657bdd99e1178dece129258e0e874ec1dd157748f69fcd11dc77fe140557c20d7dab217500a46b
|
|
7
|
+
data.tar.gz: ab245aa3bf1a4f65f392ae2c4b3e98cf20a6d45064375a2c2168b2c7214ed4d27bc78c6cd0f069d2ef82cad771ee233376742de6e22636f5b0efb81390f5b8ea
|
data/CHANGELOG.md
CHANGED
|
@@ -72,6 +72,52 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
72
72
|
|
|
73
73
|
### Fixed
|
|
74
74
|
|
|
75
|
+
- **Collections of *reactive* rows were still add-once-only — a prepended/appended
|
|
76
|
+
row's OWN token suppressed the container's token refresh (#44).** When an action
|
|
77
|
+
appended/prepended a *reactive* child row into a container whose `#id` equals the
|
|
78
|
+
append target (the most common collection shape — rows go directly into the
|
|
79
|
+
container element), that single `append`/`prepend` stream carried BOTH
|
|
80
|
+
`target="<container>"` and the row's own `data-reactive-token-value` (embedded in
|
|
81
|
+
the `<template>`). `carries_token_for?` matched
|
|
82
|
+
`include?("data-reactive-token-value") && include?(target)` in that one string →
|
|
83
|
+
both true → it concluded the container's token was already fresh and **skipped**
|
|
84
|
+
the container's `reactive:token` refresh. So the container (which owns the
|
|
85
|
+
add/remove trigger) never rolled its token forward, and the second dispatch was
|
|
86
|
+
rejected with the stale token — the list silently stopped after the first add.
|
|
87
|
+
This hit both the hand-rolled `reply.streams(Row.prepend(...), …)` form **and**
|
|
88
|
+
the `reactive_collection` / `reply.append`/`reply.prepend` helper, since they
|
|
89
|
+
share the gate. Fixed by tightening `carries_token_for?`: a stream "carries the
|
|
90
|
+
token for C" only when its action RE-RENDERS C itself (`replace`/`update`/
|
|
91
|
+
`reactive:token` at C's target) — `append`/`prepend` (which insert *children*
|
|
92
|
+
into C) never count, because a reactive child's token is not the container's. The
|
|
93
|
+
idempotency the gate provided is preserved (a caller's own self-replace still
|
|
94
|
+
suppresses the duplicate token) and the #30 sibling case still refreshes
|
|
95
|
+
correctly. Covered at the request level: a reactive row appended twice in a row
|
|
96
|
+
(the second using the token the first rolled forward) now succeeds, and each
|
|
97
|
+
response carries a `reactive:token` stream targeting the container with a
|
|
98
|
+
non-empty token. Refs cosmos#1939, #35, #30.
|
|
99
|
+
|
|
100
|
+
- **0.4.0 regression: re-renders lost the `request`, crashing `form_authenticity_token`
|
|
101
|
+
and other request-dependent helpers (#42).** The 0.4.0 render-path perf rework
|
|
102
|
+
built the cached off-request view context from a bare
|
|
103
|
+
`ActionController::Base.new.view_context`, whose controller has `request == nil`.
|
|
104
|
+
Any reactive component whose `view_template` calls `form_authenticity_token`,
|
|
105
|
+
`protect_against_forgery?`, or a host-aware URL helper (all of which read
|
|
106
|
+
`request.env`) then raised `NoMethodError: undefined method 'env' for nil` on
|
|
107
|
+
**every** re-render — through the action endpoint AND on broadcasts — so any app
|
|
108
|
+
with a reactive component rendering a Rails CSRF form could not adopt 0.4.0. Fixed
|
|
109
|
+
by building the cached view context through a request-bound controller
|
|
110
|
+
(`Phlex::Reactive.request_bound_view_context`), replicating exactly what
|
|
111
|
+
`ActionController::Renderer#render` does to set up its mock request: an
|
|
112
|
+
`ActionDispatch::Request` whose host is derived from the routes'
|
|
113
|
+
`default_url_options`. The 0.4.0 `render_in` speedup is kept — the request is set
|
|
114
|
+
up once when the per-thread context is built (not per render), and steady-state
|
|
115
|
+
render throughput/allocations are unchanged (retained-per-render stays 0). Covered
|
|
116
|
+
at the request level (a reactive `save` re-rendering a CSRF form returns 200, not
|
|
117
|
+
500), the broadcast level (the form component broadcasts without crashing), and the
|
|
118
|
+
unit level (a request is present on the cached context; host-aware `*_url` helpers
|
|
119
|
+
resolve when the renderer carries routes).
|
|
120
|
+
|
|
75
121
|
- **Multipart path silently dropped an explicit nested-hash / array param (#39).**
|
|
76
122
|
When an action declared a `:file` param alongside an explicit nested (hash/array)
|
|
77
123
|
param, the nested param was dropped on the multipart path while the JSON path
|
|
@@ -107,15 +107,49 @@ module Phlex
|
|
|
107
107
|
streams
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# component
|
|
114
|
-
#
|
|
115
|
-
#
|
|
110
|
+
# Actions that RE-RENDER the component's own root (so the root's fresh
|
|
111
|
+
# data-reactive-token-value rolls the signed token forward). `append`/
|
|
112
|
+
# `prepend` are deliberately excluded: they insert CHILDREN into the
|
|
113
|
+
# component, and a reactive child carries its OWN token — that child token is
|
|
114
|
+
# not the component's (issue #44). reactive:token is our inert token-only
|
|
115
|
+
# refresh; replace/update re-render the root.
|
|
116
|
+
SELF_RENDER_ACTIONS = %w[replace update reactive:token].freeze
|
|
117
|
+
private_constant :SELF_RENDER_ACTIONS
|
|
118
|
+
|
|
119
|
+
# True when one of `streams` already carries a fresh token by RE-RENDERING
|
|
120
|
+
# this component itself — i.e. the caller hand-built the actor's own
|
|
121
|
+
# token-bearing stream, so appending to_stream_token would double it.
|
|
122
|
+
#
|
|
123
|
+
# The match requires BOTH (a) the stream's action re-renders the component's
|
|
124
|
+
# ROOT (replace/update/reactive:token — never append/prepend, which insert
|
|
125
|
+
# children) AND (b) it targets the component's id AND (c) it actually carries
|
|
126
|
+
# a token. This is stricter than a same-string substring check on purpose:
|
|
127
|
+
# * A sibling component's replace targets a DIFFERENT id → (b) fails, so we
|
|
128
|
+
# still refresh ours (issue #30).
|
|
129
|
+
# * A reactive child row appended/prepended INTO the component carries its
|
|
130
|
+
# own token at the component's target, but the action is append/prepend →
|
|
131
|
+
# (a) fails, so we still refresh the CONTAINER's token (issue #44). Before
|
|
132
|
+
# this, the child's token at the container target suppressed the
|
|
133
|
+
# container's refresh and the list was add-once-only.
|
|
116
134
|
def carries_token_for?(streams, component)
|
|
117
135
|
target = %(target="#{ERB::Util.html_escape(component.id)}")
|
|
118
|
-
streams.any?
|
|
136
|
+
streams.any? do
|
|
137
|
+
it.include?("data-reactive-token-value") &&
|
|
138
|
+
it.include?(target) &&
|
|
139
|
+
self_render_stream_for?(it, target)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Does this turbo-stream's OPENING tag re-render `target` itself? Matches a
|
|
144
|
+
# `<turbo-stream action="<self-render>" ... target="<id>">` opening tag — the
|
|
145
|
+
# action and target on the SAME tag — so a child row's token embedded in an
|
|
146
|
+
# append/prepend `<template>` can never count as the container's own refresh.
|
|
147
|
+
def self_render_stream_for?(stream, target)
|
|
148
|
+
open_tag = stream[/<turbo-stream\b[^>]*>/]
|
|
149
|
+
return false unless open_tag&.include?(target)
|
|
150
|
+
|
|
151
|
+
action = open_tag[/\baction="([^"]+)"/, 1]
|
|
152
|
+
SELF_RENDER_ACTIONS.include?(action)
|
|
119
153
|
end
|
|
120
154
|
|
|
121
155
|
# A 200 turbo-stream carrying a namespaced custom action the client turns
|
|
@@ -83,7 +83,10 @@ module Phlex
|
|
|
83
83
|
|
|
84
84
|
# Turbo::Streams::TagBuilder needs a real VIEW CONTEXT (it calls
|
|
85
85
|
# `.formats` on it), not a controller class. Build one off-request from
|
|
86
|
-
# the configured renderer controller
|
|
86
|
+
# the configured renderer controller, bound to a real request so
|
|
87
|
+
# request-dependent helpers (form_authenticity_token, host-aware URL
|
|
88
|
+
# helpers) work during a re-render/broadcast (issue #42). Memoized PER
|
|
89
|
+
# THREAD — building a
|
|
87
90
|
# view context instantiates the renderer controller and assembles its
|
|
88
91
|
# whole helper module set, so doing it per render/broadcast was the
|
|
89
92
|
# hottest server-side allocation in the action round trip. The builder
|
|
@@ -260,7 +263,7 @@ module Phlex
|
|
|
260
263
|
cached = store[self]
|
|
261
264
|
|
|
262
265
|
unless cached && cached.renderer.equal?(current) && cached.generation == generation
|
|
263
|
-
view_context = current
|
|
266
|
+
view_context = Phlex::Reactive.request_bound_view_context(current)
|
|
264
267
|
cached = ThreadViewContext.new(
|
|
265
268
|
view_context,
|
|
266
269
|
::Turbo::Streams::TagBuilder.new(view_context),
|
data/lib/phlex/reactive.rb
CHANGED
|
@@ -78,6 +78,30 @@ module Phlex
|
|
|
78
78
|
@renderer ||= defined?(::ActionController::Base) ? ::ActionController::Base : nil
|
|
79
79
|
end
|
|
80
80
|
|
|
81
|
+
# Build an off-request view context whose controller has a REAL `request`.
|
|
82
|
+
#
|
|
83
|
+
# A bare `controller.new.view_context` (the naive off-request context) has
|
|
84
|
+
# `request == nil`, so any request-dependent helper raises
|
|
85
|
+
# `undefined method 'env' for nil` — form_authenticity_token,
|
|
86
|
+
# protect_against_forgery?, and host-aware URL helpers all read
|
|
87
|
+
# `request.env` (issue #42). We replicate exactly what
|
|
88
|
+
# ActionController::Renderer#render does to set up its mock request — build
|
|
89
|
+
# an ActionDispatch::Request from the renderer's env (which derives the host
|
|
90
|
+
# from the routes' default_url_options), bind the routes, and attach it —
|
|
91
|
+
# then return the controller's view context instead of rendering a template.
|
|
92
|
+
# The result keeps the 0.4.0 render_in speedup (no renderer.render
|
|
93
|
+
# machinery) while restoring the request those helpers need.
|
|
94
|
+
def request_bound_view_context(controller_class)
|
|
95
|
+
ar_renderer = controller_class.renderer
|
|
96
|
+
request = ::ActionDispatch::Request.new(ar_renderer.send(:env_for_request))
|
|
97
|
+
request.routes = controller_class._routes
|
|
98
|
+
|
|
99
|
+
instance = controller_class.new
|
|
100
|
+
instance.set_request!(request)
|
|
101
|
+
instance.set_response!(controller_class.make_response!(request))
|
|
102
|
+
instance.view_context
|
|
103
|
+
end
|
|
104
|
+
|
|
81
105
|
# DOM id of the host-app container a Response#flash appends into.
|
|
82
106
|
# Default "flash"; override to match your layout's flash region.
|
|
83
107
|
def flash_target
|
|
@@ -136,7 +160,7 @@ module Phlex
|
|
|
136
160
|
generation = off_request_view_context_generation
|
|
137
161
|
|
|
138
162
|
unless cache && cache[:renderer].equal?(current) && cache[:generation] == generation
|
|
139
|
-
view_context = current
|
|
163
|
+
view_context = request_bound_view_context(current)
|
|
140
164
|
cache = {
|
|
141
165
|
view_context: view_context,
|
|
142
166
|
builder: ::Turbo::Streams::TagBuilder.new(view_context),
|