phlex-reactive 0.13.3 → 0.14.0

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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1097 -630
  3. data/README.md +638 -16
  4. data/app/controllers/phlex/reactive/actions_controller.rb +85 -16
  5. data/app/controllers/phlex/reactive/fragments_controller.rb +310 -0
  6. data/app/javascript/phlex/reactive/core.js +181 -0
  7. data/app/javascript/phlex/reactive/core.min.js +4 -0
  8. data/app/javascript/phlex/reactive/core.min.js.map +11 -0
  9. data/app/javascript/phlex/reactive/early.js +175 -0
  10. data/app/javascript/phlex/reactive/early.min.js +4 -0
  11. data/app/javascript/phlex/reactive/early.min.js.map +10 -0
  12. data/app/javascript/phlex/reactive/features/bindings.js +1511 -0
  13. data/app/javascript/phlex/reactive/features/bindings.min.js +4 -0
  14. data/app/javascript/phlex/reactive/features/bindings.min.js.map +10 -0
  15. data/app/javascript/phlex/reactive/features/compute.js +541 -0
  16. data/app/javascript/phlex/reactive/features/compute.min.js +4 -0
  17. data/app/javascript/phlex/reactive/features/compute.min.js.map +10 -0
  18. data/app/javascript/phlex/reactive/features/defer.js +726 -0
  19. data/app/javascript/phlex/reactive/features/defer.min.js +4 -0
  20. data/app/javascript/phlex/reactive/features/defer.min.js.map +10 -0
  21. data/app/javascript/phlex/reactive/features/devtools.js +242 -0
  22. data/app/javascript/phlex/reactive/features/devtools.min.js +4 -0
  23. data/app/javascript/phlex/reactive/features/devtools.min.js.map +10 -0
  24. data/app/javascript/phlex/reactive/features/effects.js +362 -0
  25. data/app/javascript/phlex/reactive/features/effects.min.js +4 -0
  26. data/app/javascript/phlex/reactive/features/effects.min.js.map +10 -0
  27. data/app/javascript/phlex/reactive/features/form.js +216 -0
  28. data/app/javascript/phlex/reactive/features/form.min.js +4 -0
  29. data/app/javascript/phlex/reactive/features/form.min.js.map +10 -0
  30. data/app/javascript/phlex/reactive/features/hints.js +193 -0
  31. data/app/javascript/phlex/reactive/features/hints.min.js +4 -0
  32. data/app/javascript/phlex/reactive/features/hints.min.js.map +10 -0
  33. data/app/javascript/phlex/reactive/features/persist.js +663 -0
  34. data/app/javascript/phlex/reactive/features/persist.min.js +4 -0
  35. data/app/javascript/phlex/reactive/features/persist.min.js.map +10 -0
  36. data/app/javascript/phlex/reactive/inspect.js +21 -9
  37. data/app/javascript/phlex/reactive/inspect.min.js +2 -2
  38. data/app/javascript/phlex/reactive/inspect.min.js.map +3 -3
  39. data/app/javascript/phlex/reactive/reactive_controller.js +85 -5580
  40. data/app/javascript/phlex/reactive/reactive_controller.min.js +2 -2
  41. data/app/javascript/phlex/reactive/reactive_controller.min.js.map +13 -4
  42. data/app/javascript/phlex/reactive/runtime.js +3690 -0
  43. data/lib/generators/phlex/reactive/install/templates/phlex_reactive.rb.erb +18 -0
  44. data/lib/phlex/reactive/claude/skills/phlex-reactive-debugging/SKILL.md +5 -3
  45. data/lib/phlex/reactive/component/dsl.rb +74 -2
  46. data/lib/phlex/reactive/component/helpers.rb +104 -28
  47. data/lib/phlex/reactive/component/lazy.rb +138 -2
  48. data/lib/phlex/reactive/component/registry.rb +2 -0
  49. data/lib/phlex/reactive/component/selection.rb +71 -0
  50. data/lib/phlex/reactive/doctor/fiber_isolation.rb +86 -0
  51. data/lib/phlex/reactive/doctor.rb +166 -8
  52. data/lib/phlex/reactive/dormant.rb +66 -0
  53. data/lib/phlex/reactive/engine.rb +64 -3
  54. data/lib/phlex/reactive/fragment.rb +233 -0
  55. data/lib/phlex/reactive/inspector/report.rb +24 -3
  56. data/lib/phlex/reactive/inspector.rb +23 -2
  57. data/lib/phlex/reactive/js.rb +61 -12
  58. data/lib/phlex/reactive/mcp/tools/actions_tool.rb +3 -1
  59. data/lib/phlex/reactive/mcp/tools/find_tool.rb +4 -1
  60. data/lib/phlex/reactive/param_schema.rb +68 -54
  61. data/lib/phlex/reactive/pending/markup.rb +128 -0
  62. data/lib/phlex/reactive/pending.rb +25 -21
  63. data/lib/phlex/reactive/show_conditions.rb +84 -56
  64. data/lib/phlex/reactive/streamable.rb +20 -2
  65. data/lib/phlex/reactive/test_helpers/system/reactive_requests_matcher.rb +120 -0
  66. data/lib/phlex/reactive/test_helpers/system.rb +62 -0
  67. data/lib/phlex/reactive/version.rb +1 -1
  68. data/lib/phlex/reactive.rb +84 -0
  69. data/lib/tasks/phlex_reactive.rake +6 -2
  70. metadata +39 -1
@@ -36,6 +36,30 @@ module Phlex
36
36
  # twin of wait_for_turbo watching the Turbo progress bar.
37
37
  ACTIVE_MARKER = "data-reactive-active"
38
38
 
39
+ # The <html> attribute the client keeps its per-kind request totals in
40
+ # (issue #279), as JSON ({"action":1,"defer":0}). Lockstep with
41
+ # reactive_controller.js REQUESTS_ATTR. Written only under the verbose
42
+ # gate (dev/test by default).
43
+ REQUESTS_ATTR = "data-reactive-requests"
44
+
45
+ # Parse the attribute value into { action:, defer: } Integers — zeros for
46
+ # an absent or garbled value. Shared by the helpers and
47
+ # ReactiveRequestsMatcher.
48
+ def self.parse_reactive_requests(raw)
49
+ parsed = raw.is_a?(::String) ? ::JSON.parse(raw) : {}
50
+ parsed = {} unless parsed.is_a?(::Hash)
51
+ { action: reactive_request_total(parsed["action"]), defer: reactive_request_total(parsed["defer"]) }
52
+ rescue ::JSON::ParserError
53
+ { action: 0, defer: 0 }
54
+ end
55
+
56
+ # One total as an Integer. Only a number or a numeric string counts; any
57
+ # other JSON value (array, object, boolean, null) is garbled, so zero.
58
+ def self.reactive_request_total(value)
59
+ value.is_a?(::Numeric) || value.is_a?(::String) ? value.to_i : 0
60
+ end
61
+ private_class_method :reactive_request_total
62
+
39
63
  # Block until the reactive layer is IDLE — every dispatch round trip and
40
64
  # deferred render has settled and the <html data-reactive-active> marker is
41
65
  # gone. The system-test twin of wait_for_turbo (which watches the Turbo
@@ -92,6 +116,42 @@ module Phlex
92
116
  def have_reactive_text(id, value, **)
93
117
  have_css("##{id}", text: value, **)
94
118
  end
119
+
120
+ # How many reactive requests the page has made since the last
121
+ # reset_reactive_requests! (or since the page loaded), per kind (issue
122
+ # #279):
123
+ #
124
+ # reactive_request_count # => { action: 1, defer: 0 }
125
+ #
126
+ # A snapshot, not a wait — barrier on wait_for_reactive (or use
127
+ # have_reactive_requests) before reading it after a gesture.
128
+ def reactive_request_count
129
+ System.parse_reactive_requests(
130
+ page.evaluate_script("document.documentElement.getAttribute(#{REQUESTS_ATTR.to_json})")
131
+ )
132
+ end
133
+
134
+ # Baseline the request totals to zero. The attribute IS the client's
135
+ # store, so writing zeros re-baselines it — including after a Turbo Drive
136
+ # visit, where the <html> element (and its totals) survives navigation.
137
+ def reset_reactive_requests!
138
+ zeros = { action: 0, defer: 0 }.to_json
139
+ page.execute_script("document.documentElement.setAttribute(#{REQUESTS_ATTR.to_json}, '#{zeros}')")
140
+ nil
141
+ end
142
+
143
+ # Assert (waiting) that exactly `count` reactive requests were made — of
144
+ # `kind` (:action or :defer), or of any kind — since the last reset, AND
145
+ # that nothing is still in flight. Fails at once if the count overshoots
146
+ # (totals only grow until a reset).
147
+ #
148
+ # reset_reactive_requests!
149
+ # find("#open-panel").click
150
+ # expect(page).to have_reactive_requests(1)
151
+ # expect(page).to have_reactive_requests(0, kind: :defer)
152
+ def have_reactive_requests(count, kind: nil, timeout: nil, wait: nil)
153
+ ReactiveRequestsMatcher.new(count, kind:, wait: timeout || wait)
154
+ end
95
155
  # rubocop:enable Naming/PredicatePrefix
96
156
 
97
157
  # A waiting matcher that asserts a field's live `.value` PROPERTY (issue
@@ -190,3 +250,5 @@ module Phlex
190
250
  end
191
251
  end
192
252
  end
253
+
254
+ require "phlex/reactive/test_helpers/system/reactive_requests_matcher"
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.13.3"
5
+ VERSION = "0.14.0"
6
6
  end
7
7
  end
@@ -133,6 +133,12 @@ module Phlex
133
133
  # ActionPolicy::Unauthorized, ...).
134
134
  attr_accessor :authorization_errors
135
135
 
136
+ # Import maps `phlex_reactive:doctor` checks for the `phlex/reactive/early`
137
+ # pin (issue #307): a callable returning `{ name => Importmap::Map }`, for
138
+ # an app with more than one map (one per entry point). nil (the default)
139
+ # checks `Rails.application.importmap` only.
140
+ attr_accessor :importmaps
141
+
136
142
  # The path the action endpoint is mounted at. Default "/reactive/actions".
137
143
  # Set before boot if it collides with an app route. The client runtime
138
144
  # reads it from a <meta name="phlex-reactive-action-path"> tag if present,
@@ -532,6 +538,29 @@ module Phlex
532
538
  instance.view_context
533
539
  end
534
540
 
541
+ # --- Early triggers (issue #273) -----------------------------------
542
+
543
+ # How long (ms) a trigger captured by phlex/reactive/early before the
544
+ # controller connected stays replayable. Older entries are dropped on
545
+ # connect (warned under data-reactive-verbose): a click from long ago must
546
+ # not fire a request out of nowhere. The client reads it from a meta tag
547
+ # in the host layout (the engine can't inject into it):
548
+ # <meta name="phlex-reactive-early-ttl" content="<%= Phlex::Reactive.early_event_ttl_ms %>">
549
+ # Without the meta the client uses the same 10 s default. nil resets.
550
+ # Validated at assignment: the client reads a non-positive TTL as "use
551
+ # the default", so 0 can never mean "replay nothing" — fail here instead.
552
+ def early_event_ttl_ms=(value)
553
+ unless value.nil? || (value.is_a?(Integer) && value.positive?)
554
+ raise ArgumentError, "early_event_ttl_ms must be a positive Integer (milliseconds) or nil, got #{value.inspect}"
555
+ end
556
+
557
+ @early_event_ttl_ms = value
558
+ end
559
+
560
+ def early_event_ttl_ms
561
+ @early_event_ttl_ms ||= 10_000
562
+ end
563
+
535
564
  # --- Deferred reply segments (issue #165) --------------------------
536
565
 
537
566
  # Lifetime (seconds) of a defer token. It only needs to cover the
@@ -552,6 +581,44 @@ module Phlex
552
581
  @defer_path ||= "/reactive/defer"
553
582
  end
554
583
 
584
+ # The path prefix of the cacheable-fragment GET endpoint (issue #277):
585
+ # GET <fragment_path>/<signed id>. Default "/reactive/fragment"; set
586
+ # before boot if it collides. The shell renders the full URL, but the
587
+ # client only fetches one under the path it knows: when you change this,
588
+ # also render <meta name="phlex-reactive-fragment-path" content="…">.
589
+ attr_writer :fragment_path
590
+
591
+ def fragment_path
592
+ @fragment_path ||= "/reactive/fragment"
593
+ end
594
+
595
+ # The longest max-age (seconds) a `reactive_lazy cache:` component may
596
+ # ask the browser to keep its fragment for. A component declaring more
597
+ # is answered with this cap. Within max-age the browser reuses its copy
598
+ # WITHOUT asking the server — so this also bounds how long a revoked
599
+ # permission can keep showing a stale fragment. Default 1 hour; nil
600
+ # restores it; 0 makes every cached fragment revalidate on each use.
601
+ def fragment_cache_max_age_limit=(value)
602
+ seconds = value.is_a?(ActiveSupport::Duration) && value.to_i == value ? value.to_i : value
603
+ unless seconds.nil? || (seconds.is_a?(::Integer) && !seconds.negative?)
604
+ raise ArgumentError,
605
+ "Phlex::Reactive.fragment_cache_max_age_limit must be a whole, non-negative number of seconds " \
606
+ "(or nil for the default) — got #{value.inspect}"
607
+ end
608
+
609
+ @fragment_cache_max_age_limit = seconds
610
+ end
611
+
612
+ def fragment_cache_max_age_limit
613
+ @fragment_cache_max_age_limit ||= 3600
614
+ end
615
+
616
+ # The deterministic, no-expiry, purpose-scoped fragment id of an identity
617
+ # payload, and its verification (issue #277). See Phlex::Reactive::Fragment.
618
+ def sign_fragment(payload) = Fragment.sign(payload)
619
+
620
+ def verify_fragment(id) = Fragment.verify(id)
621
+
555
622
  # How deferred segments reach the actor: :auto (push iff capable, else
556
623
  # pull), :fetch (always pull), :stream (push; degrades to pull with a
557
624
  # warning when the capability is absent). Validated at assignment — a
@@ -1056,6 +1123,7 @@ module Phlex
1056
1123
  # The controller a correctly-mounted action path resolves to. Used by the
1057
1124
  # route guard below.
1058
1125
  ACTIONS_CONTROLLER = "phlex/reactive/actions"
1126
+ FRAGMENTS_CONTROLLER = "phlex/reactive/fragments"
1059
1127
 
1060
1128
  # True when a POST to `path` resolves to the gem's ActionsController. A host
1061
1129
  # catch-all route (match "*path", ...) appended above the engine's route
@@ -1076,6 +1144,20 @@ module Phlex
1076
1144
  false
1077
1145
  end
1078
1146
 
1147
+ # Does GET <fragment_path>/<id> resolve to the gem's FragmentsController
1148
+ # (issue #277)? A host GET catch-all shadows it otherwise, and every
1149
+ # `reactive_lazy cache:` fetch then gets the host's fallback page. The
1150
+ # doctor reports it, like the defer route.
1151
+ def fragment_route_ok?(path = fragment_path)
1152
+ return false unless defined?(::Rails) && ::Rails.application
1153
+
1154
+ ensure_routes_loaded
1155
+ recognized = ::Rails.application.routes.recognize_path("#{path}/probe", method: :get)
1156
+ recognized[:controller] == FRAGMENTS_CONTROLLER
1157
+ rescue ActionController::RoutingError, ActiveRecord::RecordNotFound
1158
+ false
1159
+ end
1160
+
1079
1161
  # Log a clear warning (once, at boot) when the action path doesn't resolve
1080
1162
  # to the gem controller — pointing at the catch-all shadow rather than
1081
1163
  # leaving an adopter to guess. Called from the engine's after_initialize.
@@ -1184,6 +1266,8 @@ loader.ignore("#{lib}/phlex/reactive/test_helpers/matchers.rb")
1184
1266
  # only when Capybara is present, so the loader must ignore it (otherwise an
1185
1267
  # eager-load in production would define browser helpers with no Capybara).
1186
1268
  loader.ignore("#{lib}/phlex/reactive/test_helpers/system.rb")
1269
+ # ...and its matcher subdirectory (issue #279), required by system.rb itself.
1270
+ loader.ignore("#{lib}/phlex/reactive/test_helpers/system")
1187
1271
  # The MCP diagnostic tool tree (issue #168) subclasses the OPTIONAL `mcp` gem's
1188
1272
  # constants (MCP::Tool) at class-definition time, so the whole mcp/ subdirectory
1189
1273
  # must stay out of the autoloader — Phlex::Reactive::MCP.load! requires it in
@@ -12,14 +12,18 @@ namespace :phlex_reactive do
12
12
  abort unless Phlex::Reactive::Doctor.run
13
13
  end
14
14
 
15
- desc "List every declared reactive action (component | action | params | file:line | auth); FORMAT=json for tooling"
15
+ desc "List every declared reactive action (component | action | params | file:line | auth); " \
16
+ "FORMAT=json for tooling, UNVERIFIED=1 for only the unverified rows"
16
17
  task actions: :environment do
17
18
  # eager_load! so every app component is in the Streamable registry the
18
19
  # Inspector reads. Plain text by default (no ANSI, like the doctor);
19
20
  # FORMAT=json emits a parseable array for tooling. Names/paths/schemas only.
21
+ # UNVERIFIED=1 keeps only actions with neither a detected authorization call
22
+ # nor a skip_verify_authorized — the review queue (issue #278).
20
23
  Rails.application.eager_load!
21
24
  format = ENV["FORMAT"].to_s.downcase == "json" ? :json : :text
22
- puts Phlex::Reactive::Inspector::Report.actions(Phlex::Reactive::Inspector.components, format:)
25
+ unverified_only = %w[1 true yes].include?(ENV["UNVERIFIED"].to_s.downcase)
26
+ puts Phlex::Reactive::Inspector::Report.actions(Phlex::Reactive::Inspector.components, format:, unverified_only:)
23
27
  end
24
28
 
25
29
  desc "Fuzzy-find a reactive component and print its actions with method-definition source — phlex_reactive:find[query]"
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.13.3
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -118,6 +118,7 @@ files:
118
118
  - README.md
119
119
  - app/assets/stylesheets/phlex/reactive/effects.css
120
120
  - app/controllers/phlex/reactive/actions_controller.rb
121
+ - app/controllers/phlex/reactive/fragments_controller.rb
121
122
  - app/javascript/phlex/reactive/compute.js
122
123
  - app/javascript/phlex/reactive/compute.min.js
123
124
  - app/javascript/phlex/reactive/compute.min.js.map
@@ -127,12 +128,43 @@ files:
127
128
  - app/javascript/phlex/reactive/confirm_predicate.js
128
129
  - app/javascript/phlex/reactive/confirm_predicate.min.js
129
130
  - app/javascript/phlex/reactive/confirm_predicate.min.js.map
