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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8811ee6a3c0f044fd7c9d16843a641296b33b8fcb5872f5bc8822257f837148e
4
- data.tar.gz: 9398a1c088680e83b8bed3398342345ceb817703183df0985a1948e29df021cf
3
+ metadata.gz: 8ec8a731ff4ae0e729288d805070e04c8860b7a51591a69b946ed42bd4996d97
4
+ data.tar.gz: '018dafe55b68a05a46ef52c4e896803a9933beecf50e87f1d428073065fd3205'
5
5
  SHA512:
6
- metadata.gz: b17a5d83f62e801d0f8673f824f929a82d1c7efc2d0aa2274034639ed94b2c8c4723f11484427f051c7be15d523a0b99cd11594bed0d701e4ff2e07604a1edd8
7
- data.tar.gz: b5990726c0531bd2d6bc2da23df8bb2dbf5220ee702306c86f8094690da8c4a1752fa8ffb7949261050858256e5fd70854491603ac2306221c9f7bf2078f943c
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
- # True when one of `streams` already carries a fresh token TARGETING this
111
- # component i.e. the caller hand-built the actor's own token-bearing
112
- # stream, so appending to_stream_token would double it. Scoped to the
113
- # component's target id (not a global substring) so a sibling component's
114
- # stream, which carries its OWN token for a DIFFERENT target, doesn't fool
115
- # us into skipping this component's refresh.
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? { it.include?("data-reactive-token-value") && it.include?(target) }
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. Memoized PER THREAD building a
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.new.view_context
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),
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.2"
6
6
  end
7
7
  end
@@ -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.new.view_context
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),
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: phlex-reactive
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson