phlex-reactive 0.4.1 → 0.4.3
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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63629dadef998637b9b65f885a00d54297821f405599a64c103e3b482e991fdc
|
|
4
|
+
data.tar.gz: 4826a5cf276f46cdf99575cb624169bacbbc3bf85115ca1256542e4c7aeb6f6b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7561f6119d33b19334078c64570bb2fc7e6e9064b0ee90c4bcbac21dbbe2e07d5ccdb0854d146848efee66bb084c895eb89a853c3931de4c7fd115a0594163d9
|
|
7
|
+
data.tar.gz: 5220bfda32b47381507fed0d3a9a9d6620f06e9a10dbd8eb877fc07a0f420a8a1e3ff2dd06805059b0b0b7f550bb76cac81a7899f258bb85508643ef61dc8e35
|
data/CHANGELOG.md
CHANGED
|
@@ -72,6 +72,53 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
72
72
|
|
|
73
73
|
### Fixed
|
|
74
74
|
|
|
75
|
+
- **Client mirror of #44: collections of *reactive* rows were STILL add-once-only in
|
|
76
|
+
the browser — `#extractToken` read the FIRST token in the response, not this
|
|
77
|
+
controller's own (#46).** The server fix in 0.4.2 (#44) made the `add` response
|
|
78
|
+
correct — the container's fresh `reactive:token` stream is present — but the
|
|
79
|
+
client still broke. After each action the reactive controller stores the
|
|
80
|
+
response's fresh token for its NEXT dispatch; `#extractToken` did
|
|
81
|
+
`html.match(/data-reactive-token-value="([^"]+)"/)` — the FIRST match in the whole
|
|
82
|
+
body. On a collection of reactive rows the response is, in body order: the
|
|
83
|
+
appended/prepended ROW (carrying its OWN token, since a reactive row is itself a
|
|
84
|
+
`data-controller="reactive"` root) FIRST, then the container's `reactive:token`
|
|
85
|
+
refresh LAST. So the list controller stored the ROW's token; its second dispatch
|
|
86
|
+
sent a row token → failed verification → the second add silently did nothing. The
|
|
87
|
+
fix reads the token that RE-RENDERS this controller's own element id — preferring
|
|
88
|
+
the dedicated `reactive:token` stream targeting `this.element.id`, then a self
|
|
89
|
+
`replace`/`update` of it — and otherwise keeps the existing token (never adopting
|
|
90
|
+
a child row's or a sibling component's token). This is the exact client analogue
|
|
91
|
+
of the #44 server rule: a stream "carries the token for C" only when it re-renders
|
|
92
|
+
C itself, never when it inserts children into C. A request-level test can't catch
|
|
93
|
+
this (the server response is already correct); covered by a bun unit test on the
|
|
94
|
+
token selection AND a real two-click browser test (a collection of reactive rows →
|
|
95
|
+
three rows after three adds) under Puma + Falcon. Refs cosmos#1939, #44, #30.
|
|
96
|
+
|
|
97
|
+
- **Collections of *reactive* rows were still add-once-only — a prepended/appended
|
|
98
|
+
row's OWN token suppressed the container's token refresh (#44).** When an action
|
|
99
|
+
appended/prepended a *reactive* child row into a container whose `#id` equals the
|
|
100
|
+
append target (the most common collection shape — rows go directly into the
|
|
101
|
+
container element), that single `append`/`prepend` stream carried BOTH
|
|
102
|
+
`target="<container>"` and the row's own `data-reactive-token-value` (embedded in
|
|
103
|
+
the `<template>`). `carries_token_for?` matched
|
|
104
|
+
`include?("data-reactive-token-value") && include?(target)` in that one string →
|
|
105
|
+
both true → it concluded the container's token was already fresh and **skipped**
|
|
106
|
+
the container's `reactive:token` refresh. So the container (which owns the
|
|
107
|
+
add/remove trigger) never rolled its token forward, and the second dispatch was
|
|
108
|
+
rejected with the stale token — the list silently stopped after the first add.
|
|
109
|
+
This hit both the hand-rolled `reply.streams(Row.prepend(...), …)` form **and**
|
|
110
|
+
the `reactive_collection` / `reply.append`/`reply.prepend` helper, since they
|
|
111
|
+
share the gate. Fixed by tightening `carries_token_for?`: a stream "carries the
|
|
112
|
+
token for C" only when its action RE-RENDERS C itself (`replace`/`update`/
|
|
113
|
+
`reactive:token` at C's target) — `append`/`prepend` (which insert *children*
|
|
114
|
+
into C) never count, because a reactive child's token is not the container's. The
|
|
115
|
+
idempotency the gate provided is preserved (a caller's own self-replace still
|
|
116
|
+
suppresses the duplicate token) and the #30 sibling case still refreshes
|
|
117
|
+
correctly. Covered at the request level: a reactive row appended twice in a row
|
|
118
|
+
(the second using the token the first rolled forward) now succeeds, and each
|
|
119
|
+
response carries a `reactive:token` stream targeting the container with a
|
|
120
|
+
non-empty token. Refs cosmos#1939, #35, #30.
|
|
121
|
+
|
|
75
122
|
- **0.4.0 regression: re-renders lost the `request`, crashing `form_authenticity_token`
|
|
76
123
|
and other request-dependent helpers (#42).** The 0.4.0 render-path perf rework
|
|
77
124
|
built the cached off-request view context from a bare
|
|
@@ -107,15 +107,49 @@ module Phlex
|
|
|
107
107
|
streams
|
|
108
108
|
end
|
|
109
109
|
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
# component
|
|
114
|
-
#
|
|
115
|
-
#
|
|
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?
|
|
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
|
|
@@ -64,6 +64,13 @@ export function registerReactiveActions() {
|
|
|
64
64
|
registerReactiveToken()
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// Escape a DOM id for safe interpolation into a RegExp (an id can legally contain
|
|
68
|
+
// regex metacharacters like `.`/`:` — e.g. an `escape:`-namespaced or dotted id).
|
|
69
|
+
// Used by #extractToken to match the stream that re-renders THIS element by id.
|
|
70
|
+
export function escapeRegExp(string) {
|
|
71
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
if (typeof window !== "undefined") {
|
|
68
75
|
if (window.Turbo) registerReactiveActions()
|
|
69
76
|
else document.addEventListener("turbo:load", registerReactiveActions, { once: true })
|
|
@@ -286,9 +293,46 @@ export default class extends Controller {
|
|
|
286
293
|
this.#tokenCache = value
|
|
287
294
|
}
|
|
288
295
|
|
|
296
|
+
// Read the next token for THIS controller — the one that re-renders THIS
|
|
297
|
+
// element's id, never just the first token in the body (issue #46). On a
|
|
298
|
+
// collection of REACTIVE rows the prepended/appended ROW carries its OWN
|
|
299
|
+
// data-reactive-token-value and it sorts FIRST in the response; the list's own
|
|
300
|
+
// fresh token rides a trailing `reactive:token` stream targeting the container.
|
|
301
|
+
// Grabbing the first match stored the ROW's token, so the list's SECOND
|
|
302
|
+
// dispatch sent a row token → failed verification → add-once-only. This mirrors
|
|
303
|
+
// the server's carries_token_for? (#44): a stream carries OUR token only when it
|
|
304
|
+
// RE-RENDERS our id (reactive:token / replace / update of `this.element.id`) —
|
|
305
|
+
// append/prepend insert children and never count. Returns undefined when no
|
|
306
|
+
// stream re-renders our id, so #currentToken keeps its existing value.
|
|
289
307
|
#extractToken(html) {
|
|
290
|
-
const
|
|
291
|
-
|
|
308
|
+
const id = this.element.id
|
|
309
|
+
if (!id) {
|
|
310
|
+
// No id to self-match (shouldn't happen for a reactive root). Fall back to
|
|
311
|
+
// the legacy first-token behavior so a single-component response still works.
|
|
312
|
+
return html.match(/data-reactive-token-value="([^"]+)"/)?.[1]
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// The dedicated token-only refresh for THIS element (partial updates / the
|
|
316
|
+
// collection container) — an attribute on the <turbo-stream> itself.
|
|
317
|
+
const tokenStream = html.match(
|
|
318
|
+
new RegExp(
|
|
319
|
+
`<turbo-stream\\b[^>]*\\baction="reactive:token"[^>]*\\btarget="${escapeRegExp(id)}"[^>]*\\bdata-reactive-token-value="([^"]+)"`,
|
|
320
|
+
),
|
|
321
|
+
)
|
|
322
|
+
if (tokenStream) return tokenStream[1]
|
|
323
|
+
|
|
324
|
+
// A full self re-render: a replace/update of THIS element whose template root
|
|
325
|
+
// carries the fresh token. Scope the token search to that one stream so a
|
|
326
|
+
// sibling/child token elsewhere in the body can't leak in.
|
|
327
|
+
const selfStream = html.match(
|
|
328
|
+
new RegExp(
|
|
329
|
+
`<turbo-stream\\b[^>]*\\baction="(?:replace|update)"[^>]*\\btarget="${escapeRegExp(id)}"[^>]*>([\\s\\S]*?)</turbo-stream>`,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
if (selfStream) return selfStream[1].match(/data-reactive-token-value="([^"]+)"/)?.[1]
|
|
333
|
+
|
|
334
|
+
// Nothing re-rendered our id — keep the current token.
|
|
335
|
+
return undefined
|
|
292
336
|
}
|
|
293
337
|
|
|
294
338
|
// True when `el` is collected by THIS reactive root and not by a nested one.
|