131
+ - app/javascript/phlex/reactive/core.js
132
+ - app/javascript/phlex/reactive/core.min.js
133
+ - app/javascript/phlex/reactive/core.min.js.map
134
+ - app/javascript/phlex/reactive/early.js
135
+ - app/javascript/phlex/reactive/early.min.js
136
+ - app/javascript/phlex/reactive/early.min.js.map
137
+ - app/javascript/phlex/reactive/features/bindings.js
138
+ - app/javascript/phlex/reactive/features/bindings.min.js
139
+ - app/javascript/phlex/reactive/features/bindings.min.js.map
140
+ - app/javascript/phlex/reactive/features/compute.js
141
+ - app/javascript/phlex/reactive/features/compute.min.js
142
+ - app/javascript/phlex/reactive/features/compute.min.js.map
143
+ - app/javascript/phlex/reactive/features/defer.js
144
+ - app/javascript/phlex/reactive/features/defer.min.js
145
+ - app/javascript/phlex/reactive/features/defer.min.js.map
146
+ - app/javascript/phlex/reactive/features/devtools.js
147
+ - app/javascript/phlex/reactive/features/devtools.min.js
148
+ - app/javascript/phlex/reactive/features/devtools.min.js.map
149
+ - app/javascript/phlex/reactive/features/effects.js
150
+ - app/javascript/phlex/reactive/features/effects.min.js
151
+ - app/javascript/phlex/reactive/features/effects.min.js.map
152
+ - app/javascript/phlex/reactive/features/form.js
153
+ - app/javascript/phlex/reactive/features/form.min.js
154
+ - app/javascript/phlex/reactive/features/form.min.js.map
155
+ - app/javascript/phlex/reactive/features/hints.js
156
+ - app/javascript/phlex/reactive/features/hints.min.js
157
+ - app/javascript/phlex/reactive/features/hints.min.js.map
158
+ - app/javascript/phlex/reactive/features/persist.js
159
+ - app/javascript/phlex/reactive/features/persist.min.js
160
+ - app/javascript/phlex/reactive/features/persist.min.js.map
130
161
  - app/javascript/phlex/reactive/inspect.js
131
162
  - app/javascript/phlex/reactive/inspect.min.js
132
163
  - app/javascript/phlex/reactive/inspect.min.js.map
133
164
  - app/javascript/phlex/reactive/reactive_controller.js
134
165
  - app/javascript/phlex/reactive/reactive_controller.min.js
135
166
  - app/javascript/phlex/reactive/reactive_controller.min.js.map
167
+ - app/javascript/phlex/reactive/runtime.js
136
168
  - lib/generators/phlex/reactive/claude/USAGE
137
169
  - lib/generators/phlex/reactive/claude/claude_generator.rb
138
170
  - lib/generators/phlex/reactive/component/USAGE
@@ -160,11 +192,15 @@ files:
160
192
  - lib/phlex/reactive/component/identity.rb
161
193
  - lib/phlex/reactive/component/lazy.rb
162
194
  - lib/phlex/reactive/component/registry.rb
195
+ - lib/phlex/reactive/component/selection.rb
163
196
  - lib/phlex/reactive/defer.rb
164
197
  - lib/phlex/reactive/deferred_render_job.rb
165
198
  - lib/phlex/reactive/doctor.rb
199
+ - lib/phlex/reactive/doctor/fiber_isolation.rb
200
+ - lib/phlex/reactive/dormant.rb
166
201
  - lib/phlex/reactive/effects.rb
167
202
  - lib/phlex/reactive/engine.rb
203
+ - lib/phlex/reactive/fragment.rb
168
204
  - lib/phlex/reactive/inspector.rb
169
205
  - lib/phlex/reactive/inspector/report.rb
170
206
  - lib/phlex/reactive/js.rb
@@ -180,6 +216,7 @@ files:
180
216
  - lib/phlex/reactive/mcp/tools/find_tool.rb
181
217
  - lib/phlex/reactive/param_schema.rb
182
218
  - lib/phlex/reactive/pending.rb
219
+ - lib/phlex/reactive/pending/markup.rb
183
220
  - lib/phlex/reactive/reply.rb
184
221
  - lib/phlex/reactive/response.rb
185
222
  - lib/phlex/reactive/settle.rb
@@ -190,6 +227,7 @@ files:
190
227
  - lib/phlex/reactive/test_helpers.rb
191
228
  - lib/phlex/reactive/test_helpers/matchers.rb
192
229
  - lib/phlex/reactive/test_helpers/system.rb
230
+ - lib/phlex/reactive/test_helpers/system/reactive_requests_matcher.rb
193
231
  - lib/phlex/reactive/version.rb
194
232
  - lib/tasks/phlex_reactive.rake
195
233
  homepage: https://github.com/zoolutions/phlex-reactive