phlex-reactive 0.12.4 → 0.12.5

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.
@@ -267,7 +267,7 @@ module Phlex
267
267
  assert_no_scope_double_nesting!(scope, params, name.to_sym) if scope
268
268
  Registry.write_entry(
269
269
  self, :actions, name.to_sym,
270
- Action.new(name: name.to_sym, params: params, schema: Phlex::Reactive::ParamSchema.compile(params))
270
+ ActionDefinition.new(name: name.to_sym, params: params, schema: Phlex::Reactive::ParamSchema.compile(params))
271
271
  )
272
272
  end
273
273
 
@@ -85,6 +85,10 @@ module Phlex
85
85
  # JS), so the client's attr check would never fire (the on()/warn_unsaved
86
86
  # precedent). Off by default → no key, no string, zero client surface.
87
87
  data[:reactive_debug] = "true" if Phlex::Reactive.debug
88
+ # Issue #237: the verbose gate for the client's zero-target op warning.
89
+ # ON by default in dev/test (Rails.env.local?), so the papercut warning
90
+ # works out of the box; production renders no attr and stays silent.
91
+ data[:reactive_verbose] = "true" if Phlex::Reactive.verbose_errors
88
92
  # Field-name scope (issue #180): the client prefixes bare binding field
89
93
  # names with `scope[...]`. Omitted entirely when undeclared — byte-stable
90
94
  # wire for unscoped components.
@@ -84,7 +84,14 @@ module Phlex
84
84
  # is the compiled Phlex::Reactive::ParamSchema (issue #109) the endpoint
85
85
  # coerces through — built ONCE at declaration so a typo'd type symbol
86
86
  # raises Phlex::Reactive::UnknownParamType at class load, not at click time.
87
- Action = Data.define(:name, :params, :schema)
87
+ #
88
+ # Named ActionDefinition (issue #233), NOT Action: this module sits in
89
+ # every reactive component's ancestry, so a bare `Action` constant
90
+ # shadows a host app's Phlex kit component of the same name under lazy
91
+ # autoloading (Kit's LazyLoader resolves through the calling class's
92
+ # const_get). Keep every constant here suffixed / implausible as a kit
93
+ # component name — and never alias the old name back.
94
+ ActionDefinition = Data.define(:name, :params, :schema)
88
95
 
89
96
  # A declared client-side computation (data binding). `inputs`/`outputs` are
90
97
  # the action-param names of the fields the reducer reads/writes; `reducer`
@@ -314,7 +314,11 @@ data-reactive-flash-level="#{level_attr}"#{dismiss_attr(dismiss_after)}>#{body}<
314
314
  def js_stream(ops, target: nil)
315
315
  json = js_ops_json(ops)
316
316
  target_attr = target ? %( target="#{ERB::Util.html_escape(target)}") : ""
317
- %(<turbo-stream action="reactive:js"#{target_attr} \
317
+ # Issue #237: carry the verbose gate on the stream element itself, so
318
+ # the client can warn on zero-target resolution even for a
319
+ # document-scoped op with no controller root to read the flag from.
320
+ verbose_attr = Phlex::Reactive.verbose_errors ? %( data-reactive-verbose="true") : ""
321
+ %(<turbo-stream action="reactive:js"#{target_attr}#{verbose_attr} \
318
322
  data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
319
323
  end
320
324
 
@@ -121,11 +121,10 @@ module Phlex
121
121
  end
122
122
 
123
123
  alternatives = negative.flat_map { |field, value| negative_alternatives(field, value) }
124
- # rubocop:disable Style/ItBlockParameter -- nested block: `it` would shadow `group`
124
+ # rubocop:disable-next Style/ItBlockParameter -- nested block: `it` would shadow `group`
125
125
  groups.flat_map do |group|
126
126
  alternatives.map { |extra| group + extra }
127
127
  end
128
- # rubocop:enable Style/ItBlockParameter
129
128
  end
130
129
 
131
130
  # --- the value language (positive) -------------------------------------
@@ -137,7 +137,13 @@ module Phlex
137
137
  "broadcast", { component: component_name, stream_action: BROADCAST_VERBS[verb], streamables: keys.size }
138
138
  ) do
139
139
  with_pgbus_broadcast_opts(exclude:, visible_to:) do
140
- html = verb == :js ? nil : render_broadcast_html(component)
140
+ # A broadcast render NEVER inherits the actor's url_options (issue
141
+ # #232): this call may run inside an action request (where the
142
+ # endpoint threaded the actor's host), but subscribers can be on
143
+ # different hosts — absolute URLs in broadcast-rendered components
144
+ # keep the process defaults, so "host-relative URLs" stays the
145
+ # broadcast contract. Off-request callers pay one nil-write pair.
146
+ html = verb == :js ? nil : Phlex::Reactive.with_url_options(nil) { render_broadcast_html(component) }
141
147
  ops_json = verb == :js ? broadcast_js_ops_json(payload) : nil
142
148
  keys.each { dispatch_broadcast(verb, it, resolved_target, html, ops_json, morph, effect) }
143
149
  end
@@ -237,8 +243,12 @@ module Phlex
237
243
  when :prepend then ::Turbo::StreamsChannel.broadcast_prepend_to(*parts, target:, html:, **wire)
238
244
  when :remove then ::Turbo::StreamsChannel.broadcast_remove_to(*parts, target:, **wire)
239
245
  when :js
246
+ # Issue #237: the broadcast op stream carries the verbose gate too
247
+ # (same server env for every subscriber of this payload).
248
+ data = { reactive_ops: ops_json }
249
+ data[:reactive_verbose] = "true" if Phlex::Reactive.verbose_errors
240
250
  ::Turbo::StreamsChannel.broadcast_action_to(
241
- *parts, action: "reactive:js", target:, attributes: { data: { reactive_ops: ops_json } }, render: false
251
+ *parts, action: "reactive:js", target:, attributes: { data: }, render: false
242
252
  )
243
253
  end
244
254
  end
@@ -487,7 +497,7 @@ module Phlex
487
497
  # Define the guided-error stub for each removed broadcast method (issue
488
498
  # #185). `verb` is referenced in define_method AND the message, so the outer
489
499
  # block param must be named — `it` is illegal here.
490
- # rubocop:disable Style/ItBlockParameter
500
+ # rubocop:disable-next Style/ItBlockParameter
491
501
  Phlex::Reactive::Streamable::REMOVED_BROADCASTS.each_key do |verb|
492
502
  define_method(verb) do |*, **|
493
503
  raise NoMethodError,
@@ -495,7 +505,6 @@ module Phlex
495
505
  "use #{name}.#{Phlex::Reactive::Streamable::REMOVED_BROADCASTS[verb]}"
496
506
  end
497
507
  end
498
- # rubocop:enable Style/ItBlockParameter
499
508
 
500
509
  private
501
510
 
@@ -122,7 +122,7 @@ module Phlex
122
122
 
123
123
  # does_not_match? is RSpec's REQUIRED negated-matcher protocol method — its
124
124
  # name is fixed by RSpec, not a predicate we get to rename.
125
- # rubocop:disable Naming/PredicatePrefix
125
+ # rubocop:disable-next Naming/PredicatePrefix
126
126
  def does_not_match?(page)
127
127
  @page = page
128
128
  # The negative is satisfied the moment the property is NOT the expected
@@ -130,7 +130,6 @@ module Phlex
130
130
  # as that holds, false if it stayed equal for the whole budget.
131
131
  poll_until { current_value != @expected }
132
132
  end
133
- # rubocop:enable Naming/PredicatePrefix
134
133
 
135
134
  def failure_message
136
135
  "expected ##{@id} to have value #{@expected.inspect} (its .value property), " \
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.12.4"
5
+ VERSION = "0.12.5"
6
6
  end
7
7
  end
@@ -513,6 +513,22 @@ module Phlex
513
513
  instance = controller_class.new
514
514
  instance.set_request!(request)
515
515
  instance.set_response!(controller_class.make_response!(request))
516
+
517
+ # Issue #232: during a reactive request the endpoint threads the actor's
518
+ # protocol/host/port via with_url_options; merge them over this
519
+ # instance's defaults so absolute URL helpers in a reply render the
520
+ # REQUESTING host. View contexts delegate url_options to their
521
+ # controller (ActionView::RoutingUrlFor), so this one override covers
522
+ # every helper spelling. Off-request (thread-local nil — jobs, console,
523
+ # broadcasts) returns super's frozen memo untouched: zero-alloc, byte-
524
+ # identical URLs. Defined on the singleton because this instance is
525
+ # MEMOIZED per thread — its request (and super's @_url_options) never
526
+ # changes, so the per-call merge is the only per-request seam.
527
+ def instance.url_options
528
+ overrides = Phlex::Reactive.current_url_options
529
+ overrides ? super.merge(overrides) : super
530
+ end
531
+
516
532
  instance.view_context
517
533
  end
518
534
 
@@ -968,6 +984,40 @@ module Phlex
968
984
  Thread.current[:phlex_reactive_connection_id] = previous
969
985
  end
970
986
 
987
+ # The acting request's url_options during a reactive request, or nil
988
+ # (issue #232). Set by the ActionsController (the action AND defer
989
+ # endpoints) so absolute URL helpers in a reply-rendered component —
990
+ # image_tag on an Active Storage attachment being the everyday case —
991
+ # carry the REQUESTING host/port/protocol instead of the process default
992
+ # (the ActiveStorage::SetCurrent move). The memoized off-request view
993
+ # context merges this over its defaults per call (see
994
+ # request_bound_view_context); nil (jobs, console, plain broadcasts)
995
+ # leaves the defaults untouched — byte-identical to before.
996
+ def current_url_options
997
+ Thread.current[:phlex_reactive_url_options]
998
+ end
999
+
1000
+ # Thread `options` for the block, restoring the previous value after.
1001
+ # `nil` CLEARS the actor options — the broadcast render uses this to keep
1002
+ # a broadcast fired inside an action on process defaults (subscribers can
1003
+ # be on different hosts; "URLs in broadcast-rendered components must be
1004
+ # host-relative" is the broadcast contract).
1005
+ def with_url_options(options)
1006
+ previous = Thread.current[:phlex_reactive_url_options]
1007
+ Thread.current[:phlex_reactive_url_options] = options.presence
1008
+ yield
1009
+ ensure
1010
+ Thread.current[:phlex_reactive_url_options] = previous
1011
+ end
1012
+
1013
+ # The url_options trio derived from a live request. `port` is optional_port
1014
+ # — nil on the default port — and is INCLUDED even when nil so a
1015
+ # default-port request clears any configured default port rather than
1016
+ # inheriting it (the merge must own the whole trio).
1017
+ def url_options_for(request)
1018
+ { protocol: request.protocol, host: request.host, port: request.optional_port }
1019
+ end
1020
+
971
1021
  # The controller a correctly-mounted action path resolves to. Used by the
972
1022
  # route guard below.
973
1023
  ACTIONS_CONTROLLER = "phlex/reactive/actions"
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.12.4
4
+ version: 0.12.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -188,13 +188,13 @@ files:
188
188
  - lib/phlex/reactive/test_helpers/system.rb
189
189
  - lib/phlex/reactive/version.rb
190
190
  - lib/tasks/phlex_reactive.rake
191
- homepage: https://github.com/mhenrixon/phlex-reactive
191
+ homepage: https://github.com/zoolutions/phlex-reactive
192
192
  licenses:
193
193
  - MIT
194
194
  metadata:
195
- homepage_uri: https://github.com/mhenrixon/phlex-reactive
196
- source_code_uri: https://github.com/mhenrixon/phlex-reactive/tree/main
197
- changelog_uri: https://github.com/mhenrixon/phlex-reactive/blob/main/CHANGELOG.md
195
+ homepage_uri: https://github.com/zoolutions/phlex-reactive
196
+ source_code_uri: https://github.com/zoolutions/phlex-reactive/tree/main
197
+ changelog_uri: https://github.com/zoolutions/phlex-reactive/blob/main/CHANGELOG.md
198
198
  rubygems_mfa_required: 'true'
199
199
  rdoc_options: []
200
200
  require_paths: