phlex-reactive 0.4.2 → 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: 8ec8a731ff4ae0e729288d805070e04c8860b7a51591a69b946ed42bd4996d97
4
- data.tar.gz: '018dafe55b68a05a46ef52c4e896803a9933beecf50e87f1d428073065fd3205'
3
+ metadata.gz: 63629dadef998637b9b65f885a00d54297821f405599a64c103e3b482e991fdc
4
+ data.tar.gz: 4826a5cf276f46cdf99575cb624169bacbbc3bf85115ca1256542e4c7aeb6f6b
5
5
  SHA512:
6
- metadata.gz: 949549bc92c39def933aea8d83cf61db41ad686c8184c8a284657bdd99e1178dece129258e0e874ec1dd157748f69fcd11dc77fe140557c20d7dab217500a46b
7
- data.tar.gz: ab245aa3bf1a4f65f392ae2c4b3e98cf20a6d45064375a2c2168b2c7214ed4d27bc78c6cd0f069d2ef82cad771ee233376742de6e22636f5b0efb81390f5b8ea
6
+ metadata.gz: 7561f6119d33b19334078c64570bb2fc7e6e9064b0ee90c4bcbac21dbbe2e07d5ccdb0854d146848efee66bb084c895eb89a853c3931de4c7fd115a0594163d9
7
+ data.tar.gz: 5220bfda32b47381507fed0d3a9a9d6620f06e9a10dbd8eb877fc07a0f420a8a1e3ff2dd06805059b0b0b7f550bb76cac81a7899f258bb85508643ef61dc8e35
data/CHANGELOG.md CHANGED
@@ -72,6 +72,28 @@ 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
+
75
97
  - **Collections of *reactive* rows were still add-once-only — a prepended/appended
76
98
  row's OWN token suppressed the container's token refresh (#44).** When an action
77
99
  appended/prepended a *reactive* child row into a container whose `#id` equals the
@@ -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 match = html.match(/data-reactive-token-value="([^"]+)"/)
291
- return match?.[1]
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Phlex
4
4
  module Reactive
5
- VERSION = "0.4.2"
5
+ VERSION = "0.4.3"
6
6
  end
7
7
  end
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.4.2
4
+ version: 0.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson