phlex-reactive 0.16.0 → 0.16.1

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.
@@ -14,9 +14,15 @@ module Phlex
14
14
  #
15
15
  # def save(title: nil)
16
16
  # authorize!(@note, to: :update?)
17
- # return conflict! if reactive_held_fields.include?("title") && title != @note.title
17
+ # return reply.morph.refused("title") if reactive_held_fields.include?("title") && title != @note.title
18
18
  # ...
19
19
  #
20
+ # A reply to the root's own request releases its marks: it answers exactly
21
+ # those values. A refusal (`reply.morph.refused`, issue #413) is sent back
22
+ # in the X-Reactive-Refused header, and the client keeps the marks it names,
23
+ # so the user's next keystroke is refused too until the action stores the
24
+ # field or the user takes the other value.
25
+ #
20
26
  # The header is UNTRUSTED input, like every request field: it carries names
21
27
  # only, never values, and is read as data — capped in bytes, count and
22
28
  # length, non-strings dropped, and filtered to the names the running action
@@ -26,6 +32,8 @@ module Phlex
26
32
  # It is a hint about what the user saw, not authorization.
27
33
  module HeldFields
28
34
  HEADER = "X-Reactive-Held"
35
+ # The response header a refusing reply sends (issue #413).
36
+ REFUSED_HEADER = "X-Reactive-Refused"
29
37
 
30
38
  # A raw header longer than this is ignored unparsed.
31
39
  MAX_BYTES = 4_096
@@ -62,6 +70,18 @@ module Phlex
62
70
  EMPTY
63
71
  end
64
72
 
73
+ # The X-Reactive-Refused value for Response#refused_fields: "*" keeps
74
+ # every mark, a JSON list keeps those names. ASCII only (a header is a
75
+ # byte string), as the client escapes X-Reactive-Held. A list past
76
+ # MAX_BYTES (a header a proxy may refuse) widens to "*": keeping more
77
+ # marks than named is safe, losing the refusal is not.
78
+ def refused_header(fields)
79
+ return "*" if fields == :all
80
+
81
+ json = JSON.generate(fields, ascii_only: true)
82
+ json.bytesize > MAX_BYTES ? "*" : json
83
+ end
84
+
65
85
  # Keep the strings among `names` that `action_def` declares as params.
66
86
  def filter(names, action_def)
67
87
  declared = declared_names(action_def)
@@ -19,6 +19,7 @@ module Phlex
19
19
  # reply.remove # drop the element (e.g. moderation queue)
20
20
  # reply.redirect(article_url(@article)) # slug changed -> Turbo.visit the new URL
21
21
  # reply.replace.stream(Totals.update(@order)) # multi-stream
22
+ # reply.morph.refused("title") # answered, not stored: keep the field held
22
23
  class Response
23
24
  attr_reader :streams, :redirect_url, :token_component, :subject_component
24
25
 
@@ -351,8 +352,11 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
351
352
  # reply never allocates an empty array per Response.
352
353
  NO_SEGMENTS = [].freeze
353
354
 
355
+ # refused: what #refused recorded (issue #413) — nil, :all, or a frozen
356
+ # list of field names.
354
357
  def initialize(streams: [], redirect_url: nil, render_self: true, token_component: nil,
355
- subject_component: nil, deferred_segments: NO_SEGMENTS, pending_segments: NO_SEGMENTS)
358
+ subject_component: nil, deferred_segments: NO_SEGMENTS, pending_segments: NO_SEGMENTS,
359
+ refused: nil)
356
360
  @streams = streams.freeze
357
361
  @redirect_url = redirect_url
358
362
  @render_self = render_self
@@ -360,9 +364,14 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
360
364
  @subject_component = subject_component
361
365
  @deferred_segments = deferred_segments.freeze
362
366
  @pending_segments = pending_segments.freeze
367
+ @refused_fields = refused
363
368
  freeze
364
369
  end
365
370
 
371
+ # What the reply refused (issue #413): :all, the field names, or nil when
372
+ # it refused nothing.
373
+ attr_reader :refused_fields
374
+
366
375
  # The recorded reply.defer segments (issue #165), in call order. The
367
376
  # Response only RECORDS them; the endpoint (via the Defer module) builds
368
377
  # the placeholder + directive streams AFTER the action's transaction
@@ -388,7 +397,8 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
388
397
  token_component: @token_component,
389
398
  subject_component: @subject_component,
390
399
  deferred_segments: @deferred_segments,
391
- pending_segments: @pending_segments
400
+ pending_segments: @pending_segments,
401
+ refused: @refused_fields
392
402
  )
393
403
  end
394
404
 
@@ -420,7 +430,8 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
420
430
  subject_component: @subject_component,
421
431
  deferred_segments: @deferred_segments +
422
432
  [Phlex::Reactive::Defer::Segment.new(component:, placeholder:, morph:)],
423
- pending_segments: @pending_segments
433
+ pending_segments: @pending_segments,
434
+ refused: @refused_fields
424
435
  )
425
436
  end
426
437
 
@@ -464,10 +475,50 @@ data-reactive-ops="#{ERB::Util.html_escape(json)}"></turbo-stream>).html_safe
464
475
  token_component: @token_component || built.token_component,
465
476
  subject_component: @subject_component,
466
477
  deferred_segments: @deferred_segments,
