Module: Phlex::Reactive
- Defined in:
- lib/phlex/reactive.rb,
lib/phlex/reactive/reply.rb,
lib/phlex/reactive/engine.rb,
lib/phlex/reactive/version.rb,
lib/phlex/reactive/response.rb,
lib/phlex/reactive/component.rb,
lib/phlex/reactive/streamable.rb,
app/controllers/phlex/reactive/actions_controller.rb,
lib/generators/phlex/reactive/install/install_generator.rb,
lib/generators/phlex/reactive/component/component_generator.rb
Overview
phlex-reactive: reactive Phlex components for Rails.
Two cooperating mixins, one client runtime, one endpoint:
* Phlex::Reactive::Streamable — gives a component a stable `id` and class
methods to render itself as a Turbo Stream (`.replace`, `.append`, ...)
and to broadcast itself (`.broadcast_replace_to`, ...). The server->client
half (controller responses + background broadcasts).
* Phlex::Reactive::Component — declares client-invokable `action`s and
emits a signed identity token + the wiring the generic `reactive`
Stimulus controller needs. The client->server half (clicks, form input).
Both halves converge on ONE re-render unit: the component, targeted by its
id. See the README for the mental model and examples.
Defined Under Namespace
Modules: Component, Generators, Streamable Classes: ActionsController, Engine, Error, InvalidToken, Reply, Response
Constant Summary collapse
- IDENTITY_PURPOSE =
Purpose string bound into every identity token's signature so a token minted for phlex-reactive can't be replayed against another verifier use.
"phlex-reactive/identity"- ACTIONS_CONTROLLER =
The controller a correctly-mounted action path resolves to. Used by the route guard below.
"phlex/reactive/actions"- VERSION =
"0.4.5"
Class Attribute Summary collapse
- .action_path ⇒ Object
-
.authorization_errors ⇒ Object
Exception classes the action endpoint renders as 403.
- .base_controller_name ⇒ Object
-
.flash_target ⇒ Object
DOM id of the host-app container a Response#flash appends into.
- .renderer ⇒ Object
- .verifier ⇒ Object
Class Method Summary collapse
-
.action_route_ok?(path = action_path) ⇒ Boolean
True when a POST to
pathresolves to the gem's ActionsController. - .base_controller ⇒ Object
-
.current_connection_id ⇒ Object
The acting client's SSE connection id during an action, or nil.
-
.flash_builder ⇒ Object
A Turbo::Streams::TagBuilder bound to an off-request view context, used to build standalone streams (e.g. a Response flash append) not tied to a specific component's id.
-
.off_request_view_context ⇒ Object
The off-request view context for the current thread, built once and reused for both the flash builder and standalone component renders.
- .off_request_view_context_generation ⇒ Object
-
.render(component) ⇒ Object
Render a Phlex component to HTML with a full (off-request) view context.
-
.request_bound_view_context(controller_class) ⇒ Object
Build an off-request view context whose controller has a REAL
request. -
.reset_flash_builder! ⇒ Object
Invalidate the per-thread context + builder for ALL threads by bumping the generation; each thread rebuilds lazily on next use.
-
.sign(payload) ⇒ Object
Signs a payload hash into an identity token.
-
.verify(token) ⇒ Object
Returns the verified payload hash, or nil if the token is invalid.
-
.warn_unless_action_route_mounted!(path: action_path, logger: default_logger) ⇒ Object
Log a clear warning (once, at boot) when the action path doesn't resolve to the gem controller — pointing at the catch-all shadow rather than leaving an adopter to guess.
- .with_connection_id(connection_id) ⇒ Object
Class Attribute Details
.action_path ⇒ Object
69 70 71 |
# File 'lib/phlex/reactive.rb', line 69 def action_path @action_path ||= "/reactive/actions" end |
.authorization_errors ⇒ Object
Exception classes the action endpoint renders as 403. Append your authorization library's error (Pundit::NotAuthorizedError, ActionPolicy::Unauthorized, ...).
61 62 63 |
# File 'lib/phlex/reactive.rb', line 61 def @authorization_errors end |
.base_controller_name ⇒ Object
177 178 179 |
# File 'lib/phlex/reactive.rb', line 177 def base_controller_name @base_controller_name ||= "ActionController::Base" end |
.flash_target ⇒ Object
DOM id of the host-app container a Response#flash appends into. Default "flash"; override to match your layout's flash region.
107 108 109 |
# File 'lib/phlex/reactive.rb', line 107 def flash_target @flash_target ||= "flash" end |
.renderer ⇒ Object
77 78 79 |
# File 'lib/phlex/reactive.rb', line 77 def renderer @renderer ||= defined?(::ActionController::Base) ? ::ActionController::Base : nil end |
.verifier ⇒ Object
73 74 75 |
# File 'lib/phlex/reactive.rb', line 73 def verifier @verifier ||= default_verifier end |
Class Method Details
.action_route_ok?(path = action_path) ⇒ Boolean
True when a POST to path resolves to the gem's ActionsController. A host
catch-all route (match "*path", ...) appended above the engine's route
SHADOWS it, so every reactive POST 404s and none of the controller runs —
the opaque "is the endpoint even mounted?" failure (issue #26). A false
here is the signal. Returns false (not raise) when nothing matches.
221 222 223 224 225 226 227 228 229 230 231 232 233 |
# File 'lib/phlex/reactive.rb', line 221 def action_route_ok?(path = action_path) return false unless defined?(::Rails) && ::Rails.application # At after_initialize (when the boot guard runs) the host's routes may not # be drawn yet, so recognize_path would see an incomplete set and report a # false shadow. Force-load routes first (idempotent — no-op if already # loaded), so the check is correct whether it runs at boot or at runtime. ensure_routes_loaded recognized = ::Rails.application.routes.recognize_path(path, method: :post) recognized[:controller] == ACTIONS_CONTROLLER rescue ActionController::RoutingError, ActiveRecord::RecordNotFound false end |
.base_controller ⇒ Object
181 182 183 |
# File 'lib/phlex/reactive.rb', line 181 def base_controller base_controller_name.constantize end |
.current_connection_id ⇒ Object
The acting client's SSE connection id during an action, or nil. Set by
the ActionsController from the X-Pgbus-Connection header. A component
action passes exclude: Phlex::Reactive.current_connection_id (or the
reactive_connection_id helper) to suppress the actor's own broadcast
echo.
200 201 202 |
# File 'lib/phlex/reactive.rb', line 200 def current_connection_id Thread.current[:phlex_reactive_connection_id] end |
.flash_builder ⇒ Object
A Turbo::Streams::TagBuilder bound to an off-request view context, used to build standalone streams (e.g. a Response flash append) not tied to a specific component's id. Cached PER THREAD alongside the context it's bound to (see off_request_view_context for why per-thread).
127 128 129 |
# File 'lib/phlex/reactive.rb', line 127 def flash_builder off_request_view_context_cache[:builder] end |
.off_request_view_context ⇒ Object
The off-request view context for the current thread, built once and reused for both the flash builder and standalone component renders. Cached PER THREAD, not per process: an ActionView context carries mutable output_buffer/view_flow state (render_in's capture swaps it), so sharing one instance across threads can interleave content on a threaded server. Rebuilt when the renderer object changes or the generation is bumped (reset_flash_builder! / Rails code reload), so a reloaded controller is never served stale.
139 140 141 |
# File 'lib/phlex/reactive.rb', line 139 def off_request_view_context off_request_view_context_cache[:view_context] end |
.off_request_view_context_generation ⇒ Object
151 152 153 |
# File 'lib/phlex/reactive.rb', line 151 def off_request_view_context_generation @off_request_view_context_generation ||= 0 end |
.render(component) ⇒ Object
Render a Phlex component to HTML with a full (off-request) view context. Uses phlex-rails' #render_in against the memoized view context — a direct component.call that skips ActionController's renderer.render machinery (~2x faster, ~half the allocations), with the same HTML and full helper access (dom_id/url_for/t/csrf). Used for a Phlex component embedded as Response#with content.
119 120 121 |
# File 'lib/phlex/reactive.rb', line 119 def render(component) component.render_in(off_request_view_context) end |
.request_bound_view_context(controller_class) ⇒ Object
Build an off-request view context whose controller has a REAL request.
A bare controller.new.view_context (the naive off-request context) has
request == nil, so any request-dependent helper raises
undefined method 'env' for nil — form_authenticity_token,
protect_against_forgery?, and host-aware URL helpers all read
request.env (issue #42). We replicate exactly what
ActionController::Renderer#render does to set up its mock request — build
an ActionDispatch::Request from the renderer's env (which derives the host
from the routes' default_url_options), bind the routes, and attach it —
then return the controller's view context instead of rendering a template.
The result keeps the 0.4.0 render_in speedup (no renderer.render
machinery) while restoring the request those helpers need.
94 95 96 97 98 99 100 101 102 103 |
# File 'lib/phlex/reactive.rb', line 94 def request_bound_view_context(controller_class) ar_renderer = controller_class.renderer request = ::ActionDispatch::Request.new(ar_renderer.send(:env_for_request)) request.routes = controller_class._routes instance = controller_class.new instance.set_request!(request) instance.set_response!(controller_class.make_response!(request)) instance.view_context end |
.reset_flash_builder! ⇒ Object
Invalidate the per-thread context + builder for ALL threads by bumping the generation; each thread rebuilds lazily on next use. Registered on Rails' reloader by the engine; also used by specs. Thread-safe (an integer bump, no shared structure to tear down).
147 148 149 |
# File 'lib/phlex/reactive.rb', line 147 def reset_flash_builder! @off_request_view_context_generation = off_request_view_context_generation + 1 end |
.sign(payload) ⇒ Object
Signs a payload hash into an identity token.
191 192 193 |
# File 'lib/phlex/reactive.rb', line 191 def sign(payload) verifier.generate(payload, purpose: IDENTITY_PURPOSE) end |
.verify(token) ⇒ Object
Returns the verified payload hash, or nil if the token is invalid.
186 187 188 |
# File 'lib/phlex/reactive.rb', line 186 def verify(token) verifier.verified(token, purpose: IDENTITY_PURPOSE) end |
.warn_unless_action_route_mounted!(path: action_path, logger: default_logger) ⇒ Object
Log a clear warning (once, at boot) when the action path doesn't resolve to the gem controller — pointing at the catch-all shadow rather than leaving an adopter to guess. Called from the engine's after_initialize.
238 239 240 241 242 243 244 245 246 247 248 |
# File 'lib/phlex/reactive.rb', line 238 def warn_unless_action_route_mounted!(path: action_path, logger: default_logger) return if action_route_ok?(path) return unless logger logger.warn( "[phlex-reactive] POST #{path} does not resolve to #{ACTIONS_CONTROLLER}. " \ "A host catch-all route (e.g. match \"*path\", ...) likely shadows it, so reactive " \ "actions will 404. Exempt #{path.delete_prefix("/")} from the catch-all, or set " \ "Phlex::Reactive.action_path to an unshadowed path. See the README integration section." ) end |
.with_connection_id(connection_id) ⇒ Object
204 205 206 207 208 209 210 |
# File 'lib/phlex/reactive.rb', line 204 def with_connection_id(connection_id) previous = Thread.current[:phlex_reactive_connection_id] Thread.current[:phlex_reactive_connection_id] = connection_id.presence yield ensure Thread.current[:phlex_reactive_connection_id] = previous end |