467
- pending_segments: @pending_segments + built.pending_segments
478
+ pending_segments: @pending_segments + built.pending_segments,
479
+ refused: @refused_fields
480
+ )
481
+ end
482
+
483
+ # Answer the posted values without storing them (issue #413). Every reply
484
+ # to a root's own request releases the fields a morph held (issue #370),
485
+ # because it answers exactly those values. A reply that refused them says
486
+ # so, and the client keeps the marks: the root's next action names the
487
+ # same fields in X-Reactive-Held again, instead of saving over the other
488
+ # change on the user's next keystroke.
489
+ #
490
+ # if reactive_held_fields.include?("title") && title != @note.title
491
+ # @conflict = title
492
+ # return reply.morph.refused("title")
493
+ # end
494
+ #
495
+ # No names keeps every mark the root holds; names (the fields' `name`
496
+ # attributes, as reactive_held_fields reports them) keep only those, and
497
+ # the root's other marks release as usual. Nothing else about the reply
498
+ # changes: it is a 200, with no error marker and no reactive:error.
499
+ # Returns a NEW Response (immutable). Dead on a redirect, like #defer.
500
+ def refused(*names)
501
+ if redirect?
502
+ raise Phlex::Reactive::Error,
503
+ "reply.refused on a redirect reply is dead — the client is navigating away, " \
504
+ "so there is no held field left to keep"
505
+ end
506
+
507
+ names = names.flatten.map { -it.to_s }.uniq.freeze
508
+ self.class.new(
509
+ streams: @streams,
510
+ redirect_url: @redirect_url,
511
+ render_self: @render_self,
512
+ token_component: @token_component,
513
+ subject_component: @subject_component,
514
+ deferred_segments: @deferred_segments,
515
+ pending_segments: @pending_segments,
516
+ refused: names.empty? ? :all : names
468
517
  )
469
518
  end
470
519
 
520
+ def refused? = !@refused_fields.nil?
521
+
471
522
  # Chain a `reactive:js` op stream — server-pushed client DOM ops (issue
472
523
  # #97) — onto this reply, so a focus/dispatch/class toggle runs on the
473
524
  # client after the render applies:
@@ -271,6 +271,12 @@ module Phlex
271
271
  @response&.redirect_url
272
272
  end
273
273
 
274
+ # The action answered without storing (reply.morph.refused, issue #413):
275
+ # the client keeps the fields it held.
276
+ def refused?
277
+ !@response.nil? && @response.refused?
278
+ end
279
+
274
280
  # The actor's turbo-stream strings, exactly as the endpoint would render
275
281
  # them (response_streams) — a legacy return is the single self-replace; a
276
282
  # Response is honored, with the same token-refresh streams appended.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.16.0"
5
+ VERSION = "0.16.1"
6
6
  end
7
7
  end
@@ -9,6 +9,7 @@ module Phlex
9
9
  attr_reader subject_component: untyped
10
10
  attr_reader deferred_segments: Array[untyped]
11
11
  attr_reader pending_segments: Array[untyped]
12
+ attr_reader refused_fields: (:all | Array[String])?
12
13
 
13
14
  # --- chaining ---
14
15
  def stream: (*String more) -> Response
@@ -17,6 +18,7 @@ module Phlex
17
18
  def also: (?untyped companion, ?morph: bool, **untyped targets) -> Response
18
19
  def defer: (untyped component, ?placeholder: untyped, ?morph: bool) -> Response
19
20
  def pending: (untyped records, ?peers: bool, ?job: untyped, ?args: untyped, **untyped opts) ?{ (?) -> untyped } -> Response
21
+ def refused: (*(String | Symbol | Array[String | Symbol]) names) -> Response
20
22
 
21
23
  # Removed: each raises a guided error naming its replacement.
22
24
  def also_update: (untyped target, **untyped) -> bot
@@ -28,6 +30,7 @@ module Phlex
28
30
  def refresh_token?: () -> bool
29
31
  def deferred?: () -> bool
30
32
  def pending?: () -> bool
33
+ def refused?: () -> bool
31
34
 
32
35
  # --- the builders `reply` delegates to ---
33
36
  def self.build_replace: (untyped component, ?morph: bool, ?effect: untyped) -> Response
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.16.0
4
+ version: 0.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -149,6 +149,9 @@ files:
149
149
  - app/javascript/phlex/reactive/features/devtools.js
150
150
  - app/javascript/phlex/reactive/features/devtools.min.js
151
151
  - app/javascript/phlex/reactive/features/devtools.min.js.map
152
+ - app/javascript/phlex/reactive/features/drag.js
153
+ - app/javascript/phlex/reactive/features/drag.min.js
154
+ - app/javascript/phlex/reactive/features/drag.min.js.map
152
155
  - app/javascript/phlex/reactive/features/effects.js
153
156
  - app/javascript/phlex/reactive/features/effects.min.js
154
157
  - app/javascript/phlex/reactive/features/effects.min.js.map
@@ -197,6 +200,8 @@ files:
197
200
  - lib/phlex/reactive/collections.rb
198
201
  - lib/phlex/reactive/component.rb
199
202
  - lib/phlex/reactive/component/countdown_binding.rb
203
+ - lib/phlex/reactive/component/css_vars_binding.rb
204
+ - lib/phlex/reactive/component/drag.rb
200
205
  - lib/phlex/reactive/component/dsl.rb
201
206
  - lib/phlex/reactive/component/editable.rb
202
207
  - lib/phlex/reactive/component/field_references.rb